Работа со списком 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 Type, typename ...Ts> struct PinsPack  {    using Pins = PinsPack<Type,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


ссылка на оригинал статьи https://habr.com/ru/post/474732/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *