C++20 in bare-metal programming, working with registers of Cortex-M microcontrollers

RP2040 peripheral registers

RP2040 peripheral registers

Today I would like to discuss a topic that I have come across one way or another almost every embedded device programmer without using real operating systems, namely direct control of microcontroller peripheral nodes. And more specifically, I would like to discuss a raise security when controlling peripheral modules without loss efficiency, flexibility and readability.

Someone might think that this topic has already been covered up and down in numerous project manuals for STM32 (for example, Habr). Those who already understand what will be discussed may think that this complete repetition of already existing similar ideas (for example, this and other articles by the author sparked my interest in this topic several years ago, but in the Rust programming language, most Cortex-M/PAC boxes/crates already use security checks when manipulating registers). However, there is no need to rush to conclusions. I I guessthat I can provide interesting and useful information for both of the above categories of readers.

Before we begin, the topic of that 'Why even bother with the register level if every manufacturer provides HAL/BSP?' I will leave it outside the scope of this article. I assume that if the reader opened the article, then he is already interested in this topic, and it is not so important to me why.

Brief description of the problem

Usually, when there is a need to work with a peripheral module directly (for example, GPIO, TIMER, RTC, DAC, SPI, ETHERNET, USB, and many more), the programmer uses the register description provided by the MCU manufacturer. And it's also usually one or more C code header files written in CMSIS.

At the moment, the format of these files can hardly be called the best that can be. One of the main problems of CMSIS is safety. Examples of errors that could be handled at the writing/compiling stage when working with registers:

// Ошибка в одной букве, регистр должен быть 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;

At first glance, it seems that the examples above are somewhat synthetic. However, my experience is that when doing a lot of register-level work, such errors, although infrequent, appear regularly (usually with a lot of copying), leading to many fun hours of debugging.

Using Hello world as an example

Let's look at an example of using a file from the manufacturer (standard CMSIS header), working minimalistic LED flasher code on one of the very popular boards STM32F4DISCOVERY. The goal is to flash the four LEDs on the legs PD12-PD15 with a period of two seconds (see comments in the code).

// 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;
}

All example code is located in one file main.cpp. There’s nothing special to explain here, we turned on port D and set the corresponding legs as outputs, started a timer to overflow once a second, and when overflowed we changed the state of the LEDs.

It is important to note the result of the build, in this case, with the clang compiler and Oz optimization:

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

And here is the same example, where the programmer made several mistakes, and he doesn’t even know about it, since the compiler didn’t tell him anything (see comments in the code).

#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;
}

And if we get ahead a little, this is what the same example in which I used my work will look like, that is, with all the above checks at the compilation stage.

#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;
}

Despite the slight differences, the style is very similar to a typical style using manufacturer's files, or CMSIS. This is done on purpose, since in my understanding most embedded systems programmers are familiar with this style, which means it will be easy to read.

Program dimensions:

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

This NOT error, the size actually turned out to be smaller. It turns out that All checks are included at the compilation stage, the usual style is preserved, and the size is reduced.

Prerequisites

I use exactly modern C++, because I think that at a high level readability And code versatilitythis language allows you to achieve the highest efficiency (and as you know, very often these things are inversely proportional). And my choice at the moment is C++20, since it was in this standard that the concepts (Of course, a lot of other cool things have appeared). This is not so much critical for working with registers, but for my projects in general, since I need static polymorphism at different levels. Moreover, according to tableC++20, although not fully, is very widely supported in gcc and clang.

The only thing I would like to mention in this vein is why not? Rust? In fact, I really liked the language itself and its concepts at first, and syntactically, in my opinion, it is generally one of the best languages ​​(yes, I have modest experience). However, after a lazy year of practicing the language, I was still forced to conclude that in the context of embedded programming (where compile-time programming can and should be actively used), Rust not only does not make a qualitative leap forward, but is even somewhat inferior to C++ in 2024. Of course, I do not exclude that in the future the situation may change.

To use C++20, you need a modern IDE/compiler, meaning it must be constantly updated. Since I don't have the budget to purchase IAR ARM or Keil licenses, I use the following free cross-platform bundle: gcc/clang+Make+VSCode. This way I am not dependent on the chip manufacturer or any other paid products.

In addition, I use extended warning flags (such as -Wall, -Wextra, -Werror and many others) and maximum optimization.

Technical task

