RP2040 peripheral registers
RP2040 peripheral registers

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

Кто-то может подумать, что данная тема уже рассказана вдоль и поперек в многочисленных мануалах по проектам для STM32 (например, хабр). Те же, кто уже понял о чем будет идти речь, может подумать что это полное повторение уже существующих подобных идей (например эта и другие статьи автора зародили во мне интерес к этой теме несколько лет назад, а вот в языке программирования Rust большинство ящиков/крейтов Cortex-M/PAC уже используют проверками безопасности при манипулировании регистрами). Однако, не нужно спешить с выводами. Я полагаю, что могу дать интересную и полезную информацию для обоих вышеперечисленных категорий читателей.

Перед началом, тему того 'Зачем вообще лезть на уровень регистров, если каждый производитель дает HAL/BSP?' я оставлю вне рамок данной статьи. Я предполагаю, что если читатель открыл статью, то ему уже интересна данная тема, и мне не столь важно почему.

Краткое описание проблемы

Обычно, когда возникает необходимость работы с периферийным модулем напрямую (к примеру GPIO, TIMER, RTC, DAC, SPI, ETHERNET, USB, и еще много их разных), программист использует описание регистров, предоставленное производителем MCU. И также обычно, что это один или несколько заголовочных файлов C кода, написанных по CMSIS.

На текущий момент, формат этих файлов трудно назвать лучшим из того, что может быть. Одна из основных проблем CMSIS - безопасность. Примеры ошибок, которые могли бы быть обработаны на этапе написания/компиляции при работе с регистрами:

  • Содержит ли регистр данное поле?

// Ошибка в одной букве, регистр должен быть AHB1ENR
RCC->APB1ENR |= RCC_AHB1ENR_GPIODEN;
  • Дублирование полей при копировании

// Ошибка, GPIO_ODR_ODR_14 был задублирован, последнее поле должно быть GPIO_ODR_ODR_15
GPIOD->ODR ^= GPIO_ODR_ODR_12 | GPIO_ODR_ODR_13 | GPIO_ODR_ODR_14 | GPIO_ODR_ODR_14;
  • Режим доступа поля/регистра

// Ошибка, регистр и поле только для записи (и даже не для чтения-модификации-записи)
if(GPIOD->BSRR & GPIO_BSRR_BR_13) { ... }
  • Переполнение регистра

// Ошибка, размер GPIOD->ODR 16 бит
GPIOD->ODR |= 1UL << 20;

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

На примере Hello world

Рассмотрим пример использования файла от производителя (стандартный CMSIS header), работающий минималистичный код светодиодной мигалки на одной из очень популярных плат STM32F4DISCOVERY. Цель - мигать четырьмя светодиодами на ногах PD12-PD15 с периодом две секунды (см. комментарии в коде).

// main.cpp

// Тот самый файл от производителя
#include "stm32f4xx.h"

// Импровизированные урезанные вектор сброса и таблица векторов прерываний
[[noreturn]] int main();
extern "C" [[noreturn]] void Reset_Handler();
[[noreturn]] void Reset_Handler() { main(); }
[[gnu::used, gnu::section(".isr_vector")]] static constexpr void (*const g_pfnVectors[])(void) = {Reset_Handler};

//Основной код мигалки
[[noreturn]] int main() {
  static constexpr auto SYSTEM_MHZ = 16UL; // Частота по умолчанию
  static constexpr auto SYST_PERIOD = (SYSTEM_MHZ * 1000000UL) - 1;
  
  // Включить порт D и установить пины 12-15 как выходы
  RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
  GPIOD->MODER |= GPIO_MODER_MODER12_0 | GPIO_MODER_MODER13_0 | GPIO_MODER_MODER14_0 | GPIO_MODER_MODER15_0;

  //  Используя системный таймер, ставим период смены состояния светодиодов в 1 секунду
  SysTick->LOAD = SYST_PERIOD;
  SysTick->VAL = SYST_PERIOD;
  SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;

  // Меняем состояния выводов
  while (true) {
    if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
      GPIOD->ODR ^= GPIO_ODR_ODR_12 | GPIO_ODR_ODR_13 | GPIO_ODR_ODR_14 | GPIO_ODR_ODR_15;
    }
  }

  return 0;
}

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

Важно отметить результат сборки, в данном случае, с компилятором clang и оптимизацией Oz:

   text    data     bss     dec     hex filename
     84       0       0      84      54 main.elf

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

#include "stm32f4xx.h"

[[noreturn]] int main();
extern "C" [[noreturn]] void Reset_Handler();
[[noreturn]] void Reset_Handler() { main(); }
[[gnu::used, gnu::section(".isr_vector")]] static constexpr void (*const g_pfnVectors[])(void) = {Reset_Handler};

[[noreturn]] int main() {
  static constexpr auto SYSTEM_MHZ = 16UL;
  static constexpr auto SYST_PERIOD = (SYSTEM_MHZ * 1000000UL) - 1;
  
  // Регистр не тот
  RCC->APB1ENR |= RCC_AHB1ENR_GPIODEN;
  // Дважды впоставил на выход пин 12, а пин 13 вообще не включили
  GPIOD->MODER |= GPIO_MODER_MODER12_0 | GPIO_MODER_MODER12_0 | GPIO_MODER_MODER14_0 | GPIO_MODER_MODER15_0;

  SysTick->LOAD = SYST_PERIOD;
  SysTick->VAL = SYST_PERIOD;
  // Попытка записи поля только для чтения (SysTick_CTRL_COUNTFLAG_Msk)
  SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_COUNTFLAG_Msk;

  while (true) {
    if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
      // Поля от другого регистра, ошибка при копировании
      GPIOD->ODR ^= GPIO_MODER_MODER12_0 | GPIO_MODER_MODER13_0 | GPIO_MODER_MODER14_0 | GPIO_MODER_MODER15_0;
    }
  }

  return 0;
}

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

#include "gpio.hpp"
#include "rcc.hpp"
#include "stk.hpp"

using namespace cpp_register;
using namespace cpp_register::constants;
using namespace stm32f407::rcc;
using namespace stm32f407::gpio;
using namespace stm32f407::stk;

[[noreturn]] int main();
extern "C" [[noreturn]] void Reset_Handler();
[[noreturn]] void Reset_Handler() { main(); }
[[gnu::used, gnu::section(".isr_vector")]] static constexpr void (*const g_pfnVectors[])(void) = {Reset_Handler};

[[noreturn]] int main() {
  static constexpr auto SYSTEM_MHZ = 16UL;
  static constexpr auto SYST_PERIOD = reg_v<(SYSTEM_MHZ * 1000000UL) - 1>;

  RCC->AHB1ENR |= RCC_AHB1ENR::GPIODEN;

  // Long form
  // GPIOD->MODER |= (GPIO_MODER::MODER[NUM_12](NUM_0) | GPIO_MODER::MODER[NUM_13](NUM_0) |
  // GPIO_MODER::MODER[NUM_14](NUM_0) | GPIO_MODER::MODER[NUM_15](NUM_0));

  // Short form
  GPIOD->MODER |= GPIO_MODER::MODER[NUM_12 | NUM_13 | NUM_14 | NUM_15](NUM_0);

  STK->LOAD = STK_LOAD::RELOAD(SYST_PERIOD);
  STK->VAL = STK_VAL::CURRENT(SYST_PERIOD);
  STK->CTRL |= (STK_CTRL::CLKSOURCE | STK_CTRL::ENABLE);

  while (true) {
    if (STK->CTRL & STK_CTRL::COUNTFLAG) {
      GPIOD->ODR ^= GPIO_ODR::ODR[NUM_12 | NUM_13 | NUM_14 | NUM_15];
    }
  }

  return 0;
}

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

Размеры программы:

   text    data     bss     dec     hex filename
     80       0       0      80      50 main.elf

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

Предварительные условия

Я использую именно современный C++, так как считаю, что при высоком уровне читаемости и универсальности кода, данный язык позволяет добиться наиболее высокой эффективности (а как известно, очень часто эти вещи обратно пропорциональны). И мой выбор на текущий момент, это C++20, так как именно в этом стандарте появились концепты (конечно, появилось много и других классных вещей). Это не столько критично именно для работы с регистрами, сколько для моих проектов в целом, так как на разных уровнях мне необходим статический полиморфизм. К тому же, согласно таблице, С++20 хоть и не полностью, но очень широко поддерживается в gcc и clang.

Единственное, что я хотел бы упомянуть в этом ключе, это почему все таки не Rust? На самом деле, сам язык и его концепции мне поначалу очень понравились, а синтаксически, на мой взгляд, это вообще один из лучших языков (да, у меня скромный опыт). Однако, после ленивой годичной практики языка, я все же был вынужден заключить, что в разрезе embedded программирования (где можно и нужно активно использовать программирование времени компиляции) Rust не только не делает качественно скачка вперед, но и даже в чем-то уступает C++ в 2024 году. Конечно, я не исключаю, что в будущем, ситуация может измениться.

Чтобы использовать C++20, нужна современная IDE/компилятор, то есть она должна быть постоянно обновляемой. Так как я не обладаю бюджетом для покупки лицензий IAR ARM или Keil, я использую следующую бесплатную кроссплатформенную связку: gcc/clang+Make+VSCode. Таким образом, я не завишу ни от производителя чипа, ни от любых других платных продуктов.

Помимо этого, использую флаги расширенных предупреждений (такие как -Wall, -Wextra, -Werror и много других) и максимальную оптимизацию.

Техническое задание

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

  • Содержит ли регистр данное поле?

  • Дублирование полей при копировании

  • Режим доступа поля/регистра (чтение, запись, установка, и т.д.)

  • Переполнение регистра

При этом, я хотел включить в решение следующие особенности:

  • Реализация записи нескольких полей в регистр без потери безопасности. Я выделал этот пункт отдельно, так как в некоторых увиденных мной реализациях эта проблема была не решена.

// Например, тут устанавливаются сразу 5 полей регистра SPI1->CR1
// Для КАЖДОГО поля должны производиться все основные проверки безопасности
SPI1->CR1 |= (SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR); 
  • Реализация массивов регистров или полей, с настраиваемым шагом. Приведу сразу несколько примеров:

    Регистр GPIOx_MODER, MCU STM32F070
    Регистр GPIOx_MODER, MCU STM32F070

    На картинке выше есть то, что я бы назвал массивом полей, 16 полей MODER размером 2 бита. И вот хорошо бы не описывать каждое поле отдельно, а записать это как некий массив полей в одну строку. То же самое касается и регистров.

    Пример массива регистров, который я бы назвал 'с шагом':

    Usb Packet buffers, MCU STM32F070
    Usb Packet buffers, MCU STM32F070

    Тут блок из четырех 16бит регистра: ADDRx_TX, COUNTx_TX, ADDRx_RX, COUNTx_RX. И таких блоков в данном MCU восемь. Так же получается, что все одинаковые регистры идут с шагом равным 8. То есть ADDR0_TX расположен на адресе 0x00, ADDR1_TX на 0x08, ADDR2_TX на 0x10...

    Нужно, чтобы это был массив регистров как в описании, так и в обращении.

  • Реализации короткой формы записи подобных полей в один регистр. Тут я просто приведу готовый пример на базе моего решения:

// Устанавливаю пины 12-15 как выходы, 'длинная' форма, корректно  
GPIOD->MODER |= (GPIO_MODER::MODER[NUM_12](NUM_0) | GPIO_MODER::MODER[NUM_13](NUM_0) |
                   GPIO_MODER::MODER[NUM_14](NUM_0) | GPIO_MODER::MODER[NUM_15](NUM_0));

// Так тоже корректно, 'короткая' форма
GPIOD->MODER |= GPIO_MODER::MODER[NUM_12 | NUM_13 | NUM_14 | NUM_15](NUM_0);
  • Автоматический bit-band, в том числе и не стандартный. Вообще, раз все привычные операции обращения к регистрам будут переписаны, не плохо бы добавить bit-band, который бы срабатывал автоматически при соблюдении ряда условий.

  • Поддержка некоторых 'динамических' операций. Иногда возникают ситуации, когда нужно записать в регистр что-то, что не известно на этапе компиляции. Как пример, запись адреса буфера для DMA, если буфер не статический.

  • Каст от уровня драйвера до уровня регистра без потери безопасности. При написании драйверов, я активно использую 'enum class'. С одной стороны, он type-safe (именно поэтому он и удобен для передачи параметров, например при инициализации). С другой стороны, чтобы записать это значение в регистр, нужен явный static_cast в uintx_t (что логично). И идея в том, чтобы делать этот каст в одном месте на уровне регистров, чтобы каждый раз не делать это руками на уровне драйверов.

  • Инструмент для автоматического составления файлов описания регистров. Напомню, что стандартный файл с описанием регистров от производителя может выглядеть так. Скорее всего, он составлен автоматически из файлов SVD. Я бы тоже хотел получать хотя бы черновой файл автоматически, чтобы само описание не отнимало много времени.

Решение

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

Я посчитал, что cpp_register будет разумно показать на простом примере, на примере GPIO микроконтроллера семейства STM32 (У меня была плата с stm32f070). Детали о данных регистрах находятся в Reference Manual на чип stm32f070.

Перед началом, я приведу описание регистров модуля на уровне CMSIS (анализировать этот кусок кода не надо), и на уровне cpp_register, просто чтобы показать разницу в объеме описанного (~400 строк против ~65).

CMSIS описание GPIO
// stm32f0xx.h

/** 
  * @brief General Purpose IO
  */

typedef struct
{
  __IO uint32_t MODER;        /*!< GPIO port mode register,                                  Address offset: 0x00 */
  __IO uint16_t OTYPER;       /*!< GPIO port output type register,                           Address offset: 0x04 */
  uint16_t RESERVED0;         /*!< Reserved,                                                                 0x06 */
  __IO uint32_t OSPEEDR;      /*!< GPIO port output speed register,                          Address offset: 0x08 */
  __IO uint32_t PUPDR;        /*!< GPIO port pull-up/pull-down register,                     Address offset: 0x0C */
  __IO uint16_t IDR;          /*!< GPIO port input data register,                            Address offset: 0x10 */
  uint16_t RESERVED1;         /*!< Reserved,                                                                 0x12 */
  __IO uint16_t ODR;          /*!< GPIO port output data register,                           Address offset: 0x14 */
  uint16_t RESERVED2;         /*!< Reserved,                                                                 0x16 */
  __IO uint32_t BSRR;         /*!< GPIO port bit set/reset registerBSRR,                     Address offset: 0x18 */
  __IO uint32_t LCKR;         /*!< GPIO port configuration lock register,                    Address offset: 0x1C */
  __IO uint32_t AFR[2];       /*!< GPIO alternate function low register,                Address offset: 0x20-0x24 */
  __IO uint16_t BRR;          /*!< GPIO bit reset register,                                  Address offset: 0x28 */
  uint16_t RESERVED3;         /*!< Reserved,                                                                 0x2A */
}GPIO_TypeDef;

#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */

#define AHB2PERIPH_BASE       (PERIPH_BASE + 0x08000000)

#define GPIOA_BASE            (AHB2PERIPH_BASE + 0x00000000)
#define GPIOB_BASE            (AHB2PERIPH_BASE + 0x00000400)
#define GPIOC_BASE            (AHB2PERIPH_BASE + 0x00000800)
#define GPIOD_BASE            (AHB2PERIPH_BASE + 0x00000C00)
#define GPIOE_BASE            (AHB2PERIPH_BASE + 0x00001000)
#define GPIOF_BASE            (AHB2PERIPH_BASE + 0x00001400)

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)

/******************************************************************************/
/*                                                                            */
/*                       General Purpose IOs (GPIO)                           */
/*                                                                            */
/******************************************************************************/
/*******************  Bit definition for GPIO_MODER register  *****************/
#define GPIO_MODER_MODER0          ((uint32_t)0x00000003)
#define GPIO_MODER_MODER0_0        ((uint32_t)0x00000001)
#define GPIO_MODER_MODER0_1        ((uint32_t)0x00000002)
#define GPIO_MODER_MODER1          ((uint32_t)0x0000000C)
#define GPIO_MODER_MODER1_0        ((uint32_t)0x00000004)
#define GPIO_MODER_MODER1_1        ((uint32_t)0x00000008)
#define GPIO_MODER_MODER2          ((uint32_t)0x00000030)
#define GPIO_MODER_MODER2_0        ((uint32_t)0x00000010)
#define GPIO_MODER_MODER2_1        ((uint32_t)0x00000020)
#define GPIO_MODER_MODER3          ((uint32_t)0x000000C0)
#define GPIO_MODER_MODER3_0        ((uint32_t)0x00000040)
#define GPIO_MODER_MODER3_1        ((uint32_t)0x00000080)
#define GPIO_MODER_MODER4          ((uint32_t)0x00000300)
#define GPIO_MODER_MODER4_0        ((uint32_t)0x00000100)
#define GPIO_MODER_MODER4_1        ((uint32_t)0x00000200)
#define GPIO_MODER_MODER5          ((uint32_t)0x00000C00)
#define GPIO_MODER_MODER5_0        ((uint32_t)0x00000400)
#define GPIO_MODER_MODER5_1        ((uint32_t)0x00000800)
#define GPIO_MODER_MODER6          ((uint32_t)0x00003000)
#define GPIO_MODER_MODER6_0        ((uint32_t)0x00001000)
#define GPIO_MODER_MODER6_1        ((uint32_t)0x00002000)
#define GPIO_MODER_MODER7          ((uint32_t)0x0000C000)
#define GPIO_MODER_MODER7_0        ((uint32_t)0x00004000)
#define GPIO_MODER_MODER7_1        ((uint32_t)0x00008000)
#define GPIO_MODER_MODER8          ((uint32_t)0x00030000)
#define GPIO_MODER_MODER8_0        ((uint32_t)0x00010000)
#define GPIO_MODER_MODER8_1        ((uint32_t)0x00020000)
#define GPIO_MODER_MODER9          ((uint32_t)0x000C0000)
#define GPIO_MODER_MODER9_0        ((uint32_t)0x00040000)
#define GPIO_MODER_MODER9_1        ((uint32_t)0x00080000)
#define GPIO_MODER_MODER10         ((uint32_t)0x00300000)
#define GPIO_MODER_MODER10_0       ((uint32_t)0x00100000)
#define GPIO_MODER_MODER10_1       ((uint32_t)0x00200000)
#define GPIO_MODER_MODER11         ((uint32_t)0x00C00000)
#define GPIO_MODER_MODER11_0       ((uint32_t)0x00400000)
#define GPIO_MODER_MODER11_1       ((uint32_t)0x00800000)
#define GPIO_MODER_MODER12         ((uint32_t)0x03000000)
#define GPIO_MODER_MODER12_0       ((uint32_t)0x01000000)
#define GPIO_MODER_MODER12_1       ((uint32_t)0x02000000)
#define GPIO_MODER_MODER13         ((uint32_t)0x0C000000)
#define GPIO_MODER_MODER13_0       ((uint32_t)0x04000000)
#define GPIO_MODER_MODER13_1       ((uint32_t)0x08000000)
#define GPIO_MODER_MODER14         ((uint32_t)0x30000000)
#define GPIO_MODER_MODER14_0       ((uint32_t)0x10000000)
#define GPIO_MODER_MODER14_1       ((uint32_t)0x20000000)
#define GPIO_MODER_MODER15         ((uint32_t)0xC0000000)
#define GPIO_MODER_MODER15_0       ((uint32_t)0x40000000)
#define GPIO_MODER_MODER15_1       ((uint32_t)0x80000000)

/******************  Bit definition for GPIO_OTYPER register  *****************/
#define GPIO_OTYPER_OT_0           ((uint32_t)0x00000001)
#define GPIO_OTYPER_OT_1           ((uint32_t)0x00000002)
#define GPIO_OTYPER_OT_2           ((uint32_t)0x00000004)
#define GPIO_OTYPER_OT_3           ((uint32_t)0x00000008)
#define GPIO_OTYPER_OT_4           ((uint32_t)0x00000010)
#define GPIO_OTYPER_OT_5           ((uint32_t)0x00000020)
#define GPIO_OTYPER_OT_6           ((uint32_t)0x00000040)
#define GPIO_OTYPER_OT_7           ((uint32_t)0x00000080)
#define GPIO_OTYPER_OT_8           ((uint32_t)0x00000100)
#define GPIO_OTYPER_OT_9           ((uint32_t)0x00000200)
#define GPIO_OTYPER_OT_10          ((uint32_t)0x00000400)
#define GPIO_OTYPER_OT_11          ((uint32_t)0x00000800)
#define GPIO_OTYPER_OT_12          ((uint32_t)0x00001000)
#define GPIO_OTYPER_OT_13          ((uint32_t)0x00002000)
#define GPIO_OTYPER_OT_14          ((uint32_t)0x00004000)
#define GPIO_OTYPER_OT_15          ((uint32_t)0x00008000)

/****************  Bit definition for GPIO_OSPEEDR register  ******************/
#define GPIO_OSPEEDR_OSPEEDR0     ((uint32_t)0x00000003)
#define GPIO_OSPEEDR_OSPEEDR0_0   ((uint32_t)0x00000001)
#define GPIO_OSPEEDR_OSPEEDR0_1   ((uint32_t)0x00000002)
#define GPIO_OSPEEDR_OSPEEDR1     ((uint32_t)0x0000000C)
#define GPIO_OSPEEDR_OSPEEDR1_0   ((uint32_t)0x00000004)
#define GPIO_OSPEEDR_OSPEEDR1_1   ((uint32_t)0x00000008)
#define GPIO_OSPEEDR_OSPEEDR2     ((uint32_t)0x00000030)
#define GPIO_OSPEEDR_OSPEEDR2_0   ((uint32_t)0x00000010)
#define GPIO_OSPEEDR_OSPEEDR2_1   ((uint32_t)0x00000020)
#define GPIO_OSPEEDR_OSPEEDR3     ((uint32_t)0x000000C0)
#define GPIO_OSPEEDR_OSPEEDR3_0   ((uint32_t)0x00000040)
#define GPIO_OSPEEDR_OSPEEDR3_1   ((uint32_t)0x00000080)
#define GPIO_OSPEEDR_OSPEEDR4     ((uint32_t)0x00000300)
#define GPIO_OSPEEDR_OSPEEDR4_0   ((uint32_t)0x00000100)
#define GPIO_OSPEEDR_OSPEEDR4_1   ((uint32_t)0x00000200)
#define GPIO_OSPEEDR_OSPEEDR5     ((uint32_t)0x00000C00)
#define GPIO_OSPEEDR_OSPEEDR5_0   ((uint32_t)0x00000400)
#define GPIO_OSPEEDR_OSPEEDR5_1   ((uint32_t)0x00000800)
#define GPIO_OSPEEDR_OSPEEDR6     ((uint32_t)0x00003000)
#define GPIO_OSPEEDR_OSPEEDR6_0   ((uint32_t)0x00001000)
#define GPIO_OSPEEDR_OSPEEDR6_1   ((uint32_t)0x00002000)
#define GPIO_OSPEEDR_OSPEEDR7     ((uint32_t)0x0000C000)
#define GPIO_OSPEEDR_OSPEEDR7_0   ((uint32_t)0x00004000)
#define GPIO_OSPEEDR_OSPEEDR7_1   ((uint32_t)0x00008000)
#define GPIO_OSPEEDR_OSPEEDR8     ((uint32_t)0x00030000)
#define GPIO_OSPEEDR_OSPEEDR8_0   ((uint32_t)0x00010000)
#define GPIO_OSPEEDR_OSPEEDR8_1   ((uint32_t)0x00020000)
#define GPIO_OSPEEDR_OSPEEDR9     ((uint32_t)0x000C0000)
#define GPIO_OSPEEDR_OSPEEDR9_0   ((uint32_t)0x00040000)
#define GPIO_OSPEEDR_OSPEEDR9_1   ((uint32_t)0x00080000)
#define GPIO_OSPEEDR_OSPEEDR10    ((uint32_t)0x00300000)
#define GPIO_OSPEEDR_OSPEEDR10_0  ((uint32_t)0x00100000)
#define GPIO_OSPEEDR_OSPEEDR10_1  ((uint32_t)0x00200000)
#define GPIO_OSPEEDR_OSPEEDR11    ((uint32_t)0x00C00000)
#define GPIO_OSPEEDR_OSPEEDR11_0  ((uint32_t)0x00400000)
#define GPIO_OSPEEDR_OSPEEDR11_1  ((uint32_t)0x00800000)
#define GPIO_OSPEEDR_OSPEEDR12    ((uint32_t)0x03000000)
#define GPIO_OSPEEDR_OSPEEDR12_0  ((uint32_t)0x01000000)
#define GPIO_OSPEEDR_OSPEEDR12_1  ((uint32_t)0x02000000)
#define GPIO_OSPEEDR_OSPEEDR13    ((uint32_t)0x0C000000)
#define GPIO_OSPEEDR_OSPEEDR13_0  ((uint32_t)0x04000000)
#define GPIO_OSPEEDR_OSPEEDR13_1  ((uint32_t)0x08000000)
#define GPIO_OSPEEDR_OSPEEDR14    ((uint32_t)0x30000000)
#define GPIO_OSPEEDR_OSPEEDR14_0  ((uint32_t)0x10000000)
#define GPIO_OSPEEDR_OSPEEDR14_1  ((uint32_t)0x20000000)
#define GPIO_OSPEEDR_OSPEEDR15    ((uint32_t)0xC0000000)
#define GPIO_OSPEEDR_OSPEEDR15_0  ((uint32_t)0x40000000)
#define GPIO_OSPEEDR_OSPEEDR15_1  ((uint32_t)0x80000000)

/* Old Bit definition for GPIO_OSPEEDR register maintained for legacy purpose */
#define GPIO_OSPEEDER_OSPEEDR0     GPIO_OSPEEDR_OSPEEDR0
#define GPIO_OSPEEDER_OSPEEDR0_0   GPIO_OSPEEDR_OSPEEDR0_0
#define GPIO_OSPEEDER_OSPEEDR0_1   GPIO_OSPEEDR_OSPEEDR0_1
#define GPIO_OSPEEDER_OSPEEDR1     GPIO_OSPEEDR_OSPEEDR1
#define GPIO_OSPEEDER_OSPEEDR1_0   GPIO_OSPEEDR_OSPEEDR1_0
#define GPIO_OSPEEDER_OSPEEDR1_1   GPIO_OSPEEDR_OSPEEDR1_1
#define GPIO_OSPEEDER_OSPEEDR2     GPIO_OSPEEDR_OSPEEDR2
#define GPIO_OSPEEDER_OSPEEDR2_0   GPIO_OSPEEDR_OSPEEDR2_0
#define GPIO_OSPEEDER_OSPEEDR2_1   GPIO_OSPEEDR_OSPEEDR2_1
#define GPIO_OSPEEDER_OSPEEDR3     GPIO_OSPEEDR_OSPEEDR3
#define GPIO_OSPEEDER_OSPEEDR3_0   GPIO_OSPEEDR_OSPEEDR3_0
#define GPIO_OSPEEDER_OSPEEDR3_1   GPIO_OSPEEDR_OSPEEDR3_1
#define GPIO_OSPEEDER_OSPEEDR4     GPIO_OSPEEDR_OSPEEDR4
#define GPIO_OSPEEDER_OSPEEDR4_0   GPIO_OSPEEDR_OSPEEDR4_0
#define GPIO_OSPEEDER_OSPEEDR4_1   GPIO_OSPEEDR_OSPEEDR4_1
#define GPIO_OSPEEDER_OSPEEDR5     GPIO_OSPEEDR_OSPEEDR5
#define GPIO_OSPEEDER_OSPEEDR5_0   GPIO_OSPEEDR_OSPEEDR5_0
#define GPIO_OSPEEDER_OSPEEDR5_1   GPIO_OSPEEDR_OSPEEDR5_1
#define GPIO_OSPEEDER_OSPEEDR6     GPIO_OSPEEDR_OSPEEDR6
#define GPIO_OSPEEDER_OSPEEDR6_0   GPIO_OSPEEDR_OSPEEDR6_0
#define GPIO_OSPEEDER_OSPEEDR6_1   GPIO_OSPEEDR_OSPEEDR6_1
#define GPIO_OSPEEDER_OSPEEDR7     GPIO_OSPEEDR_OSPEEDR7
#define GPIO_OSPEEDER_OSPEEDR7_0   GPIO_OSPEEDR_OSPEEDR7_0
#define GPIO_OSPEEDER_OSPEEDR7_1   GPIO_OSPEEDR_OSPEEDR7_1
#define GPIO_OSPEEDER_OSPEEDR8     GPIO_OSPEEDR_OSPEEDR8
#define GPIO_OSPEEDER_OSPEEDR8_0   GPIO_OSPEEDR_OSPEEDR8_0
#define GPIO_OSPEEDER_OSPEEDR8_1   GPIO_OSPEEDR_OSPEEDR8_1
#define GPIO_OSPEEDER_OSPEEDR9     GPIO_OSPEEDR_OSPEEDR9
#define GPIO_OSPEEDER_OSPEEDR9_0   GPIO_OSPEEDR_OSPEEDR9_0
#define GPIO_OSPEEDER_OSPEEDR9_1   GPIO_OSPEEDR_OSPEEDR9_1
#define GPIO_OSPEEDER_OSPEEDR10    GPIO_OSPEEDR_OSPEEDR10
#define GPIO_OSPEEDER_OSPEEDR10_0  GPIO_OSPEEDR_OSPEEDR10_0
#define GPIO_OSPEEDER_OSPEEDR10_1  GPIO_OSPEEDR_OSPEEDR10_1
#define GPIO_OSPEEDER_OSPEEDR11    GPIO_OSPEEDR_OSPEEDR11
#define GPIO_OSPEEDER_OSPEEDR11_0  GPIO_OSPEEDR_OSPEEDR11_0
#define GPIO_OSPEEDER_OSPEEDR11_1  GPIO_OSPEEDR_OSPEEDR11_1
#define GPIO_OSPEEDER_OSPEEDR12    GPIO_OSPEEDR_OSPEEDR12
#define GPIO_OSPEEDER_OSPEEDR12_0  GPIO_OSPEEDR_OSPEEDR12_0
#define GPIO_OSPEEDER_OSPEEDR12_1  GPIO_OSPEEDR_OSPEEDR12_1
#define GPIO_OSPEEDER_OSPEEDR13    GPIO_OSPEEDR_OSPEEDR13
#define GPIO_OSPEEDER_OSPEEDR13_0  GPIO_OSPEEDR_OSPEEDR13_0
#define GPIO_OSPEEDER_OSPEEDR13_1  GPIO_OSPEEDR_OSPEEDR13_1
#define GPIO_OSPEEDER_OSPEEDR14    GPIO_OSPEEDR_OSPEEDR14
#define GPIO_OSPEEDER_OSPEEDR14_0  GPIO_OSPEEDR_OSPEEDR14_0
#define GPIO_OSPEEDER_OSPEEDR14_1  GPIO_OSPEEDR_OSPEEDR14_1
#define GPIO_OSPEEDER_OSPEEDR15    GPIO_OSPEEDR_OSPEEDR15
#define GPIO_OSPEEDER_OSPEEDR15_0  GPIO_OSPEEDR_OSPEEDR15_0
#define GPIO_OSPEEDER_OSPEEDR15_1  GPIO_OSPEEDR_OSPEEDR15_1

/*******************  Bit definition for GPIO_PUPDR register ******************/
#define GPIO_PUPDR_PUPDR0          ((uint32_t)0x00000003)
#define GPIO_PUPDR_PUPDR0_0        ((uint32_t)0x00000001)
#define GPIO_PUPDR_PUPDR0_1        ((uint32_t)0x00000002)
#define GPIO_PUPDR_PUPDR1          ((uint32_t)0x0000000C)
#define GPIO_PUPDR_PUPDR1_0        ((uint32_t)0x00000004)
#define GPIO_PUPDR_PUPDR1_1        ((uint32_t)0x00000008)
#define GPIO_PUPDR_PUPDR2          ((uint32_t)0x00000030)
#define GPIO_PUPDR_PUPDR2_0        ((uint32_t)0x00000010)
#define GPIO_PUPDR_PUPDR2_1        ((uint32_t)0x00000020)
#define GPIO_PUPDR_PUPDR3          ((uint32_t)0x000000C0)
#define GPIO_PUPDR_PUPDR3_0        ((uint32_t)0x00000040)
#define GPIO_PUPDR_PUPDR3_1        ((uint32_t)0x00000080)
#define GPIO_PUPDR_PUPDR4          ((uint32_t)0x00000300)
#define GPIO_PUPDR_PUPDR4_0        ((uint32_t)0x00000100)
#define GPIO_PUPDR_PUPDR4_1        ((uint32_t)0x00000200)
#define GPIO_PUPDR_PUPDR5          ((uint32_t)0x00000C00)
#define GPIO_PUPDR_PUPDR5_0        ((uint32_t)0x00000400)
#define GPIO_PUPDR_PUPDR5_1        ((uint32_t)0x00000800)
#define GPIO_PUPDR_PUPDR6          ((uint32_t)0x00003000)
#define GPIO_PUPDR_PUPDR6_0        ((uint32_t)0x00001000)
#define GPIO_PUPDR_PUPDR6_1        ((uint32_t)0x00002000)
#define GPIO_PUPDR_PUPDR7          ((uint32_t)0x0000C000)
#define GPIO_PUPDR_PUPDR7_0        ((uint32_t)0x00004000)
#define GPIO_PUPDR_PUPDR7_1        ((uint32_t)0x00008000)
#define GPIO_PUPDR_PUPDR8          ((uint32_t)0x00030000)
#define GPIO_PUPDR_PUPDR8_0        ((uint32_t)0x00010000)
#define GPIO_PUPDR_PUPDR8_1        ((uint32_t)0x00020000)
#define GPIO_PUPDR_PUPDR9          ((uint32_t)0x000C0000)
#define GPIO_PUPDR_PUPDR9_0        ((uint32_t)0x00040000)
#define GPIO_PUPDR_PUPDR9_1        ((uint32_t)0x00080000)
#define GPIO_PUPDR_PUPDR10         ((uint32_t)0x00300000)
#define GPIO_PUPDR_PUPDR10_0       ((uint32_t)0x00100000)
#define GPIO_PUPDR_PUPDR10_1       ((uint32_t)0x00200000)
#define GPIO_PUPDR_PUPDR11         ((uint32_t)0x00C00000)
#define GPIO_PUPDR_PUPDR11_0       ((uint32_t)0x00400000)
#define GPIO_PUPDR_PUPDR11_1       ((uint32_t)0x00800000)
#define GPIO_PUPDR_PUPDR12         ((uint32_t)0x03000000)
#define GPIO_PUPDR_PUPDR12_0       ((uint32_t)0x01000000)
#define GPIO_PUPDR_PUPDR12_1       ((uint32_t)0x02000000)
#define GPIO_PUPDR_PUPDR13         ((uint32_t)0x0C000000)
#define GPIO_PUPDR_PUPDR13_0       ((uint32_t)0x04000000)
#define GPIO_PUPDR_PUPDR13_1       ((uint32_t)0x08000000)
#define GPIO_PUPDR_PUPDR14         ((uint32_t)0x30000000)
#define GPIO_PUPDR_PUPDR14_0       ((uint32_t)0x10000000)
#define GPIO_PUPDR_PUPDR14_1       ((uint32_t)0x20000000)
#define GPIO_PUPDR_PUPDR15         ((uint32_t)0xC0000000)
#define GPIO_PUPDR_PUPDR15_0       ((uint32_t)0x40000000)
#define GPIO_PUPDR_PUPDR15_1       ((uint32_t)0x80000000)

/*******************  Bit definition for GPIO_IDR register  *******************/
#define GPIO_IDR_0                 ((uint32_t)0x00000001)
#define GPIO_IDR_1                 ((uint32_t)0x00000002)
#define GPIO_IDR_2                 ((uint32_t)0x00000004)
#define GPIO_IDR_3                 ((uint32_t)0x00000008)
#define GPIO_IDR_4                 ((uint32_t)0x00000010)
#define GPIO_IDR_5                 ((uint32_t)0x00000020)
#define GPIO_IDR_6                 ((uint32_t)0x00000040)
#define GPIO_IDR_7                 ((uint32_t)0x00000080)
#define GPIO_IDR_8                 ((uint32_t)0x00000100)
#define GPIO_IDR_9                 ((uint32_t)0x00000200)
#define GPIO_IDR_10                ((uint32_t)0x00000400)
#define GPIO_IDR_11                ((uint32_t)0x00000800)
#define GPIO_IDR_12                ((uint32_t)0x00001000)
#define GPIO_IDR_13                ((uint32_t)0x00002000)
#define GPIO_IDR_14                ((uint32_t)0x00004000)
#define GPIO_IDR_15                ((uint32_t)0x00008000)

/******************  Bit definition for GPIO_ODR register  ********************/
#define GPIO_ODR_0                 ((uint32_t)0x00000001)
#define GPIO_ODR_1                 ((uint32_t)0x00000002)
#define GPIO_ODR_2                 ((uint32_t)0x00000004)
#define GPIO_ODR_3                 ((uint32_t)0x00000008)
#define GPIO_ODR_4                 ((uint32_t)0x00000010)
#define GPIO_ODR_5                 ((uint32_t)0x00000020)
#define GPIO_ODR_6                 ((uint32_t)0x00000040)
#define GPIO_ODR_7                 ((uint32_t)0x00000080)
#define GPIO_ODR_8                 ((uint32_t)0x00000100)
#define GPIO_ODR_9                 ((uint32_t)0x00000200)
#define GPIO_ODR_10                ((uint32_t)0x00000400)
#define GPIO_ODR_11                ((uint32_t)0x00000800)
#define GPIO_ODR_12                ((uint32_t)0x00001000)
#define GPIO_ODR_13                ((uint32_t)0x00002000)
#define GPIO_ODR_14                ((uint32_t)0x00004000)
#define GPIO_ODR_15                ((uint32_t)0x00008000)

/****************** Bit definition for GPIO_BSRR register  ********************/
#define GPIO_BSRR_BS_0             ((uint32_t)0x00000001)
#define GPIO_BSRR_BS_1             ((uint32_t)0x00000002)
#define GPIO_BSRR_BS_2             ((uint32_t)0x00000004)
#define GPIO_BSRR_BS_3             ((uint32_t)0x00000008)
#define GPIO_BSRR_BS_4             ((uint32_t)0x00000010)
#define GPIO_BSRR_BS_5             ((uint32_t)0x00000020)
#define GPIO_BSRR_BS_6             ((uint32_t)0x00000040)
#define GPIO_BSRR_BS_7             ((uint32_t)0x00000080)
#define GPIO_BSRR_BS_8             ((uint32_t)0x00000100)
#define GPIO_BSRR_BS_9             ((uint32_t)0x00000200)
#define GPIO_BSRR_BS_10            ((uint32_t)0x00000400)
#define GPIO_BSRR_BS_11            ((uint32_t)0x00000800)
#define GPIO_BSRR_BS_12            ((uint32_t)0x00001000)
#define GPIO_BSRR_BS_13            ((uint32_t)0x00002000)
#define GPIO_BSRR_BS_14            ((uint32_t)0x00004000)
#define GPIO_BSRR_BS_15            ((uint32_t)0x00008000)
#define GPIO_BSRR_BR_0             ((uint32_t)0x00010000)
#define GPIO_BSRR_BR_1             ((uint32_t)0x00020000)
#define GPIO_BSRR_BR_2             ((uint32_t)0x00040000)
#define GPIO_BSRR_BR_3             ((uint32_t)0x00080000)
#define GPIO_BSRR_BR_4             ((uint32_t)0x00100000)
#define GPIO_BSRR_BR_5             ((uint32_t)0x00200000)
#define GPIO_BSRR_BR_6             ((uint32_t)0x00400000)
#define GPIO_BSRR_BR_7             ((uint32_t)0x00800000)
#define GPIO_BSRR_BR_8             ((uint32_t)0x01000000)
#define GPIO_BSRR_BR_9             ((uint32_t)0x02000000)
#define GPIO_BSRR_BR_10            ((uint32_t)0x04000000)
#define GPIO_BSRR_BR_11            ((uint32_t)0x08000000)
#define GPIO_BSRR_BR_12            ((uint32_t)0x10000000)
#define GPIO_BSRR_BR_13            ((uint32_t)0x20000000)
#define GPIO_BSRR_BR_14            ((uint32_t)0x40000000)
#define GPIO_BSRR_BR_15            ((uint32_t)0x80000000)

/****************** Bit definition for GPIO_LCKR register  ********************/
#define GPIO_LCKR_LCK0             ((uint32_t)0x00000001)
#define GPIO_LCKR_LCK1             ((uint32_t)0x00000002)
#define GPIO_LCKR_LCK2             ((uint32_t)0x00000004)
#define GPIO_LCKR_LCK3             ((uint32_t)0x00000008)
#define GPIO_LCKR_LCK4             ((uint32_t)0x00000010)
#define GPIO_LCKR_LCK5             ((uint32_t)0x00000020)
#define GPIO_LCKR_LCK6             ((uint32_t)0x00000040)
#define GPIO_LCKR_LCK7             ((uint32_t)0x00000080)
#define GPIO_LCKR_LCK8             ((uint32_t)0x00000100)
#define GPIO_LCKR_LCK9             ((uint32_t)0x00000200)
#define GPIO_LCKR_LCK10            ((uint32_t)0x00000400)
#define GPIO_LCKR_LCK11            ((uint32_t)0x00000800)
#define GPIO_LCKR_LCK12            ((uint32_t)0x00001000)
#define GPIO_LCKR_LCK13            ((uint32_t)0x00002000)
#define GPIO_LCKR_LCK14            ((uint32_t)0x00004000)
#define GPIO_LCKR_LCK15            ((uint32_t)0x00008000)
#define GPIO_LCKR_LCKK             ((uint32_t)0x00010000)

/****************** Bit definition for GPIO_AFRL register  ********************/
#define GPIO_AFRL_AFR0            ((uint32_t)0x0000000F)
#define GPIO_AFRL_AFR1            ((uint32_t)0x000000F0)
#define GPIO_AFRL_AFR2            ((uint32_t)0x00000F00)
#define GPIO_AFRL_AFR3            ((uint32_t)0x0000F000)
#define GPIO_AFRL_AFR4            ((uint32_t)0x000F0000)
#define GPIO_AFRL_AFR5            ((uint32_t)0x00F00000)
#define GPIO_AFRL_AFR6            ((uint32_t)0x0F000000)
#define GPIO_AFRL_AFR7            ((uint32_t)0xF0000000)

/****************** Bit definition for GPIO_AFRH register  ********************/
#define GPIO_AFRH_AFR8            ((uint32_t)0x0000000F)
#define GPIO_AFRH_AFR9            ((uint32_t)0x000000F0)
#define GPIO_AFRH_AFR10            ((uint32_t)0x00000F00)
#define GPIO_AFRH_AFR11            ((uint32_t)0x0000F000)
#define GPIO_AFRH_AFR12            ((uint32_t)0x000F0000)
#define GPIO_AFRH_AFR13            ((uint32_t)0x00F00000)
#define GPIO_AFRH_AFR14            ((uint32_t)0x0F000000)
#define GPIO_AFRH_AFR15            ((uint32_t)0xF0000000)

/* Old Bit definition for GPIO_AFRL register maintained for legacy purpose ****/
#define GPIO_AFRL_AFRL0            GPIO_AFRL_AFR0
#define GPIO_AFRL_AFRL1            GPIO_AFRL_AFR1
#define GPIO_AFRL_AFRL2            GPIO_AFRL_AFR2
#define GPIO_AFRL_AFRL3            GPIO_AFRL_AFR3
#define GPIO_AFRL_AFRL4            GPIO_AFRL_AFR4
#define GPIO_AFRL_AFRL5            GPIO_AFRL_AFR5
#define GPIO_AFRL_AFRL6            GPIO_AFRL_AFR6
#define GPIO_AFRL_AFRL7            GPIO_AFRL_AFR7

/* Old Bit definition for GPIO_AFRH register maintained for legacy purpose ****/
#define GPIO_AFRH_AFRH0            GPIO_AFRH_AFR8
#define GPIO_AFRH_AFRH1            GPIO_AFRH_AFR9
#define GPIO_AFRH_AFRH2            GPIO_AFRH_AFR10
#define GPIO_AFRH_AFRH3            GPIO_AFRH_AFR11
#define GPIO_AFRH_AFRH4            GPIO_AFRH_AFR12
#define GPIO_AFRH_AFRH5            GPIO_AFRH_AFR13
#define GPIO_AFRH_AFRH6            GPIO_AFRH_AFR14
#define GPIO_AFRH_AFRH7            GPIO_AFRH_AFR15

/****************** Bit definition for GPIO_BRR register  *********************/
#define GPIO_BRR_BR_0              ((uint32_t)0x00000001)
#define GPIO_BRR_BR_1              ((uint32_t)0x00000002)
#define GPIO_BRR_BR_2              ((uint32_t)0x00000004)
#define GPIO_BRR_BR_3              ((uint32_t)0x00000008)
#define GPIO_BRR_BR_4              ((uint32_t)0x00000010)
#define GPIO_BRR_BR_5              ((uint32_t)0x00000020)
#define GPIO_BRR_BR_6              ((uint32_t)0x00000040)
#define GPIO_BRR_BR_7              ((uint32_t)0x00000080)
#define GPIO_BRR_BR_8              ((uint32_t)0x00000100)
#define GPIO_BRR_BR_9              ((uint32_t)0x00000200)
#define GPIO_BRR_BR_10             ((uint32_t)0x00000400)
#define GPIO_BRR_BR_11             ((uint32_t)0x00000800)
#define GPIO_BRR_BR_12             ((uint32_t)0x00001000)
#define GPIO_BRR_BR_13             ((uint32_t)0x00002000)
#define GPIO_BRR_BR_14             ((uint32_t)0x00004000)
#define GPIO_BRR_BR_15             ((uint32_t)0x00008000)

// cpp_register описание GPIO

#include "register.hpp

namespace stm32f0x0::gpio {

template <const cpp_register::RegisterAddress address> struct GPIO_T {
	static constexpr cpp_register::Register<address + 0x0, cpp_register::AccessMode::RW, uint32_t, struct MODER> MODER{};
	static constexpr cpp_register::Register<address + 0x4, cpp_register::AccessMode::RW, uint32_t, struct OTYPER> OTYPER{};
	static constexpr cpp_register::Register<address + 0x8, cpp_register::AccessMode::RW, uint32_t, struct OSPEEDR> OSPEEDR{};
	static constexpr cpp_register::Register<address + 0xC, cpp_register::AccessMode::RW, uint32_t, struct PUPDR> PUPDR{};
	static constexpr cpp_register::Register<address + 0x10, cpp_register::AccessMode::R, uint32_t, struct IDR> IDR{};
	static constexpr cpp_register::Register<address + 0x14, cpp_register::AccessMode::RW, uint32_t, struct ODR> ODR{};
	static constexpr cpp_register::Register<address + 0x18, cpp_register::AccessMode::W, uint32_t, struct BSRR> BSRR{};
	static constexpr cpp_register::Register<address + 0x1C, cpp_register::AccessMode::RW, uint32_t, struct LCKR> LCKR{};
	static constexpr cpp_register::Register<address + 0x20, cpp_register::AccessMode::RW, uint32_t, struct AFR, 2> AFR{};
};

inline constexpr GPIO_T<0x48001400> const *GPIOF{};
inline constexpr GPIO_T<0x48000C00> const *GPIOD{};
inline constexpr GPIO_T<0x48000800> const *GPIOC{};
inline constexpr GPIO_T<0x48000400> const *GPIOB{};
inline constexpr GPIO_T<0x48000000> const *GPIOA{};


 struct GPIO_MODER {
	static constexpr cpp_register::Field<decltype(GPIOF->MODER), (1UL << 0), cpp_register::AccessMode::RW, 2, 16> MODER{};
};

struct GPIO_OTYPER {
	static constexpr cpp_register::Field<decltype(GPIOF->OTYPER), (1UL << 0), cpp_register::AccessMode::RW, 1, 16> OT{};
};

struct GPIO_OSPEEDR {
	static constexpr cpp_register::Field<decltype(GPIOF->OSPEEDR), (1UL << 0), cpp_register::AccessMode::RW, 2, 16> OSPEEDR{};
};

struct GPIO_PUPDR {
	static constexpr cpp_register::Field<decltype(GPIOF->PUPDR), (1UL << 0), cpp_register::AccessMode::RW, 2, 16> PUPDR{};
};

struct GPIO_IDR {
	static constexpr cpp_register::Field<decltype(GPIOF->IDR), (1UL << 0), cpp_register::AccessMode::R, 1, 16> IDR{};
};

struct GPIO_ODR {
	static constexpr cpp_register::Field<decltype(GPIOF->ODR), (1UL << 0), cpp_register::AccessMode::RW, 1, 16> ODR{};
};

struct GPIO_BSRR {
	static constexpr cpp_register::Field<decltype(GPIOF->BSRR), (1UL << 16), cpp_register::AccessMode::W, 1, 16> BR{};
	static constexpr cpp_register::Field<decltype(GPIOF->BSRR), (1UL << 0), cpp_register::AccessMode::W, 1, 16> BS{};
};

struct GPIO_LCKR {
	static constexpr cpp_register::Field<decltype(GPIOF->LCKR), (1UL << 16), cpp_register::AccessMode::RW, 1> LCKK{};
	static constexpr cpp_register::Field<decltype(GPIOF->LCKR), (1UL << 0), cpp_register::AccessMode::RW, 1, 16> LCK{};
};

struct GPIO_AFR {
	static constexpr cpp_register::Field<decltype(GPIOF->AFR), (1UL << 0), cpp_register::AccessMode::RW, 4, 8> AFR{};
};

}

Теперь, я бы хотел показать то, как я решил каждый пункт ТЗ. И начать я бы хотел с основного, с проверок безопасности. Я подробно разберу одну из проверок, так как все они работают схожими принципами. Использовать я буду регистры GPIO, описание которых было дано выше.

Содержит ли регистр данное поле?

// Устанавливаю в регистре GPIO->OSPEEDR значение другого регистра GPIO->OTYPER (8ой пин)
GPIOA->OSPEEDR |= GPIO_OTYPER::OT[NUM_8]; // Ошибка времени написания/компиляции кода!

// Диагностика будет работать и со многими полями (тут тоже будет ошибка написания/компиляции)
GPIOA->OSPEEDR |= GPIO_OSPEEDR::OSPEEDR[NUM_11](NUM_1) | GPIO_OTYPER::OT[NUM_8] | GPIO_OSPEEDR::OSPEEDR[NUM_9](NUM_0) | GPIO_OSPEEDR::OSPEEDR[NUM_10](NUM_1);

Как я уже упоминал, даже при одновременной операции с несколькими полями, проводится каждая проверка для каждого поля.

Я бы также хотел показать то, как это выглядит за столом разработчика. То, что что-то не так видно еще во время написания кода:

Красное подчеркивание во время написания кода при ошибке
Красное подчеркивание во время написания кода при ошибке

Хоть описание ошибки и не очень хорошее, уже можно видеть что что-то не так. Попробуем скомпилировать:

sources/main.cpp:16:42:   required from here
components\cpp_register/register.hpp:574:15:   required by the constraints of 'template<unsigned int tpAddress, unsigned char tpAccess, class SizeT, class FieldT, short unsigned int tpRegNumber, short unsigned int tpStep> template<class Value>  requires (field<Value>) && (is_same_v<Field, typename Value::Register::Field>) && static_cast<bool>((Value::sc_Access) & cpp_register::Register<tpAddress, tpAccess, SizeT, FieldT, tpRegNumber, tpStep>::sc_Access & (cpp_register::AccessMode::SET)) void cpp_register::Register<tpAddress, tpAccess, SizeT, FieldT, tpRegNumber, tpStep>::operator|=(Value) const'
components\cpp_register/register.hpp:572:34: note: the expression 'is_same_v<Field, typename Value::Register::Field> [with tpAddress = 1207959560; tpAccess = 31; SizeT = long unsigned int; FieldT = stm32f0x0::gpio::OSPEEDR; tpRegNumber = 1; tpStep = 4; Value = cpp_register::Field<const cpp_register::Register<1207964676, 31, long unsigned int, stm32f0x0::gpio::OTYPER, 1, 4>, 256, 31, 1, 16, 0>]' evaluated to 'false'
  572 |   requires field<Value> && (std::is_same_v<Register::Field, typename Value::Register::Field>) &&
      |                            ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
make: *** [build\debug\obj/sources/main.o] Ошибка 1

Тут уже 'более' понятно, что не выполняется требование того, что данное поле принадлежит этому же регистру.

А вот как может выглядеть подсветка синтаксиса:

Пример подсветки синтаксиса
Пример подсветки синтаксиса

Очень важно и то, что авто-дополнение также отлично работает, как для регистров, так и для полей.

Такой же принцип обнаружения ошибок используется и для всех проверок. А я напомню, что это:

  • Содержит ли регистр данное поле?

  • Запись одного и того же поля дважды

  • Режим доступа поля/регистра (поддерживаемые режимы доступа в реализации)

  • Переполнение регистра

Функциональность модуля

Теперь я бы хотел продемонстрировать все доступные операции cpp_register (возвращаясь к гибкости и читаемости):

  • Установка бита/битов '|=' в регистре (автоматический bit-band внутри, см. реализацию)

GPIOA->ODR |= GPIO_ODR::ODR[NUM_4];
  • Сброс бита/битов '&=' в регистре (автоматический bit-band внутри, см. реализацию)

GPIOA->ODR &= GPIO_ODR::ODR[NUM_4] | GPIO_ODR::ODR[NUM_10];

Важно: традиционный оператор сброса '&=~' был заменен на '&=' нарочно (для удобства), однако все еще возможно использование '&=~'. По сути, '~' не имеет эффекта для полей.

  • Запись бита/битов '=' в регистр

GPIOA->BSRR = GPIO_BSRR::BR[NUM_5];
  • Переключение бита/битов '^=' в регистре

GPIOA->ODR ^= GPIO_ODR::ODR[NUM_4 | NUM_10 | NUM_15];
  • Чтение бита/битов регистра, бинарный '&'

// Для одного поля вернет boolean
if(GPIOA->IDR & GPIO_IDR::IDR[NUM_0]) { ... }

// Несколько полей вернет беззнаковый тип размера регистра
if(GPIOA->IDR & (GPIO_IDR::IDR[NUM_0] | GPIO_IDR::IDR[NUM_2])) { ... }
  • Операторы сравнения ('==' and '!=') также были перегружены

// Реальный пример с кварцем
enum class : unsigned {
    HSI = 0b01,
    HSE = 0b10,
    MASK = 0b11
}

// Проверка того, что установился HSE (чтение по маске -> сравнение со значением HSE)
while(RCC_CFGR::SWS(reg_v<HSE>) != (RCC->CFGR & RCC_CFGR::SWS(reg_v<MASK>))) { ... }
  • Чтение регистра целиком '*'

const volatile auto value = *(GPIOA->IDR);
  • Только для записи доступны динамические значения (проверок быть не может, кроме размеров и режима доступа). К примеру, результаты вычислений отправятся по UART или буфер в DMA

// Магическое число, вычисленное ранее
uint8_t magic = 42U;
UART1->DR = magic;

// Локальный буффер по DMA
template<const size_t N>
bool send(char (&buf)[N]) {
  <...>
  DMA1->CPAR2 = &buf;
  <...>
}
  • Взятие адреса регистра, унарный '&' (операция времени компиляции)

const uint32_t address = &(GPIOA->IDR);
  • О загадочных NUM_x. На самом деле, это константы времени компиляции, которые записываются в поля (см. RegVal реализации). Создать данную константу проще всего через встраиваемую шаблонную переменную времени компиляции.

// Период системного таймера
static constexpr auto SYSTEM_MHZ = 16UL;
static constexpr auto SYST_PERIOD = reg_v<(SYSTEM_MHZ * 1000000UL) - 1>;
SYST->RVR = SYST_RVR::RELOAD(SYST_PERIOD);
  • Как уже упоминалось, операции с несколькими полями за раз

SYST->CSR |= (SYST_CSR::CLKSOURCE | SYST_CSR::ENABLE);
  • На примере MODER, операции с несколькими похожими полями, можно использовать два варианта написания.

// Полная форма
// Установить GPIOD пины 12, 13, 14, 15 как выходы
GPIOD->MODER |= (GPIO_MODER::MODER[NUM_12](NUM_0) | GPIO_MODER::MODER[NUM_13](NUM_0) 
                | GPIO_MODER::MODER[NUM_14](NUM_0) | GPIO_MODER::MODER[NUM_15](NUM_0));
// Короткая форма
// Установить GPIOD пины 12, 13, 14, 15 как выходы
GPIOD->MODER |= GPIO_MODER::MODER[NUM_12 | NUM_13 | NUM_14 | NUM_15](NUM_0);
  • Массивы регистров на примере AFR

GPIOA->AFR[NUM_1] |= GPIO_AFR::AFR[NUM_2](NUM_1 | NUM_0);
  • Немного подробнее о переменных времени компиляции для полей, варианты создания

// Беззнаковое целое
static constexpr auto SYST_CLOCK = reg_v<168000000UL>;
// Значение enum class
enum class Mode : uint32_t { Input, Output };

...
static constexpr auto MODE = cpp_register::reg_v<Mode::Output>; // каст не нужен
// Адрес статического буффера известен во время компиляции и может быть использован
static uint8_t buffer[8];
REG_TEST->TEST1 = TEST1_POINTER::VAL1(reg_v<buffer>);

Помимо операций, я бы хотел немного раскрыть тему автоматического bit-band.Если вдруг кто-то не знает, то это фишка ядер Cortex-M3/M4, которая позволяет слегка уменьшить размер и увеличить скорость исполняемого кода. И самое главное, с помощью bit-band можно превратить не атомарные операции чтение->модификация->запись в атомарную запись. Чтобы включить данную фичу в модуле, нужно передать -DCORTEX_M_BIT_BAND при сборке проекта. В этом случае, для операций установки и сброса битов bit-band будет применен автоматически, если значения удовлетворяют определенным условиям. Еще важно и то, что пользовательский алгоритм также поддерживается, например RP2040 имеет свой алгоритм (-DBIT_BAND_CUSTOM), и он может быть добавлен в cpp_register.

Реализация

Я опишу только самые основные моменты реализации.

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

По сути, вся основная реализация - это один заголовочный файл - register.hpp. Попробую пройтись по основным моментам этого файла.

Для начала я определил размерность адреса регистра как size_t, потому что обычно (хоть и не всегда), адрес регистра имеет ту же размерность, что и адресное пространство процессора. Как минимум, это справедливо для всех 32 битных Cortex-M систем, что я встречал.

// Register address size is 32 bits all Cortex-M and the most of ARM
using RegisterAddress = size_t;

Далее я задумался, а что вообще можно писать в регистр, а что не следует? Например, в CMSIS это обычно какое-то беззнаковое число, и обычно размера в 32 бита, как и большинство регистров.

// Пример определения полей уже упомянутого регистра MODER в файле от производителя
<...>
#define GPIO_MODER_MODER14 ((uint32_t)0x30000000)
#define GPIO_MODER_MODER14_0 ((uint32_t)0x10000000)
#define GPIO_MODER_MODER14_1 ((uint32_t)0x20000000)

#define GPIO_MODER_MODER15 ((uint32_t)0xC0000000)
#define GPIO_MODER_MODER15_0 ((uint32_t)0x40000000)
#define GPIO_MODER_MODER15_1 ((uint32_t)0x80000000)
<...>

Поразмышляв на эту тему, я смог выделить три варианта. Этот выбор описывает нижеследующий концепт:

/**
 * @brief Compile-time check if the type if one of the register value type: unsigned arithmetic, pointer (to static) or enum (class) with base
 * unsigned arithmetic
 *
 * @tparam Value Type to check
 */
template <typename Value>
concept register_value =
    std::is_unsigned_v<Value> || std::is_pointer_v<Value> 
    || (std::is_enum_v<Value> && std::is_unsigned_v<std::underlying_type_t<Value>>);

Согласно концепции, значением регистра может быть:

  1. Беззнаковый арифметический тип.

  2. Указатель - полезно для DMA, в случае статического буфера, адрес известен на этапе компиляции, а значит может быть проверен на nullptr.

  3. Тип перечисления с базовым беззнаковым арифметическим типом.

Небольшое пояснение по пункту 3. На уровне драйверов, удобно принимать параметры типом enum class, потому что он имеет только заранее определенные значения и type-safe. Отсюда же вытекает необходимость вручную прописывать 'static_cast' каждый раз на этом же уровне драйверов. И вот чтобы этого не делать, я решил оставить 'static_cast' внутри уровня регистров, принимая значения 'enum class' на этом же уровне. Конкретнее, пример таких значений 'enum class' для GPIO:

enum class Mode : uint32_t { Input, Output, Alternate, Analog };
enum class Type : uint32_t { PushPull, OpenDrain };
enum class Speed : uint32_t { Low, Medium, High = 0b11 };
enum class Pull : uint32_t { None, Up, Down };
enum class Alternative : uint32_t { AF0, AF1, AF2, AF3, AF4, AF5, AF6, AF7 };

Раз уж я заговорил о концептах. На данный момент, я бы выделил три основных инструмента для реализации уже много раз упомянутых проверок безопасности: static_assert, SFINAE, constraints and concepts (c C++20). Все эти способы используются в проекте, однако больше всего используются именно концепты, так как они имеют плюсы двух предыдущих инструментов: текст ошибки более или менее понятен (static_assert) и ошибки видны еще во время написания (SFINAE). Описания принципов SFINAE и возможностей концептов выходят за рамки этой статьи. Самое важное то, что именно это трио используются для вызова ошибок времени компиляции в случае, если программист сделал запрещенную операцию с регистром или полем.

Теперь я хотел бы показать три основных класса программы, и первый из них, класс RеgVal имеет следующий вид (оставлены только критически важные сущности, полностью класс можно посмотреть тут):

/**
 * @brief Class to make const Register value to write to register
 *
 * @tparam tpValue Desired const value, can be one of unsigned arithmetic, pointer (to static) or enum (class) with base unsigned arithmetic
 * @tparam tpOffset Desired const offset (additional, considered only valid only for unsigned and enum values)
 */
template <const auto tpValue, const uint8_t tpOffset = 0>
requires register_value<decltype(tpValue)> && register_offset<decltype(tpValue), tpOffset> && register_pointer<tpValue, tpOffset>
class RegVal final {
...
public:
  static constexpr uint8_t sc_Offset = tpOffset;
  static constexpr auto sc_Value = cast_value(tpValue, sc_Offset);
  static constexpr auto sc_IsPointer = std::is_pointer_v<decltype(tpValue)>;
  
  /**
   * @brief Operator '|' to make 'or' with RegVal objects
   *
   * @constraints:  1) Value should be RegVal type
   *                2) Value bits of the operands should not be the same
   *                3) Values should not be a pointers
   *
   * @tparam Value Const value that should be 'or'
   * @return RegVal<sc_Value | Value::sc_Value> produced type with new result value (the offset loose the sense)
   */
  template <typename Value>
  requires reg_val<Value> && (!((std::is_pointer_v<decltype(Value::sc_Value)>) || (std::is_pointer_v<decltype(sc_Value)>))) &&
           (!(sc_Value & Value::sc_Value))
  consteval auto operator|(const Value) const -> RegVal<sc_Value | Value::sc_Value> {
    return {};
  }
};
...

Класс принимает значение типа auto, а также его сдвиг, по умолчанию равный нулю. Важно обратить внимание на ограничения, описанные в строке 8, слева на право: значение должно удовлетворять концепту register_value (описан выше), register_offset говорит что смещение в битах не может быть больше размера типа в битах, а register_pointer проверят указатель на nullptr и сдвиг на 0. Публичный интерфейс содержит три переменные, чье назначение очевидно из названий.

Идея этого класса в том, чтобы получить, скажем так, истинный constexpr параметр. Именно этот класс помогает осуществить очень важный трюк, необходимый для реализации всего модуля. Данный трюк - использование перегрузок операторов (как можно было догадаться перечисления доступных операторов) совместно с параметрами времени компиляции. Под истинным constexpr параметром я понимаю параметр функции, который может использоваться вместе со static_assert, SFINAE, concept and requires, и так далее. Приведу небольшой пример (см. комментарии к коду):

// Ошибочный пример
void Function(bool parameter) {
  static_assert(!parameter, "Понятное дело, что так нельзя")
}

// Ошибочный пример
consteval void Function(bool parameter) {
  static_assert(!parameter, "И даже так нельзя, так как даже параметры 
                "constexpr/consteval функций не являются constexpr/consteval")
}

// Очевидное решение, использование non-type параметров, работает даже с функциями времени выполнения
template<const bool parameter>
void Function() {
  static_assert(!parameter, "Да, так действительно можно")
}

// Но что делать с перегрузками? 
// Я не знаю способа, использовать в перегрузках non-type парметры
SomeObject& operator[](const MyEnumIter iter) const {
  static_assert(Iter < SomeValue, "Так нельзя, это стандартная перегрузка, параметр не constexpr")
  <...>
}

// Нет подобного синтаксиса
template<const MyEnumIter iter>
SomeObject& operator[]() const {
  static_assert(Iter < SomeValue, "Такое компилятор не понимает")
  <...>
}

// Именно эту проблему и решает класс RegVal в данном модуле
template<reg_val Value> // concept RegVal
SomeObject& operator[](const Value /* Важен только тип, сам параметр не прописывается*/) const { 
  static_assert(Value::value < SomeValue, "А вот так будет можно в любой функции")
  <...>
}

Важно и то, что класс поддерживает операцию битового 'или' (' | '), и тут так-же есть одна очень важная идея, идущая через весь модуль: опираясь на текущий тип объекта и значения его полей, операция ' | ' возвращает новый, производный тип, который зависит также от значений времени компиляции принимаемого объекта. Рассмотрим поближе:

  template <typename Value>
  // Принимается только RegVal, значения обоих обектов не должны быть указателями, значение не должно быть одним и тем-же
  requires reg_val<Value> && (!((std::is_pointer_v<decltype(Value::sc_Value)>) || (std::is_pointer_v<decltype(sc_Value)>))) &&
           (!(sc_Value & Value::sc_Value))
  consteval auto operator|(const Value) const -> /* Тот самый производный тип */ RegVal<sc_Value | Value::sc_Value> {
    return {}; // Возвращаем объект по умолчанию
  }

Второй важный класс, это класс, реализующий тип поля, вот его сокращенное описание (оставлены только критически важные сущности, полностью класс можно посмотреть тут):

/**
 * @brief Field class with data and operations for the register fields
 *
 * @tparam T The register type of the field
 * @tparam tpValue The value of the field
 * @tparam tpAccess The access mode of the field
 * @tparam tpFieldSize The size of the field
 * @tparam tpFieldNumber The number of the same fields in the register
 * @tparam tpStep The span between the same fields in the register
 */
template <typename T, const typename T::Size tpValue, const uint8_t tpAccess, const uint8_t tpFieldSize, const uint8_t tpFieldNumber = 1,
          const uint8_t tpStep = 0>
requires val_valid::register_value<decltype(tpValue)>
class Field final {
public:
  using Register = T;                         // Type of the register which contains the field
  static constexpr auto sc_Value = tpValue;   // The field value
  static constexpr auto sc_Access = tpAccess; // The field access mode

  // Operator '|' to make 'or'  with field objects
  template <typename Value>
  requires field<Value> && std::is_same_v<typename Value::Register, Register>
  [[nodiscard]] consteval auto operator|(const Value) const noexcept -> Field<Register, (sc_Value | Value::sc_Value), (sc_Access & Value::sc_Access), sc_Size, sc_Number>;

  // Operator '~' for '&=' operation (reset bit). Only for the compatibility.
  [[nodiscard]] consteval auto operator~() const noexcept -> Field<Register, sc_Value, sc_Access, sc_Size, sc_Number>;

  // Operator '[]' to create the same fields in the register
  template <typename FieldNumber>
  requires val_valid::reg_val<FieldNumber> && (sc_Number > 1) && (FieldNumber::sc_Offset <= sc_Number)
  [[nodiscard]] consteval auto operator[](const FieldNumber) const noexcept
      -> Field<Register, scl_FormField(sc_Value, FieldNumber::sc_Value, FieldNumber::sc_Offset), (scl_isCompound(FieldNumber::sc_Value) | sc_Access), sc_Size, sc_Number>;

  // Operator '()' to create multi-bit fields for not a pointer
  template <typename BitNumber>
  requires val_valid::reg_val<BitNumber> && (!BitNumber::sc_IsPointer)
  [[nodiscard]] consteval auto operator()(const BitNumber) const noexcept
      -> Field<Register, scl_WriteField(BitNumber::sc_Value), sc_Access, sc_Size, sc_Number>;

  // Operator '()' to create multi-bit fields for a pointer
  template <typename BitNumber>
  requires val_valid::reg_val<BitNumber> && (BitNumber::sc_IsPointer)
  [[nodiscard]] consteval auto operator()(const BitNumber) const noexcept
      -> Field<Register, pointer_cast<BitNumber::sc_Value>::value, sc_Access, sc_Size, sc_Number>;

  // Comparison operator overload to check if some bits are set, need to compare not-constexpr value
  [[nodiscard]] inline constexpr bool operator==(const Register::Size value) const noexcept;
  [[nodiscard]] inline constexpr bool operator!=(const Register::Size value) const noexcept;
};

Думаю, что проще всего показать работу этого класса на примере. Я предлагаю взять простой случай с GPIO, так как эти регистры знакомы многим, например уже упомянутый регистр MODER. Напомню его описание:

Регистр GPIOx_MODER, MCU STM32F070
Регистр GPIOx_MODER, MCU STM32F070

А вот описание его полей используя класс Field:

// То, как это выглядит в файле, три строки
struct GPIO_MODER {
	static constexpr cpp_register::Field<decltype(GPIOF->MODER), (1UL << 0), cpp_register::AccessMode::RW, 2, 16> MODER{};
};
// То же самое, но с пояснениями
// Инкапсуляция полей регистра
struct GPIO_MODER {
    // Constexpr объект, не занимает места в памяти
	static constexpr cpp_register::Field<
      decltype(GPIOF->MODER),        // (T) тип регистра, нужно для проверки принадлежности к регистру, сам порт (GPIOF) не важен
      (1UL << 0),                    // (tpValue) начальное смещение поля, здесь за основу берется MODER 0
      cpp_register::AccessMode::RW,  // (tpAccess) режим доступа к полю (чтение, запись, ...)
      2,                             // (tpFieldSize) размер поля, тут 2 бита
      16                             // (tpFieldNumber = 1) количество одинаковых полей, тут 16 (MODER0...MODER15)
      /* 0 */>                       // (tpStep = 0) не всегда одинаковые поля идут подряд, переменная указывает шаг между полями (тут идут подряд => 0)  
      MODER{};
};

А вот, еще раз, размер объявления этого-же регистра в CMSIS, где не реализованы массивы полей:

Далеко не пару строк, описывается каждый бит
/******************************************************************************/
/*                                                                            */
/*                            General Purpose I/O                             */
/*                                                                            */
/******************************************************************************/
/******************  Bits definition for GPIO_MODER register  *****************/
#define GPIO_MODER_MODER0 ((uint32_t)0x00000003)
#define GPIO_MODER_MODER0_0 ((uint32_t)0x00000001)
#define GPIO_MODER_MODER0_1 ((uint32_t)0x00000002)

#define GPIO_MODER_MODER1 ((uint32_t)0x0000000C)
#define GPIO_MODER_MODER1_0 ((uint32_t)0x00000004)
#define GPIO_MODER_MODER1_1 ((uint32_t)0x00000008)

#define GPIO_MODER_MODER2 ((uint32_t)0x00000030)
#define GPIO_MODER_MODER2_0 ((uint32_t)0x00000010)
#define GPIO_MODER_MODER2_1 ((uint32_t)0x00000020)

#define GPIO_MODER_MODER3 ((uint32_t)0x000000C0)
#define GPIO_MODER_MODER3_0 ((uint32_t)0x00000040)
#define GPIO_MODER_MODER3_1 ((uint32_t)0x00000080)

#define GPIO_MODER_MODER4 ((uint32_t)0x00000300)
#define GPIO_MODER_MODER4_0 ((uint32_t)0x00000100)
#define GPIO_MODER_MODER4_1 ((uint32_t)0x00000200)

#define GPIO_MODER_MODER5 ((uint32_t)0x00000C00)
#define GPIO_MODER_MODER5_0 ((uint32_t)0x00000400)
#define GPIO_MODER_MODER5_1 ((uint32_t)0x00000800)

#define GPIO_MODER_MODER6 ((uint32_t)0x00003000)
#define GPIO_MODER_MODER6_0 ((uint32_t)0x00001000)
#define GPIO_MODER_MODER6_1 ((uint32_t)0x00002000)

#define GPIO_MODER_MODER7 ((uint32_t)0x0000C000)
#define GPIO_MODER_MODER7_0 ((uint32_t)0x00004000)
#define GPIO_MODER_MODER7_1 ((uint32_t)0x00008000)

#define GPIO_MODER_MODER8 ((uint32_t)0x00030000)
#define GPIO_MODER_MODER8_0 ((uint32_t)0x00010000)
#define GPIO_MODER_MODER8_1 ((uint32_t)0x00020000)

#define GPIO_MODER_MODER9 ((uint32_t)0x000C0000)
#define GPIO_MODER_MODER9_0 ((uint32_t)0x00040000)
#define GPIO_MODER_MODER9_1 ((uint32_t)0x00080000)

#define GPIO_MODER_MODER10 ((uint32_t)0x00300000)
#define GPIO_MODER_MODER10_0 ((uint32_t)0x00100000)
#define GPIO_MODER_MODER10_1 ((uint32_t)0x00200000)

#define GPIO_MODER_MODER11 ((uint32_t)0x00C00000)
#define GPIO_MODER_MODER11_0 ((uint32_t)0x00400000)
#define GPIO_MODER_MODER11_1 ((uint32_t)0x00800000)

#define GPIO_MODER_MODER12 ((uint32_t)0x03000000)
#define GPIO_MODER_MODER12_0 ((uint32_t)0x01000000)
#define GPIO_MODER_MODER12_1 ((uint32_t)0x02000000)

#define GPIO_MODER_MODER13 ((uint32_t)0x0C000000)
#define GPIO_MODER_MODER13_0 ((uint32_t)0x04000000)
#define GPIO_MODER_MODER13_1 ((uint32_t)0x08000000)

#define GPIO_MODER_MODER14 ((uint32_t)0x30000000)
#define GPIO_MODER_MODER14_0 ((uint32_t)0x10000000)
#define GPIO_MODER_MODER14_1 ((uint32_t)0x20000000)

#define GPIO_MODER_MODER15 ((uint32_t)0xC0000000)
#define GPIO_MODER_MODER15_0 ((uint32_t)0x40000000)
#define GPIO_MODER_MODER15_1 ((uint32_t)0x80000000)

А вот пример полей с шагом для регистра IPR модуля NVIC:

Регистр NVIC_IPR, ARMv6m architecture
Регистр NVIC_IPR, ARMv6m architecture
// Между одинаковыми полями зарезервированно по 6 бит, это же прописанно последним параметром
struct NVIC_IPR {
  static constexpr cpp_register::Field<decltype(NVIC->IPR), (1UL << 6), cpp_register::AccessMode::RW, 2, 4, 6> PRI_N{};
};

