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

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

В очередной раз таким студентам была дана задача поморгать 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

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


  1. 0xd34df00d
    23.06.2019 23:01

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


    1. lamerok Автор
      23.06.2019 23:12

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


      1. 0xd34df00d
        24.06.2019 00:08

        Тут нет рекурсии.

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


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


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

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


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

        Что-то типа


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

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


        1. lamerok Автор
          24.06.2019 09:36

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


  1. Alex_ME
    23.06.2019 23:52

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


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


    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) ;
      }
    } ;


    1. lamerok Автор
      24.06.2019 09:33

      Работает это дело примерно так:
      Чтобы было проще понять на пальцах, считайте, что 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 и Т не определен.


  1. Goron_Dekar
    24.06.2019 08:14

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


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


    1. lamerok Автор
      24.06.2019 08:46

      Инициализацию стека и векторов прерываний на С++ сделал, правда это больше на Си смахивает.

      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. evgeniy1294
      24.06.2019 12:51
      +1

      Встречал статьи, в том числе и на хабре, где программисты startup'ы писали на плюсах. Однако этот подход требует довольно глубокого понимания компилятора и линкера.
      А так да, инициализация и работа со стеком и таблицей прерываний на плюсах очень приятная.


      1. hhba
        24.06.2019 17:39

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


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


        1. 0xd34df00d
          24.06.2019 19:07

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


          1. hhba
            24.06.2019 23:07

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


  1. staticmain
    24.06.2019 10:01

    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>());


    Я не имею целью сказать, что автор статьи выбрал не тот инструмент. Проблема в том, что культура писать абстракционный негарантируемый код для микроконтроллеров прививается студентам.


    1. lamerok Автор
      24.06.2019 10:54

      Дело в том, что метопрограммирование оно предполагает, что вы пишите код не для микроконтроллера, а для компилятора. Т.е. Весь этот код, ну кроме функций 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) ;


      1. hhba
        24.06.2019 17:45

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


        1. hhba
          24.06.2019 17:51

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


    1. 0xd34df00d
      24.06.2019 19:09

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

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


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


  1. mpa4b
    24.06.2019 10:50
    +1

    В инклудах например на stm32 все без исключения регистры, в т.ч. и GPIO, объявлены как volatile. У вас — нет. Как следствие, вы гордитесь вот этим: вот этим
    хотя вас должно это настораживать. Включите чуть более сильную оптимизацию, оптимизацию всей программы целиком (не знаю как в IAR, я про -flto в GCC) и компилятор может вам полностью убрать ваш код, потому что нет volatile.


    1. lamerok Автор
      24.06.2019 10:56

      Да согласен… В этом и есть причина оптимизации, в реальности код получится один в Си и С++. Хорошее замечание.


  1. olekl
    24.06.2019 11:19

    Ух ты, а неплохо С++ продвинулся на микроконтроллеры… По сравнению с тем, как на Embed переключение пина из In в Out занимало 600+ тактов :)


    1. NordicEnergy
      24.06.2019 12:17

      600 тактов это не проблема С++, это проблема рукожопа, который написал библиотеку. На С++11 можно так же писать вполне хороший код, который не будет уступать по результату привычному С.

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

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


      1. hhba
        24.06.2019 17:50

        Но имхо на С++ гораздо проще написать плохой код, чем на С

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


      1. Goron_Dekar
        24.06.2019 19:28
        +1

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


        1. evgeniy1294
          25.06.2019 17:14

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


          1. lamerok Автор
            25.06.2019 17:28

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


  1. Orange11Sky
    26.06.2019 02:56

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

    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(); }
    }



    1. lamerok Автор
      27.06.2019 08:37

      Ну да, я как раз там писал:

      Можно сделать виртуальный базовый класс, для всех 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;
      }
      


      1. staticmain
        27.06.2019 12:45

        И это считается более читаемым, чем это:?

        #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);
            }
        }