Хабрахабр

Трактат о Pinе. Мысли о настройке и работе с пинами на С++ для микроконтроллеров (на примере CortexM)

Я лишь начал очень медленно вкуривать функциональное программирование и формальные методы. Последнее время я сильно увлекся вопросом надежности софта для микроконтроллеров, 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 назад Билл Гейтс за это проклял бы разработчика, но не думаю, что сейчас размер кода такая серьезная проблема для современных микроконтроллеров, поэтому она не считается.

Я не могу показывать студентам код, который мы используем на основной работе из-за NDA, поэтому решил использовать что-то самописное, что с одной стороны, позволит студентам показать все от самого низкого уровня (как обращаться к регистрам), а с другой стороны позволит писать бизнес логику, не задумываясь о внутренностях с наименьшим количеством ошибок.

Логично, что вторым должны быть порты и пины. Первым таким шагом, была обертка над регистрами. Поэтому с них и начнем.

Порт

Порт — это средство общения микроконтроллера с внешним миром.

на них можно подавать аналоговый сигнал, который затем будет поступать на входы АЦП, а таже функционировать в альтернативном режиме (это когда порт работает в режиме какой-нибудь периферии, скажем UART, SPI или USB). Порт может работать как цифровой вход и цифровой выход, некоторые порты могут работать в аналоговом режиме, т.е. Рассмотрим только два режима цифровой вход и выход: Для упрощения аналоговый и альтернативные режимы рассматривать не будем.

  • В режиме цифрового выхода на порт можно вывести 0 или 1,
    0 — соответствует низкому уровню напряжения (земле);
    1 — высокому уровню (питанию).
  • В режиме цифрового входа порт считывает уровень напряжения на ножке.
    0 — соответствует низкому значению;
    1 — высокому.

Есть еще несколько настроек портов, такие как подтяжка к 0 или 1, для того чтобы порт не "висел" в воздухе и еще настройка типа выхода (c открытым колектором или двухтактный), но с точки зрения программирования нам эти вещи сейчас не интересны.

Ради экономии текста, опустим метод Reset(). Давайте ограничимся простой абстракцией порта у которого есть методы Set() — установка 1, Reset() — установка 0, Get() — чтение состояния порта, SetInput() — установка в режим входа,SetOutput() — установка в режим выхода.

Можно описать Port следующим классом:

drawing

Или, используя обертку над регистрами из прошлой статьи, кодом:

template <typename T> struct Port
__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}) ) ; } } ;

Кому интересно, как сделан атомарный доступ см ниже:

Атомарный доступ с помощью инструкций LDREX и CLREX

Первоисточник: Атомарные операции в 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 для любого микроконтроллера будет выглядеть следующим образом:

drawing

Чтобы не создавать отдельно объекты класс 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() & ( 1 << pinNum)) >> pinNum; } 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); }
} ;

Внимательный читатель заметит, что не у всех микроконтроллеров 15 пинов на одном порту. Сразу же добавили частичку статической проверки: пинов на порту у нас 16, поэтому пользователю уже не позволено передавать значение больше 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 встроенная функция, которая вызывается еще до инициализации всех переменных и объектов. можно быть уверенным, что до того как объекты будут инициализированы, вся необходимая периферия микроконтроллера уже будет настроена и можно смело вызывать методы объектов или статических классов.

Пример таких настроек (система тактирования, порты, SPI для драйвера e-paper):

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::Intput, //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::Data8bit, 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 на вход и выход, поэтому просто так удалять методы SetInput() и SetOutput() нельзя. С Pinaми по идее ничего больше делать не надо. В связи с этим снова вернемся к классу 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() & ( 1 << pinNum)) >> 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); }
} ;

Полный код тут:

#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() & ( 1 << pinNum)) >> pinNum; } __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 используется для светодиода, должен работать в режиме выхода и может быть настроен только на режим выхода во время работы.
    То в код мы переведем это так, как тип, принимающий только один порт 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ов

5, GPIOC. В качестве эксперимента, я взял свою плату, на ней 4 светодиода, и они как раз находятся на разных портах: GPIOA. 8, GPIOC. 5, 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();
}

Поэтому я сделал класс PinsPack: Вроде бы нормально, но, во-первых много кода, если Pinов будет 10, то придется 10 раз писать одно и то же — нехорошо.

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

А как это сделать на С++, я попробую описать в следующей статье.

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть