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

Последнее время я сильно увлекся вопросом надежности софта для микроконтроллеров, 0xd34df00d посоветовал мне сильнодействующие препараты, но к сожалению руки пока не дошли до изучения Haskell и Ivory для микроконтроллеров, да и вообще до совершенно новых подходов к разработке ПО отличных от ООП. Я лишь начал очень медленно вкуривать функциональное программирование и формальные методы.

Все мои потуги в этих направлениях это, как было сказано в комментарии ради любви к технологиям, но есть подозрение, что сейчас никто не даст мне применять такие подходы (хотя, как говориться, поживем увидим). Уж больно специфические навыки должны быть у программиста, который все это дело будет поддерживать. Полагаю, что написав однажды программу на таком языке, моя контора будет долго искать человека, который сможет принять такой код, поэтому на практике для студентов и для работы я все еще по старинке использую С++.

Продолжу развивать тему о встроенном софте для небольших микроконтроллеров в устройствах для safety critical систем.

На этот раз попробую предложить способ работы с конкретными ножками микроконтроллера, используя обертку над регистрами, которую я описал в прошлой статье Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)

Чтобы было общее представление того о чем я хочу рассказать, приведу небольшой кусок кода:

using Led1Pin = Pin, 5U, PinWriteableConfigurable> ;
using Led2Pin = Pin, 5U, PinWriteableConfigurable> ;
using Led3Pin = Pin, 8U, PinWriteable> ;
using Led4Pin = Pin, 9U, PinWriteable> ;
using ButtonPin = Pin, 10U, PinReadable> ;

//Этот вызов развернется в  2 строчки
// GPIOA::BSRR::Set(32) ; // reinterpret_cast(0x40020018) = 32U 
// GPIOС::BSRR::Set(800) ; // reinterpret_cast(0x40020818) = 800U 
 PinsPack::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 следующим классом:


drawing

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

template  
struct Port
{
  __forceinline static void Set(std::uint32_t value)
  {
    T::BSRR::Write(static_cast(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(pinNum * 2U) ;

    auto value = T::MODER::Get() ;       // получаем значение регистра MODER
    value &= ~(mask <<  offset);   // очищаем настройку для нужного порта
    value |= (value << offset) ;      // ставим новую настройку(На вход)
     *reinterpret_cast(T::MODER::Address) = value ; //Записываем новое значение в регистр
  }

  //Здесь вариант с атомарной установкой значения...  
  __forceinline static void SetOutput(std::uint32_t pinNum)
  {
    assert(pinNum <= 15U);
    using ModerType = typename T::MODER::Type ;
    AtomicUtils::Set(  
            T::MODER::Address,
            T::MODER::FieldValues::Output::Mask,
            T::MODER::FieldValues::Output::Value,
            static_cast(pinNum * uint8_t{2U})
            ) ;
  }    
} ;

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


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

Первоисточник: Атомарные операции в Cortex-M3


Команда LDREX загружает значение по указанному адресу в регистр и взводит специальный флаг процессора, сигнализирующий об эксклюзивном доступе к памяти.
STREX — проверяет не был-ли нарушен эксклюзивный доступ к памяти, если нет, то записывает значение из входного регистра по указанному адресу и сбрасывает флаг эксклюзивного доступа. При этом в выходном регистре будет записан ноль. Если между LDREX и STREX произошло прерывание и оно что-то записало в память (а оно обязательно хоть регистры, да сохранит в стек), то STREX ничего не запишет в память и в выходной регистре будет записана 1. Это значит, что значение в памяти могло изменится (а могло и нет) и нам надо снова перечитать его из памяти модифицировать и снова попытаться его сохранить. Естественно, чем меньше кода между LDREX и STREX, тем меньше вероятность, что там произойдёт прерывание и больше шансов обновить значение с первого раза.
template 
struct AtomicUtils
{
   static void Set(T address, T mask, T value, T offset)
  {
    T oldRegValue ;
    T newRegValue ;
    do
    {
      oldRegValue = *reinterpret_cast(address);
      newRegValue = oldRegValue;
      newRegValue &= ~(mask << (offset));
      newRegValue |= (value << (offset));
    } while (
      !AtomicUtils::TryToWrite(reinterpret_cast(address),
                                             oldRegValue,
                                             newRegValue)
      ) ;
  }

private:
 static bool TryToWrite(volatile T* ptr, T oldValue, T newValue)
  {
    using namespace std ;
    // читаем значение переменной и сравниваем со старым значением
    if(__LDREX(ptr) == static_cast(oldValue))
    {
     // пытаемся записать в переменную новое значение
      return (__STREX(static_cast(newValue), static_cast(ptr)) == 0) ;
    }
    __CLREX();
   return false ;
  }

};

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


Pin

Конкретный Pin порта мы хотим сделать безопасным и процесоро-независимым. Нужно запретить делать пользователю, то чего ему делать не положено и отвязать его от микропроцессора.

Итак, конкретный Pin должен иметь связь с портом, номер пина и Get() и Set() методы. Эти два метода актуальны для любого микроконтроллера. Так же как, скорее всего, для любого микроконтроллера актуальны методы настройки Pin на вход или выход. А вот перевод в альтернативный режим или в аналоговый не всегда поддерживается микроконтроллерами, поэтому, чтобы не зависеть от микроконтроллеров не будем добавлять эту возможность, ниже я поясню, этот момент. Наша абстракция Pin для любого микроконтроллера будет выглядеть следующим образом:


drawing

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

template
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, 5U> ;
using Led4 = Pin, 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::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
struct Pin
{
  using PortType = Port ;
  static constexpr uint32_t pin = pinNum ;

  //Метод Set() должен быть доступен  только для пинов настроенных на выход
  __forceinline template::value>>
  static void Set()
  {
    static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
    Port::Set(uint8_t(1U) << pinNum) ;
  }

  //Метод быть Get() должен доступен только для пинов настроенных на вход  
  __forceinline template::value>>
  static auto Get()
  {
    return Port::Get() ;
  }

  //Метод должен быть доступен только для пина способного настроиться на выход  
  __forceinline template::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
struct Pin
{
  using PortType = Port ;
  static constexpr uint32_t pin = pinNum ;

  constexpr Pin() = default;

  __forceinline template::value>>
  static void Set()
  {
    static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
    Port::Set(uint8_t(1U) << pinNum) ;
  }

  __forceinline template::value>>
  static void Reset()
  {
    static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
    Port::Reset((uint8_t(1U) << (pinNum)) << 16) ;
  }

  __forceinline template::value>>
  static void Toggle()
  {
    static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
    Port::Toggle(uint8_t(1U) << pinNum) ;
  }

  __forceinline template::value>>
  static auto Get()
  {
    return Port::Get() ;
  }

  __forceinline template::value>>
  static void SetInput()
  {
    static_assert(pinNum <= 15U, "There are only 16 pins on port") ;
    Port::SetInput(pinNum);
  }

  __forceinline template::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, 5U, PinWriteableConfigurable> ;


  • пин GPIOC.3используется для светодиода, должен работать в режиме выхода, возможности настройки у него нет.
    Для программиста это означает, что настройка будет происходить в функции __low_level_init через регистры, т.е. программист будет иметь возможность только устанавливать состояние порта через метод Set(). Поэтому конфигурация Pina будет выполнена следующим образом:
using Led3Pin = Pin, 8U, PinWriteable> ;


  • пин GPIOC.13 используется для кнопки и может работать только в режиме чтения.
using Button1Pin = Pin, 13U, PinReadable> ; 


  • пин GPIOC.12 для другой кнопки настроен на вход, но может еще и сам себя в этот режим конфигурировать, то:
using Button2Pin = Pin, 12U, PinReadableConfigurable> ; 


  • Ну и на порте GPIOC.11 находится пин, который может работать в любом режиме:
using SuperPin = Pin, 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(0x40020018) = 32 ;

ну или на более привычном CMSIS варианте

GPIOA->BSRR = GPIO_BSRR_BS5 ;

В принципе это вся идея, но тут меня посетила мысль, а что, если мне одновременно нужно установить (или режим поменять или сбросить) сразу несколько Pinов, находящихся на разных портах?


Набор Pinов

В качестве эксперимента, я взял свою плату, на ней 4 светодиода, и они как раз находятся на разных портах: GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9;
Первое что приходит в голову, это вот такой код:

//конфигурируем Pinы
using Led1Pin = Pin, 5U, PinWriteable> ;
using Led2Pin = Pin, 5U, PinWriteable> ;
using Led3Pin = Pin, 8U, PinWriteable> ;
using Led4Pin = Pin, 9U, PinWriteable> ;

void main()
{
   Led1Pin::Set();
   Led2Pin::Set();
   Led3Pin::Set();
   Led4Pin::Set();
}

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

template
struct PinsPack{
   __forceinline inline static void Set()
  {
    Pass((T::Set(), true)...) ;
  }
  ...
private:
  //Вспомогательный метод для распаковки вариативного шаблона
  __forceinline template
  static void inline Pass(Args... ) 
  {
  }
} ;

После этого можно будет написать проще, оно тоже развернется в те же 4 строчки:

void main()
{
  PinsPack::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

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

© Habrahabr.ru