Как поморгать 4 светодиодами на CortexM используя С++17, tuple и немного фантазии

    Всем доброго здравия!

    При обучении студентов разработке встроенного программного обеспечения для микроконтроллеров в университете я использую С++ и иногда даю особо интересующимся студентам всякие задачки для определения особо больных на голову одаренных учеников.

    В очередной раз таким студентам была дана задача поморгать 4 светодиодами, используя язык С++ 17 и стандартную библиотеку С++, без подключения дополнительных библиотек, типа CMSIS и их заголовочных файлов с описанием структур регистров и так далее… Побеждает тот, у кого код в ROM будет занимать наименьший размер и меньше всего затрачено ОЗУ. Оптимизация компилятора при этом не должна быть выше Medium. Компилятор IAR 8.40.1.
    Победитель едет на Канары получает 5 за экзамен.

    Сам я до этого тоже эту задачу не решал, поэтому расскажу как её решили студенты и что получилось у меня. Предупреждаю сразу, навряд ли такой код можно будет использовать в реальных приложениях, потому и разместил публикацию в раздел «Ненормальное программирование», хотя кто знает.

    Условия задачи


    Есть 4 светодиода на портах GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9. Ими нужно поморгать. Чтобы было с чем сравнивать мы взяли код написанный на Си:

    void delay() {
      for (int i = 0; i < 1000000; ++i){
      }
    }
    
    int main() { 
       for(;;)   {
         GPIOA->ODR ^= (1 << 5);
         GPIOC->ODR ^= (1 << 5);
         GPIOC->ODR ^= (1 << 8);
         GPIOC->ODR ^= (1 << 9);     
         delay();
       }  
      return 0 ;
    }

    Функция delay() здесь чисто формальная, обычный цикл, её оптимизировать нельзя.
    Предполагается, что порты уже настроены на выход и на них подано тактирование.
    А также сразу скажу, что bitbanging не использовался, чтобы код был переносимым.

    Этот код занимает 8 байт на стеке и 256 байт в ROM на Medium оптимизации
    255 bytes of readonly code memory
    1 byte of readonly data memory
    8 bytes of readwrite data memory

    255 байт из-за того, что часть памяти ушла под таблицу векторов прерывания, вызовы функций IAR для инициализации блока с плавающей точкой, всякие отладочные функции и функция __low_level_init, где собственно порты настроились.

    Итак, полные требования:

    • Функция main() должна содержать как можно меньше кода
    • Нельзя использовать макросы
    • Компилятор IAR 8.40.1 поддерживающий С++17
    • Нельзя использовать заголовочные файлы CMSIS, типа "#include «stm32f411xe.h»
    • Можно использовать директиву __forceinline для встраиваемых функций
    • Оптимизация компилятора Medium

    Решение студентов


    Вообще решений было несколько, я покажу только одно… оно не оптимальное, но мне понравилось.

    Так как нельзя использовать заголовочные файлы, студенты первым делом сделали класс Gpio, который должен хранить ссылку на регистры порта по их адресам. Для этого они используют оверлей структуры, скорее всего идею взяли отсюда: Structure overlay:

    class Gpio {
    public:
    __forceinline inline void Toggle(const std::uint8_t bitNum) volatile {
        Odr ^= bitNum ;
      }  
    private:
      volatile std::uint32_t Moder;
      volatile std::uint32_t Otyper;
      volatile std::uint32_t Ospeedr;
      volatile std::uint32_t Pupdr;  
      volatile std::uint32_t Idr;      
      volatile std::uint32_t Odr;       
      //Проверка что структура выравнена
      static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6); 
    } ;

    Как видно они сразу определили класс Gpio с атрибутами, которые должны быть расположены по адресам соответствующих регистров и метод для переключения состояния по номеру ножки:
    Затем определили структуру для GpioPin, содержащую указатель на Gpio и номер ножки:

    struct GpioPin
    {
      volatile Gpio* port ;
      std::uint32_t pinNum ;
    } ;

    Затем они сделали массив светодиодов, которые сидят на конкретных ножках порта и пробежались по нему вызвав метод Toggle() каждого светодиода:

    const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5},
                          {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5},
                          {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9},  
                          {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} 
    } ;
    
    struct LedsDriver {
      __forceinline static inline void ToggelAll()  {
        for (auto& it: leds)    {
          it.port->Toggle(it.pinNum);
        }
      }
    } ;

    Ну собственно и весь код:
    
    constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ;
    constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ;
    class Gpio {
    public:
    __forceinline inline void Toggle(const std::uint8_t bitNum) volatile {
        Odr ^= bitNum ;
      }  
    private:
      volatile std::uint32_t Moder;
      volatile std::uint32_t Otyper;
      volatile std::uint32_t Ospeedr;
      volatile std::uint32_t Pupdr;  
      volatile std::uint32_t Idr;      
      volatile std::uint32_t Odr;        
    } ;
    
    //Проверка что структура выравнена
    static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6);
    
    struct GpioPin {
      volatile Gpio* port ;
      std::uint32_t pinNum ;
    } ;
    
    const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5},
                          {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5},
                          {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9},  
                          {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} 
    } ;
    
    struct LedsDriver {
      __forceinline static inline void ToggelAll()  {
        for (auto& it: leds)    {
          it.port->Toggle(it.pinNum);
        }
      }
    } ;
    
    int main() { 
       for(;;)   {
         LedsContainer::ToggleAll() ;
         delay();
       }  
      return 0 ;
    }


    Статистика их кода на Medium оптимизации:
    275 bytes of readonly code memory
    1 byte of readonly data memory
    8 bytes of readwrite data memory

    Хорошее решение, но памяти занимает много :)

    Решение мое


    Я конечно решил не искать простых путей и решил действовать по серьезному :).
    Светодиоды находятся на разных портах и разных ножках. Первое что необходимо, это сделать класс Port, но чтобы избавиться от указателей и переменных, которые занимают ОЗУ, нужно использовать статические методы. Класс порт может выглядеть так:

    template <std::uint32_t addr>
    struct Port {  
     //здесь скоро что-то будет
    };

    В качестве параметра шаблона у него будет адрес порта. В заголовочнике "#include "stm32f411xe.h", например для порта А, он определен как GPIOA_BASE. Но заголовочники нам использовать запрещено, поэтому просто нужно сделать свою константу. В итоге класс можно будет использовать так:

    constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ;
    constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800  ;
    using PortA = Port<GpioaBaseAddr> ;
    using PortC = Port<GpiocBaseAddr> ;
    

    Чтобы поморгать нужен метод Toggle(const std::uint8_t bit), который будет переключать необходимый бит с помощью операции исключающее ИЛИ. Метод должен быть статическим, добавляем его в класс:

    template <std::uint32_t addr>
    struct Port {  
     //сразу применяем директиву __forceinline, чтобы компилятор воспринимал эту функцию как встроенную
      __forceinline inline static void Toggle(const std::uint8_t bitNum)  {
        *reinterpret_cast<std::uint32_t*>(addr+20) ^= (1 << bitNum) ; //addr + 20 адрес ODR регистра
      }
    };

    Отлично Port<> есть, он может переключать состояние ножки. Светодиод сидит на конкретной ножке, поэтому логично сделать класс Pin, у которого в качестве параметров шаблона будет Port<> и номер ножки. Поскольку тип Port<> у нас шаблонный, т.е. разный для разного порта, то передавать мы можем только универсальный тип T.

    template <typename T, std::uint8_t pinNum>
    struct Pin {
      __forceinline inline static void Toggle()   {
        T::Toggle(pinNum) ;
      }
    } ;

    Плохо, что мы можем передать любую чепуху типа T у которой есть метод Toggle() и это будет работать, хотя предполагается что передавать мы должны только тип Port<>. Чтобы от этого защититься, сделаем так, чтобы Port<> наследовался от базового класса PortBase, а в шаблоне будем проверять, что наш переданный тип действительно базируется на PortBase. Получаем следующее:

    constexpr std::uint32_t OdrAddrShift = 20U;
    struct PortBase {
    };
    
    template <std::uint32_t addr>
    struct Port: PortBase {  
      __forceinline inline static void Toggle(const std::uint8_t bit)  {    
        *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ;
      }
    };
    
    template <typename T, std::uint8_t pinNum, 
    class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> //Вот и защита
    struct Pin {
      __forceinline inline static void Toggle()  {
        T::Toggle(pinNum) ;
      }
    } ;

    Теперь шаблон инстанциируется, только если наш класс имеет базовый класс PortBase.
    По идее уже можно использовать эти классы, давайте посмотрим, что получится без оптимизации:

    using PortA = Port<GpioaBaseAddr> ;
    using PortC = Port<GpiocBaseAddr> ;
    
    using Led1 = Pin<PortA, 5> ;
    using Led2 = Pin<PortC, 5> ;
    using Led3 = Pin<PortC, 8> ;
    using Led4 = Pin<PortC, 9> ;
    
    int main() { 
       for(;;)   {
         Led1::Toggle();
         Led2::Toggle();
         Led3::Toggle();
         Led4::Toggle();
         delay();
       }  
      return 0 ;
    }

    271 bytes of readonly code memory
    1 byte of readonly data memory
    24 bytes of readwrite data memory

    Откуда взялись эти дополнительные 16 байт в ОЗУ и 16 байт в ROM. Они взялись из того, факта, что мы передаем в функцию Toggle(const std::uint8_t bit) класса Port параметр bit, и компилятор, при входе в функцию main сохраняет на стеке 4 дополнительных регистра, через которые передает этот параметр, потом использует эти регистры в которых сохраняется значения номера ножки для каждого Pin и при выходе из main восстанавливает эти регистры из стека. И хотя по сути это какая-то полностью бесполезная работа, так как функции встроенные, но компилятор действует в полном соответствии со стандартом.
    От этого можно избавиться убрав класс порт вообще, передать адрес порта в качестве параметра шаблона для класса Pin, а внутри метода Toggle() высчитывать адрес регистра ODR:

    constexpr std::uint32_t OdrAddrShift = 20U;
    template <std::uint32_t addr, std::uint8_t pinNum, 
    struct Pin {
      __forceinline inline static void Toggle()  {
        *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift ) ^= (1 << bit) ;
      }
    } ;
    using Led1 = Pin<GpioaBaseAddr, 5> ; 

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

    Ставим оптимизацию на Medium и смотрим результат:
    251 bytes of readonly code memory
    1 byte of readonly data memory
    8 bytes of readwrite data memory

    Вау вау вау… у нас на 4 байта меньше
    сишного кода
    255 bytes of readonly code memory
    1 byte of readonly data memory
    8 bytes of readwrite data memory


    Как такое может быть? Давайте взглянем на ассемблер в отладчике для С++ кода(слева) и Си кода(справа):

    image

    Видно, что во-первых, компилятор все функции сделал встроенные, теперь нет никаких вызовов вообще, а во вторых, он оптимизировал использование регистров. Видно, в случае с Си кодом, для хранения адресов портов компилятор использует то регистр R1, то R2 и делает дополнительную операции каждый раз после переключения бита (сохранить адрес в регистре то в R1, то в R2). Во втором же случае он использует только регистр R1, а поскольку 3 последних вызова на переключение всегда с порта C, то надобности сохранять тот же самый адрес порта С в регистре уже нет. В итоге экономится 2 команды и 4 байта.

    Вот оно чудо современных компиляторов :) Ну да ладно. В принципе можно было на этом остановится, но пойдем дальше. Оптимизировать еще что-то, думаю, уже не выйдет, хотя возможно не прав, если есть идеи, пишите в комментариях. А вот с количеством кода в main() можно поработать.

    Теперь хочется, чтобы все светодиоды были бы где нибудь в контейнере, и можно было бы вызывать метод, переключить все… Вот как-то так:

    int main() { 
       for(;;)   {
         LedsContainer::ToggleAll() ;
         delay();
       }  
      return 0 ;
    }

    Мы не будем тупо вставлять переключение 4 светодиодов в функцию LedsContainer::ToggleAll, потому что это неинтересно :). Мы хотим светодиоды положить в контейнер и потом пройтись по ним и вызывать у каждого метод Toggle().

    Студенты использовали массив для того, чтобы хранить указатели на светодиоды. Но у меня разные типы, например: Pin<PortA, 5>, Pin<PortC, 5>, и указатели на разные типы я хранить в массиве не могу. Можно сделать виртуальный базовый класс, для всех Pin, но тогда появится таблица виртуальных функций и уделать выиграть студентов мне не удастся.

    Поэтому будем использовать кортеж. Он позволяет хранить у себя объекты разных типов. Выглядеть это дело будет выглядеть так:

    class LedsContainer {  
     private: 
       constexpr static auto records = std::make_tuple (
                                                       Pin<PortA, 5>{},
                                                       Pin<PortC, 5>{},
                                                       Pin<PortC, 8>{},
                                                       Pin<PortC, 9>{}    
        ) ;
      using tRecordsTuple = decltype(records) ;
    }

    Отлично есть контейнер, он хранит все светодиоды. Теперь добавим в него метод ToggleAll():

    class LedsContainer {  
     public:
       __forceinline static inline void ToggleAll()   {
            //сейчас придумаем как тут перебрать все элементы кортежа
       }    
     private: 
       constexpr static auto records = std::make_tuple (
                                                       Pin<PortA, 5>{},
                                                       Pin<PortC, 5>{},
                                                       Pin<PortC, 8>{},
                                                       Pin<PortC, 9>{}    
        ) ;
      using tRecordsTuple = decltype(records) ;
    }

    Просто так пройтись по элементам кортежа нельзя, так как получение элемента кортежа должно происходить только на этапе компиляции. Для доступа к элементам кортежа есть темплейтный метод get. Ну т.е. если напишем так std::get<0>(records).Toggle(), то вызовется метод Toggle() для объекта класса Pin<PortA, 5>, если std::get<1>(records).Toggle(), то вызовется метод Toggle() для объекта класса Pin<PortС, 5> и так далее…

    Можно было утереть студентам нос и просто написать так:

     __forceinline static inline void ToggleAll()   {
       std::get<0>(records).Toggle();
       std::get<1>(records).Toggle();
       std::get<2>(records).Toggle();
       std::get<3>(records).Toggle();
       }    

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

    class class LedsContainer {  
      friend int main() ;
      public:         
       __forceinline static inline void ToggleAll()     {
         // создаем последовательность индексов 3,2,1,0 и вызываем соответствующий метод, куда передается эта последовательность
          visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());
        }   
        
      private:       
        __forceinline  template<std::size_t... index>    
        static inline void visit(std::index_sequence<index...>)   { 
          Pass((std::get<index>(records).Toggle(), true)...); // распаковываем в последовательность get<3>(records).Toggle(), get<2>(records).Toggle(), get<1>(records).Toggle(), get<0>(records).Toggle()
        }
        
        __forceinline template<typename... Args> 
        static void inline Pass(Args... )  {//Вспомогательный метод для распаковки вариативного шаблона
        }
       constexpr static auto records = std::make_tuple (
                                                       Pin<PortA, 5>{},
                                                       Pin<PortC, 5>{},
                                                       Pin<PortC, 8>{},
                                                       Pin<PortC, 9>{}    
        ) ;
      using tRecordsTuple = decltype(records) ;
    }

    Выглядит страшновато, но я предупреждал в начале статьи, что способ шизанутый не очень обычный…

    Вся эта магия сверху на этапе компиляции делает буквально следующее:

    //Это вызов 
    LedsContainer::ToggleAll() ;
    //Преобразуется в эти 4 вызова:
    Pin<PortС, 9>().Toggle() ;
    Pin<PortС, 8>().Toggle() ;
    Pin<PortC, 5>().Toggle() ;
    Pin<PortA, 5>().Toggle() ;
    //А поскольку у нас метод Toggle() inline, то в это:
     *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 9) ;
     *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 8) ;
     *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 5) ;
     *reinterpret_cast<std::uint32_t*>(0x40020014 ) ^= (1 << 5) ;

    Вперед компилировать и проверять размер кода без оптимизации:

    Код который компилим
    #include <cstddef>
    #include <tuple>
    #include <utility>
    #include <cstdint>
    #include <type_traits>
    
    //#include "stm32f411xe.h"
    
    #define __forceinline  _Pragma("inline=forced")
    constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ;
    constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ;
    constexpr std::uint32_t OdrAddrShift = 20U;
    
    struct PortBase
    {
    };
    
    template <std::uint32_t addr>
    struct Port: PortBase
    {  
      __forceinline inline static void Toggle(const std::uint8_t bit)
      {    
        *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift) ^= (1 << bit) ;
      }
    };
    
    template <typename T, std::uint8_t pinNum, 
    class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>>
    struct Pin
    {
      __forceinline inline static void Toggle()
      {
        T::Toggle(pinNum) ;
      }
    } ;
    
    
    using PortA = Port<GpioaBaseAddr> ;
    using PortC = Port<GpiocBaseAddr> ;
    
    //using Led1 = Pin<PortA, 5> ;
    //using Led2 = Pin<PortC, 5> ;
    //using Led3 = Pin<PortC, 8> ;
    //using Led4 = Pin<PortC, 9> ;
    
    class LedsContainer {  
      friend int main() ;
      public:    
         
          __forceinline static inline void ToggleAll()     {
         // создаем последовательность индексов 3,2,1,0 и вызываем соответствующий метод, куда передается эта последовательность
          visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());
        }   
      private:   
        __forceinline  template<std::size_t... index>    
        static inline void visit(std::index_sequence<index...>)         { 
          Pass((std::get<index>(records).Toggle(), true)...);
        }
        
        __forceinline template<typename... Args> 
        static void inline Pass(Args... )     {      
        }
        constexpr static auto records = std::make_tuple (
                                                         Pin<PortA, 5>{},
                                                         Pin<PortC, 5>{},
                                                         Pin<PortC, 8>{},
                                                         Pin<PortC, 9>{}    
          ) ;
        using tRecordsTuple = decltype(records) ;
    } ;
    
    void delay() {
      for (int i = 0; i < 1000000; ++i){
      }
    }
    
    int main() { 
       for(;;)   {
         LedsContainer::ToggleAll() ;
         //GPIOA->ODR ^= 1 << 5;
         //GPIOC->ODR ^= 1 << 5;
         //GPIOC->ODR ^= 1 << 8;
         //GPIOC->ODR ^= 1 << 9;     
         
         delay();
       }  
      return 0 ;
    }


    Ассемблерный пруф, распаковалось как планировали:
    image

    Видим, что по памяти перебор, на 18 байтов больше. Проблемы все те же, плюсом еще 12 байт. Не стал разбираться откуда они… может кто пояснит.
    283 bytes of readonly code memory
    1 byte of readonly data memory
    24 bytes of readwrite data memory

    Теперь тоже самое на Medium оптимизации и о чудо… получили код идентичный С++ реализации в лоб и оптимальнее Си кода.
    251 bytes of readonly code memory
    1 byte of readonly data memory
    8 bytes of readwrite data memory

    Ассемблер
    image

    Как видите победил я, и поехал на Канары и довольный отдыхаю в Челябинске :), но студенты тоже молодцы, экзамен сдали успешно!

    Кому интересно, код тут

    Где можно такое использовать, ну я придумал, например такое, у нас есть параметры в EEPROM памяти и класс описывающий эти параметры (Читать, писать, инициализировать в начальное значение). Класс шаблонный, типа Param<float<>>, Param<int<>> и нужно, например, все параметры сбросить в default значения. Как раз тут и можно все их положить в кортеж, так как тип разный и вызвать у каждого параметра метод SetToDefault(). Правда, если таких параметров будет 100, то ПЗУ отъестся много, зато ОЗУ не пострадает.

    P.S. Надо признаться, что на максимальной оптимизации этот код по размеру получается такой же как на Си и на моем решении. И все потуги программиста по улучшению кода сводятся к одному и тому же коду на ассемблере.

    P.S1 Спасибо 0xd34df00d за дельный совет. Можно упростить распаковку кортежа с помощью std::apply(). Код функции ToggleAll() тогда упроститься до такого:

     __forceinline static inline void ToggleAll() 
        {
          std::apply([](auto... args) { (args.Toggle(), ...); }, records);
        }   

    К сожалению в IAR std::apply в текущей версии еще не реализован, но работать будет также, см на реализацию с std::apply
    Support the author
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 34

      +1

      Непонятно, зачем в 2019 году вся эта ерунда с ручной рекурсией по списку индексов, когда давно есть std::apply и folding expressions. Можно ж в полторы строчки с C++17-то.

        0
        Тут нет рекурсии. Про apply посмотрю, спасибо за наводку. Но вообще компиляторы для микроконтроллеров не все библиотечные функции из С++17 поддерживают. Например, конкретно std::apply там и не реализован. И кстати, не могли бы показать, как в полторы строчки это сделать, я не совсем уловил, как кортеж распаковать в последовательность вызовов методов элементов кортежа…
          +1
          Тут нет рекурсии.

          А, тьфу. Я сначала слишком бегло прочитал, сорри. Это лучше, да :)


          Но вообще тогда Pass не нужен, можно завернуть в initializer_list<bool> или типа того. У которого, кстати, порядок вычисления аргументов (если через {}) полностью специфицирован, в отличие от. Не то, чтобы это было важно в этом случае, но привычка хорошая, ИМХО.


          Но вообще компиляторы для микроконтроллеров не все библиотечные функции из С++17 поддерживают. Например, конкретно std::apply там и не реализован.

          Ну, значит, IAR этот C++17 не поддерживает, увы.


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

          Что-то типа


          std::apply(records, [](auto... args) { (args.Toggle(), ...); });

          В реальном коде выглядит как-то так, например.

            0
            Спасибо, добавил в конец статьи.
        0

        Всегда с интересом смотрю на реализацию работы с периферией с помощью шаблонной магии!


        Можете пояснить, как работает магия с проверкой?


        template <typename T, std::uint8_t pinNum, 
        class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> //Вот и защита
        struct Pin {
          __forceinline inline static void Toggle()  {
            T::Toggle(pinNum) ;
          }
        } ;
          0
          Работает это дело примерно так:
          Чтобы было проще понять на пальцах, считайте, что std::enable_if_t<> это функция
          T enabled_t(bool), на входе она получает, либо true, либо false. На выходе либо тип T, если передали true, либо ничего и тогда T не определен. Т.е:
          std::enabled_t(true) возвратит тип T и будет T = T,
          std::enabled_t(false) возвратит ничего и T= ничего.

          Если T не определен, то компилятор не сможет выполнить T::Toggle(pinNum); Так как T не существует. И собственно выдаст вам ошибку, что нельзя передать такой T.

          Упрощенно запишем так:

          если Т является подтипом PortBase, то функция std::enable_if_t<> возвратит Т и T =T и шаблон будет таким
          template <T, pinNum>

          если Т не является подтипом PortBase, то то функция std::enable_if_t<> ничего не возвратит и T = ничего и шаблон будет таким
          template <,pinNum>
          и наш класс не соберется

          Собственно std::is_base_of<PortBase, T>::value>, как раз и проверят, является ли T подтипом PortBase. Если да, то возвратит true, и T=T, если нет то false и Т не определен.
          0

          С++17 и без bitbanding. Ну как же так?
          А линкер скрипт, инициацию стека, вектор прерываний копирование памяти взяли из библиотеки. А зря, плюсы и тут бы помогли.


          Современные плюсы должны делать С из-за огромных компилтайм возможностей.

            0
            Инициализацию стека и векторов прерываний на С++ сделал, правда это больше на Си смахивает.
            class DummyModule {
              public:
                static void HandleInterrupt() {};
            } ;
            #define __vectortable _Pragma("location=\".intvec\"") 
            using tInterruptFunction = void (*)() ;
            using tInterruptVectorItem = union 
            
            __vectortable const tInterruptVectorItem __vector_table[] = {  
              {     .pPtr = __sfe( "CSTACK" )   }, 
              {     __iar_program_start //Reset  }, 
                // Non maskable interrupt, Clock Security System
              {   DummyModule::HandleInterrupt }, 
              {   DummyModule::HandleInterrupt },    // All class of fault
              {   DummyModule::HandleInterrupt  },  // Memory management
              {   DummyModule::HandleInterrupt },   // Pre-fetch fault, memory access fault
              {   DummyModule::HandleInterrupt },   // Undefined instruction or illegal state
              {   0  },      //Reserved
              {   0  },      //Reserved
              {   0  },      //Reserved
              {   0  },      //Reserved
              {   OsWrapper::Rtos::HandleSvcInterrupt },   
              {   DummyModule::HandleInterrupt  },     // Debug Monitor
              {    0  },     // Reserved 
              {  OsWrapper::Rtos::HandlePendSvInterrupt  },    
              {  OsWrapper::Rtos::HandleSysTickInterrupt  }      
            }
            
              +1
              Встречал статьи, в том числе и на хабре, где программисты startup'ы писали на плюсах. Однако этот подход требует довольно глубокого понимания компилятора и линкера.
              А так да, инициализация и работа со стеком и таблицей прерываний на плюсах очень приятная.
                0

                Угу, вот пример выше как раз показывает, насколько "приятно" и, самое главное, "понятно" выглядят все эти вещи на плюсах.


                Хотя я уверен, что плюсы в скором времени победят Си в эмбеде, но очевидно скорость этого процесса прямо зависит от смертности среди Си-программистов. Должно исчезнуть поколение людей, знающих, что можно жить без всех этих "приятных" вещей.

                  –1

                  Жить-то можно, понятное дело. Можно и без С жить, в конце концов. Но нужно ли?

                    0

                    Никто и не утверждает, что все продемонстрированное выше не нужно. Наверное нужно же...

              +3
              class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>>

              std::apply(records, [](auto... args) { (args.Toggle(), ...); });

              visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());


              Я, возможно, чего-то не понимаю, но разве не должен код, максимально приближенный к железу быть максимально понятен и упрощен для надежности? С для микроконтроллеров используют не просто так, а по причине того, что он, обладая достаточно простым синтаксисом может читаться как последовательные инструкции, близкие по уровню к ассемблерному коду. В любой момент можно сделать cc -S и увидеть ассемблерный листинг для того, чтобы убедиться, что сгенерированный код максимально соответствует задуманному.

              Можете ли вы гарантировать, что вышеприведенный код не обращается к памяти за пределами списка или не выкидывается компилятором (как, например, memset в конце блока) по какой-либо причине? Можете ли вы без подготовки изобразить на псевдо-ассемблере как должен выглядеть листинг вот этой строчки?

              visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());


              Я не имею целью сказать, что автор статьи выбрал не тот инструмент. Проблема в том, что культура писать абстракционный негарантируемый код для микроконтроллеров прививается студентам.
                +2
                Дело в том, что метопрограммирование оно предполагает, что вы пишите код не для микроконтроллера, а для компилятора. Т.е. Весь этот код, ну кроме функций Toggle() был написан для компилятора, который преобразовал этот код в последовательные вызовы Toggle() каждого светодиода.
                Вот так
                /Этот вызов 
                LedsContainer::ToggleAll() ;
                //Преобразуется в эти 4 вызова:
                Pin<PortС, 9>().Toggle() ;
                Pin<PortС, 8>().Toggle() ;
                Pin<PortC, 5>().Toggle() ;
                Pin<PortA, 5>().Toggle() ;
                //А поскольку у нас метод Toggle() inline, то в это:
                 *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 9) ;
                 *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 8) ;
                 *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 5) ;
                 *reinterpret_cast<std::uint32_t*>(0x40020014 ) ^= (1 << 5) ;


                и собственно поэтому и вопрос типа:
                Можете ли вы гарантировать, что вышеприведенный код не обращается к памяти за пределами списка или не выкидывается компилятором (как, например, memset в конце блока) по какой-либо причине? Можете ли вы без подготовки изобразить на псевдо-ассемблере как должен выглядеть листинг вот этой строчки?
                visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());


                смысла не имеет, потому что этого кода нет в коде микроконтроллера. Он был выполнен на этапе компиляции и стандарт гарантирует что никаких выходов за границу списка не будет, так как функции библиотечные.
                Вопрос тут в конечном счет в удобстве использования в дальнейшем… Теперь чтобы новый светодиод добавить нужно просто его добавить в список (в кортеж) в данном случае…
                Добавляем новые светодиоды в список
                constexpr static auto records = std::make_tuple (
                                                                   Pin<PortA, 5>{},
                                                                   Pin<PortC, 5>{},
                                                                   Pin<PortC, 8>{},
                                                                   Pin<PortC, 9>{},
                                                                   Pin<PortC, 2>{},
                                                                   Pin<PortC, 6>{}
                    


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

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

                    0

                    Пардон, не массив конечно, а список. С телефона даже поправить теперь не могу...

                  –1
                  Я, возможно, чего-то не понимаю, но разве не должен код, максимально приближенный к железу быть максимально понятен и упрощен для надежности?

                  Для какого рода надёжности? Для гарантии отсутствия аллокаций? Для гарантии максимальной глубины рекурсии и потребного стека? Для гарантии соответствия семантики программы требуемой спецификации?


                  Что характерно, ответ во всех этих случаях «нет», кстати.

                  +2
                  В инклудах например на stm32 все без исключения регистры, в т.ч. и GPIO, объявлены как volatile. У вас — нет. Как следствие, вы гордитесь вот этим: вот этим
                  хотя вас должно это настораживать. Включите чуть более сильную оптимизацию, оптимизацию всей программы целиком (не знаю как в IAR, я про -flto в GCC) и компилятор может вам полностью убрать ваш код, потому что нет volatile.
                    +1
                    Да согласен… В этом и есть причина оптимизации, в реальности код получится один в Си и С++. Хорошее замечание.
                    +1
                    Ух ты, а неплохо С++ продвинулся на микроконтроллеры… По сравнению с тем, как на Embed переключение пина из In в Out занимало 600+ тактов :)
                      0
                      600 тактов это не проблема С++, это проблема рукожопа, который написал библиотеку. На С++11 можно так же писать вполне хороший код, который не будет уступать по результату привычному С.

                      Тут скорее не С++ продвинулся в развитии, а культура писать на нем для МК развилась. Еще 5 лет назад за плюсы в реал-таймовых железках создателя бы обмазали фекалиями, а сейчас это уже норма.

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

                        Именно. Остаётся надеяться на улучшение качества кода анмасс, но это невозможно без более жёсткой специализации в отрасли. Что конечно не лучшим образом скажется на сроках, стоимости и краткосрочном качестве изделий. Так что остаётся только с интересом наблюдать за процессом.э, т.к. лично я для себя вижу мало шансов пересесть с "деревенского Си" на подобные чудеса.

                          +1

                          И кресты тоже продвинулись в embed. В 17 году у iar даже c++14 отсутствовал. А gcc-only код традиционно (и, увы, оправдано) не любят в продакшене

                            0
                            Боюсь, iar-only код не любят ещё больше, у них довольно много специфики. Для меня стандартом является возможность сборки проекта gcc и clang — я и мои коллеги должны иметь возможность выкачать исходники из git, поставить подходящий компилятор из открытых репозиториев и собрать его, позвав cmake.
                              0

                              Iar опция есть strict standard и код будет полностью соответствовать стандарту С++, благо за последние 4 года они сдели правильные шаги и даже получили сертификат на соо вествие стандарту надежности. Т. Е. можно быть уверенным, что std библиотеки, да и вообще компилятор полгость следует стандарту и ошибок там не много. Чего не скажешь про gcc. Поэтому его в продакшене и недолюбливают. А вот GreenHills и IAR юзают вплоть до космоса и военки.

                        +1
                        Замечательная статья.
                        Мне показалось интересным и возможно более наглядным решение с использованием шаблонной специализации функции. Не претендуя на минимум памяти и скурпулезность, привожу свой вариант:
                        template<std::uint32_t, std::uint32_t> class Led
                        {
                        public:
                        	void Toggle();
                        };
                        template<std::uint32_t addr, std::uint32_t bitNum > void Led<addr, bitNum>::Toggle() {
                        	*reinterpret_cast<std::uint32_t*>(addr + 20) ^= (1 << bitNum);
                        }
                        int main()
                        {
                        	Led<GpioaBaseAddr, 5> Led1;
                        	Led<GpiocBaseAddr, 5> Led2;
                        	Led<GpiocBaseAddr, 8> Led3;
                        	Led<GpiocBaseAddr, 9> Led4;
                        	for (;;) {
                        		Led1.Toggle();
                        		Led2.Toggle();
                        		Led3.Toggle();
                        		Led4.Toggle();
                        		delay();
                        	}
                        }


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

                        class LedBase
                        {
                        public:
                        	void virtual Toggle() = 0;
                        };
                        
                        template<std::uint32_t, std::uint32_t> class Led : public LedBase
                        {
                        public:
                        	void virtual Toggle();
                        };
                        template<std::uint32_t addr, std::uint32_t bitNum > void Led<addr, bitNum>::Toggle() {
                        	*reinterpret_cast<std::uint32_t*>(addr + 20) ^= (1 << bitNum);
                        }
                        int main()
                        {
                        	............
                                LedBase* leds[] = { &Led1, &Led2, &Led3,  &Led4 };
                        	for (LedBase* led : leds) { led->Toggle(); }
                        }


                          0
                          Ну да, я как раз там писал:
                          Можно сделать виртуальный базовый класс, для всех Pin, но тогда появится таблица виртуальных функций и уделать выиграть студентов мне не удастся.

                          Проблема в том, что у нас Led это объекты разных классов, и хранить указатели разных классов в массиве нельзя, надо будет делать, как вы сделали базовый класс с виртуальной функцией, а вот в кортеже можно хранить объекты разных типов.

                          И задача была была памяти не кушать, чтобы студентов обыграть :)
                          Поэтому создавать объекты было неправильно… Можно было бы еще сделать вот так:
                          template <typename... Types>
                          class Container {};
                          
                          using Led1 = Pin<PortA, 5> ;
                          using Led2 = Pin<PortC, 5> ;
                          using Led3 = Pin<PortC, 8> ;
                          
                          class LedsController {
                            public:
                              __forceinline template<typename... args>
                               inline constexpr static void ToggleAll()    {
                                toggleAll(tLedsController()) ;
                              }
                            
                            private:       
                              using tLedsController = Container<Led1, Led2, Led3> ;    // вот тут делаем шаблонный тип с разными классами на входе
                            
                            __forceinline template<typename ...Args> 
                             constexpr inline void static toggleAll(Container<Args...> obj)  {
                                pass((Args::Toggle(), true)...) ;  // проходим по каждому типу в списке и вызываем у него Toggle()
                            }    
                            
                            __forceinline  template<typename... Args> 
                            inline constexpr static void pass(Args&&...) {}
                          } ;
                          
                          int main() { 
                            LedsController::ToggleAll() ;
                            return 0;
                          }

                          Это вырождается в ту же самую последовательность
                          
                          int main() {  
                            Led1::Toggle() ;
                            Led2::Toggle() ;
                            Led3::Toggle() ;
                            return 0;
                          }
                          
                            0
                            И это считается более читаемым, чем это:?

                            #define LEDS_COUNT (4)
                            
                            static lep32_led_rgb_a_t leds[LEDS_COUNT] = {
                                {{ LEP32_GPIOA,  9}, { LEP32_GPIOA, 10}, { LEP32_GPIOA, 11}, { LEP32_GPIOA, 12}},
                                {{ LEP32_GPIOA, 10}, { LEP32_GPIOA, 11}, { LEP32_GPIOA, 12}, { LEP32_GPIOA,  9}},
                                {{ LEP32_GPIOA, 11}, { LEP32_GPIOA, 12}, { LEP32_GPIOA,  9}, { LEP32_GPIOA, 10}},
                                {{ LEP32_GPIOA, 12}, { LEP32_GPIOA,  9}, { LEP32_GPIOA, 10}, { LEP32_GPIOA, 11}}
                            };
                            
                            void main(void) {
                                  LEP32_RCC->apb2enr |= LEP32_RCC_APB2ENR_PORTA_ENABLE |
                                                      LEP32_RCC_APB2ENR_PORTB_ENABLE |
                                                      LEP32_RCC_APB2ENR_PORTC_ENABLE; 
                                 // можно выкинуть в отдельную функцию
                            
                                u32 i;
                                for (i = 0; i < LEDS_COUNT; i++) {
                                // можно посворачивать в отдельные функции типа set red/blue
                                    lep32_gpio_setvcc((&leds[0])->vcc.io,   1 << (&leds[0])->vcc.pin); 
                                    lep32_gpio_setgnd((&leds[0])->red.io,   1 << (&leds[0])->red.pin);
                                    lep32_gpio_sethiz((&leds[0])->green.io, 1 << (&leds[0])->green.pin);
                                    lep32_gpio_sethiz((&leds[0])->blue.io,  1 << (&leds[0])->blue.pin);
                                }
                            }
                            
                              0
                              Я не знаю, мне и то и то понятно :), но точно будет занимать меньше кода в ОЗУ, так как никакого массива нет вообще в этом решении, и нет никаких временных переменных, типа i для обхода массива.

                              По ПЗУ вопрос, потому что в вашем коде цикл, скорее всего он сожрет столько же кода, а может чуть больше, сколько и разворачивание в последовательность вызовов Toggle()

                              Но вот с точки зрения удобства поддержки и расширения, возможно тоже все просто… потому что, все что надо будет добавить это новый светодиод/ножку (тип) в список:
                              using tLedsController = Container<Led1, Led2, Led3, Pin<PortC, 9>> ;

                              Хотя в вашем решении тоже только в массив надо добавить…

                              В общем основное преимущество тут: компактность кода ны выходе, при простоте и понятности использования… Один раз написал страшный код, который реализует распаковку списка и потом юзай очень просто и понятно…

                              int main() { 
                                LedsController::ToggleAll() ;
                                return 0;
                              }

                              Кроме того, и глобальных переменных тут нет и пользователь кода, не сможет сделать ничего плохого, так как кроме как ToggleAll() ему ничего недоступно, даже обратиться к элементу списка у него возможности нет, ну потому что его нет :). А в вашем коде, любой программист в любом месте может обратиться к любому элементу массива, а так как он не const у вас, то еще и поменять, а это уже нехорошо, потенциально небезопасный код.
                                0
                                Ну так и в моем коде массива на O3 не будет. И цикла не будет.
                                  0
                                  При оптимизации? цикл то есть
                                  for (i = 0; i < LEDS_COUNT; i++)
                                  куда он денется? Или я чего-то не понял?
                                    +1
                                    en.wikipedia.org/wiki/Loop_unrolling

                                    volatile int a = 0;
                                    
                                    void test(void)
                                    {
                                        int i;
                                        for (i = 0; i < 5; i++)
                                            a += 1;
                                    }


                                    -O0:
                                    a:
                                            .zero   4
                                    test():
                                            push    rbp
                                            mov     rbp, rsp
                                            mov     DWORD PTR [rbp-4], 0
                                    .L3:
                                            cmp     DWORD PTR [rbp-4], 4
                                            jg      .L4
                                            mov     eax, DWORD PTR a[rip]
                                            add     eax, 1
                                            mov     DWORD PTR a[rip], eax
                                            add     DWORD PTR [rbp-4], 1
                                            jmp     .L3
                                    .L4:
                                            nop
                                            pop     rbp
                                            ret


                                    -O3:
                                    test():
                                            mov     eax, DWORD PTR a[rip]
                                            add     eax, 1
                                            mov     DWORD PTR a[rip], eax
                                            mov     eax, DWORD PTR a[rip]
                                            add     eax, 1
                                            mov     DWORD PTR a[rip], eax
                                            mov     eax, DWORD PTR a[rip]
                                            add     eax, 1
                                            mov     DWORD PTR a[rip], eax
                                            mov     eax, DWORD PTR a[rip]
                                            add     eax, 1
                                            mov     DWORD PTR a[rip], eax
                                            mov     eax, DWORD PTR a[rip]
                                            add     eax, 1
                                            mov     DWORD PTR a[rip], eax
                                            ret
                                    a:
                                            .zero   4
                                      0
                                      Ок, понял, т.е при оптимизации. Но я там в статье писал, что при оптимизации все потуги приведутся к одному ассемблерному коду.
                                      P.S. Надо признаться, что на максимальной оптимизации этот код по размеру получается такой же как на Си и на моем решении. И все потуги программиста по улучшению кода сводятся к одному и тому же коду на ассемблере.

                                      Распаковка списка, делает это без оптимизации, иногда требуется без оптимизации программы поставлять для сертификации, правда в последнее время, если пользуешь сертифицированный по IEC 61508 компилятор, то оптимизацию разрешают (могу ошибся, но все равно не всю, насчет code motion не уверен, врать не буду).
                                    0
                                    А вы уверены, что `-O3` — это то, что надо в embedded пихать везде? И мне кажется, что мигание светодиодами это точно то место, где `-Os` и никак иначе.

                          Only users with full accounts can post comments. Log in, please.