Работа со списком Pinов, на С++ для микроконтроллеров (на примере CortexM)


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


    В прошлой статье я обещал написать о том, как можно работать со списком портов.
    Сразу скажу, что уже все было решено до меня аж в 2010 году, вот статья: Работа с портами ввода-вывода микроконтроллеров на Си++ . Человек написавший это в 2010 просто красавчик.


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


    В выше указанной статье работа со списками типов была сделана с помощью C++03, когда еще шаблоны имели фиксированное число параметров, а функции не могли быть constexpr выражениями. С тех пор С++ "немного изменился", поэтому давайте попробуем сделать тоже самое, но на С++17. Добро пожаловать под кат:


    Задача


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


    Собственно, то, что мы хотим сделать, можно показать кодом:


    using Pin1 = Pin<GPIOС, 1>;
    using Pin2 = Pin<GPIOB, 1>;
    using Pin3 = Pin<GPIOA, 1>;
    using Pin4 = Pin<GPIOC, 2>;
    using Pin5 = Pin<GPIOA, 3>;
    
    int main()
    {
       // Хотим чтобы все Pinы установились в три действия:     
       // В порт GPIOA установилось 10 GPIOA->BSRR = 10 ;  // (1<<1) | (1 << 3) ;
       // В порт GPIOB установилось 2 GPIOB->BSRR = 2 ;  // (1 << 1)
       // В порт GPIOC установилось 6 GPIOB->BSRR = 6 ; // (1 << 1) | (1 << 2); 
        PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; 
    
        return 0;
    }

    Про регистр BSRR

    Для тех, кто не в курсе микроконтроллерных дел, GPIOA->BSRR регистр отвечает за атомарную установку или сброс значений на ножках микроконтроллера. Этот регистр 32 битный. Первые 16 бит отвечают за установку 1 на ножках, вторые 16 бит за установку 0 на ножках.


    Например, для того, чтобы установить ножку номер 3 в 1, нужно в регистре BSRR установить третий бит в 1. Чтобы сбросить ножку номер 3 в 0 нужно в этом же регистре BSRRустановить 19 бит в 1.


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



    Ну или другими словами:


    Чтобы компилятор сделал за нас:


    • проверку, что список содержит только уникальные Pin
    • создание списка портов, определив на каких портах находятся Pin,
    • вычисление значение, которое нужно поставить в каждый порт

    А затем программа


    • установила это значение

    И сделать это нужно максимально эффективно, чтобы даже без оптимизации код был минимальным. Собственно это вся задача.


    Начнем с первого пунктика: Проверка того, что список содержит уникальные Pin.


    Проверка списка на уникальность


    Напомню, у нас есть список Pinов:


    PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ;

    Нечаянно можно сделать так:


    PinsPack<Pin1, Pin2, Pin3, Pin4, Pin1> ; // Два раза в списке Pin1

    Хотелось бы, чтобы компилятор отловил такую оплошность и сообщил об этом пианисту.


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


    • Из исходного списка создадим новый список без дубликатов,
    • Если тип исходного списка и тип списка без дубликатов не совпадают, то значит в исходном списке были одинаковые Pin и программист ошибся.
    • Если совпадают, то все хорошо, дубликатов нет.

    Для формирования нового списка без дубликатов, коллега посоветовал не изобретать велосипед и воспользоваться подходом из библиотеки Loki. У него я этот подход и спер. Почти то же самое что и в 2010 году, но с переменным числом параметров.


    Код который позаимствован у коллеги, который позаимствовал идею из Loki
    namespace PinHelper
    {
     template<typename ... Types> struct Collection  { }; 
    
     ///////////////// Заимствуем идею NoDuplicates из библиотеки LOKI ////////////////
    template<class X, class Y> struct Glue;
    template<class T, class... Ts> 
    struct Glue<T, Collection<Ts...>> {
        using Result = Collection<T, Ts...>; };
    
    template<class Q, class X> struct Erase;
    
    template<class Q>
    struct Erase<Q, Collection<>> {
       using Result = Collection<>;};
    
    template<class Q, class... Tail>
    struct Erase<Q, Collection<Q, Tail...>> {
       using Result = Collection<Tail...>;};
    
    template<class Q, class T, class... Tail>
    struct Erase<Q, Collection<T, Tail...>> {
       using Result = typename Glue<T, typename Erase<Q, Collection<Tail...>>::Result>::Result;};
    
    template <class X> struct NoDuplicates;
    
    template <> struct NoDuplicates<Collection<>>
    {
        using Result = Collection<>;
    };
    
    template <class T, class... Tail>
    struct NoDuplicates< Collection<T, Tail...> >
    {
    private:
        using L1 = typename NoDuplicates<Collection<Tail...>>::Result;
        using L2 = typename Erase<T,L1>::Result;
    public:
        using Result = typename Glue<T, L2>::Result;
    };
    ///////////////// LOKI ////////////////
    }

    Как теперь можно этим пользоваться? Да очень просто:


    using Pin1 = Pin<GPIOC, 1>;
    using Pin2 = Pin<GPIOB, 1>;
    using Pin3 = Pin<GPIOA, 1>;
    using Pin4 = Pin<GPIOC, 2>;
    using Pin5 = Pin<GPIOA, 3>;
    using Pin6 = Pin<GPIOC, 1>;
    
    int main() {
        //Два раза Pin1 в списке, да еще и Pin6 имеет тот же самый тип
        using PinList = Collection<Pin1, Pin2, Pin3, Pin4, Pin1, Pin6> ; 
        using  TPins =  typename NoDuplicates<PinList>::Result;
        // сработает static_assert. Так как  будут сравниваться два типа списков
        // начальный:        Collection<Pin1, Pin2, Pin3, Pin4, Pin1, Pin6>
        // и без дубликатов: Collection<Pin1, Pin2, Pin3, Pin4>
        // очевидно, что типы разные
        static_assert(std::is_same<TPins, PinList>::value, 
                      "Беда: Одинаковые пины в списке") ;    
        return 0;
    }

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


    Кстати, для уверенности в правильности списка пинов для портов можно использовать следующий подход:
    // Сгенерируем список пинов для портов с типом
    // PinsPack<Port<GPIOB, 0>, Port<GPIOB, 1> ... Port<GPIOB, 15>>
    using GpiobPort = typename GeneratePins<15, GPIOB>::type
    // Тоже самое для порта А
    using GpioaPort = typename GeneratePins<15, GPIOA>::type
    
    int main() {
       //Обращаться к пину по номеру: Установить GPIOA.0 в 1
       Gpioa<0>::Set() ; 
       //Установить GPIOB.1 в 0
       Gpiob<1>::Clear() ;
    
       using LcdData = Collection<Gpioa<0>, Gpiob<6>, Gpiob<2>, Gpioa<3>, Gpioc<7>, Gpioa<4>, Gpioc<3>, Gpioc<10>> ;
       using TPinsLcd =  typename NoDuplicates<LcdData>::Result;
       static_assert(std::is_same<TPinsB, LcdData>::value, "Беда: Одинаковые пины в списке для шины данных LCD") ;
    
       //Пишем A в линию данных для индикатора
       LcdData::Write('A');      
    }

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


    int main()
    {
       return 0 ;
    }

    Давайте добавим немного кода и попробуем сделать метод Set() для установки пинов в списке.


    Метод установки Pinов в порте


    Забежим немного вперед в самый конец задачи. В конечном итоге необходимо реализовать метод Set(), который автоматически, на основании Pinов в списке, определял бы какие значения в какой порт нужно установить.


    Кодом, что мы хотим
    using Pin1 = Pin<GPIOA, 1>;
    using Pin2 = Pin<GPIOB, 2>;
    using Pin3 = Pin<GPIOA, 2>;
    using Pin4 = Pin<GPIOC, 1>;
    using Pin5 = Pin<GPIOA, 3>;
    
    int main()
    {         
        PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ;
       // Этот код должен преобразоваться в 3 линии кода
       // GPIOA->BSRR = 14 ; // (1<<1) | (1 << 2) | (1 << 3) ;
       // GPIOB->BSRR = 4 ;  // (1 << 2)
       // GPIOB->BSRR = 2 ;  // (1 << 1); 
    
    }

    Поэтому объявим класс, который будет содержать список Pinов, а в нем определим публичный статический метод Set().


    template <typename ...Ts>
    struct PinsPack 
    {
       using Pins = PinsPack<Ts...> ;
    public:
        __forceinline static void Set(std::size_t mask)
       {
       }   
    } ;

    Как видно, метод Set(size_t mask) принимает какое-то значение (маску). Эта маска есть число, которое нужно поставить в порты. По умолчанию она равна 0xffffffff, это означает, что мы хотим поставить все Pinы в списке (максимум 32). Если передать туда другое значение, например, 7 == 0b111, то установиться должны только первые 3 пина в списке и так далее. Т.е. маска накладываемая на список Pinов.


    Формирование списка портов


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


    Наши Pinы назначены на разные порты:


    using Pin1 = Pin<Port<GPIOA>, 1>;
    using Pin2 = Pin<Port<GPIOB>, 2>;
    using Pin3 = Pin<Port<GPIOA>, 2>;
    using Pin4 = Pin<Port<GPIOC>, 1>;
    using Pin5 = Pin<Port<GPIOA>, 3>;

    У этих 5 Pinoв всего 3 уникальных порта (GPIOA, GPIOB, GPIOC). Если мы объявим список PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>, то из него нужно получить список из трех портов Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>


    Класс Pin содержит в себе тип порта и в упрощенном виде выглядит так:


    template<typename Port, uint8_t pinNum>
    struct Pin 
    {
      using PortType = Port ;
      static constexpr uint32_t pin = pinNum ;
    ...
    }

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


    template <typename... Types>
    struct Collection{} ;

    Теперь определим список уникальных портов, а заодно проверим, что список пинов не содержит одинаковых пинов. Это сделать несложно :


    template <typename ...Ts>
    struct PinsPack 
    {
       using Pins = PinsPack<Ts...> ;
    private:
       // Формируем список пинов без дубликатов
       using  TPins =  typename NoDuplicates<Collection<Ts...>>::Result;
       // Проверяем совпадает ли исходный список пинов со списком без дубликатов
       static_assert(std::is_same<TPins, Collection<Ts...>>::value, 
                     "Беда: Одинаковые пины в списке") ;   
       // Формируем список уникальных портов
       using Ports = typename 
                         NoDuplicates<Collection<typename Ts::PortType...>>::Result;
    ...
    } ;

    Идем дальше...


    Обход списка портов


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


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


    Обходить будем "рекурсивно", пока в шаблоне еще есть параметры, будем вызвать функцию с этим же именем.


    template <typename ...Ts>
    struct PinsPack 
    {
       using Pins = PinsPack<Ts...> ;
    private:
      __forceinline template<typename Port, typename ...Ports>
      constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask)  
      {
        // Проверяем, что параметры шаблона еще не закончены
        if constexpr (sizeof ...(Ports) != 0U)
        {
          Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ;
        }
      }
    }

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


    __forceinline template<typename Port, typename ...Ports>
    constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask)    
    {
      // Получить значение маски для порта
      auto result = GetPortValue<Port>(mask) ; 
      // Установить в порт расчитанное значение
      Port::Set(result) ;
    
      if constexpr (sizeof ...(Ports) != 0U)
      {
        Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ;
      }
    }

    Этот метод будет выполняться в runtime, так как параметр mask передается в функцию из вне. А из-за того, что мы не можем гарантировать, что в метод SetPorts() будет передаваться константа, метод GetValue() тоже начнет выполняться во время исполнения.


    И хотя, в статье Работа с портами ввода-вывода микроконтроллеров на Си++ написано, что в подобном методе компилятор определил, что передалась константа и расчитал значение для записи в порт на этапе компиляции, мой компилятор сделал такой трюк только при максимальной оптимизации.
    А хотелось бы, чтобы GetValue() выполнялся во время компиляции при любых настройках компилятора.


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


    Чтобы обеспечить явную передачу константного значения сделаем дополнительный метод с передачей mask в шаблоне:


    __forceinline template<std::size_t mask, typename Port, typename ...Ports>
    constexpr static void SetPorts(Collection<Port, Ports...>)  
    {
      using MyPins = PinsPack<Ts...> ;
      // метод вызывается в compile time, так как значение value взято из шаблона
      constexpr auto result = GetPortValue<Port>(mask) ; 
      Port::Set(result) ;
    
      if constexpr (sizeof ...(Ports) != 0U)  
      {
        MyPins::template SetPorts<mask,Ports...>(Collection<Ports...>()) ;
      }
    }

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


    Расчет значения, которое необходимо установить в порт


    У нас есть список портов, который мы получили из списка Pinов, для нашего примера это список: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>.
    Нужно взять элемент этого списка, например, порт GPIOA, затем в списке Pinов найти все Pinы, которые привязаны к этому порту и рассчитать значение для установки в порт. А затем тоже самое сделать со следующим портом.


    Еще раз: В нашем случае список Pinов, из которых нужно получить список уникальных портов такой:
    using Pin1 = Pin<Port<GPIOC>, 1>;
    using Pin2 = Pin<Port<GPIOB>, 1>;
    using Pin3 = Pin<Port<GPIOA>, 1>;
    using Pin4 = Pin<Port<GPIOC>, 2>;
    using Pin5 = Pin<Port<GPIOA>, 3>;
    
    using Pins = PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ;

    Значит для порта GPIOA значение должно (1 << 1 ) | (1 << 3) = 10, а для порта GPIOC — (1 << 1) | (1 << 2) = 6, а для GPIOB (1 << 1 ) = 2


    Функция для расчета принимает запрашиваемый порт и если Pin находится на том же порту, что и запрашиваемый порт, то она должна установить в маске бит, соответствующий позиции этого Pina в списке, единицу (1).
    На словах объяснить не просто, лучше посмотрим сразу в код:


    template <typename ...Ts>
    struct PinsPack
    {
      using Pins = PinsPack<Ts...> ;
    private:
      __forceinline template<class QueryPort>
      constexpr static auto GetPortValue(std::size_t mask) 
      {
        std::size_t result = 0;  
        // Для того, чтобы узнать какая будет маска нужно
        // 1. Проверить, что порт пина и запрашиваемый порт совпадают
        // 2. Если совпадают взять нулевой бит маски и установить его в результирующее 
        // значениe (т.е по номеру пина на порте), например, если Pin с индексом 0 в 
        // списке пинов висит на выводе порта номер 10, то для в результирующее значение 
        // для порта нужно установить(через ИЛИ) значение (1 << 10) и так далее
        // 3. Сдвинуть маску на 1 в право
        // 4. Повторить шаги 1-3 для остальных пинов в списке
        pass{(result |= ((std::is_same<QueryPort, typename Ts::PortType>::value ? 1 : 0) & 
                                                     mask) * (1 << Ts::pin), mask >>= 1)...} ;
        return result;
      }
    } ;      

    Установка рассчитанного для каждого порта значения в порты


    Теперь мы знаем значение, которое нужно установить в каждом порту. Осталось доделать публичный метод Set(), который будет виден пользователю, чтобы все это хозяйство вызвалось:


    template <typename ...Ts>
    struct PinsPack
    {
      using Pins = PinsPack<Ts...> ; 
    
      __forceinline static void Set(std::size_t mask)
      {
        // Передаем список портов и маску для установки
        SetPorts(Ports(), mask) ;
      }
    }

    Как и в случае с SetPorts() сделаем дополнительный шаблонный метод, чтобы гарантировать передачу mask как константы, передав её в атрибуте шаблона.


    template <typename ...Ts>
    struct PinsPack
    {
      using Pins = PinsPack<Ts...> ;
      // Значение по умолчанию 0xffffffff, чтобы можно было одновременно устанавливать 32 пина
      __forceinline template<std::size_t mask =  0xffffffffU>
      static void Set()
      {
        SetPorts<mask>(Ports()) ;
      }
    }
    

    В финальном виде наш класс для списка Pinов будет выглядеть следующим образом:
    using namespace PinHelper ;
    
    template <typename ...Ts>
    struct PinsPack
    {
       using Pins = PinsPack<Ts...> ;
    
     private:
    
       using  TPins =  typename NoDuplicates<Collection<Ts...>>::Result;
       static_assert(std::is_same<TPins, Collection<Ts...>>::value, 
                     "Беда: Одинаковые пины в списке") ;   
       using Ports = typename 
                         NoDuplicates<Collection<typename Ts::PortType...>>::Result;
    
       template<class Q>
       constexpr static auto GetPortValue(std::size_t mask) 
       {
         std::size_t result = 0;  
         auto rmask = mask ;
         pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & mask) * (1 << Ts::pin), mask>>=1)...};
         pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & ~rmask) * ((1 << Ts::pin) << 16), rmask>>=1)...};
         return result;
       }      
    
       __forceinline template<typename Port, typename ...Ports>
       constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask)
       {
         auto result = GetPortValue<Port>(mask) ;
         Port::Set(result & 0xff) ;
    
         if constexpr (sizeof ...(Ports) != 0U)
         {
           Pins::template SetPorts<Ports...>(Collection<Ports...>(), mask) ;
         }
       }
    
       __forceinline template<std::size_t mask, typename Port, typename ...Ports>
       constexpr static void SetPorts(Collection<Port, Ports...>)
       {
         constexpr auto result = GetPortValue<Port>(mask) ;
         Port::Set(result & 0xff) ;
    
         if constexpr (sizeof ...(Ports) != 0U)
         {
           Pins::template SetPorts<mask, Ports...>(Collection<Ports...>()) ;
         }
       }
    
       __forceinline template<typename Port, typename ...Ports>
       constexpr static void WritePorts(Collection<Port, Ports...>, std::size_t mask)
       {
         auto result = GetPortValue<Port>(mask) ;
         Port::Set(result) ;
    
         if constexpr (sizeof ...(Ports) != 0U)
         {
           Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ;
         }
       }
    
        __forceinline template<std::size_t mask, typename Port, typename ...Ports>
       constexpr static void WritePorts(Collection<Port, Ports...>)
       {
         Port::Set(GetPortValue<Port>(mask)) ;
    
         if constexpr (sizeof ...(Ports) != 0U)
         {
           Pins::template WritePorts<mask, Ports...>(Collection<Ports...>()) ;
         }
       }
    
    public:
        static constexpr size_t size = sizeof ...(Ts) + 1U ;
    
       __forceinline static void Set(std::size_t mask  )
       {
         SetPorts(Ports(), mask) ;
       }
    
       __forceinline template<std::size_t mask =  0xffffffffU>
       static void Set()
       {
         SetPorts<mask>(Ports()) ;
       }
    
        __forceinline static void Write(std::size_t mask)
       {
         WritePorts(Ports(), mask) ;
       }
    
       __forceinline template<std::size_t mask =  0xffffffffU>
       static void Write()
       {
         WritePorts<mask>(Ports()) ;
       }
    
    } ;
    

    В результате, всем этим дело можно воспользоваться следующим образом:


    using Pin1 = Pin<GPIOC, 1>;
    using Pin2 = Pin<GPIOB, 1>;
    using Pin3 = Pin<GPIOA, 1>;
    using Pin4 = Pin<GPIOC, 2>;
    using Pin5 = Pin<GPIOA, 3>;
    using Pin6 = Pin<GPIOA, 5>;
    using Pin7 = Pin<GPIOC, 7>;
    using Pin8 = Pin<GPIOA, 3>;
    
    int main() 
    {
        //1. Этот вызов развернется, как и планировалось в 3 строки, эквивалентные псевдокоду:
        // GPIOA->BSRR = (1 << 1) | (1 << 3) 
        // GPIOB->BSRR = (1 << 1) 
        // GPIOC->BSRR = (1 << 1) | (1 << 2) 
        PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; // Вызвался метод Set<0xffffffffU>()  
    
        //2. Этот вызов развернется, в 3 строки, эквивалентные псевдокоду:
        // GPIOA->BSRR = (1 << 1) 
        // GPIOB->BSRR = (1 << 1) 
        // GPIOC->BSRR = (1 << 1) | (1 << 2)
        PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5, Pin6>::Set<7>() ;   
    
       //3. А это уже сгенерит немного кода и всяких шаблонных функций, 
       // так как someRunTimeValue не известно на этапе компиляции, то 
       // функция SetPorts перестает быть constexpr со всеми вытекающими
        PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set(someRunTimeValue) ;
    
        using LcdData =  PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5, Pin6, Pin7, Pin8> ;
        LcdData::Write('A') ;
    }

    Более полный пример, можно посмотреть тут:
    https://onlinegdb.com/r1eoXQBRH


    Быстродействие


    Как вы помните мы хотели добиться, чтобы наш вызов преобразовался в 3 строки, в порт A установилось 10, в порт B — 2 и в порт С — 6


    using Pin1 = Pin<GPIOС, 1>;
    using Pin2 = Pin<GPIOB, 1>;
    using Pin3 = Pin<GPIOA, 1>;
    using Pin4 = Pin<GPIOC, 2>;
    using Pin5 = Pin<GPIOA, 3>;
    
    int main()
    {
       // Хотим чтобы все Pinы установились в три действия:     
       // В порт GPIOA установилось 10 GPIOA->BSRR = 10 ;  // (1<<1) | (1 << 3) ;
       // В порт GPIOB установилось 2 GPIOB->BSRR = 2 ;  // (1 << 1)
       // В порт GPIOC установилось 6 GPIOB->BSRR = 6 ; // (1 << 1) | (1 << 2); 
        PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; 
    
        return 0;
    }

    Давайте посмотрим, что у нас получилось при полностью отключенной оптимизации



    Я подкрасил зеленым значения портов и вызовы установок этих значений в порты. Видно, что все сделано так как мы задумывали, компилятор для кадого из портов подстчитал значение и просто вызвал функцию для установки этих значений в нужные порты.
    Если функции установки также сделать inline, то в конечном итоге получится один вызов записи значения в BSRR регистр для каждого порта.


    Собственно это всё. Кому интересно, код лежит тут.


    Пример лежит тут.


    https://onlinegdb.com/ByeA50wTS

    Похожие публикации

    Средняя зарплата в IT

    113 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 5 065 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 47

      0
      Чижов — голова и Александреску — голова.
        0
        А хорошо ли позволять программисту включать в PinsPack ноги от разных портов? Ведь не залезая внутрь реализации, создается ложное ощущение одновременности операции установки/сброса группы ног, а на самом деле компилятор ее размазывает в последовательность действий с портами, скрывая эту последовательность от программиста. Все таки, на мой взгляд, эмбеддер не должен настолько далеко дистанцироваться от железа, с которым он работает.
          0
          Это и есть HAL, когда мы представляем в виде одной сущности несколько более низкоуровневых, когда делаешь параллельную шину на МК где у каждого порта меньше выводов чем необходимая разрядность это прям существенно помогает. Эмбеддер должен думать о задаче по хорошему а не каждый раз вспоминать путь до неё. Таким образом будет более продуктивным. А оптимизации оставить уже на тот момент когда действительно видны проблемы.
            0
            Пример с параллельной шиной не очень удачный применительно к теме статьи, так как на шине обычно необходимо иметь возможность выставлять любое значение, а не только все 0 / все 1. Кстати, ради интереса, а с какими реальными устройствами Вы связываете однокристаллку по параллельной шине шириной более 16 бит?
              0
              Так можно собрать конструкцию которая будет выставлять необходимые биты в нужное значение (это не проблема). Порой бывает в stm32 что даже 16 бит собрать по одному порту это достаточно сложно — т.к. ноги заняты другими функциями.
                0
                Ну давайте попытаемся представить реальную ситуации, когда нам может потребоваться широкая 16-битная параллельная шина:
                1. Нам надо связаться с каким-то скоростным внешним устройством, скорость обмена настолько важна, что никакие SPI нас не устраивают
                2. Нам надо связаться с каким-то уникальным внешним внешним устройством, которое существует только с параллельным интерфейсом (да еще и шире байта), при этом скорость не критична
                В первом случае мы ради скорости все равно будем вынуждены использовать один порт, а не «склеивать» его из отдельных линий разных портов (в крайнем случае, если уложимся в быстродействие, можно разделить 16-битное слово на два байтовых полуслова, но явно не собирать его из россыпи отдельных бит). Если никакими ремапингами альтернативных функций мы не можем для параллельной шины выделить хотя бы два «непрерывных» байта, то придется выбрать контроллер с большим числом ног, так как собрав шину из отдельных битов разных портов мы сильно потеряем в скорости обмена.
                Во втором случае, действительно, можно набрать шину требуемой ширины из отдельных бит разных портов, но, положа руку на сердце, насколько часто в реальной практике встречается такая ситуация?
                  +1

                  В радиолюбительской практике достаточно часто. Причём вариант 2 преобладает.

                    0
                    Возможно, но по моему в любительской практике более распространен ардуиновский подход, а не использование шаблонной «магии» C++. Впрочем, буду рад, если я в этом ошибаюсь.
                0
                Так можно же, для этого метод Write есть:
                 __forceinline static void Write(std::size_t mask)
                   {
                     WritePorts(Ports(), mask) ;
                   }
                

                Вы связываете однокристаллку по параллельной шине шириной более 16 бит?

                Я ни с какими, студенты светодиодами моргают, там их 32 штуки :) я просто ради интереса.
              –1
              Согласен с no111u3, добавлю ещё, что даже когда вы работаете с регистрами, не все так очевидно.
              Если, к примеру, хотите поставить бит в порт через регистр ODR:
              GPIOA->ODR |= 0b010 ;
              Это выглядит как одна операция, но на самом деле здесь 3 операции, чтение, установка, запись.
                –1
                Ваш пример как раз подтверждает, что эмбеддер, хочешь — не хочешь, должен хорошо представлять архитектуру железа с которым работает. Для того, чтобы установить/сбросить бит (или набор бит) порта, в Cortex M существует регистр BSRR (о чем Вы сами же пишете в статье). Использовать для этого регистр ODR можно только если не знать архитектуру железа.

                Как пример неудачного излишнего абстрагирования от железа можно привести библиотеки Arduino, где дошли до того, что для установки значения одного пина затрачивается несколько десятков тактов процессора, зато программист«программист» изолирован от того, что GPIO существуют не сами по себе, а организованы в порты, каждый из которых имеет свой набор регистров. Я понимаю, что Вы, используя «магию» шаблонов C++, реализовали все то же самое гораздо изящней и без таких излишних накладных расходов, но все равно сильно сомневаюсь в том, что это действительно надо разработчику.
                  +2
                  Отчасти согласен, но тогда лучше писать на ассемблере. Ведь есть и другие регистры, в которых нет атомарного доступа, можно конечно перевести все установки битов на бит бендинг, но это очень специфичная фенечка для архитектуры.

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

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

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

                  Тоже самое можно сказать и про HAL, вот в новой статье STM32 fast start. Часть 2 Hello World на HAL, отладка в Atollic TrueSTUDIO же показано, что моргнул светодиодом за 1.54 КБ оперативы и 4.69 — это мне кажется крутовато.

                  Но опять же HALом же куча народа пользуется и не задумывается о том, что там вообще происходит.
                    +1
                    Так я тоже согласен почти со всеми Вашими аргументами из этого комментария. Давайте вернемся к моему первому замечанию:
                    А хорошо ли позволять программисту включать в PinsPack ноги от разных портов?
                    Я же вовсе не возражал против Вашего подхода в принципе, а только против неявного для программиста смешивания битов разных портов. Ведь если мы будем вынуждены для битов нескольких портов написать не одну, а две (ну пусть три) операции над разными битсетами, мы же не переломимся от непосильного труда, зато будет явно видно, что это не единовременная операция.

                    По поводу ассемблера: я застал времена, когда для программирования 8051 это было безальтернативно. Я ни в коем случае не хочу в них возвращаться (чур меня!). Но я не против посмотреть, что нагенерил компилятор в критичных местах, и, при необходимости, переписать их на инлайн ассемблере. Правда, уже затрудняюсь вспомнить, когда мне последний раз потребовались ассемблерные вставки, в основном его приходится только читать. Но это уже зависит от того, у кого какая стоит задача.
              +1
              Коллеги, а не кажется ли вам, что это ту мач?
              Вот пишете Вы под микроконтроллер, памяти в обрез, герцев мало, код хотите видеть простой, понятный и предсказуемый, так как хороший дебаггер в реальном времени это не про нас.
              И вот вместо приблизительно такого кода:
              GPIO_BSRR_Write(GPIOA, (1<<1) | (1 << 3));
              GPIO_BSRR_Write(GPIOB, (1 << 1));
              GPIO_BSRR_Write(GPIOC, (1<<1) | (1 << 3));

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

              Куда то мы не туда сворачиваем…
                –2
                По моему это лютый оверкил. Вот нафига все эти пляски с бубнами. Чем все эти удобства помогут при написании например драйвера CAN шины? Или вы всё время с пинами работаете?
                  0

                  Ну не все же сразу :), я же для студентов, пока самое большое только Spi и драйвер для Еpaper… они делали и Modbus протокол. Пока времени нет описать все…

                    0
                    И что в modbus вам понадобилось пины пачками дёргать?
                    Всё таки для секаса с пинами есть vhdl и verilog, или встроенные аппаратные модули, а тут всё таки более высокоуровневые конструкции обычно.
                    Просто возмите любой поект и посмотрите на процент кода который работает непосредственно с портами.
                      +2
                      Ну с портами фронт работ большой (кнопки, светодиоды, реле, переключатели всякие, однопроводные протоколы, индикаторы с параллельными шинами), но вообще код, который напрямую с аппаратурой работает у меня составляет не более 10%, это же не значит, что аппаратуру не надо описывать как-то.
                      Я же не говорю, что надо бросаться делать так, это просто пример, что в принципе на С++17 это сделать уже проще, чем было 10 лет назад и доступно каждому, ведь не многие понимают как Loki работает, а с constexpr функция это намного проще, уже ближе к нормальному программированию.
                        –1
                        А вы не задумывались что проще сделать описание этой аппаратуры и преобразовывать в код скриптами (такой препроцессор), чем делать костыли на шаблонах и потом делать тоже самое, но только для C++17.
                          +2
                          А чем скрипт отличается от С++. Вот и есть тоже самое практически, считайте это скрипт, только сразу лежит с кодом рядом, и запускается одновременно с компиляцией программы.
                            0
                            Просто попытки превратить C++ в perl выглядят довольно забавно.
                            Но я так понял вы еще этого не осознали.
                  +2

                  У вас же памяти в обрез, какая ещё дебажная сборка?


                  А вообще тут возникает некоторый такой водораздел и потенциальный барьер, когда вы вместо того, чтобы доверять себе, обзаводитесь достаточно умным компилятором с достаточно умной системой типов и начинаете доверять ему. Правда, C++ «по эту сторону» барьера.

                    0
                    >>У вас же памяти в обрез, какая ещё дебажная сборка?
                    Если без дебаггера ни как — мы в таких случая деоптимизируем только функции выборочно.

                    Другой важный момент — производительность, функции на шаблонах и других расширенных возможностях компилятора очень хорошо оптимизируются, стэк схлопывается и все превращается в несколько инструкций, но стоит только эту функцию деоптимизировать и у вас может быть 70-80 кратный разрыв в производительности с ее оптимизированой версией. Если такая функция вызывается из прерывания и занимает 30 микросекунд — то после деоптимизации один только ее вызов превратится в 2 миллисекунды, а если их несколько…

                    >>вы вместо того, чтобы доверять себе, обзаводитесь достаточно умным компилятором с достаточно умной системой типов и начинаете доверять ему
                    Эх, это даже не смешно, тонны примеров из прошлого можно даже не приводить, есть свежачок :) Работаю на Мико32, есть GCC и тул чэйн и вот мы недавно выясняем что эти замечательные тулзы компилируют С++ с ошибками в случае виртуальных функций, если в С функции параметр 16 битный то стек едет, есть и другие косяки.
                    Увы, доверять это не про нас.
                      0
                      функции на шаблонах и других расширенных возможностях компилятора очень хорошо оптимизируются, стэк схлопывается и все превращается в несколько инструкций, но стоит только эту функцию деоптимизировать и у вас может быть 70-80 кратный разрыв в производительности с ее оптимизированой версией.

                      Функция что на шаблонах, что не на шаблонах оптимизируются одинаково, поэтому если он 70-80 кратный разрыв производительности на шаблонах, то такой же будет и на обычной функции.
                      Там другое, там кода генерится много, если оптимизацию не включить, потому что получается много разных функции. Но в итоге в прерывании все равно вызовется какая-то одна…

                      А вот проблема с быстродействием может случиться при использовании constexpr функций, когда она вдруг становится не такой и все начинает вызываться в run time.

                      >>вы вместо того, чтобы доверять себе, обзаводитесь достаточно умным компилятором с достаточно умной системой типов и начинаете доверять ему
                      Эх, это даже не смешно, тонны примеров из прошлого можно даже не приводить, есть свежачок :)

                      Для этого и надо использовать сертифицированный на безопасность компилятор, например IAR имеет такой сертификат
                        +3
                        Эх, это даже не смешно, тонны примеров из прошлого можно даже не приводить, есть свежачок :) Работаю на Мико32, есть GCC и тул чэйн и вот мы недавно выясняем что эти замечательные тулзы компилируют С++ с ошибками в случае виртуальных функций, если в С функции параметр 16 битный то стек едет, есть и другие косяки.

                        Так я ж сразу сказал, что C++ ещё по эту сторону. Впрочем, я почти уверен, что у вас там где-то UB, а не gcc кривой.


                        А вообще там, где нужна надёжность, на С точно писать не стоит. Как минимум, на чем-то формально верифицированном и потом экстрагировать.

                          –1
                          Не видя кода вы говорите, что вероятней всего есть UB? Как вы это делаете? :)
                          А если серьезно, я не говорил что крив сам GCC, кривой тулчейн, а конкретно backend под мико32.
                          Увеличение сложности системы (да, увеличение: больше кода — больше точек отказа, спросите ребят из бэк-енда современных веб технологий считают ли они что все сделано правильно?) должно быть оправдано.
                            0
                            Не видя кода вы говорите, что вероятней всего есть UB? Как вы это делаете? :)

                            По опыту работы с кодом на плюсах и на сях, увы. Я не видел ни одного проекта без проблем с объёмом больше 1000 строк кода. Просто иногда проекту пока ещё везёт.


                            Увеличение сложности системы (да, увеличение: больше кода — больше точек отказа, спросите ребят из бэк-енда современных веб технологий считают ли они что все сделано правильно?) должно быть оправдано.

                            Безусловно. Но ИМХО правильнее относиться к вещи из исходного поста как к библиотечному коду.


                            У вас же не вызывает удивления, что сложность сишного стека (из компилятора и вашего кода) куда больше сложности вашей программы, если бы вы писали её на языке ассемблера?

                              0
                              Мы всегда исходили из простого постулата — этот код/утилита сохранит нам время или заберет его?
                              Возьмем копилятор С++ и написание на асме, выгода на лицо. Поэтому за компилятор мы будем бороться до последнего :)
                              А вот эта библиотека какой выигрыш нам даст? Сколько мы ее будем интегрировать, отлаживать и какой выигрыш получим в конце? Если баланс сходится в плюс то используем, а если нет — то зачем?
                              Не поймите меня не правильно, я не против конкретно этого примера из статьи, но в масштабах индустрии вижу как целое поколение инженеров выбирает усложнение из соображений «это прикольно» нежели из соображений «это выгодно».
                              Недавний пример, пишем под код SoftCore NIOS II, один ведуший (!!) инженер решает завернуть все вызовы к APB регистрам в классы на шаблонах, вместо классических функций Write & Read уже протестированных, отлаженных и надежных как кувалда.
                              По итогу за каждую запись в регистр мы платили +22% (по сравнению с простой Write) в текстовом сегменте программы и -30% от производительности того же сишного кода и где то через пол года нашли баг в разыменовании 0 указателя, а через год разработки уткнулись в предел памяти. Все это нам стоило еще пары месяцев работы только чтоб все это разгрести, а выгода от С++ классов для доступа к регистрам была 0, я не шучу, все та же запись, но по другому выглядела.
                              Гениальность в простоте… но уходят десятилетия чтоб научиться эту простоту создавать.
                                0

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


                                Другое дело, если это отдельный проект и бизнес выделил на это средства, немного потратим сейчас, зато получим бонусы в будущем. В таком случае это имеет смысл.


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

                      0

                      Возможно, но если это написано один раз и оформлено в виде библиотеки, то почему нет… Чем каждый раз писать одно и тоже и иметь потенциальный риск ошибки, один раз написали такой "мостроузный" код, отладили и все, тем более, что по факту тут, особо ничего не делается.
                      Макросы же люди пишут, их отлаживать, например ещё труднее, и ловить в них ошибки, тут то проще все.

                      0
                      Set, Reset и Toggle — это востребованные операции для пинов, для списка пинов — это будут Write и Read, которых тут нет и реализовать которые значительно сложнее. Также нельзя объединять списки пинов с другими списками и пинами, лично я такое тоже использую достаточно часто.
                        0
                        Write то есть, onlinegdb.com/r1eoXQBRH, Read в примере нет, но тоже не сложный, а вот объединения списков нет действительно.
                          0

                          Во-первых, это не Write, в Write должны передаваться данные, а не просто маска. Маска — это константа, а данные обычно нет, их нельзя пропустить через constexpr функцию и получить на выходе константу, вместо этого будет генериться огромное количество кода… Во-вторых, у меня даже Write не работает. Например, пишем:


                          PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Write(7);

                          И на gcc 9.2 получаем одну инструкцию, хотя пины в списке для трех портов


                          ldr r3, [r3, #16] 

                          Ага, там вроде просто заглушки в некоторых местах, а на git Write нет…

                            0
                            Да похоже в Git забыл запушить…
                            А так Write работает и на GCC 9.2
                            gcc.godbolt.org/z/JpB_GU — строки 178-189 для двух Write(7) и Write('A')
                            Он правда оптимизировал их.
                            А вот в рантайме
                            gcc.godbolt.org/z/p7ryy6 — строки 263 — 331
                            Но тоже вроде работает…
                            Для расчет значения используется вот такая функция:
                             template<class Q>
                               constexpr static auto GetPortValue(std::size_t mask) 
                               {
                                 std::size_t result = 0;  
                                 auto rmask = mask ;
                                 //Для установки нужных битов
                                 pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0)  & 
                                     mask) * (1 << Ts::pin), mask>>=1)...};
                                 //Для сброса нужных битов
                                 pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & 
                                     ~rmask) * ((1 << Ts::pin) << 16), rmask>>=1)...};
                                 return result;
                               }      
                            


                            Если mask будет константа, то функция будет выполнена в компайл тайме, если нет, то в рам тайме, со всеми вытекающими.
                            Но для определения компайлтайма, и введена функция Write<7>();
                            А в рантайме использовать можно Write(7);
                              0

                              Возьмем простой пример:


                              for (uint32_t i = 0; i < 10; ++i)
                              {
                                  PinList<PA7, PA6, PA5, PA4, PA3, PA2, PA1, PA0>::write(GPIOA->IDR);
                              }

                              У компилятора есть все необходимые данные чтобы на этапе компиляции определить, что write() можно свести к:


                              GPIOA->BSRR = 0xFF'0000 | (GPIOA->IDR & 0xFF);

                              Или даже записи в половинку порта, что еще немного эффективнее… А во что это скомпилируется при использовании GetPortValue()?

                                0
                                Да будет не айс, но это из-за того, что по сути вызов
                                PinList<PA7, PA6, PA5, PA4, PA3, PA2, PA1, PA0>::write(GPIOA->IDR);

                                вырождается в вызов
                                PinList<PA7, PA6, PA5, PA4, PA3, PA2, PA1, PA0>::write(*reinterpert_cast<volatile uint32_t*>(IDR_ADDRESS));

                                А в компайлтайм компилятор С++ (по стандарту) reinterpret_cast делать не умеет. Ну т.е. по стандарту у компилятора нет необходимых данных. Отсюда все вытекающие проблемы.
                                  0

                                  Будет не айс, потому что такая реализация, из-за этого и Read/Write реализуются элементарно. Даже старенькая либа Чижова находила подобные последовательности пинов и генерила более эффективный код, хотя далеко не всегда… Если я, допустим, пишу класс для дисплея и хочу передавать туда все пины данных в виде списка пинов, то какой смысл это делать если даже для 8-ми подряд идущих пинов получим достаточно медленную реализацию? Сейчас у меня в подобной либе есть строка:


                                  PinList<Pins::RS, Pins::WR, Pins::Data>::write(data);

                                  Т.е. пишем 8 бит данных(PB15..8) и одновременно сбрасываем RS и WR, если все пины на одном порту, то получаем:


                                  ldr r2, [pc, #24]   
                                  ldr r3, [pc, #24]
                                  orr.w r0, r2, r0, lsl #8 
                                  str r0, [r3, #24]

                                  Если же все 10 пинов будут идти вразброс таким образом, что никаких закономерностей обнаружено не будет, тогда получится нечто похожее на результат после GetPortValue(), но это в самом худшем случае.

                                    0
                                    Если пины на одном порту и идут последовательно, то не париться и сразу маску накладывать, правильно я понял?
                                      +1

                                      Я уже принцип описывал. После сортировки пинов по портам ищутся последовательности пинов, их может быть много и пины не обязательно идут подряд. Допустим есть последовательность PA5, PB3, PA2, PB1, PA1, где самый правый пин проецируется на нулевой бит входных данных. Берем крайний PA1, разница между номером бита пина и данных равна 1 — 0 = 1, для PA2 она 2 — 2 = 0, а для PA5 получим 5 — 4 = 1. Для PA1 и PA5 разница одинаковая, значит можно два бита данных для этих пинов выделить маской, которую посчитать не проблема, сдвинуть на 1 влево, аналогичную операцию проделать для оставшегося PA2 и добавить маску очистки всех пинов данного порта. Это основа, опционально можно искать реверсные цепочки и т.д....

                                        0
                                        Ага понял, добавлю, спасибо.
                        +1

                        Наконец дошли руки прочитать саму статью, а не только комменты к ней строчить.


                        Про проверку на уникальность — ну вы ж сами пишете, 2019-й год, constexpr, все дела. Зачем вся эта ерунда с темплейтами и Loki не первой свежести, когда можно просто


                        template<typename...> struct Typelist {};
                        
                        constexpr bool all_unique(Typelist<>) { return true; }
                        
                        template<typename H, typename... T>
                        constexpr bool all_unique(Typelist<H, T...>)
                        {
                            return !(std::is_same_v<H, T> || ...) && all_unique(Typelist<T...> {});
                        }

                        Полный пример
                        #include <iostream>
                        #include <type_traits>
                        
                        template<typename...> struct Typelist {};
                        
                        struct Pin1 {};
                        struct Pin2 {};
                        struct Pin3 {};
                        struct Pin4 {};
                        struct Pin5 {};
                        
                        constexpr bool all_unique(Typelist<>) { return true; }
                        
                        template<typename H, typename... T>
                        constexpr bool all_unique(Typelist<H, T...>)
                        {
                            return !(std::is_same_v<H, T> || ...) && all_unique(Typelist<T...> {});
                        }
                        
                        int main()
                        {
                            static_assert(all_unique(Typelist<Pin1, Pin2, Pin3> {}));
                            static_assert(!all_unique(Typelist<Pin1, Pin2, Pin3, Pin2> {}));
                        }

                        Примерно аналогичный подход можно применить и для установки портов, правда, там исходная версия пахнет не 03-м годом, а примерно 14-м, поэтому профит не так заметен, но всё же:


                        __forceinline template<typename ...Ports>
                        constexpr static void SetPorts(Collection<Ports...>, std::size_t mask)    
                        {
                          (Ports::Set(GetPortValue<Ports>(mask)), ...);
                        }

                        (правда, этот код я уже не проверял, так как слишком много вспомогательных функций писать надо, но оно должно работать)


                        По моему опыту, использование folding expressions и constexpr сильно снижает время компиляции подобного кода, по крайней мере, на clang.

                          0

                          Спасибо, как говорится век живи век учись. В
                          IAR C++ 17 появился только 6 месяцев назад, полноценный, начиная с версии 8.40.2, поэтому опыта использования его было не много. С fold expression только только начал пользоваться и уже понял, что они существенно могут сократить гемор.
                          И момент такой, что хоть IAR и поддерживает синтаксис C++17, библиотечные функции в нем не все реализованы, надо проверить, есть ли там all_unique. Подозреваю, что нет.
                          Ещё раз спасибо, как всегда, очень полезное замечание.

                            +1

                            all_unique точно не библиотечная, но вон пишется в 6 строк.


                            А вам спасибо за статью!

                              0

                              Фу блин спутал с unique :) все понял ...

                            0

                            Я еще повнимательнее посмотрел, и подумал, что от дубликатов то все равно надо будет избавляться… Кроме проверки на уникальность, нужно формировать список портов, по которым бегать. (Не очень эффективно для компилятора, но зато кода не так много, так используется NoDuplicates из Loki)


                            // Формируем список пинов без дубликатов
                               using  TPins =  typename NoDuplicates<Collection<Ts...>>::Result;
                               // Проверяем совпадает ли исходный список пинов со списком без дубликатов
                               static_assert(std::is_same<TPins, Collection<Ts...>>::value, 
                                             "Беда: Одинаковые пины в списке") ;   
                               // Формируем список уникальных портов
                               using Ports = typename 
                                                 NoDuplicates<Collection<typename Ts::PortType...>>::Result;

                            Можно конечно его формировать по другому, просто идти и смотреть, что таких портов еще нет в списке добавляем, есть не добавляем — не через Loki, но почему бы уже готовым велосипедом не воспользоваться, тем более, что он используется для двух целей: Формирование списка уникальных портов и проверки списка пинов на уникальность


                            А вызов через fold expression для установки портов — замечательная идея, немного подправлю и если время будет новую статью забабахаю :)

                              0
                              Я еще повнимательнее посмотрел, и подумал, что от дубликатов то все равно надо будет избавляться…

                              Зачем? Если есть дубликаты, то срабатывает ассерт, и компиляция прекращается.

                                0
                                Зачем? Если есть дубликаты, то срабатывает ассерт, и компиляция прекращается.

                                Чтобы список портов сделать…
                                Вначале берем Pinы, и вытаскиваем из них все порты на которых они сидят
                                Например:


                                using Pin1 = Port<GPIOB, 3>;
                                using Pin2 = Port<GPIOB, 3>;
                                using Pin3 = Port<GPIOA, 3>;
                                
                                Typelist<Pin1, Pin2, Pin3> ;

                                из него получается список портов:


                                Typelist<GPIOB, GPIOB, GPIOA> ;

                                и его нужно сократить до:


                                Typelist<GPIOB, GPIOA> ;

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


                                using Ports = typename 
                                                     NoDuplicates<Collection<typename Ts::PortType...>>::Result ;

                                А заодно этот NoDuplicates еще использую для проверки того, что сам список Pinов не имеет дубликатов.

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

                          Самое читаемое