The most important requirement was to implement all sorts of compile-time checks when working with microcontroller registers. This means that they should not have an impact on the size or speed of the resulting firmware. Let me remind you that the verification data is:

  • Does the register contain this field?

  • Duplicate fields when copying

  • Field/register access mode (read, write, set, etc.)

  • Register overflow

At the same time, I wanted to include the following features in the solution:

  • Implementation of writing multiple fields to a register without loss of security. I highlighted this point separately, since in some implementations I saw this problem was not solved.

// Например, тут устанавливаются сразу 5 полей регистра SPI1->CR1
// Для КАЖДОГО поля должны производиться все основные проверки безопасности
SPI1->CR1 |= (SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_MSTR); 
  • Implementation of arrays of registers or fields, with customizable steps. Let me give you a few examples:

    Register GPIOx_MODER, MCU STM32F070

    Register GPIOx_MODER, MCU STM32F070

    In the picture above there is what I would call an array of fields, 16 MODER fields of size 2 bits. And it would be nice not to describe each field separately, but to write it down as an array of fields in one line. The same goes for registers.

    An example of a register array that I would call 'stepped':

    USB Packet buffers, MCU STM32F070

    USB Packet buffers, MCU STM32F070

    Here is a block of four 16-bit registers: ADDRx_TX, COUNTx_TX, ADDRx_RX, COUNTx_RX. And there are eight such blocks in this MCU. It also turns out that all the same registers are in steps of 8. That is, ADDR0_TX is located at address 0x00, ADDR1_TX at 0x08, ADDR2_TX at 0x10…

    It is necessary that this be an array of registers both in description and in circulation.

  • Implementation of a short form for recording similar fields in one register. Here I will simply give a ready-made example based on my solution:

// Устанавливаю пины 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);
  • Auto bit-band, including non-standard ones. In general, since all the usual operations of accessing registers will be rewritten, it would not be a bad idea to add a bit-band that would work automatically if a number of conditions are met.

  • Support for some 'dynamic' operations. Sometimes situations arise when you need to write something to a register that is not known at the compilation stage. As an example, writing the buffer address for DMA if the buffer is not static.

  • Cast from driver level to register level without loss of security. When writing drivers, I actively use 'enum class'. On the one hand, it is type-safe (which is why it is convenient for passing parameters, for example during initialization). On the other hand, to write this value to a register, you need an explicit static_cast in uintx_t (which is logical). And the idea is to do this casting in one place at the register level, so as not to do it manually at the driver level every time.

  • A tool for automatically generating register description files. Let me remind you that a standard file with a description of registers from the manufacturer may look like So. Most likely, it is compiled automatically from files SVD. I would also like to receive at least a draft file automatically, so that the description itself does not take much time.

Solution

I'd like to start by demonstrating how it works cpp_register (that's what I called the result of my work), that is, from the answer to the technical specifications. Details of how this solution was implemented are below in the section implementation.

I thought it would be reasonable to show cpp_register with a simple example, using the example GPIO microcontroller of the STM32 family (I had a board with stm32f070). Details about these registers are in Reference Manual on the stm32f070 chip.

Before we begin, I will give a description of the module registers at the level CMSIS (analyze this piece of code Not necessary), and at the level cpp_register, just to show the difference in volume described (~400 lines vs ~65).

CMSIS description of 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{};
};

}

Now, I would like to show you how I decided each point of the technical specifications. And I would like to start with the basics, with security checks. I will analyze one of the checks in detail, since they all work on similar principles. I will use the GPIO registers, the description of which was given above.

Does the register contain this field?

// Устанавливаю в регистре 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);

As I already mentioned, even when operating on multiple fields at the same time, each check is carried out for each field.

I'd also like to show what it looks like at a developer's desk. The fact that something is wrong is visible even while writing the code:

Red underline when writing code when there is an error

Red underline when writing code when there is an error

Although the description of the error is not very good, you can already see that something is wrong. Let's try to compile:

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

It's already heremore' it is clear that the requirement that this field belongs to the same register is not met.

Here's what it might look like backlight syntax:

Syntax highlighting example

Syntax highlighting example

It is also very important that auto-completion also works great for both registers and fields.

The same principle of error detection is used For everyone checks. Let me remind you that this is:

  • Does the register contain this field?

  • Recording the same field twice

  • Field/register access mode (supported access modes in implementation)

  • Register overflow

Module functionality

Now I would like to demonstrate all the available operations cpp_register (returning to flexibility And readability):

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

Important: The traditional reset operator '&=~' was replaced with '&=' on purpose (for convenience), but it is still possible to use '&=~'. Essentially '~' has no effect on fields.

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])) { ... }
// Реальный пример с кварцем
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);
  • Dynamic values ​​are only available for writing (there can be no checks, except for the size and access mode). For example, calculation results will be sent via UART or a buffer to 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);
  • About the mysterious NUM_x. In fact, these are compile-time constants that are written to fields (see RegVal implementation). The easiest way to create this constant is through an inline compile-time template variable.

// Период системного таймера
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);
  • Using the example of MODER, an operation with several similar fields, two spellings can be used.

// Полная форма
// Установить 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);
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>);

Besides operations, I would like to expand a little on the topic of automatic bit-band.In case anyone doesn’t know, this is a feature of the Cortex-M3/M4 cores, which allows you to slightly reduce the size and increase the speed of the executable code. AND the most The main thingusing bit-band can be turned Not atomic operations read->modify->write to atomic recording. To enable this feature in a module, you need to pass -DCORTEX_M_BIT_BAND when building the project. In this case, for bit set and reset operations, the bit-band will be applied automatically if the values ​​satisfy certain conditions. It is also important that a custom algorithm is also supported, for example RP2040 has its own algorithm (-DBIT_BAND_CUSTOM), and it can be added to cpp_register.

Implementation

I will describe only the most basic aspects of the implementation.

For those interested, the implementation is now live Here. This is not some kind of finished product, there is something to improve, but the general idea will be clear from the repository.

Essentially, the entire core implementation is one header file – register.hpp. I'll try to go through the main points of this file.

To begin with, I defined the register address dimension as size_t, because usually (though not always), the register address has the same dimension as the processor address space. At a minimum, this is true for all 32-bit Cortex-M systems that I have encountered.

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

Then I started thinking about what can be written into the register and what should not be written? For example, in CMSIS this is usually some kind of unsigned number, and is usually 32 bits in size, like most registers.

// Пример определения полей уже упомянутого регистра 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)
<...>

After thinking about this topic, I was able to identify three options. This selection describes the following concept:

/**
 * @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>>);

According to the concept, the register value can be:

  1. Unsigned arithmetic type.

  2. Pointer – useful for DMA, in the case of a static buffer, the address is known at the compilation stage, which means it can be checked for nullptr.

  3. An enumeration type with an underlying unsigned arithmetic type.

A short explanation on point 3. At the driver level, it is convenient to accept parameters of the type enum classbecause it only has predefined values ​​and is type-safe. This also results in the need to manually register 'static_cast' each time at the same driver level. And to avoid this, I decided to leave 'static_cast' inside the register level, accepting 'enum class' values ​​at the same level. More specifically, an example of these 'enum class' values ​​for 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 };

Since I'm talking about concepts. At the moment, I would highlight three main tools for implementing those already mentioned many times security checks: static_assert, SFINAE, constraints and concepts (from C++20). All these methods are used in the project, but the ones that are most used are concepts as they have advantages of the two previous tools: the error text is more or less clear (static_assert) and errors are visible even while writing (SFINAE). Descriptions of SFINAE principles and concept capabilities are beyond the scope of this article. The most important that it is this trio that is used to cause compile-time errors if the programmer has performed an illegal operation on a register or field.

Now I'd like to show three main classes of the program, and the first of them, class RegVal has the following form (left only critical entities, the entire class can be viewed here):

/**
 * @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 {};
  }
};
...

The class accepts a value of type auto, as well as its offset, which is zero by default. It is important to pay attention to the restrictions described in line 8, from left to right: the value must satisfy the concept register_value (described above), register_offset says that the bit offset cannot be greater than the bit size of the type, and register_pointer will check the pointer to nullptr and the offset to 0. The public interface contains three variables, whose purpose is obvious from the names.

The idea of ​​this class is to get, let's say, true constexpr parameter. It is this class that helps to perform a very important trick necessary for the implementation of the entire module. This trick is the use of operator overloads (as you might guess, enumeration of available operators) together with compile-time parameters. By true constexpr parameter I mean a function parameter that can be used in conjunction with static_assert, SFINAE, concept and requires, and so on. Let me give you a small example (see comments to the code):

// Ошибочный пример
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, "А вот так будет можно в любой функции")
  <...>
}

It is also important that the class supports the bitwise 'or' operation (' | '), and there is also one very important idea that runs through the entire module: based on the current type of the object and the values ​​of its fields, the ' | ' operation. ' returns a new, derived type that also depends on the compile-time values ​​of the received object. Let's take a closer look:

  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 {}; // Возвращаем объект по умолчанию
  }

The second important class is the class that implements the field type, here is its abbreviated description (left only critical entities, the entire class can be viewed here):

/**
 * @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;
};

I think the easiest way to show how this class works is with an example. I suggest taking the simple case of GPIO, since these registers are familiar to many, for example the already mentioned MODER register. Let me remind you of its description:

Register GPIOx_MODER, MCU STM32F070

Register GPIOx_MODER, MCU STM32F070

And here is a description of its fields using the class 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{};
};

And here, once again, is the size of the declaration of the same register in CMSIS, where arrays of fields are not implemented:

Not a couple of lines, every bit is described
/******************************************************************************/
/*                                                                            */
/*                            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)

And here is an example of fields with a step for the IPR register of the NVIC module:

Register NVIC_IPR, ARMv6m architecture

Register 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{};
};

Now about access modesI slightly expanded the standard list of access modes, adding many others besides 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

Access modes are checked for each field during register operations. By the way, about operations on registers…

I will also provide a declaration of the Register class (again, details in here):

/**
 * @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 {
<...>
// Тут все стандартные операции, что я перечислял ранее
};

And here is an example with a description of the GPIO registers for stm32f070, along with the already familiar MODER register:

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{};

And again, an example of an array of registers with a step for USB, mentioned in the technical specifications:

// Два последних параметров, 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>);

A peripheral block can be declared as follows:

// Указатель для сохранения схожести с 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{};

And lastly, I would like to give an example of one operation, setting a bit from the Register class, and analyze it a little

  /**
   * @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;
    }
  }

Yes, I would like to talk about the already mentioned bit-band. I figured that since I was overloading all bit operations, it would be logical to integrate bit-band. After all, it would be very cool if the programmer no longer had to manually add bit-band where possible, and the compiler inserted it itself, adding atomicity and increasing performance. But how do you know when it can be used and where it can’t? To do this, you can look at the code for the corresponding function:

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;
}

And only if all three conditions are met can you use bit-band:

  • Cortex-M3/M4 core architecture, set with define at the build stage

  • The register must be within a certain address range (not all of them are)

  • Installed/erased only one bit*

Technically, it is possible with two adjacent bits, but then ARM64 operations are involved and performance loss begins even with the usual option.

So far I have only been able to scratch the surface. To further explore the implementation, it is wise to take a closer look at the repository.

And finally, how can you use it?

First, you need to create a file describing the registers of the desired peripheral module (as in CMSIS), which will contain a description of all the registers of the module and their fields. It would be too expensive to compose descriptions manually, so a simple redneck python script was written (tools/svd2cpp.py) which translates the SVD description into cpp_register format.

An SVD file is an xml file describing registers from the manufacturer, usually looks like *chip_family_name.svd (STM32F0x0.svd for example). This file can usually be obtained from the MCU manufacturer's website. In general, this is a common approach, for example Cortex-M/PAC crates in the Rust language also use a similar script (svd2rust.py).

To use this script, you need to pass it a *.svd file and the desired peripheral modules. To get a list of available modules, transfer only the *.svd file.

For example, for stm32f070

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

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

However, svd files are often inaccurate, especially in sizes and access modes. Besides this, the script is not perfect. That's why, I recommended I would use a script to get a draft description, and then check each register with my hands.

How example of register description, I can show you the project I’m working on a little bit now. And here you can see examples of using cpp_register.

Conclusion

In the article I showed how, with a little imagination and C++20, you can significantly improve peripheral register manipulation. In the article, I focused more on the result of my work, and not on how I wrote this module. Therefore, if a modest description of the implementation of the solution is not enough for you, then you can always go to repository or ask me directly. If I see interest in the internal implementation, I will describe it in more detail.

However, even so, the material turned out to be too voluminous, and I hope that it will still be feasible for the reader.

Thanks to everyone who showed interest!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *