Добрый день, жители Хабра. Данный пост будет посвящен программированию на C++, и использованию constexpr объектов с целью повышения уровня удобства и одновременно оптимизации кода с точки зрения размера и производительности.

В процессе работы над одним из проектов, задумался: "нельзя ли сделать удобный доступ к GPIO портам на STM32, и при этом сделать его оптимальным по размеру кода и производительности". Что я хотел получить:

  1. Использования контекстных подсказок и автодополнения при работе с GPIO.

  2. Получение максимально оптимального кода. 1-2 ассемблерных инструкции.

  3. Потенциальная возможность добавить проверки на уровне компиляции, которые не будут влиять на производительность.

Изначально я посмотрел как доступ к портам организован на платформе Arduino, и конечно данный способ далеко не оптимален с точки зрения производительности. Сначала происходит поиск порта по индексу, и только потом обращение. Тут мне в голову пришла мысль о использовании constexpr выражений и классов для реализации обоих пунктов сразу. Итак приступим. В моем случае код не будет кросплатформенным, т.к. можно считать что это часть HAL (Hardware Abstraction Layer). Код был написан для микроконтроллера STM32F103xxx.

Для начала определим адреса портов.

static constexpr const uint32_t GPIOA_BASE = 0x40010800;

static constexpr const uint32_t GPIOB_BASE = 0x40010C00;

static constexpr const uint32_t GPIOC_BASE = 0x40011000;

Теперь определим настройки порты, которые будут нам доступны.

enum class GpioMode : uint8_t {

InputAnalog = 0x00,

InputFloating,

InputWithPullup,

OutputPushPull = 0x04,

OutputOpenDrain,

AlternatePushPull,

AlternateOpenDrain

};

enum class GpioOutputSpeed : uint8_t {

Input,

Max10Mhz,

Max2Mhz,

Max50Mhz

};

Напишем класс порта. Класс оформим в виде шаблона. Все функции класса определим как static inline. Это делается для оптимизации кода. Шаблонный класс в данном случае используется для группировки функций, хранения параметорв в виде constexpr значений. Т.е. данные параметры будут доступны только на этапе компиляции, а после компиляции, код будет оптимизирован до минимального количества инструкции. В идеале до одной-двух ассемблерных инструкций при доступе к порту, даже при компиляции с опцией "-O0". В вункции доступа к портам добавим барьерные инструкции dsb.

Небольшое отступление. На собеседованиях часто задают вопрос про volatile, которым мягко говоря задолбали уже. У меня большая просьба к тем кто проводит собеседования в сфере embedded: "не могли бы вы с ходу, не подглядывая, своими словами рассказать для чего нужны инструкции dmb, dsb и isb в системе команд arm"? Полагаю, вопрос про volatile отпадет сам собой.

template<uint32_t GpioAddr, uint8_t pinNo> 
struct GpioPin {     

static constexpr const uint32_t GpioAddress = GpioAddr;
static constexpr const uint8_t  GpioPinNo = pinNo;
static constexpr const uint32_t GpioPinMask = (1 << pinNo);
static constexpr const uint32_t GpioConfPerReg = 8;

static inline void mode(const GpioMode mode, const GpioOutputSpeed oSpeed = GpioOutputSpeed::Input){
    if constexpr (GpioPinNo < GpioConfPerReg){
        static constexpr const uint32_t maskBitCount = 4;
        static constexpr const uint32_t maskOffset = (pinNo * maskBitCount) & 0x1F;
        static constexpr const uint32_t mask = (1 << (maskBitCount + 1)) - 1;
        reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL &= ~(mask << maskOffset);
        reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL |= ((static_cast<uint32_t>(mode) & 0x03) << maskOffset);
        reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL |= ((static_cast<uint32_t>(oSpeed) & 0x03) << (maskOffset + 2));
    } else {
        // Error
        // TO DO: add error compile time error message. 
    }
}

static inline bool get() {
    return (reinterpret_cast<volatile GPIO* const>(GpioAddress)->IDR & GpioPinMask);
}

static inline void set() {
    reinterpret_cast<volatile GPIO* const>(GpioAddress)->BSRR = GpioPinMask;
    asm volatile("dsb;");
}

static inline void reset(){
    reinterpret_cast<volatile GPIO* const>(GpioAddress)->BRR = GpioPinMask;
    asm volatile("dsb;");
}

static inline void invert(){
    reinterpret_cast<volatile GPIO* const>(GpioAddress)->ODR ^= GpioPinMask;
    asm volatile("dsb;");
}

}

Теперь объявим определения портов в виде прведенных к типу адресов.

#define GPIOA (reinterpret_cast<volatile GPIO* const>(GPIOA_BASE))

#define GPIOB (reinterpret_cast<volatile GPIO* const>(GPIOB_BASE))

#define GPIOC (reinterpret_cast<volatile GPIO* const>(GPIOC_BASE))

Осталось только объявить constexpr классы:

constexpr const GpioPin<GPIOA_BASE, 0> PA0;

constexpr const GpioPin<GPIOA_BASE, 1> PA1;

constexpr const GpioPin<GPIOA_BASE, 2> PA2;

constexpr const GpioPin<GPIOB_BASE, 0> PB0;

constexpr const GpioPin<GPIOB_BASE, 1> PB1;

constexpr const GpioPin<GPIOB_BASE, 2> PB2;

constexpr const GpioPin<GPIOB_BASE, 3> PB3;

.....

constexpr const GpioPin<GPIOC_BASE, 0> PC0;

constexpr const GpioPin<GPIOC_BASE, 1> PC1;

constexpr const GpioPin<GPIOC_BASE, 2> PC2;

Что можно было усовершенстворовать? Внутри класса можно добавить различные проверки, которые будут выполнятся на этапе компиляции. Например проверки адресов.

И наконец пример использования. Выглядит как обычный класс, но компилпируетсся в 2-3 иассемблерных инструкции. При этом работает автодополнение, покрайне мере в eclipse.

PA0.set();

PA0.invert();

PA0.set();

PA0.reset();

PA0.invert();

Комментарии (43)


  1. Goron_Dekar
    19.09.2021 19:38
    +1

    Мне во всех реализациях классов gpio для stm не хватает массовых (желательно ещё и ленивых) настроек нескольких портов. Я понимаю, что можно или писать на регистрах, или делать по одному, благо перенастройка портов явление редкое, но мой внутренний перфекционист ждёт этого решения.

    Сам брался писать его, но пока всё придумать элегантно решение не хватает знания плюсов. В общем, планировал сделать это так: метод класса порта при настройке возвращает временный объект, эти объекты присоеденяются друг к другу а вся работа по настройке происходит в их деструкторе.

    IO(A,12)->cfg(OutPP) <<
    IO(A,13)->cfg(OutPP) <<
    IO(A,15)->cfg(OutPP);

    Пока не знаю как это сделать гарантированно compile-time с выводом ошибки в случае невозможности разрешения.


    1. kovserg
      20.09.2021 14:29

      Всегда было интересно: что мешает сделать все настройки в before compile time обычным скриптом (perl,python,lua,php,js,groovy,...), который сгенерирует из входного запроса оптимальный код инициализации, необходимые константы, картинки и документацию. И затем с чистой совестью вызывать инициализацию периферии. Зачем это всё пихать в compile-time, при этом требуя компилятор с поддержкой последних стандартов? Не ужели стоя и в гамаке предпочтительнее.


      1. Goron_Dekar
        29.09.2021 09:04

        Поддерживал я как-то проект с таким подходом. Очень сложно при отладке и при мажорных изменениях.


    1. DSarovsky
      20.09.2021 14:53

      Это есть у Чижова, список типов, инстанцированный нужным набором портов. При разработке форка его mcucpp я тоже такое добавил, чтобы при настройке PinList-а одной строкой все используемые порты настроить (+ добавил уникальность). Вот так примерно выглядит:

      template<typename... _Ports>
      class PortList<TemplateUtils::TypeList<_Ports...> >
      {
      public:
      	static void Enable()
      	{
      		(_Ports::Enable(), ...);
      	}
      
      	static void Disable()
      	{
      		(_Ports::Disable(), ...);
      	}
      };


      1. lamerok
        25.09.2021 13:10

        Тут _Ports понимается, как список пинов уже? или отдельный пин?


        1. DSarovsky
          25.09.2021 22:51

          My bad, не сразу понял, что Goron_Dekar имел ввиду. Перечитал и осознал, что идея именно для списка пинов применять настройки. Мой пример делает это для списка портов, но и для пинов есть соответствующий функционал, который заключается в получении для списка пинов списка портов (причем уникального).
          Получаю список портов так:

          using UsedPorts = typename Unique<TypeList<typename _Pins::Port...>>::type;

          Ну, собственно, этот список портов уже применим для предложенного выше кода.


          1. mctMaks
            27.09.2021 10:36

            Прошу прощения, тут ниже ветка с вашим упоминанием:

            Недавно я общался с @DSarovsky , он делает библиотеку, похожую по идеологии, но базирующуюся на более современных стандартах C++. Я там смотрел GPIO и USB

            подскажите пожалуйста, а все желающие могут посмотреть код? интерес скорее академический.


            1. DSarovsky
              27.09.2021 11:11

              Конечно, буду более чем рад. Можно зайти в любую из моих статей, везде внизу есть ссылка на github (здесь не буду оставлять, думаю, это не очень красиво будет, как реклама).


              1. mctMaks
                27.09.2021 12:03

                а слона ссылку то я и не заметил, спасибо)


    1. sled
      18.11.2021 09:26

      такой вариант Вам подойдет? (easyelectronics.ru Удобная настройка GPIO портов)

      библиотека на #define, пример:

      GPIO_INIT_PIN(GPIOA,3,GPIO_MODE_INPUT_PULL_DOWN);

      GPIO_INIT_PIN(GPIOA,4,GPIO_MODE_OUTPUT2_OPEN_DRAIN_UP);

      или мультибит:

      GPIO_MULTI_INIT(GPIOA,BIN(1010101010101010),GPIO_MODE_OUTPUT50_OPEN_DRAIN_DOWN);


      1. mctMaks
        18.11.2021 10:19

        не самое удачное решение, так как:

        • нет автоподсказок по параметрам при использовании макроса, что выливается в необходимости поиска места написания макроса каждый раз, когда через полгода приходится возвращаться к коду. Настройка пинов да и в целом периферии это 5%, ну максимум 10% , от объема кода проекта. Поэтому настройка должна быть простой и с подсказками;

        • макросы это иногда хорошо, если код на чистом Си. Тут же обсуждали С++ и шаблоны, что немного безопасней, так как есть проверка кода на этапе компиляции. Тут же ошибка проявится только после компиляции, и повезет если сразу станет понятно где она;

        • "BIN(1010101010101010)" - блондинка в красном платье?

        Лучше тогда смотреть: http://easyelectronics.ru/rabota-s-portami-vvoda-vyvoda-mikrokontrollerov-na-si.html

        // Например, для списка пинов
        typedef PinList<Pa1, Pa2, Pa3, Pb3, Pb4> MyPins;
        MyPins::Write(0x55);

        Выше как раз упоминали автора сего кода, Константина Чижова. Статья образцовая, так как последовательная и достаточно детальная.


        1. sled
          20.11.2021 06:02

          Спасибо за пояснения!
          Но для простолюдинов непонятны все эти высокие материи, соответственно никто не пользуется.

          Вот еще статья про использование шаблонов и описаний из SVD - Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)

          BIN() - функция для препроцессора по конвертации последовательности бит в байты/слова.


          1. mctMaks
            20.11.2021 16:27

            BIN() - функция для препроцессора по конвертации последовательности бит в байты/слова

            это знаю. проблема в том, что в последовательности 0 и 1 очень легко ошибся и весьма не просто сказать какой пин стоит в каком состоянии. Поэтому видя "010010010101" попробуй угадать какой пин в какое состояние встал. особенно если для одних stm32 00 - это режим работы на вход, а для других 00 - режим работы с аналоговым сигналом.

            Но для простолюдинов непонятны все эти высокие материи, соответственно никто не пользуется.

            с вами сейчас готова поспорить миллионная армия пользователей arduino. Они пишут на языке, близком по синтаксису к С++. При этом большая часть не задумывается и, наверно, не понимает как это работает. Просто берет и пользует.

            Вопрос в другом, что сами вендоры не стремятся внедрять код на С++ к себе. У тех же ST явно хватит ресурсов сделать HAL на шаблонах (учитывая что touchGFX который они купили, как раз плюсовый код и дает на выходе). но почему это не идет в массы, я не понимаю.

            Спасибо за пояснения!

            да не за что, это просто мнение основанное на опыте чтения своих же проектов спустя 2-3 года после закрытия)

            p.s.

            "BIN(1010101010101010)" - блондинка в красном платье

            это отсылка на фильм матрица. где оператор в бинарном коде на экране видел людей.


  1. oleg-m1973
    19.09.2021 19:43
    +4

    А зачем здесь все эти constexpr, какой в них смысл? По-моему, они здесь не нужны вообще. Этот класс и без них скомпилируется в 2-3 инструкции.

    И зачем такое количество reinterpret_cast, почему бы сразу не объявить константу нужного типа?


    1. EvgenySbl Автор
      20.09.2021 06:39

      Добрый день!

      Возможно, я не совсем правильно понял вопрос. Дело в том, что любая константа будет занимать место в памяти, не важно, будет ли это оперативная память или сегмент ".text", который, как правило, располагается во flash памяти микроконтроллера. Если скомпилировать данный код без оптимизации, т.е. с опцией "-O0", то, он реально скомпилируется в 2-3 ассемблерных инструкции, чего нельзя сказать об обычном классе. Касательно, "-Os" или "-O3", вероятно разница будет небольшой. Если Вы можете показать как это сделать другим способом, я с удовольсивием приму Ваши предложения. Можно поэксперементировать с различными online компиляторами для проверки результата.


      1. oleg-m1973
        20.09.2021 13:13
        +1

        Насчёт того, почему постоянно вызывается reinterpret_cast, понял - его нельзя использовать в compile-time, поэтому нельзя объявить constexpr-указатель (не нашёл сходу, как это обойти).

        Насчёт static const - непонятно, зачем вы вообще компилируете без оптимизации.

        И ещё небольшое замечание - в методе mode() условие if constexpr (GpioPinNo < GpioConfPerReg){ можно заменить на static_assert(GpioPinNo < GpioConfPerReg)


      1. mmatrosov
        20.09.2021 14:27

        любая константа будет занимать место в памяти

        Если нет ODR-использования константы (как и любой другой переменной), то компилятор постарается её выкинуть. Если он не может избавиться от её значения - то да, придётся сохранить в сегменте .text.


    1. mctMaks
      20.09.2021 10:55

       зачем такое количество reinterpret_cast

      Для того чтобы использовать структуру, определенную в хедере на контроллер.

      Тут либо делать каст, либо как написал @juramehanik ниже воспользоваться парсером и получить уже готовые структуры для работы с периферией.


  1. juramehanik
    19.09.2021 20:50
    +1

    Для начала определим адреса портов.

    ох не так надо людей завлекать плюсами в микроконтроллерах=)
    Что мешает подключать cmsis и быть более кросплатформенным, ну хотя бы не использовать магические числа?
    Есть готовые плюсовые проекты которые парсят SVD файлы камней (и не только одного конкретного производителя) и на основе этого создают классы работы с периферией, почему бы там не применить свои идеи, чтоб не строить весь этот велосипед с нуля, а добавить к существующему свою блестящую звездочку? (здесь точно про это статьи две было, но пока не нашел с наскоку).


    1. EvgenySbl Автор
      20.09.2021 06:56

      Доброе утро.

      Я и не пытаюсь кого либо завлекать плюсами (C++) таким образом. Изначально, я написал прототип загрузчика для STM32, который работает по USB, и позволяет практически полноценно отлаживать код в отладчике среды разработки. Это вещь не кросплатформенная, от слова совсем. Затем понял что получилась неплохая альтернатива Arduino, но еще и с отладкой. Многие привыкли использовать эту среду разработки, но... Ее библиотеки мне не нравились ввиду неоптимальности. Ничего не имею против, но хотелось сделать это более оптимально. Так пришел к разработке собственной недорогой платформы для обучения программированию МК. (Откровенно говоря, даже 9 летнего племянника научил ей пользоваться). В итоге, решил доработать загрузчик, и написать базовый набор библиотек, которые будут с одной стороны просты для использования детьми, а с другой максимально оптимизированы вплоть до уровня ассемблерных инструкций.


    1. Delsian
      20.09.2021 14:29

      Кстати да. Мне нравится, как сделано в Zephyr - стандартный DTS на этапе компиляции превращается в вызовы CMSIS, соответствующего объявленному камню. И переход с STM32, например, на Nordic происходит максимально безболезненно.


  1. lamerok
    19.09.2021 21:01
    +1

    Можно не делать constexpr const объекты, они не нужны.

    using PA0 = GpioPin<GPIOA_BASE, 2> PA2;;
    PAO::set();

    Далее пины можно опять в шаблон запихать и управлять и настраивать их пачками, также без издержек, все в одну ассемблерную команду переведется.

    Вообще тема норм, я что то подобное писал тут:

    ,https://habr.com/ru/post/459204/

    И тут

    https://habr.com/ru/post/473612/

    https://habr.com/ru/post/474732/


    1. EvgenySbl Автор
      20.09.2021 07:00

      Спасибо. Полностью согласен с использованием "using", но мне хотелось получить Arduino-like код, и при этом оптимизированный с точки зрения машинных инструкций. Не хочется на начальном этапе объяснять детям чем отличается "::" от "->" или ".". За ссылки еще раз спасибо, обязательно посмотрю.


    1. EvgenySbl Автор
      20.09.2021 07:14

      Касательно настройки пачками, думал, как это правилно сделать. Обязательно воспользуюсь способом. Спасибо.


  1. abutorin
    19.09.2021 22:38

    На библиотеку Константина Чижова не смотрели? http://easyelectronics.ru/rabota-s-portami-vvoda-vyvoda-mikrokontrollerov-na-si.html


    1. EasyLy
      20.09.2021 00:34
      +1

      Дополню, что в той статье скорее рассказывается про GPIO и группы контактов, но по факту, эта библиотека связывает эти самые GPIO с классами прочих устройств (таймеров, UART, SPI, I2C...). Потому что в наше время линии этих устройств могут быть выведены на произвольные ноги. И вот GPIO этой библиотеки прекрасно всё связывает. Плюс драйверы устройств написаны весьма и весьма компактно. В общем, mcupp охватывает так много всего, что я сам ею пользуюсь, и информирую о её мощи при любом удобном случае. Так что после прочтения статьи по ссылке, надо понимать, что статья показывает только часть возможностей.

      Недавно я общался с @DSarovsky , он делает библиотеку, похожую по идеологии, но базирующуюся на более современных стандартах C++. Я там смотрел GPIO и USB. GPIO реализовано в соответствии с заветами товарища Чижова, а USB у Чижова нет, не сравнить. Формирование дескрипторов - замечательное и простое. Работа с конечными точками - понятная. Скорость - предельно возможная, за счёт использования двойного буфера. Про драйвера других устройств пока не скажу, не довелось изучить в бою.


      1. mctMaks
        20.09.2021 11:04

         эта библиотека связывает эти самые GPIO с классами прочих устройств

        а вот связывания действительно не хватает. Прокидывание GPIO в шаблон того же I2C/SPI/USART описано, да и самому написать не сложно. А вот например, привязку DMA канала через шаблон с проверкой, что этот канал не был настроен раньше, я пока не встречал. Сам пробую сделать, но пока особо не получается.

         Я там смотрел GPIO и USB

        я б тоже глянул с целью повышения образованности


  1. Ardt2
    19.09.2021 23:18
    +1

    Я как-то вот так недавно написал:

    //
    enum class PinFunct : uint32_t
    {
      // 0x03(11) - Резерв. ODR - 1/0 Подтяжка верх/вниз.
      AnalogInput = 0x00UL, FloatInput = 0x01UL << 2, PullInput = 0x02UL << 2,
      PushPull = 0x00UL << 2, OpenDrain = 0x01UL << 2,
      AF_PushPull = 0x02UL << 2, AF_OpenDrain = 0x03UL << 2,
    };
    
    enum class PinMode : uint32_t
    {
        Input = 0x00UL,
        Out2MHz = 0x02UL, Out10Mhz = 0x01UL, Out50Mhz = 0x03UL,
    };
    //
    inline uint32_t operator|(PinFunct funct, PinMode mode)
    {
        return (uint32_t)funct | (uint32_t)mode;
    }
    //
    template<PinFunct Funct, PinMode Mode>
    class TPin : public TReg
    {
        private:
            TPin() = delete;
    protected:
        GPIO_TypeDef &amp;Gpio;
        uint8_t Pin;
    
    public:
        TPin(GPIO_TypeDef &amp;gpio, uint8_t pin)
            : Gpio(gpio), Pin(pin)
        {
            // ASSERT(Pin &lt;= 15);
            // ASSERT(apb2enr(Gpio) != 0);
    
            if((Rcc.APB2ENR &amp; apb2enr(Gpio)) == 0)
            {
                Rcc.APB2ENR |= apb2enr(Gpio);
            }
    
            if(Pin &lt; 8)
            {
                SetCRL();
            }
            else
            {
                SetCRH();
            }
        }
    
        void __attribute__((always_inline))
        On()
        {
            Gpio.BSRR = Mask();
        }
    
        void __attribute__((always_inline))
        Off()
        {
            Gpio.BRR = Mask();
        }
    
        bool __attribute__((always_inline))
        IsOn()
        {
            return Gpio.IDR &amp; Mask();
        }
    
        void SetCRL(PinFunct funct, PinMode mode)
        {
            Gpio.CRL &amp;= ~(0x0FUL &lt;&lt; Pin * 4);
            Gpio.CRL |= (Funct | Mode) &lt;&lt; Pin * 4;
        }
        void SetCRH(PinFunct funct, PinMode mode)
        {
            Gpio.CRH &amp;= ~(0x0FUL &lt;&lt; (Pin - 8) * 4);
            Gpio.CRH |= (funct | mode) &lt;&lt; (Pin - 8) * 4;
        }
    
    //
    protected:
        constexpr uint32_t __attribute__((always_inline))
        Mask()
        {
            return 0x01UL &lt;&lt; Pin;
        }
    
    //
    private:
        constexpr uint32_t apb2enr(GPIO_TypeDef &amp;gpio) const
        {
            if(&amp;Gpio == &amp;GpioA)
            {
                return RCC_APB2ENR_IOPAEN;
            }
            else if(&amp;Gpio == &amp;GpioB)
            {
                return RCC_APB2ENR_IOPBEN;
            }
            else if(&amp;Gpio == &amp;GpioC)
            {
                return RCC_APB2ENR_IOPCEN;
            }
            else
            {
                return 0;
            }
        }
    
        void SetCRL()
        {
            Gpio.CRL &amp;= ~(0x0FUL &lt;&lt; Pin * 4);
            Gpio.CRL |= (Funct | Mode) &lt;&lt; Pin * 4;
        }
        void SetCRH()
        {
            Gpio.CRH &amp;= ~(0x0FUL &lt;&lt; (Pin - 8) * 4);
            Gpio.CRH |= (Funct | Mode) &lt;&lt; (Pin - 8) * 4;
        }
    
    };
    
    //
    inline TPin<PinFunct::, PinMode::Out2MHz> TestPA15pin(GpioA, 15);
    

    Про массовую настройку выводов у меня есть статья: https://habr.com/ru/post/448288/


  1. Nufunello
    20.09.2021 07:02

    static inline void mode(const GpioMode mode, const GpioOutputSpeed oSpeed = GpioOutputSpeed::Input){

    if constexpr (GpioPinNo < GpioConfPerReg)

    Попробуй static_assert, чтобы не писать "if else и в else ошибка"


  1. EvgenySbl Автор
    20.09.2021 07:09

    Доброе утро!

    Касательно применения static_assert, полностью согласен. Но на самом деле это даже не срвсем вопрос отображения ошибок времени компиляции. Это просто пример как можно сделать и как проверить. На самом деле, если я правильно помню, в коныигурационеый регистр помещаются настройки первых 8ми пинов, а вторые 8 пинов 16и битного порта должны конфигурироваться с использованием другого регистра, по этому в коде даже не ошибка должна генерироваться, а использоваться другой регистр.

    За комментарий еще раз спасибо.


  1. sami777
    20.09.2021 12:20

    Автор, вы меня заинтриговали! Каким образом инструкции dmb, dsb и isb могут заменить необходимость использования volatile?


    1. EvgenySbl Автор
      20.09.2021 14:15

      Они не могут заменить. Просто часто, на собеседованиях, человеку который знает что: "

      1. Компилятор в процессе оптимизации делает реордеринг инструкций.

      2. В процессе выполнения инструкции конвейерезируются, иногда приостанавливаются, иногда ускорябтся с применением байпасов (не ожидая окнчания предыдущей инструкции).

      3. Еще к тому же и выполняются не в том порядке. А для ожидания применяются барьерные инструкции (кода, данных) "

        задают вопросы про volatile и непременно хотят услышать тупой книжный ответ про оптимизацию. А ответ своими словами не принимается, что говорит о некомпетентности собеседующего.


      1. EvgenySbl Автор
        20.09.2021 14:25

        Я даже могу назвать одну из компаний в СПБ, где посчитали что у меня недостаточно опыта из за таких вот вопросов. Или еще была ситуация, когда мне задали вопрос о том, сколько можно помледовательно поставить операционных усилителей для того чтобы не было самовозбуждения. Я понял что нажо мной решили постебаться, и спокойно сказал: "главное чтобы полюсов не было", хотя мог бы оиветить "главное чтобы положительная обратная связь была недостаточна или чтобы не соблюдался юаланс фаз и амплитуд". Полагаю не все поняли о каких поюсах шла речь. А я имел ввиду расчет схемы с использованием преобразования Лапласа, когда возникает понятие нулей и полюсов, что определят устойчивость схемы....


  1. mmatrosov
    20.09.2021 14:30

    Небольшое замечание - constexpr объекты автоматически будут const, так что нет смысла писать для них `constexpr const`. А методы, объявленные в теле класса, автоматически inline, так что тоже нет необходимости это указывать ещё раз.


  1. Leopoldius
    20.09.2021 15:04

    @EvgenySbl, Касательно dsb и прочих барьеров, все у них хорошо, если не считать того, что в некоторых случаях можно получить пенальти по тактам. Т.е. процессор будет сидеть и педалить в ожидании завершения транзакций на шине AXI/AHB/whatever через которую он соединен с внешним миром.

    В некоторых случаях это может быть критично, особенно при ногодрыге или быстром обмене, когда у тебя Chip select на spi, например с ручным приводом и ты общаешься на частотах 5-10 МГц и выше. Что приведет к задержке включения CS например или отключения его. Как следствие страстная любовь с трудноотлавливаемыми глюками.

    Как по мне, каждому решению свое место должно быть. Сие есть мое IMHO :-)


    1. EvgenySbl Автор
      20.09.2021 15:13

      С одной стороны да, с другой стороны нет. В некоторых случаях это как раз помогает. Предположим что мы в ассемблерном коде подготовим две инструкции записи в порт заранее. А затем пропишем две str или им подобные инструкции последовательно. Есть ли гарантия что они выполнятся именно в том порядке? Во первых процессор может их зареордерить. Во вторых нужно учесть является ли данная память cachable, если да, то какой тип кеша и как это отразить на записи данных. Во третьих возникнет вопрос того что реально попадет на gpio порт и будет ли раельно сформирован glitch. В третих скорость gpio порта может быть, а как правило так и есть, значительно ниже скорости процесора....


      1. Leopoldius
        20.09.2021 15:24

        Безусловно. Мой посыл был в том, что нужно понимать когда и для чего барьер следует использовать.


      1. Reflector
        20.09.2021 16:40

        Volatile дает гарантии строгого порядка для volatile объектов, т.е. если дважды что-то записать в BSRR, то генерируемые инструкции будут идти в том же порядке, хотя между ними компилятор может вставить обращение к не volatile объектам. А если у нас есть две инструкции записи в порт, то никакого реордеринга не будет, кортексы так просто не умеют, уж точно не для портов висящих на одной шине, потому DSB тут не нужна, она только тормозов добавит, а у F1 и так порты медленные.


        1. Leopoldius
          20.09.2021 17:26

          Спасибо за дополнение. Об этом собственно и речь, что нужно понимать что ты делаешь и зачем ты это делаешь. )))


        1. EvgenySbl Автор
          20.09.2021 18:39

          На самом деле в психологии есть два хороших правила.

          1. Единственный способ выиграть спор,- это не спорить.

          2. Если Вы (я) ошиблись, сразу признавайте свою ошибку.

          Хорошо, пррнято. А что если сначала записать odr, а потом прочитать idr? Транзакция на шине успеет пройти или конвейерезированная инструкция прочитает предыдущее значение? Это не спор, а попытка показать что все зависит от реализации в данном случае.


          1. Reflector
            20.09.2021 21:08

            А реализация от чего зависит? Мы же с регистрами работаем, тип памяти у нас Device и никакого кеширования, строгий порядок записи и чтения гарантируется уже только этим. Есть же периферия которая требует установки одного бита, а следом другого в том же регистре. Не вместе и не в обратном порядке и все это работает без никаких барьеров. А чтение из IDR вообще зависит от многих факторов, если есть быстрый мк, но медленные порты или задан не самый подходящий режим, типа OpenDrain с подтяжкой, то после записи в ODR придется вставлять задержку просто потому, что уровень сигнала на выходе нарастает слишком медленно и одного DSB может не хватить. Toggle() довольно часто пытаются так делать, читают IDR, инвертируют биты и пишут в ODR... Твой invert(), кстати, тоже проблемный, т.к. не атомарный, второй invert() над тем же портом в прерывании может все поломать.


            1. EvgenySbl Автор
              20.09.2021 23:20

              ОК. Еще один психологический прием, полезный руководителям и не только. Стоит выждать пока собеседник остынет и появится возможность возобновить общение. Это все из Карнеги. Но если почитать Э.Берна, то сечас Вы, (заметте, я написал с большой буквы, и не "ты", а "Вы"), играете в игру "Спор". А я не собираюсь спорить, спасибо. Мы похоже уже несколько ушли от обсуждения основной темы, и близки к тому чтобы перейти на личности ).

              Если говорить по теме, то .... Конечно есть переферия и конечно работает, но, не всегда. Кто-то программирует простые системы, кто-то системы более сложные. И в сложных системах очень многое зависит от настроек MMU/MPU, от использования кэша. Если работает в одном случае, это не означает что будет работать в другом.

              Касательно toggle, то, тут Вы безусловно правы. Правда я не использую IDR в качестве базовой величины, в моем случае это ODR, что вполне логично (ODR = ORD xor Bitmask).

              А дальше вопрос процессора, компилятора и уровня оптимизации. К сожалению , в системе команд thumb/tumb2, насколько я помню, не предусмотрены битовые операции с ячейкой памяти, а соответственно операция инвертирования бита порта происходит посредством <load, invert, store> c применением регистра общего назначения, и именно по этому не является атомарной. Если бы stm32, имел регистр аналогичный BSRR, BRRR, но с названием BERR (где E - EOR), вероятно операция была бы атомарной.

              PA1.reset();     // Первое обращение идет с подготовкой. Загрузка адреса и битовой маски 2 инструкции + 1 запись в порт + dsb.
                ldr r3, .L2    // Грузим адрес порта
                eor r2, r2, #2     // Битовая маска PIN1 r2 = 0;  r2 |= 0x02
                str r2, [r3, #12]  // Запись в порт (сброс бита).
                dsb                // Барьерная инструкция
              
              PA1.set();       // Далее одна ассемблерная инструкция + dsb
                str r2, [r3, #16]  // Запись в порт (установка бита)
                dsb;               // Барьерная инструкция
              
              PA1.reset();     // Далее одна ассемблерная инструкция + dsb
                str r2, [r3, #20]  // Запись в порт (сброс бита)
                dsb;               // Барьерная инструкция
              
              PA1.invert();    // Три ассемблерных инструкции + dsb
                ldr r2, [r3, #12] // Читаем ODR
                eor r2, r2, #2    // Делаем OR
                str r2, [r3, #12] // Записываем в порт
                dsb               // Барьерная инструкция
              
              PA1.invert(); // Три ассемблерных инструкции + dsb
                ldr r2, [r3, #12]
                eor r2, r2, #2
                str r2, [r3, #12]
                dsb

              Если говорить о прерываниях, то смотря о чем речь. Насколько я помню, NVIC, повторно не позволит войти в прерывание с тем же приоритетом. А использование того же пина порта, при обработке в другом приерывании без выхода из текущего будет являться логической ошибкой программирования. Если же такая необходимость сущетвует можно защитить операцию инвертирования с применением запрета вызова прервыаний совсем или только некоторых прерываний. Для этих целей можно использовать, как флаг в регистре процессора, так и basepri, или же запрет прерывания на уровне переферии.

              Касательно (PullUP / PullDown c режимами OpenDrain) и PushPull, в целом, это немного из другой оперы. Сигнал на линии не имеет прямого отношения к теме касающейся барьерных инструкций. За комментарий спасибо. "Если хочешь научиться играть, то всегда выбирай сильного противника/союзника",- это про Вас.


              1. Reflector
                21.09.2021 00:20

                Никто в здравом уме не станет для региона памяти в котором расположены регистры периферии менять тип памяти с Device на Normal и т.д., иначе вообще ничего работать не будет, потому рассуждать о подобным гипотетических ситуациях не имеет смысла.

                И я говорил не про один и тот же пин порта, а про разные пины одного порта, один можно тоглить в главном цикле, а второй в прерывании и будет глючить. Даже HAL_GPIO_TogglePin() раньше работал чисто с ODR, а теперь тоже переделали через BSRR.