Теперь о режимах доступа, я немного расширил стандартный список режимов доступа, добавив туда много других помимо read, write, read-write.

  // All operation that can be done with fields registers  
  enum AvailableOperation : uint8_t {
    NONE = 0,

    SET = (1 << 0),    // Equivalent to '|='
    RESET = (1 << 1),  // Equivalent to '&=~'
    ASSIGN = (1 << 2), // Equivalent to '='
    TOGGLE = (1 << 3), // Equivalent to '^='
    READ = (1 << 4),   // Equivalent to '&' or '*'

    MASK = 0xFF
  };

  // All options of access modes for fields
  static constexpr auto RW = (SET | RESET | ASSIGN | TOGGLE | READ);   // read/write
  static constexpr auto R = (READ);                                    // read-only
  static constexpr auto W = (SET | RESET | ASSIGN | TOGGLE);           // write-only
  static constexpr auto RC_W0 = (RESET | READ);                        // read/clear (by '0')
  static constexpr auto RC_W1 = (SET | READ);                          // read/clear (by '1')
  static constexpr auto RC_R = (READ);                                 // read/clear (by read)
  static constexpr auto RC_W = (SET | RESET | ASSIGN | TOGGLE | READ); // read/clear (by write)
  static constexpr auto RS = (SET | READ);                             // read/set
  static constexpr auto RS_R = (READ);                                 // read/set by read
  static constexpr auto RWO = (SET | RESET | ASSIGN | TOGGLE | READ);  // read/write once
  static constexpr auto WO = (SET | RESET | ASSIGN | TOGGLE);          // write once
  static constexpr auto RT_W = (SET | RESET | ASSIGN | TOGGLE | READ); // read-only/trigger
  static constexpr auto RT_W1 = (SET | READ);                          // read-only/write trigger
  static constexpr auto T = (TOGGLE);                                  // toggle
  static constexpr auto RES = (NONE);                                  // reserved

Режимы доступа проверяются для каждого поля при операциях с регистрами. Кстати, об операциях над регистрами...

Также приведу объявление класса Register (опят же, детали в тут):

/**
 * @brief Register class with data and operations with it
 *
 * @tparam tpAddress The address of the register
 * @tparam tpAccess The access mode for the register
 * @tparam SizeT The size type of the register
 * @tparam FieldT The type of field according the register
 * @tparam tpRegNumber The quantity of the same registers in the array
 * @tparam tpStep The step between the same registers in the array
 *
 */
template <const RegisterAddress tpAddress, const uint8_t tpAccess, typename SizeT, typename FieldT, const uint16_t tpRegNumber = 1,
          const uint16_t tpStep = sizeof(SizeT)>
class Register final {
<...>
// Тут все стандартные операции, что я перечислял ранее
};

И вот пример с описанием регистров GPIO для stm32f070, вместе с уже знакомым регистром MODER:

template <const cpp_register::RegisterAddress address> struct GPIO_T {
	static constexpr cpp_register::Register<address + 0x0, cpp_register::AccessMode::RW, uint32_t, struct MODER> MODER{};
	static constexpr cpp_register::Register<address + 0x4, cpp_register::AccessMode::RW, uint32_t, struct OTYPER> OTYPER{};
	static constexpr cpp_register::Register<address + 0x8, cpp_register::AccessMode::RW, uint32_t, struct OSPEEDR> OSPEEDR{};
	static constexpr cpp_register::Register<address + 0xC, cpp_register::AccessMode::RW, uint32_t, struct PUPDR> PUPDR{};
	static constexpr cpp_register::Register<address + 0x10, cpp_register::AccessMode::R, uint32_t, struct IDR> IDR{};
	static constexpr cpp_register::Register<address + 0x14, cpp_register::AccessMode::RW, uint32_t, struct ODR> ODR{};
	static constexpr cpp_register::Register<address + 0x18, cpp_register::AccessMode::W, uint32_t, struct BSRR> BSRR{};
	static constexpr cpp_register::Register<address + 0x1C, cpp_register::AccessMode::RW, uint32_t, struct LCKR> LCKR{};
	static constexpr cpp_register::Register<address + 0x20, cpp_register::AccessMode::RW, uint32_t, struct AFR, 2> AFR{};
};
// На примере MODER
static constexpr cpp_register::Register<
  address + 0x0,                    // (tpAddress) адрес регистра относительно периферийного блока
  cpp_register::AccessMode::RW,     // (tpAccess)  режим доступа регистра, так же как и выше
  uint32_t,                         // (SizeT) типа размерности регистра, проверка на переполнение
  struct MODER                      // (FieldT) уникальный тип, используется полями для проверки принадлежности
  /* 1U */                          // (tpRegNumber) как и с полями, количество регистров в массиве одинаковых
  /* sizeof(uint32_t) */            // (tpStep) шаг, также как и полей
  > MODER{};

И опять же, пример массива регистров с шагом для USB, упомянутый в ТЗ:

// Два последних параметров, 8 регистров, по 8 байт между каждым
template <const cpp_register::RegisterAddress address> struct USB_BUFFER_T {
  static constexpr cpp_register::Register<address + 0x0, cpp_register::AccessMode::RW, uint16_t, struct USB_ADDRn_TX, 8U, 8U> USB_ADDRn_TX{};
  static constexpr cpp_register::Register<address + 0x2, cpp_register::AccessMode::RW, uint16_t, struct USB_COUNTn_TX, 8U, 8U> USB_COUNTn_TX{};
  static constexpr cpp_register::Register<address + 0x4, cpp_register::AccessMode::RW, uint16_t, struct USB_ADDRn_RX, 8U, 8U> USB_ADDRn_RX{};
  static constexpr cpp_register::Register<address + 0x6, cpp_register::AccessMode::RW, uint16_t, struct USB_COUNTn_RX, 8U, 8U> USB_COUNTn_RX{};
};

// Массивы удобны прежде всего для обращения, как пример, фрагмент функции ниже
constexpr auto EP = _EP_NUMBER<0U>;
constexpr auto EP_TYPE = _EP_TYPE<EndpointType::Control>;
constexpr auto EP_BUFFER_SIZE = 64U;

USB_BUFFER->USB_ADDRn_TX[EP] = USB_ADDRn_TX::ADDRn_TX(reg_v<_MESSAGE_MEMORY_ADDRESS>);
USB_BUFFER->USB_ADDRn_RX[EP] = USB_ADDRn_RX::ADDRn_RX(reg_v<_MESSAGE_MEMORY_ADDRESS + EP_BUFFER_SIZE>);
USB_BUFFER->USB_COUNTn_TX[EP] = USB_COUNTn_TX::COUNTn_TX(constants::ZERO);
USB_BUFFER->USB_COUNTn_RX[EP] = USB_COUNTn_RX::BLSIZE | USB_COUNTn_RX::NUM_BLOCK(reg_v<EP_BUFFER_SIZE / 32 - 1>);

Блок периферии может быть объявлен следующим образом:

// Указатель для сохранения схожести с CMSIS
inline constexpr GPIO_T<0x48001400> const *GPIOF{};
inline constexpr GPIO_T<0x48000C00> const *GPIOD{};
inline constexpr GPIO_T<0x48000800> const *GPIOC{};
inline constexpr GPIO_T<0x48000400> const *GPIOB{};
inline constexpr GPIO_T<0x48000000> const *GPIOA{};

И последнее, я хотел бы привести пример одной операции, установки бита из класса Register, и немного разобрать её

  /**
   * @brief 'Set' or '|=' for the register (with bit-band)
   *
   * @constraints:  1) Value should be Field type
   *                2) The register should have the field (Value)
   *                3) Access mode for the register and the field should be AccessMode::SET
   *
   * @tparam Value The value (Field type) should be set
   *
   */
  template <typename Value>
  requires field<Value> && (std::is_same_v<Register::Field, typename Value::Register::Field>) &&
           (static_cast<bool>(Value::sc_Access & sc_Access & AccessMode::SET))
  inline void operator|=(const Value) const noexcept {
    constexpr auto bitBandAddress = get_bit_band_address(Value{});
    // Если адрес был персчитан (удовлетворял условиям), то применяем bit-band
    if constexpr (sc_Address != bitBandAddress) {
      *reinterpret_cast<volatile Size *>(bitBandAddress) = 1;
    } else {
      *reinterpret_cast<volatile Size *>(sc_Address) = *reinterpret_cast<volatile Size *>(sc_Address) | Value::sc_Value;
    }
  }

Да, я хотел бы поговорить об уже упомянутом bit-band. Я посчитал, что раз я перегружаю все битовые операции, то логично было бы встроить bit-band. Ведь было бы очень здорово, если бы программисту больше не пришлось руками добавлять bit-band где возможно, а компилятор сам его вставлял, добавляя атомарность и поднимая производительность. Но как понять когда его можно применить, а где нет? Для этого можно взглянуть на код соответствующей функции:

template <typename Value>
requires val_valid::register_value<Value>
[[nodiscard]] consteval RegisterAddress bit_band_address(RegisterAddress pAddress, const Value pValue) {
  RegisterAddress m_Address = pAddress;
  Value m_Value = pValue;

  // Check for only one bit is set or Value is not zero
  if ((m_Value & (m_Value - 1)) || (0 == m_Value)) [[unlikely]] {
    return m_Address;
  }

  // Check the register is fitted to the bit-band area
  if ((m_Address >= Region::sc_Origin) && (m_Address < (Region::sc_Origin + Region::sc_Length))) [[likely]] {
    // Recalculate value according to the bit-band formula
    uint8_t bitNum = 0;
    for (; !(m_Value & (static_cast<decltype(m_Value)>(1) << bitNum)); bitNum++) {
      // Error to avoid infinite loop in case the value is not correct
      if (bitNum >= (sizeof(m_Value) * 8))
        return m_Address;
    }
    m_Address = Alias::sc_Origin + 32 * (m_Address - Region::sc_Origin) + 4 * bitNum;
  }

  return m_Address;
}

И вот только при соблюдении всех трех условий можно применять bit-band:

  • Архитектура ядра Cortex-M3/M4, задается c define на этапе сборки

  • Регистр должен входить в определенный диапазон адресов (не все из них входят)

  • Устанавливается/стирается только один бит*

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

Пока что я смог только пройтись по верхам. Для дальнейшего изучения реализации разумно пристально ознакомиться с репозиторием.

И наконец, как это можно использовать

Для начала, нужно составить файл описания регистров желаемого периферийного модуля (как и в CMSIS), где будет дано описание всех регистров модуля и их полей. Составлять описания вручную было бы слишком накладно, поэтому был написан простой, быдло-кодовый python скрипт, (tools/svd2cpp.py) который переводит SVD описание в формат cpp_register.

SVD файл, это xml файл описания регистров от производителя, обычно имеет вид *chip_family_name.svd (STM32F0x0.svd например). Данный файл обычно можно получить на сайте производителя MCU. Вообще, это распространенный подход, например Cortex-M/PAC crates в языке Rust также используют подобный скрипт (svd2rust.py) .

Чтобы использовать данный скрипт, нужно передать ему *.svd файл и желаемые периферийные модули. Чтобы получить список доступных модулей - передать только *.svd файл.

Например, для stm32f070

// Получить список модулей
py .\svd2cpp.py STM32F0x0.svd

// Создать файлы описания для GPIO, STK, RCC
py .\svd2cpp.py STM32F0x0.svd GPIO STK RCC

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

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

Вывод

В статье я показал то, как имея немного фантазии и C++20, можно значительно улучшить манипулирование регистрами периферии. В статье я больше сосредоточился на результате своей работы, а не на том как я писал данный модуль. Поэтому, если вам недостаточно скромного описания реализации решения, то вы всегда можете пройти в репозиторий или спросить меня напрямую. Если я увижу интерес к внутренней портянке реализации, то я опишу её более подробно.

Тем не менее, даже так материал получился слишком объемным, и я надеюсь, что он все-таки будет посильным для читателя.

Спасибо всем, кто проявил интерес!

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


  1. tttinnny
    19.06.2024 05:03
    +1

    Было бы неплохо ещё затронуть тему корутин, раз разговор зашёл про 20 стандарт(кстати, в расте асинки не требуют хипа).


  1. AVKinc
    19.06.2024 05:03

    "Потеря безопасности" это конечно эпично. Установка операционной системы на микроконтроллер и есть потеря безопасности. Дрыгание ногами через функции это потеря безопасности. Безумный уровень абстракций это потеря безопасности. А управление контроллером через регистры это и есть безопасность. Гвонокодинг добрался до микроконтроллеров. Я никогда не думал, что это случится. Но вот, дожили.


    1. big_dig_dev Автор
      19.06.2024 05:03
      +4

      Аргументы будут? Как тогда сделать правильно?


      1. zebulon_stumpf
        19.06.2024 05:03
        +4

        Вы на правильном пути. Compile time error checking намного лучше защищает код от сложных ошибок. Более того использование c++ часто разрешает оптимизации которые компилятор не делает даже когда весь код в макросах..Я уже не говорю про полную потерю error checking с макросами


    1. Gay_Lussak
      19.06.2024 05:03
      +7

      Напрямую в регистр можно записать случайно бит где вместо Open Drain поставится Push-Pull и сгорит вход. А теперь смотрим за руками: пишем компайл-тайм функцию записи в регистр, проверяем внутри допустимость записи для того или иного регистра. Вуаля - вход не спален. Ну и где безопаснее?


      1. man55
        19.06.2024 05:03

        Не подскажете ли, как указать компилятору, подключен ли к выходу светодиод или вход от периферии или может быть монтажное ИЛИ на диодах? Лучше с примером исходника.


        1. Gay_Lussak
          19.06.2024 05:03
          +1

          А кто вам сказал, что компилятор должен проверять еще и схемотехнику?


          1. man55
            19.06.2024 05:03

            Вы. В предыдущем сообщении. Компайл-тайм не может думать за программиста, ограничить возможность установки выхода в пушпул или нет. Предложденное - это просто перенос точки ошибки с прямой работы с регистрами на гораздо менее читабельные для embedded-программиста сущности.

            Подчеркиваю - для embedded-программиста!!!


            1. mayorovp
              19.06.2024 05:03
              +1

              Вы случайно embedded-программистов с embedded-говнокодерами не путаете?

              И вообще, что такого нечитаемого в строчке, допустим, led1->turn_on() (раз уж вы тут про светодиоды заговорили)?

              Почему возможность указать в одном месте какая периферия куда подключена - вдруг плохо, и "embedded-программист" должен держать в уме эту информацию? Кстати, что ваши "embedded-программисты" делают, если возникла необходимость изменить схему?


              1. man55
                19.06.2024 05:03

                С говнокодерами уж точно не путаю.

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

                "Нормальная абстракция" в моем понимании:

                Один (ровно один, 1) заголовочный файл "hw_defines.h" с описанием целевой платы, какой GPIO на светодиод, какой на кнопку, какой канал I2C на какую периферию, какой канал DMA на прием данных по какому SPI и т.п.

                И несколько файлов вида "hw_gpio.c", "hw_uart.c" и т.п. для работы с аппаратно зависимой периферией на регистрах.

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

                На выходе - абстрации вида

                led_on(LED_STATUS), который дергает функцию из hw_gpio.c;

                button_getstate(BUTTON_LEFT), который тоже дергает функцию из hw_gpio.c;

                flash_write(*data, ...), который в свою очередь дергает функции из hw_spi.c

                и т.п.

                Все абстракции имеют четко оговоренные в том самом codestyle имена, правила вызовов и возврата и т.п. И тем самым покрытые один раз написанными юнит-тестами, не зависимыми от железа.

                В результате мигрирование с платы на плату и даже с изделия на изделие в рамках одного типа процессора - это переписывание одного файла defines.h и дописывание файлов hw_xxx.c для периферии, которой раньше в проектах не было.

                Мигрирование с проца на проц - переписывание тех самых файлов hw_xxx.c.

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

                Меньше кода - меньше ошибок. Меньше специфичных для конкретной реализации конкретного языка (или упаси Б.г компилятора) - меньше проблем с совместимостью. Хотел бы я посмотреть на портирование ПО на проц, под который нет компилятора С++20 (а такие в Китае водятся, поверьте).

                Короче - прицип бритвы Оккама, которому примененный в статье подход противоречит.

                Если что, у меня опыт в embedded 25+ лет и изделия, к которым я руку приложил, работают по всей стране сотнями тысяч в режиме 24\7 (не бытовуха).


                1. big_dig_dev Автор
                  19.06.2024 05:03
                  +3

                  А какой фрагмент статьи заставил вас думать, что нужно размазывать работу с регистрами ровным слоем по всем исходникам? Или городить безумные слои абстракции?

                  Я вообще не 'показал подход' - я лишь показал, как добавить безопастность и не потерять ничего.


  1. antonwork
    19.06.2024 05:03
    +4

    Compile time - это прекрасно. Мощь современного C++ с его шаблонами и constexpr (которые даже на CUDA-ядрах работают, например) сложно игнорировать. Проверки и защиты на этапе компиляции - это круто. Идентичный результирующий объем инструкций на выходе из компилятора - великолепно. Дополнительные (неочевидные) возможности для оптимизатора.

    Автор проделал колоссальную работу. Спасибо ему.

    На мой взгляд проблема в том, что не существует универсального подхода, нет общепризнанного стандарта. Есть бесконечное число разработок разного уровня и качества. Каждый по-своему решает однотипные задачи. И погружаться в эти решения нет желания и возможности. Здесь применили одно, в другом проекте иное. Где-то ошибка (да, она тоже может быть в этом слое абстракции).

    И похоже, что понятный всем стандарт на сегодняшний день выглядит примерно так:

    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

    Хорошо, если когда-нибудь появится что-то вроде STL (с настолько же глубокой проработкой) для мира MCU для решения задач "подергать ножку" или "установить таймер".


    1. qwerty19106
      19.06.2024 05:03
      +3

      Библиотека автора пока еще далека от CMSIS даже, т.к. нет работы с полями регистров (имена, R/W и R/O режимы, битовые маски). И совсем не сравнить с библиотеками на Rust, где для полей регистров на уровне типов указывается, есть ли вторичные эффекты при чтении/записи поля.

      Но главное, что вендоры пока даже не пытаются исправлять SVD файлы, т.к. ими никто не пользуется. Перепутанные имена регистров, отсутствующая периферия, или наоборот несуществующая, RO поля, помеченные как RW и т.д.

      Мало кто готов спуститься до этого уровня и поправить SVD файлы для нужного чипа. А вендоры не готовы принимать исправления. И пока это не поменяется, не возможно сделать автогенерацию кода регистров, и 99% разработчиков будут продолжать писать на С библиотеках вендоров.

      В Rust кстати эту проблему решили тем что написали свой стандарт SVD. Ну и пишут с 0 SVD файлы, что довольно трудоемко, хотя они и выходят раз в 8 меньше файлов вендоров.


      1. big_dig_dev Автор
        19.06.2024 05:03

        Библиотека автора пока еще далека от CMSIS даже, т.к. нет работы с полями регистров (имена, R/W и R/O режимы, битовые маски).

        Не могли бы вы пояснить, что вы имеете ввиду?


        1. qwerty19106
          19.06.2024 05:03
          +1

          Посмотрел подробнее примеры в конце статьи, поля регистров всё-таки есть (тут я был не прав).

          А запутали меня строки:
          GPIOA->ODR &= GPIO_ODR::ODR[NUM_4] | GPIO_ODR::ODR[NUM_10];

          Из которых не следует что поля регистров вообще где-то описаны, или генерятся из SVD файлов.

          Думаю это просто неудачный пример, т.к. регистры GPIO не подразумевают нормальных имен полей. Предлагаю эту часть статьи переписать, взяв например регистры и поля RCC. Тогда плюсы автогенерации из SVD будут всем очевидны.

          P.S. Теперь ясно, что оно гораздо лучше CMSIS, при условии что вы сами поправите/напишите SVD файлы нужной перифирии.


    1. big_dig_dev Автор
      19.06.2024 05:03
      +1

      Я согласен с вами. Не понял только один момент:

      И похоже, что понятный всем стандарт на сегодняшний день выглядит примерно так:

      RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

      Но ведь это же и есть одна из главных фишек моего решения

      // Аналогичная строчка на cpp_register
      RCC->AHB1ENR |= RCC_AHB1ENR::GPIODEN;

      Или имеется ввиду то, что под капотом?


      1. Indemsys
        19.06.2024 05:03

        Проблема не в записи RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

        Проблема в том что стоит за этими AHB1ENR, GPIODEN. Туда ли они указывают куда надо, и можно ли именно в это время туда обращаться.
        Мегабайтные хидеры с описанием регистров микроконтроллеров никто не обязывался делать абсолютно безошибочными.
        На своей практике я пришел к мысли что лучший способ писать так:

          reg8 = 0
                 + LSHIFT(1, 7) // TIE      | Transmit Interrupt Enable
                 + LSHIFT(1, 6) // RIE      | Receive Interrupt Enable
                 + LSHIFT(1, 5) // TE       | Transmit Enable
                 + LSHIFT(1, 4) // RE       | Receive Enable
                 + LSHIFT(0, 3) // MPIE     | Multi-Processor Interrupt Enable
                 + LSHIFT(1, 2) // TEIE     | Transmit End Interrupt Enable
                 + LSHIFT(0, 0) // CKE[1:0] | Clock Enable. 0 0: On-chip baud rate generator
          ;
          R_SCI1->SCR = reg8;
        

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

        Запись RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; - это чтение-модификация-запись. А можно ли так делать в конкретной ситуации? Что тут произойдет в случае прерывания? Как С++ ответит на такой вопрос? А как в отладчике будет выглядеть проход по шаблонам. Можно ли будет просто найти соответствие между ассемблерным кодом и кодом шаблона?
        При работе с регистрами периферии есть проблемы посерьезнее простой путаницы в именах битов. Там проблема просто понять работу этих битов и их зависимости. Поэтому лучший путь никак не маскировать и не шаблонизировать, а использовать прямые однозначные записи по явным адресам с подробными коментариями.


        1. big_dig_dev Автор
          19.06.2024 05:03

          Если я правильно понял, что своим примером вы обозначили, что заменяете 'чтение-модификация-запись' на 'запись'. Никто не запрещает так делать и с моим решением.

          Отвечая на вопросы:

          А можно ли так делать в конкретной ситуации? Что тут произойдет в случае прерывания?

          А как на это отвечает CMSIS? Не всегда возможно заменить 'чтение-модификация-запись' на запись 'запись' Думаю, что тут программист может за это отвечать.

          Как С++ ответит на такой вопрос?

          Также как и C

          А как в отладчике будет выглядеть проход по шаблонам?

          Я особой разницы не вижу.

           Можно ли будет просто найти соответствие между ассемблерным кодом и кодом шаблона?

          Можно, активно это делал во время разработки.


          1. Indemsys
            19.06.2024 05:03

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

            Можно, активно это делал во время разработки.

            Вот это и было бы интересно посмотреть. И сравнить с отладкой без избыточных текстовых конструкций.


            1. big_dig_dev Автор
              19.06.2024 05:03

              Просто ваше решение опирается на сомнительные имена.

              Честно говоря, я вообще не понимаю, что это значит

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

              Приведите, пожалуйста, пример таких проблем, которые я упускаю (как это вообще можно понять из данной статьи?).

              Я повторю, как и для комментатора выше, если я посвятил время теме регистров, это не значит что я только и я только этому и посвящаю свое время. И если я тчо-то не описал, это не значит что этого нет.


              Вопрос в том, какие проблемы значительные тогда? Вы привели кусок кода (без контекста), по которому не особо и понятно, что это и для чего. При этом, выглядит еще и непонятнее того, что я предлагаю.

              Там проблема просто понять работу этих битов и их зависимости. Поэтому лучший путь никак не маскировать и не шаблонизировать, а использовать прямые однозначные записи по явным адресам с подробными коментариями.

              Я и сам уважаю принцнцип Keep It Simple Stupid. Однако, по такой логике, вообще ничего нельзя использовать, если не уверен до конца как это работает внутри, ни одной библиотеки, вообще.


  1. Ivan_Kadochnikov
    19.06.2024 05:03
    +1

    Может немного не в тему.

    Недавно довелось разбираться с кодом, в котором управление периферией (драйверы) были реализованы через классы (и прочие инструменты С++).

    Претензии у меня были следующие:

    • Стремление к абстрагированию и обобщению лишь «захламляет» код. Не получится сделать обобщённые подходы к настройке АЦП и таймера высокого разрешения, надо будет всё равно погружаться в документацию на МК и вникать в назначение аппаратных регистров и битовых полей.

    • Вводит дополнительный барьер для понимания кода. То есть, если бы управление аппаратурой осуществлялось напрямую через регистры (либо небольшой набор простых функций/макросов) для понимания целей манипуляции достаточно документации на МК. В случае с реализованным подходом нужно: либо качественная документация на класс драйвера (по объёму соизмеримая с описанием на аппаратные регистры в документации на МК), либо лезть разбираться в код (без комментариев, с несколькими наследованиями и тд, за что я даже не брался).


    1. big_dig_dev Автор
      19.06.2024 05:03
      +1

      То, что вы перечислили, это же не проблемы C++

      На самом деле, с точки зрения концепции, при высоко абстрагированном подходе, С и С++ не сильно и отличаются. Взять хотя-бы тот-же 'объектно-ориентированный' C, с указателями на функции и композицией. На нем ведь написано очень много чего, тот же Linux например.

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

      Однако, правда и в том, что C++ программы склонны к переусложнению, просто из-за объема языка и его возможностей (довольно часто попадался такой код). Если именно это главная проблема, то я бы посмотрел на Rust.