Всем доброго здравия!
В прошлой статье я обещал написать о том, как можно работать со списком портов.
Сразу скажу, что уже все было решено до меня аж в 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;
}
Для тех, кто не в курсе микроконтроллерных дел, 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 году, но с переменным числом параметров.
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ы, которые привязаны к этому порту и рассчитать значение для установки в порт. А затем тоже самое сделать со следующим портом.
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()) ;
}
}
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 регистр для каждого порта.
Собственно это всё. Кому интересно, код лежит тут.