Последнее время я сильно увлекся вопросом надежности софта для микроконтроллеров, 0xd34df00d посоветовал мне сильнодействующие препараты, но к сожалению руки пока не дошли до изучения Haskell и Ivory для микроконтроллеров, да и вообще до совершенно новых подходов к разработке ПО отличных от ООП. Я лишь начал очень медленно вкуривать функциональное программирование и формальные методы.
Все мои потуги в этих направлениях это, как было сказано в комментарии ради любви к технологиям, но есть подозрение, что сейчас никто не даст мне применять такие подходы (хотя, как говориться, поживем увидим). Уж больно специфические навыки должны быть у программиста, который все это дело будет поддерживать. Полагаю, что написав однажды программу на таком языке, моя контора будет долго искать человека, который сможет принять такой код, поэтому на практике для студентов и для работы я все еще по старинке использую С++.
Продолжу развивать тему о встроенном софте для небольших микроконтроллеров в устройствах для safety critical систем.
На этот раз попробую предложить способ работы с конкретными ножками микроконтроллера, используя обертку над регистрами, которую я описал в прошлой статье Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)
Чтобы было общее представление того о чем я хочу рассказать, приведу небольшой кусок кода:
using Led1Pin = Pin<Port<GPIOA>, 5U, PinWriteableConfigurable> ;
using Led2Pin = Pin<Port<GPIOC>, 5U, PinWriteableConfigurable> ;
using Led3Pin = Pin<Port<GPIOC>, 8U, PinWriteable> ;
using Led4Pin = Pin<Port<GPIOC>, 9U, PinWriteable> ;
using ButtonPin = Pin<Port<GPIOC>, 10U, PinReadable> ;
//Этот вызов развернется в 2 строчки
// GPIOA::BSRR::Set(32) ; // reinterpret_cast<volataile uint32_t *>(0x40020018) = 32U
// GPIOС::BSRR::Set(800) ; // reinterpret_cast<volataile uint32_t *>(0x40020818) = 800U
PinsPack<Led1Pin, Led2Pin, Led3Pin, Led4Pin>::Set() ;
//Ошибка компиляции, вывод к которому подключена кнопка настроен только на вход
ButtonPin::Set()
auto res = ButtonPin::Get() ;
Я решил разбить повествование на две статьи. На часть с мыслями по-поводу организации настройки портов и работы с Pinaми, и часть экспериментальную, описывающую объединение Pinов, которая, полагаю не будет иметь особо практического применения из-за сложности, но возможно будет интересна для того, чтобы показать насколько С++ может быть эффективным.
Введение
Как я уже говорил, я обучаю студентов разработке ПО для измерительных устройств. Работа в университете — это мое хобби, основное место работы с университетом не связано, но тоже коррелирует с разработкой встроенного софта, в том числе и для высоко-надежных систем.
На ранних стадиях преподавания я рассказывал студентов про CMSIS и автоматические системы первоначальной настройки микроконтроллера (типа Cube), но через некоторое время понял:
- во-первых, студенты не понимают откуда что в коде берется, как оно вообще там все работает, что реально происходит в микроконтроллере;
- во вторых, это вообще не подходит для того, чтобы разрабатывать встроенный софт для надежного промышленного применения;
- можно приписать еще и в третьих, что для того чтобы моргнуть светодиодом сгенерируется столько кода, что лет так 30 назад Билл Гейтс за это проклял бы разработчика, но на не думаю, что сейчас размер кода такая серьезная проблема для современных микроконтроллеров, поэтому она не считается.
Я не могу показывать студентам код, который мы используем на основной работе из-за DNA, поэтому решил использовать что-то самописное, что с одной стороны, позволит студентам показать все от самого низкого уровня (как обращаться к регистрам), а с другой стороны позволит писать бизнес логику, не задумываясь о внутренностях с наименьшим количеством ошибок.
Первым таким шагом, была обертка над регистрами. Логично, что вторым должны быть порты и пины. Поэтому с них и начнем.
Порт
Порт — это средство общения микроконтроллера с внешним миром.
Порт может работать как цифровой вход и цифровой выход, некоторые порты могут работать в аналоговом режиме, т.е. на них можно подавать аналоговый сигнал, который затем будет поступать на входы АЦП, а таже функционировать в альтернативном режиме (это когда порт работает в режиме какой-нибудь периферии, скажем UART, SPI или USB). Для упрощения аналоговый и альтернативные режимы рассматривать не будем. Рассмотрим только два режима цифровой вход и выход:
- В режиме цифрового выхода на порт можно вывести 0 или 1,
0 — соответствует низкому уровню напряжения (земле);
1 — высокому уровню (питанию). - В режиме цифрового входа порт считывает уровень напряжения на ножке.
0 — соответствует низкому значению;
1 — высокому.
Есть еще несколько настроек портов, такие как подтяжка к 0 или 1, для того чтобы порт не "висел" в воздухе и еще настройка типа выхода (c открытым колектором или двухтактный), но с точки зрения программирования нам эти вещи сейчас не интересны.
Давайте ограничимся простой абстракцией порта у которого есть методы Set()
— установка 1, Reset()
— установка 0, Get()
— чтение состояния порта, SetInput()
— установка в режим входа,SetOutput()
— установка в режим выхода. Ради экономии текста, опустим метод Reset()
.
Можно описать Port следующим классом:
Или, используя обертку над регистрами из прошлой статьи, кодом:
template <typename T>
struct Port
{
__forceinline static void Set(std::uint32_t value)
{
T::BSRR::Write(static_cast<typename T::BSRR::Type>(value)) ;
}
__forceinline static auto Get()
{
return T::IDR::Get() ;
}
__forceinline static void SetInput(std::uint32_t pinNum)
{
assert(pinNum <= 15U);
using ModerType = typename T::MODER::Type ;
static constexpr auto mask = T::MODER::FieldValues::Input::Mask ;
const ModerType offset = static_cast<ModerType>(pinNum * 2U) ;
auto value = T::MODER::Get() ; // получаем значение регистра MODER
value &= ~(mask << offset); // очищаем настройку для нужного порта
value |= (value << offset) ; // ставим новую настройку(На вход)
*reinterpret_cast<volatile Type *>(T::MODER::Address) = value ; //Записываем новое значение в регистр
}
//Здесь вариант с атомарной установкой значения...
__forceinline static void SetOutput(std::uint32_t pinNum)
{
assert(pinNum <= 15U);
using ModerType = typename T::MODER::Type ;
AtomicUtils<ModerType>::Set(
T::MODER::Address,
T::MODER::FieldValues::Output::Mask,
T::MODER::FieldValues::Output::Value,
static_cast<ModerType>(pinNum * uint8_t{2U})
) ;
}
} ;
Кому интересно, как сделан атомарный доступ см ниже:
Первоисточник: Атомарные операции в Cortex-M3
Команда LDREX загружает значение по указанному адресу в регистр и взводит специальный флаг процессора, сигнализирующий об эксклюзивном доступе к памяти.
STREX — проверяет не был-ли нарушен эксклюзивный доступ к памяти, если нет, то записывает значение из входного регистра по указанному адресу и сбрасывает флаг эксклюзивного доступа. При этом в выходном регистре будет записан ноль. Если между LDREX и STREX произошло прерывание и оно что-то записало в память (а оно обязательно хоть регистры, да сохранит в стек), то STREX ничего не запишет в память и в выходной регистре будет записана 1. Это значит, что значение в памяти могло изменится (а могло и нет) и нам надо снова перечитать его из памяти модифицировать и снова попытаться его сохранить. Естественно, чем меньше кода между LDREX и STREX, тем меньше вероятность, что там произойдёт прерывание и больше шансов обновить значение с первого раза.
template <typename T>
struct AtomicUtils
{
static void Set(T address, T mask, T value, T offset)
{
T oldRegValue ;
T newRegValue ;
do
{
oldRegValue = *reinterpret_cast<volatile T*>(address);
newRegValue = oldRegValue;
newRegValue &= ~(mask << (offset));
newRegValue |= (value << (offset));
} while (
!AtomicUtils<T>::TryToWrite(reinterpret_cast<volatile T *>(address),
oldRegValue,
newRegValue)
) ;
}
private:
static bool TryToWrite(volatile T* ptr, T oldValue, T newValue)
{
using namespace std ;
// читаем значение переменной и сравниваем со старым значением
if(__LDREX(ptr) == static_cast<uint32_t>(oldValue))
{
// пытаемся записать в переменную новое значение
return (__STREX(static_cast<uint32_t>(newValue), static_cast<volatile uint32_t*>(ptr)) == 0) ;
}
__CLREX();
return false ;
}
};
Все сделали класс и забыли о нем. Его вообще не надо использовать на уровне бизнес логики, так как он совсем не безопасный, он служит чисто как обертка над регистрами, которые отвечают за работу с портом микроконтроллера. Вообще все его методы должны быть приватными (как показано в дизайне), чтобы у программиста уровня приложения не было соблазна использовать эти методы и не накликать беду. Я сейчас не будут делать эти методы приватными и специально добавлять друзей, дабы не загромождать итак уже довольно большой код, но посыл понятен. Переходим к Pinам
Pin
Конкретный Pin порта мы хотим сделать безопасным и процесоро-независимым. Нужно запретить делать пользователю, то чего ему делать не положено и отвязать его от микропроцессора.
Итак, конкретный Pin должен иметь связь с портом, номер пина и Get()
и Set()
методы. Эти два метода актуальны для любого микроконтроллера. Так же как, скорее всего, для любого микроконтроллера актуальны методы настройки Pin на вход или выход. А вот перевод в альтернативный режим или в аналоговый не всегда поддерживается микроконтроллерами, поэтому, чтобы не зависеть от микроконтроллеров не будем добавлять эту возможность, ниже я поясню, этот момент. Наша абстракция Pin для любого микроконтроллера будет выглядеть следующим образом:
Можно сделать обычный класс, можно сделать полностью статический. Чтобы не создавать отдельно объекты класс Pin
здесь я сделаю статический класс, но ничего не запрещает сделать это обычным классом. В общем это уже дело реализации, остановлюсь на статическом классе, мне кажется он проще и кода меньше.
template<typename Port, uint8_t pinNum>
struct Pin
{
using PortType = Port ;
static constexpr uint32_t pin = pinNum ;
static void Set()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::Set(1U << pinNum) ;
}
static void Reset()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::Reset(1U << (pinNum)) << 16) ;
}
static auto Get()
{
return Port::Get() ;
}
static void SetInput()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::SetInput(pinNum);
}
static void SetOutput()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::SetOutput(pinNum);
}
} ;
Сразу же добавили частичку статической проверки: пинов на порту у нас 16, поэтому пользователю уже не позволено передавать значение больше 15. Внимательный читатель заметит, что не у всех микроконтроллеров 15 пинов на одном порту. Да, можно этот параметр задавать в парметре шаблона, а можно просто константой. Здесь, я хотел показать, что у нас уже есть возможность запретить пользователю сделать неправильные вещи на уровне типа. По сути мы объявили тип Pin, который не может принимать значение номера Pina больше 15.
Использовать класс можно так:
using Led1 = Pin<Port<GPIOA>, 5U> ;
using Led4 = Pin<Port<GPIOC>, 9U> ;
Led1::Set() ;
Led4::Set() ;
Теперь немного отойду от темы и затрону вопрос как можно настраивать аппаратную часть для устройства.
Немного о настройке аппаратной части микроконтроллера
Пользователь Vadimatorikda в статье Пять лет использования C++ под проекты для микроконтроллеров в продакшене поделился минусами использования С++ для своих проектов. В том числе описал и проблемы с которыми встречался я. С моей точки зрения он сделал очень правильные вывод:
использование «универсальных конструкторов модулей» лишь без надобности усложняет программу. Куда проще оказывается поправить регистры конфигурации под новый проект, чем копаться в связях между объектами, а потом еще и в библиотеке HAL-а;
Проекты в которых я работал всегда имеют специальные спецификации, которые описывают все настройки микроконтроллера и всей его периферии. Эти настройки в большинстве случаев не меняются в течении работы и поэтому нет смысла засовывать во все классы возможность настройки пинов, портов, системы тактирования и так далее… Логичнее сделать классы простыми и только с той функциональностью, которая действительно нужна для работы. Это избавит от необходимости лазить по разным местам и искать где, что настраивается и перенастраиваться.
Обычно для настройки периферии я использую функцию __low_level_int() это IAR встроенная функция, которая вызывается еще до инициализации всех переменных и объектов. Т.е. можно быть уверенным, что до того как объекты будут инициализированы, вся необходимая периферия микроконтроллера уже будет настроена и можно смело вызывать методы объектов или статических классов.
extern "C"
{
int __low_level_init(void)
{
//Switch on external 16 MHz oscillator
RCC::CR::HSEON::Enable::Set() ;
while (!RCC::CR::HSERDY::Enable::IsSet())
{
}
//Switch system clock on external oscillator
RCC::CFGR::SW::Hse::Set() ;
while (!RCC::CFGR::SWS::Hse::IsSet())
{
}
//Switch on clock on PortA and PortC, PortB
RCC::AHB1ENRPack<
RCC::AHB1ENR::GPIOCEN::Enable,
RCC::AHB1ENR::GPIOAEN::Enable,
RCC::AHB1ENR::GPIOBEN::Enable
>::Set() ;
RCC::APB1ENRPack<
RCC::APB1ENR::TIM5EN::Enable,
RCC::APB1ENR::SPI2EN::Enable
>::Set() ;
// LED1 on PortA.5, set PortA.5 as output
GPIOA::MODER::MODER5::Output::Set() ;
// PortB.13 - SPI3_CLK, PortB.15 - SPI2_MOSI, PB1 -CS, PB2- DC, PB8 -Reset
GPIOB::MODERPack<
GPIOB::MODER::MODER1::Output, //CS
GPIOB::MODER::MODER2::Output, //DC
GPIOB::MODER::MODER8::Output, //Reset
GPIOB::MODER::MODER9::Output, //Busy
GPIOB::MODER::MODER13::Alternate, //CLK
GPIOB::MODER::MODER15::Alternate, //MOSI
>::Set() ;
GPIOB::AFRHPack<
GPIOB::AFRH::AFRH13::Af5,
GPIOB::AFRH::AFRH15::Af5
>::Set() ;
// LED2 on PortC.9, LED3 on PortC.8, LED4 on PortC.5 so set PortC.5,8,9 as output
GPIOC::MODERPack<
GPIOC::MODER::MODER5::Output,
GPIOC::MODER::MODER8::Output,
GPIOC::MODER::MODER9::Output
>::Set() ;
SPI2::CR1Pack<
SPI2::CR1::MSTR::Master, //SPI2 master
SPI2::CR1::BIDIMODE::Unidirectional2Line,
SPI2::CR1::DFF::Data16bit,
SPI2::CR1::CPOL::Low,
SPI2::CR1::CPHA::Phase1edge,
SPI2::CR1::SSM::NssSoftwareEnable,
SPI2::CR1::BR::PclockDiv64,
SPI2::CR1::LSBFIRST::MsbFisrt,
SPI2::CR1::CRCEN::CrcCalcDisable
>::Set() ;
SPI2::CRCPR::CRCPOLY::Set(10U) ;
return 1;
}
}
Замечу
Так как с помощью регистров можно сделать очень много плохих вещей, то функция __low_level_init() должна быть единственным местом, где идет обращение к регистрам и настраивается процессоро-зависимая часть периферии, в любом другом коде, обращение к регистрам должно быть строго запрещено.
Если, же все таки нужно, что-то перенастроить, например режим работы Pina с выхода на вход и обратно (если вы, скажем реализуете программно какой-нибудь однопроводной интерфейс), то необходимо будет вызывать метод соответствующей периферии(если ей позволено настраиваться спецификацией).
С Pinaми по идее ничего больше делать не надо. Как я уже сказал, в редких случая (см пример выше) нам нужна настройка Pina на вход и выход, поэтому просто так удалять методы SetInput()
и SetOutput()
нельзя. В связи с этим снова вернемся к классу Pin
Расширенный класс для Pin
Как я уже говорил, обычно в проектах, в которых я участвую, все прописано в спецификациях, в том числе и режимы настройки каждого пина каждого порта. Некоторые пины настроены только как вход, а некоторые как выход на всю свою жизнь.
И у нас должна быть возможность сделать так, чтобы у Pinа, настроенного на вход не было возможности вызвать метод Set()
, и наоборот для Pinа, настроенного на выход, не было даже намека на метод Get()
.
Пины же, которые могут работать в обоих режимах, должны быть конфигурируемы. Для этого можно ввести интерфейсы:
struct PinConfigurable{}; //Pin можно сконфигурировать
struct PinReadable{}; //Pin можно считать
struct PinWriteable{}; //В Pin можно записать
struct PinReadableConfigurable: PinReadable, PinConfigurable{}; //Pin можно читать и конфигурировать
struct PinWriteableConfigurable: PinWriteable, PinConfigurable{}; //В Pin можно писать и конфигурировать
struct PinAlmighty: PinReadableConfigurable, PinWriteableConfigurable{}; //Всемогущий Pin
Всемогущий Pin может делать что угодно, но он самый небезопасный и здесь он чисто для примера.
C помощью SFINAE можно определить набор методов для Pin, имеющих разные интерфейсы, чтобы не загружать сильно код покажу только 3 метода:
template<typename Port, uint8_t pinNum, typename Interface>
struct Pin
{
using PortType = Port ;
static constexpr uint32_t pin = pinNum ;
//Метод Set() должен быть доступен только для пинов настроенных на выход
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinWriteable, T>::value>>
static void Set()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::Set(uint8_t(1U) << pinNum) ;
}
//Метод быть Get() должен доступен только для пинов настроенных на вход
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinReadable, T>::value>>
static auto Get()
{
return Port::Get() ;
}
//Метод должен быть доступен только для пина способного настроиться на выход
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinWriteableConfigurable, T>::value>>
static void SetOutput()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::SetOutput(pinNum);
}
} ;
#ifndef REGISTERS_PIN_HPP
#define REGISTERS_PIN_HPP
#include "susudefs.hpp" //for __forceinline
#include "port.hpp" //for Port
struct PinConfigurable
{
};
struct PinReadable
{
};
struct PinWriteable
{
};
struct PinReadableConfigurable: PinReadable, PinConfigurable
{
};
struct PinWriteableConfigurable: PinWriteable, PinConfigurable
{
};
struct PinAlmighty: PinReadableConfigurable, PinWriteableConfigurable
{
};
template<typename Port, uint8_t pinNum, typename Interface>
struct Pin
{
using PortType = Port ;
static constexpr uint32_t pin = pinNum ;
constexpr Pin() = default;
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinWriteable, T>::value>>
static void Set()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::Set(uint8_t(1U) << pinNum) ;
}
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinWriteable, T>::value>>
static void Reset()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::Reset((uint8_t(1U) << (pinNum)) << 16) ;
}
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinWriteable, T>::value>>
static void Toggle()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::Toggle(uint8_t(1U) << pinNum) ;
}
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinReadable, T>::value>>
static auto Get()
{
return Port::Get() ;
}
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinReadableConfigurable, T>::value>>
static void SetInput()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::SetInput(pinNum);
}
__forceinline template<typename T = Interface,
class = typename std::enable_if_t<std::is_base_of<PinWriteableConfigurable, T>::value>>
static void SetOutput()
{
static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
Port::SetOutput(pinNum);
}
} ;
#endif //REGISTERS_PIN_HPP
Сделав такой класс, можем посмотреть в спецификацию настройки периферии, в ней может быть прописано, что то типа такого:
Порт | Режим | Возможность настройки |
---|---|---|
GPIOA.5 | Output | Да |
GPIOC.3 | Output | Нет |
GPIOC.13 | Input | Нет |
GPIOC.12 | Input | Да |
GPIOC.11 | Input/Output | Да |
- пин
GPIOA.5
используется для светодиода, должен работать в режиме выхода и может быть настроен только на режим выхода во время работы.
То в код мы переведем это так, как тип, принимающий только один порт GPIOA.5 и имеющий у себя только два метода для установки состояния Pinа и его конфигурирования:Set()
SetOutput()
:
using Led1Pin = Pin<Port<GPIOA>, 5U, PinWriteableConfigurable> ;
- пин
GPIOC.3
используется для светодиода, должен работать в режиме выхода, возможности настройки у него нет.
Для программиста это означает, что настройка будет происходить в функции__low_level_init
через регистры, т.е. программист будет иметь возможность только устанавливать состояние порта через методSet()
. Поэтому конфигурация Pina будет выполнена следующим образом:
using Led3Pin = Pin<Port<GPIOC>, 8U, PinWriteable> ;
- пин
GPIOC.13
используется для кнопки и может работать только в режиме чтения.
using Button1Pin = Pin<Port<GPIOC>, 13U, PinReadable> ;
- пин
GPIOC.12
для другой кнопки настроен на вход, но может еще и сам себя в этот режим конфигурировать, то:
using Button2Pin = Pin<Port<GPIOC>, 12U, PinReadableConfigurable> ;
- Ну и на порте
GPIOC.11
находится пин, который может работать в любом режиме:
using SuperPin = Pin<Port<GPIOC>, 11U, PinAlmighty> ;
Сконфигурировав так пины, мы позволим пользователю (программисту) делать только то, что утверждено спецификацией:
Led1Pin::SetOutput() ;
Led1Pin::Set() ;
Led1::SetInput() ; //Ошибка, нет SetInput() мeтода. Не поддерживает PinReadableConfigurable
auto res = Led1Pin()::Get(); //Ошибка, нет Get() метода. Только PinWriteable
Led3::SetOuptut(); //Ошибка, нет SetOutput() метода. Не поддерживает PinWriteableConfigurable
auto res = Button1Pin::Get() ;
Button1Pin::Set(); //Ошибка, нет Set() метода. Не поддерживает PinWriteable
Button1Pin::SetInput(); //Ошибка, нет SetInput() метода. Не поддерживает PinReadableConfigurable
Button2Pin::SetInput() ;
Button2Pin::Get() ;
SuperPin::SetInput() ;
res = SuperPin::Get() ;
SuperPin::SetOutput() ;
SuperPin::Set() ;
Т.е. вся идея конфигурирования заключается в том, что программист смотрит спецификацию, ищет настройку пина и переписывает её в конфигурацию Pin
, задавая соответствующий тип, а затем на уровне бизнес логики использует только те функции типаPin
, которые позволены в соответствии со спецификацией. Если Pin
, настроен как PinReadable
, то уж извините ни перенастроить его, ни установить в него из уровня приложении будет невозможно.
Быстродействие
Быстродействие здесь точно такое же как и у Си и у ассемблерного кода, все методы сделаны принудительно inline, поэтому вызов функции, например Set()
даже в режиме без оптимизации преобразуется в простой вызов установки бита, например:
Led1Pin::Set() ;
полностью идентично строке:
*reinterpret_cast<volataile uint32_t*>(0x40020018) = 32 ;
ну или на более привычном CMSIS варианте
GPIOA->BSRR = GPIO_BSRR_BS5 ;
В принципе это вся идея, но тут меня посетила мысль, а что, если мне одновременно нужно установить (или режим поменять или сбросить) сразу несколько Pinов, находящихся на разных портах?
Набор Pinов
В качестве эксперимента, я взял свою плату, на ней 4 светодиода, и они как раз находятся на разных портах: GPIOA.5
, GPIOC.5
, GPIOC.8
, GPIOC.9
;
Первое что приходит в голову, это вот такой код:
//конфигурируем Pinы
using Led1Pin = Pin<Port<GPIOA>, 5U, PinWriteable> ;
using Led2Pin = Pin<Port<GPIOC>, 5U, PinWriteable> ;
using Led3Pin = Pin<Port<GPIOC>, 8U, PinWriteable> ;
using Led4Pin = Pin<Port<GPIOC>, 9U, PinWriteable> ;
void main()
{
Led1Pin::Set();
Led2Pin::Set();
Led3Pin::Set();
Led4Pin::Set();
}
Вроде бы нормально, но, во-первых много кода, если Pinов будет 10, то придется 10 раз писать одно и то же — нехорошо. Поэтому я сделал класс PinsPack:
template<typename ...T>
struct PinsPack{
__forceinline inline static void Set()
{
Pass((T::Set(), true)...) ;
}
...
private:
//Вспомогательный метод для распаковки вариативного шаблона
__forceinline template<typename... Args>
static void inline Pass(Args... )
{
}
} ;
После этого можно будет написать проще, оно тоже развернется в те же 4 строчки:
void main()
{
PinsPack<Led1Pin, Led2Pin, Led3Pin, Led4Pin>::Set() ;
//развернется в те же 4 строчки
// Led1Pin::Set(); -> GPIOA::BSRR::Set(32) ;
// Led2Pin::Set(); -> GPIOC::BSRR::Set(32) ;
// Led3Pin::Set(); -> GPIOC::BSRR::Set(256) ;
// Led4Pin::Set(); -> GPIOC::BSRR::Set(512) ;
}
Поэтому во вторых, такой код не оптимальный, ведь по сути мы можем сделать все установки в 2 строчки:
GPIOA::BSRR::Set(32) ; //Установить GPIOA.5 в 1
GPIOС::BSRR::Set(800) ; //Установить сразу GPIOC.5, GPIOC.8, GPIOC.9
А как это сделать на С++, я попробую описать в следующей статье.
Автор: lamerok