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


В прошлой статье я обещал написать о том, как можно работать со списком портов.
Сразу скажу, что уже все было решено до меня аж в 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

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


  1. GarryC
    20.12.2019 17:26

    Чижов — голова и Александреску — голова.


  1. Serge78rus
    20.12.2019 18:21

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


    1. no111u3
      20.12.2019 20:03
      +1

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


      1. Serge78rus
        21.12.2019 11:54

        Пример с параллельной шиной не очень удачный применительно к теме статьи, так как на шине обычно необходимо иметь возможность выставлять любое значение, а не только все 0 / все 1. Кстати, ради интереса, а с какими реальными устройствами Вы связываете однокристаллку по параллельной шине шириной более 16 бит?


        1. no111u3
          21.12.2019 14:37

          Так можно собрать конструкцию которая будет выставлять необходимые биты в нужное значение (это не проблема). Порой бывает в stm32 что даже 16 бит собрать по одному порту это достаточно сложно — т.к. ноги заняты другими функциями.


          1. Serge78rus
            21.12.2019 17:52

            Ну давайте попытаемся представить реальную ситуации, когда нам может потребоваться широкая 16-битная параллельная шина:

            1. Нам надо связаться с каким-то скоростным внешним устройством, скорость обмена настолько важна, что никакие SPI нас не устраивают
            2. Нам надо связаться с каким-то уникальным внешним внешним устройством, которое существует только с параллельным интерфейсом (да еще и шире байта), при этом скорость не критична
            В первом случае мы ради скорости все равно будем вынуждены использовать один порт, а не «склеивать» его из отдельных линий разных портов (в крайнем случае, если уложимся в быстродействие, можно разделить 16-битное слово на два байтовых полуслова, но явно не собирать его из россыпи отдельных бит). Если никакими ремапингами альтернативных функций мы не можем для параллельной шины выделить хотя бы два «непрерывных» байта, то придется выбрать контроллер с большим числом ног, так как собрав шину из отдельных битов разных портов мы сильно потеряем в скорости обмена.
            Во втором случае, действительно, можно набрать шину требуемой ширины из отдельных бит разных портов, но, положа руку на сердце, насколько часто в реальной практике встречается такая ситуация?


            1. no111u3
              21.12.2019 18:07
              +1

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


              1. Serge78rus
                21.12.2019 18:34

                Возможно, но по моему в любительской практике более распространен ардуиновский подход, а не использование шаблонной «магии» C++. Впрочем, буду рад, если я в этом ошибаюсь.


        1. lamerok Автор
          21.12.2019 16:36

          Так можно же, для этого метод Write есть:

           __forceinline static void Write(std::size_t mask)
             {
               WritePorts(Ports(), mask) ;
             }
          

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

          Я ни с какими, студенты светодиодами моргают, там их 32 штуки :) я просто ради интереса.


    1. lamerok Автор
      20.12.2019 20:16

      Согласен с no111u3, добавлю ещё, что даже когда вы работаете с регистрами, не все так очевидно.
      Если, к примеру, хотите поставить бит в порт через регистр ODR:
      GPIOA->ODR |= 0b010 ;
      Это выглядит как одна операция, но на самом деле здесь 3 операции, чтение, установка, запись.


      1. Serge78rus
        21.12.2019 11:40

        Ваш пример как раз подтверждает, что эмбеддер, хочешь — не хочешь, должен хорошо представлять архитектуру железа с которым работает. Для того, чтобы установить/сбросить бит (или набор бит) порта, в Cortex M существует регистр BSRR (о чем Вы сами же пишете в статье). Использовать для этого регистр ODR можно только если не знать архитектуру железа.

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


        1. lamerok Автор
          21.12.2019 15:03
          +1

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

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

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

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

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

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


          1. Serge78rus
            21.12.2019 18:18
            +1

            Так я тоже согласен почти со всеми Вашими аргументами из этого комментария. Давайте вернемся к моему первому замечанию:

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

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


  1. Lau
    20.12.2019 23:41

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

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

    Куда то мы не туда сворачиваем…


    1. kovserg
      21.12.2019 00:16
      -3

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


      1. lamerok Автор
        21.12.2019 05:23

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


        1. kovserg
          21.12.2019 15:06

          И что в modbus вам понадобилось пины пачками дёргать?
          Всё таки для секаса с пинами есть vhdl и verilog, или встроенные аппаратные модули, а тут всё таки более высокоуровневые конструкции обычно.
          Просто возмите любой поект и посмотрите на процент кода который работает непосредственно с портами.


          1. lamerok Автор
            21.12.2019 16:58
            +1

            Ну с портами фронт работ большой (кнопки, светодиоды, реле, переключатели всякие, однопроводные протоколы, индикаторы с параллельными шинами), но вообще код, который напрямую с аппаратурой работает у меня составляет не более 10%, это же не значит, что аппаратуру не надо описывать как-то.
            Я же не говорю, что надо бросаться делать так, это просто пример, что в принципе на С++17 это сделать уже проще, чем было 10 лет назад и доступно каждому, ведь не многие понимают как Loki работает, а с constexpr функция это намного проще, уже ближе к нормальному программированию.


            1. kovserg
              21.12.2019 17:22
              -1

              А вы не задумывались что проще сделать описание этой аппаратуры и преобразовывать в код скриптами (такой препроцессор), чем делать костыли на шаблонах и потом делать тоже самое, но только для C++17.


              1. lamerok Автор
                21.12.2019 17:41
                +2

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


                1. kovserg
                  22.12.2019 03:28

                  Просто попытки превратить C++ в perl выглядят довольно забавно.
                  Но я так понял вы еще этого не осознали.


    1. 0xd34df00d
      21.12.2019 05:10
      +1

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


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


      1. Lau
        21.12.2019 13:43

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

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

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


        1. lamerok Автор
          21.12.2019 15:14

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

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

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

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

          Для этого и надо использовать сертифицированный на безопасность компилятор, например IAR имеет такой сертификат


        1. 0xd34df00d
          21.12.2019 17:41
          +2

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

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


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


          1. Lau
            22.12.2019 02:11

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


            1. 0xd34df00d
              22.12.2019 02:25

              Не видя кода вы говорите, что вероятней всего есть UB? Как вы это делаете? :)

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


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

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


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


              1. Lau
                22.12.2019 17:41

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


                1. lamerok Автор
                  22.12.2019 18:38

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


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


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


    1. lamerok Автор
      21.12.2019 05:20

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


  1. Reflector
    21.12.2019 05:43


    1. lamerok Автор
      21.12.2019 05:45

      Write то есть, onlinegdb.com/r1eoXQBRH, Read в примере нет, но тоже не сложный, а вот объединения списков нет действительно.


      1. Reflector
        21.12.2019 11:43

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


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

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


        ldr r3, [r3, #16] 

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


        1. lamerok Автор
          21.12.2019 14:49

          Да похоже в 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);


          1. Reflector
            21.12.2019 15:48

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


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


            1. lamerok Автор
              21.12.2019 16:46

              Да будет не айс, но это из-за того, что по сути вызов

              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 делать не умеет. Ну т.е. по стандарту у компилятора нет необходимых данных. Отсюда все вытекающие проблемы.


              1. Reflector
                21.12.2019 17:08

                Будет не айс, потому что такая реализация, из-за этого и 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(), но это в самом худшем случае.


                1. lamerok Автор
                  21.12.2019 17:38

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


                  1. Reflector
                    21.12.2019 18:02
                    +1

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


                    1. lamerok Автор
                      21.12.2019 18:03

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