📜 ⬆️ ⬇️

Use template + constexpr to create mask masks for microcontroller peripherals at compile time (C ++ 14)

Introduction


This small note does not contain an obvious solution to the problem set out below, to which I had to go through several sleepless nights.

Task: on the basis of user-defined data on how the microcontroller's peripheral unit should work, to obtain at the compilation stage an array of mask configurations of its registers that could be used in real time. In this case, it is required at the compilation stage to check that all the parameters specified are correct (the microcontroller periphery will be configured correctly).


')
Interested in how this can be done, please under the cat.

Table of contents:


  1. The main problems and limitations of C ++ 14.
  2. Decision based on the fact that the user is never wrong.
  3. Accounting for custom errors.
  4. Expand the capabilities of the class.
  5. About abbreviated records.
  6. Total.

Main problems and limitations of C ++ 14


As soon as I ran into the task described in the introduction, I immediately remembered the C ++ language specifier - constexpr . Since the need to solve this problem was identified at the design stage of the library, all limitations of the C ++ 11 standard, which was originally planned to build the library (the inability to use loops inside constexpr methods, etc.), were taken into account, and The C ++ 14 standard was selected, in which many of the C ++ 11 limitations were removed (C ++ 17 was not deliberately chosen, since it was not too different from C ++ 14 in terms of the tasks set for the library, but This is its support from GCC, also selected as the main The library compiler, at the time of this writing, is not very stable).

But despite the fact that constexpr functions in C ++ 14 have become almost as flexible as real-time functions, they have only one fat minus, crossing out most of the advantages - the absence of any debugging. About it was written on Habré here . From this article I drew the main idea:
But the problems do not end there. When you write some constexpr-function, which will then often use, it would be good to return a readable error. Here you can mistakenly assume that static_assert is suitable for this. But static_assert cannot be used, since the parameters of the functions cannot be constexpr, which is why the values ​​of the parameters are not guaranteed to be known at the compilation stage.

How to display errors? The only more or less normal way I found is to throw an exception:
And since exceptions are not supported in the constexpr methods, we will simply get an error that using throw in constexpr is impossible. If we were processing something in a loop, then we can never know which element we fell on (when checking which element we caused an exception).

Moreover, the situation is as follows: static_assert is impossible, throw is impossible, printf and interruption of compilation are not allowed.

Decision based on the fact that the user is never wrong


How to deal with all these limitations and the inability to debug? To begin with, suppose that the USER is NEVER MISTAKING (yes, futuristic, but for now let's take this as an axiom). Then constexpr, it turns out, in principle, does not need debugging and throwing exceptions that the input data parameters are incorrect.

For example, consider the class of the object controlling the output of the microcontroller's port, initially switched on by someone (a clock signal was sent to the peripheral unit) and with the output configured at the output at the desired speed (the most blinking LED). In essence, an object of our class must have methods for switching the output state from 1 to 0 and back. More from our facility is not required.

However, in order not to produce entities, suppose that we have some structure that describes the configuration of one entire output. This structure is used not only by the class of our object, but also by others (for example, those that initialized the module for us in advance and configured the output to the desired state). This structure will look like this:

/* *   . */ struct __attribute__( ( packed ) ) pin_config_t { EC_PORT_NAME port; //   // ( : EC_PORT_NAME::A ). EC_PORT_PIN_NAME pin_name; //   // ( : EC_PORT_PIN_NAME::PIN_0 ). EC_PIN_MODE mode; //   // ( : EC_PIN_MODE::OUTPUT ). EC_PIN_OUTPUT_CFG output_config; //   // ( : EC_PIN_OUTPUT_CFG::NOT_USE ). EC_PIN_SPEED speed; //   // ( : EC_PIN_SPEED::MEDIUM ). EC_PIN_PULL pull; //   // ( : EC_PIN_PULL::NO ). EC_PIN_AF af; //    // ( : EC_PIN_AF::NOT_USE ). EC_LOCKED locked; //     //     // global_port  // (  EC_LOCKED::NOT_LOCKED ). EC_PIN_STATE_AFTER_INIT state_after_init; //      // (  ,      ). // ( EC_PIN_STATE_AFTER_INIT::NO_USE). }; 

The structure uses the following enum classes.
 /********************************************************************** *  enum class-. **********************************************************************/ /* *    . */ enum class EC_PORT_PIN_NAME { PIN_0 = 0, PIN_1 = 1, PIN_2 = 2, PIN_3 = 3, PIN_4 = 4, PIN_5 = 5, PIN_6 = 6, PIN_7 = 7, PIN_8 = 8, PIN_9 = 9, PIN_10 = 10, PIN_11 = 11, PIN_12 = 12, PIN_13 = 13, PIN_14 = 14, PIN_15 = 15 }; /* *  . */ enum class EC_PIN_MODE { INPUT = 0, // . OUTPUT = 1, // . AF = 2, //  . ANALOG = 3 //  . }; /* *  . */ enum class EC_PIN_OUTPUT_CFG { NO_USE = 0, //     . PUSH_PULL = 0, // "-". OPEN_DRAIN = 1 // " ". }; /* *  . */ enum class EC_PIN_SPEED { LOW = 0, // . MEDIUM = 1, // . FAST = 2, // . HIGH = 3 //   }; /* *   */ enum class EC_PIN_PULL { NO_USE = 0, //  . UP = 1, //   . DOWN = 2 //   . }; /* *   ,  . */ enum class EC_PIN_AF { AF_0 = 0, NO_USE = AF_0, SYS = AF_0, AF_1 = 1, TIM1 = AF_1, TIM2 = AF_1, AF_2 = 2, TIM3 = AF_2, TIM4 = AF_2, TIM5 = AF_2, AF_3 = 3, TIM8 = AF_3, TIM9 = AF_3, TIM10 = AF_3, TIM11 = AF_3, AF_4 = 4, I2C1 = AF_4, I2C2 = AF_4, I2C3 = AF_4, AF_5 = 5, SPI1 = AF_5, SPI2 = AF_5, I2S2 = AF_5, AF_6 = 6, SPI3 = AF_6, I2S3 = AF_6, AF_7 = 7, USART1 = AF_7, USART2 = AF_7, USART3 = AF_7, AF_8 = 8, UART4 = AF_8, UART5 = AF_8, USART6 = AF_8, AF_9 = 9, CAN1 = AF_9, CAN2 = AF_9, TIM12 = AF_9, TIM13 = AF_9, TIM14 = AF_9, AF_10 = 10, OTG_FS = AF_10, AF_11 = 11, ETH = AF_11, AF_12 = 12, FSMC = AF_12, SDIO = AF_12, AF_13 = 13, DCMI = AF_13, AF_14 = 14, AF_15 = 15, EVENTOUT = AF_15 }; /* *       set_locked_key_port  * set_locked_keys_all_port   global_port. * !       global_port.    *          - . *     -  . */ enum class EC_LOCKED { NOT_LOCKED = 0, //   . LOCKED = 1 //  . }; /* *      * ( ,     ). */ enum class EC_PIN_STATE_AFTER_INIT { NO_USE = 0, RESET = 0, SET = 1 }; 


As mentioned earlier, the object of our class should only change the state at the output of the output (legs). Since the library is written under stm32f2 (and only), it is reasonable to use for this purpose the BSR register available in the physical GPIO block of each port, which allows writing the unit (1) to bits 0-15 to set the corresponding bit (writing unit (1) to The 0th bit will set the output status of port 0 to 1), and writing the unit (1) to 16-31 will reset the corresponding bit - 16 (writing the unit (1) to the 31st bit will reset 31-16 = the 15th output of the port to 0).

As you can see, the task of setting the desired output to 1 is reduced to writing to BSR register 1 << output_number , and resetting to recording 1 << output_number + 16 .

For these purposes, it is enough for us to take the port and pin_name fields from the received from the user field structure. All other fields we do not need.

Denote a general view of the class of our object:

 class pin { public: constexpr pin ( const pin_config_t* const pin_cfg_array ); void set ( void ) const; void reset ( void ) const; void set ( uint8_t state ) const; void set ( bool state ) const; void set ( int state ) const; private: constexpr uint32_t p_bsr_get ( const pin_config_t* const pin_cfg_array ); constexpr uint32_t set_msk_get ( const pin_config_t* const pin_cfg_array ); constexpr uint32_t reset_msk_get ( const pin_config_t* const pin_cfg_array ); const uint32_t p_bsr; const uint32_t bsr_set_msk, bsr_reset_msk; }; 

As you can see, the class has the following methods:


All these methods are used by the user in real time. Consider them.

 /* *      <<1>>, *     . */ void pin::set ( void ) const { *M_U32_TO_P(this->p_bsr) = this->bsr_set_msk; } /* *      <<0>>, *     . */ void pin::reset ( void ) const { *M_U32_TO_P(this->p_bsr) = this->bsr_reset_msk; } /* *      , *     . */ void pin::set ( uint8_t state ) const { if ( state ) { this->set(); } else { this->reset(); } } void pin::set ( bool state ) const { this->set( static_cast< uint8_t >( state ) ); } void pin::set ( int state ) const { this->set( static_cast< uint8_t >( state ) ); } 

The set and reset methods use the define defined below to explicitly convert the value in the uint32_t variable to a pointer to the uint32_t variable.

 //    uint32_t     uint32_t. //     . #define M_U32_TO_P(point) ((uint32_t *)(point)) 

At the moment, we have figured out how the object's methods work with ready-made masks, the most important thing remains (for the sake of which this article was written) to prepare them.

The class has three methods:

  1. set_msk_get - returns the value of the uint32_t variable, which is the mask of the BSR register for setting user-specified output to <1 >>.
  2. reset_msk_get - Returns the value of the uint32_t variable, which is the mask of the BSR register for resetting user-defined output to << 0 >>.
  3. p_bsr_get - returns the value of the uint32_t variable, which contains the address of the register BSR on the physical memory card of the microcontroller.

Knowing that the user is definitely not mistaken when specifying the parameters of the structure, we can write the following code:

 /********************************************************************** *  constexpr . **********************************************************************/ /* *       "1"   BSR. */ constexpr uint32_t pin::set_msk_get ( const pin_config_t* const pin_cfg_array ) { return 1 << M_EC_TO_U8(pin_cfg_array->pin_name); } /* *       "0"   BSR. */ constexpr uint32_t pin::reset_msk_get ( const pin_config_t* const pin_cfg_array ) { return 1 << M_EC_TO_U8( pin_cfg_array->pin_name ) + 16; } /* *      BSR,    . */ constexpr uint32_t pin::p_bsr_get( const pin_config_t* const pin_cfg_array ) { uint32_t p_port = p_base_port_address_get( pin_cfg_array->port ); return p_port + 0x18; } 

These functions use define to convert the value of the enum class to the uint8_t variable.

 //  enum class  uint8_t. #define M_EC_TO_U8(ENUM_VALUE) ((uint8_t)ENUM_VALUE) 

Also, the p_bsr_get method uses the p_base_port_address_get method, which does not belong to any particular class, and taking the value of the enum class of EC_PORT_NAME (port name) returns the physical address of the beginning of the location of the registers of this port on the physical microcontroller map. It looks like this:

General method p_base_port_address_get
 /* *        - *        . */ constexpr uint32_t p_base_port_address_get( EC_PORT_NAME port_name ) { switch( port_name ) { #ifdef PORTA case EC_PORT_NAME::A: return 0x40020000; #endif #ifdef PORTB case EC_PORT_NAME::B: return 0x40020400; #endif #ifdef PORTC case EC_PORT_NAME::C: return 0x40020800; #endif #ifdef PORTD case EC_PORT_NAME::D: return 0x40020C00; #endif #ifdef PORTE case EC_PORT_NAME::E: return 0x40021000; #endif #ifdef PORTF case EC_PORT_NAME::F: return 0x40021400; #endif #ifdef PORTG case EC_PORT_NAME::G: return 0x40021800; #endif #ifdef PORTH case EC_PORT_NAME::H: return 0x40021C00; #endif #ifdef PORTI case EC_PORT_NAME::I: return 0x40022000; #endif } } 
The class constructor that fills the constants of the reset / set masks and the register address is as follows.

 /********************************************************************** *  constexpr . **********************************************************************/ constexpr pin::pin ( const pin_config_t* const pin_cfg_array ): p_bsr ( this->p_bsr_get( pin_cfg_array ) ), bsr_set_msk ( this->set_msk_get( pin_cfg_array ) ), bsr_reset_msk ( this->reset_msk_get( pin_cfg_array ) ) {}; 

Technically, now you can use this class, but we remember that the user can not always enter all the parameters of the structure correctly ...

Accounting for custom errors.


Now that we have a working and debugged class, it remains only to refine the input structure check and we can safely use the objects of our class. But, as mentioned earlier, to do this in constexpr is comfortable - impossible. But the solution is - template. Since all objects in the user code should be set globally (this is the main condition for using the library, which can be read in the document of its (library) description, link to which will be given at the end of the article), the use of template-s seems to be the most reasonable. The fact is that in the template, static_assert is allowed. Also, they can use all sorts of code to conduct more complex checks. And, most importantly:

If you create a template class inherited from a structure, perform all the necessary checks in the constructor of this class, and then declare an object of the given template class globally in the user's code, the compiler will allow you to apply an implicit type conversion to the structure from which the class was inherited.

This opportunity simply can not be used!

 /********************************************************************** *  template . **********************************************************************/ template < EC_PORT_NAME PORT, EC_PORT_PIN_NAME PIN_NAME, EC_PIN_MODE MODE, EC_PIN_OUTPUT_CFG OUTPUT_CONFIG, EC_PIN_SPEED SPEED, EC_PIN_PULL PULL, EC_PIN_AF AF, EC_LOCKED LOCKED, EC_PIN_STATE_AFTER_INIT STATE_AFTER_INIT > class pin_config_check_param : public pin_config_t { public: constexpr pin_config_check_param(): pin_config_t( { .port = PORT, .pin_name = PIN_NAME, .mode = MODE, .output_config = OUTPUT_CONFIG, .speed = SPEED, .pull = PULL, .af = AF, .locked = LOCKED, .state_after_init = STATE_AFTER_INIT } ) { /* *       . */ #if defined(STM32F205RB)|defined(STM32F205RC)|defined(STM32F205RE) \ |defined(STM32F205RF)|defined(STM32F205RG) static_assert( PORT >= EC_PORT_NAME::A && PORT <= EC_PORT_NAME::H, "Invalid port name. The port name must be A..H." ); #endif static_assert( PIN_NAME >= EC_PORT_PIN_NAME::PIN_0 && PIN_NAME <= EC_PORT_PIN_NAME::PIN_15, "Invalid output name. An output with this name does not" "exist in any port. The output can have a name PIN_0..PIN_15." ); static_assert( MODE >= EC_PIN_MODE::INPUT && MODE <= EC_PIN_MODE::ANALOG, "The selected mode does not exist. " "The output can be set to mode: INPUT, OUTPUT, AF or ANALOG." ); static_assert( OUTPUT_CONFIG == EC_PIN_OUTPUT_CFG::PUSH_PULL || OUTPUT_CONFIG == EC_PIN_OUTPUT_CFG::OPEN_DRAIN, "A non-existent output mode is selected. " "The output can be in the mode: PUSH_PULL, OPEN_DRAIN." ); static_assert( SPEED >= EC_PIN_SPEED::LOW && SPEED <= EC_PIN_SPEED::HIGH, "A non-existent mode of port speed is selected. " "Possible modes: LOW, MEDIUM, FAST or HIGH." ); static_assert( PULL >= EC_PIN_PULL::NO_USE && PULL <= EC_PIN_PULL::DOWN, "A non-existent brace mode is selected." "The options are: NO_USE, UP or DOWN." ); static_assert( AF >= EC_PIN_AF::AF_0 && AF <= EC_PIN_AF::AF_15, "A non-existent mode of the alternative port function is selected." ); static_assert( LOCKED == EC_LOCKED::NOT_LOCKED || LOCKED == EC_LOCKED::LOCKED, "Invalid port lock mode selected." ); static_assert( STATE_AFTER_INIT == EC_PIN_STATE_AFTER_INIT::NO_USE || STATE_AFTER_INIT == EC_PIN_STATE_AFTER_INIT::SET, "The wrong state of the output is selected." "The status can be: NO_USE, UP or DOWN." ); }; }; 

Now we can declare an object of this class in the user code:

 const pin_config_check_param< EC_PORT_NAME::C, EC_PORT_PIN_NAME::PIN_4, EC_PIN_MODE::OUTPUT, EC_PIN_OUTPUT_CFG::PUSH_PULL, EC_PIN_SPEED::MEDIUM, EC_PIN_PULL::NO_USE, EC_PIN_AF::NO_USE, EC_LOCKED::LOCKED, EC_PIN_STATE_AFTER_INIT::SET > lcd_res; 


After that, when creating an object of the pin class, refer to it as if it were a regular structure:

 const constexpr pin pin_lcd_res( &lcd_res ); 

Then, in the user code, you can use the methods of this object:

 void port_test ( void ) { pin_lcd_res.reset(); pin_lcd_res.set(); } 

Extend class features


We got a class that is quite suitable for controlling the leg in exit mode. However, not to create a separate class for input? We will slightly modify the class so that it can be used both for outputs configured for output and for input. We also add a method for inverting the output state.

We add two (2) constants to the class:

  1. p_bb_odr_read - there will be a pointer to the output bit of the object in the ODR register (the position set by the user at the output of the output, if the output is used at the output). Used bit-banding area.
  2. p_bb_idr_read - there will be a pointer to the output bit of the object in the IDR register (this register contains the actual state of the output inputs, regardless of how the output is configured). Used bit-banding area.

We write methods that return the values ​​of these constants for a particular output.

 /* *     bit_banding *  ,      . */ constexpr uint32_t pin::bb_p_idr_read_get ( const pin_config_t* const pin_cfg_array ) { uint32_t p_port = p_base_port_address_get( pin_cfg_array->port ); uint32_t p_idr = p_port + 0x10; return M_GET_BB_P_PER(p_idr, M_EC_TO_U8(pin_cfg_array->pin_name)); } /* *     bit banding  , *       . */ constexpr uint32_t pin::odr_bit_read_bb_p_get ( const pin_config_t* const pin_cfg_array ) { uint32_t p_port = p_base_port_address_get( pin_cfg_array->port ); uint32_t p_reg_odr = p_port + 0x14; return M_GET_BB_P_PER(p_reg_odr, M_EC_TO_U8(pin_cfg_array->pin_name)); } 

This is used to define M_GET_BB_P_PER which, by uint32_t value of the register address in the field of the periphery and uint32_t, the port bit number returns the bit-banding address of this bit.

define M_GET_BB_P_PER
 //********************************************************************* // ,    . //********************************************************************* #define BIT_BAND_SRAM_REF 0x20000000 #define BIT_BAND_SRAM_BASE 0x22000000 //   RAM  Bit Banding . #define MACRO_GET_BB_P_SRAM(reg, bit) \ ((BIT_BAND_SRAM_BASE + (reg - BIT_BAND_SRAM_REF)*32 + (bit * 4))) #define BIT_BAND_PER_REF ((uint32_t)0x40000000) #define BIT_BAND_PER_BASE ((uint32_t)0x42000000) //      Bit Banding . #define M_GET_BB_P_PER(ADDRESS,BIT) \ ((BIT_BAND_PER_BASE + (ADDRESS - BIT_BAND_PER_REF)*32 + (BIT * 4))) 


Let's add in the constructor initialization of these commands.

 /********************************************************************** *  constexpr . **********************************************************************/ constexpr pin::pin ( const pin_config_t* const pin_cfg_array ): p_bsr ( this->p_bsr_get( pin_cfg_array ) ), p_bb_odr_read ( this->odr_bit_read_bb_p_get( pin_cfg_array ) ), bsr_set_msk ( this->set_msk_get( pin_cfg_array ) ), bsr_reset_msk ( this->reset_msk_get( pin_cfg_array ) ), p_bb_idr_read ( this->bb_p_idr_read_get( pin_cfg_array ) ) {}; 

Well, let's add real-time functions that will work with these constants:

 /* *      , *     . */ void pin::invert( void ) const { if (*M_U32_TO_P_CONST(p_bb_odr_read)) { //   1,   0. this->reset(); } else { this->set(); } } /* *      . */ int pin::read() const { return *M_U32_TO_P_CONST(p_bb_idr_read); } 

It uses another 1 define (M_U32_TO_P_CONST), which converts the value stored in the uint32_t variable into a pointer to the uint32_t variable write-protected.

 //    uint32_t     uint32_t. //    ,    ( ). #define M_U32_TO_P_CONST(point) ((const uint32_t *const)(point)) 

In the end, our class has acquired the following form:

 class pin { public: constexpr pin ( const pin_config_t* const pin_cfg_array ); void set ( void ) const; void reset ( void ) const; void set ( uint8_t state ) const; void set ( bool state ) const; void set ( int state ) const; void invert ( void ) const; int read ( void ) const; private: constexpr uint32_t p_bsr_get ( const pin_config_t* const pin_cfg_array ); constexpr uint32_t set_msk_get ( const pin_config_t* const pin_cfg_array ); constexpr uint32_t reset_msk_get ( const pin_config_t* const pin_cfg_array ); constexpr uint32_t odr_bit_read_bb_p_get ( const pin_config_t* const pin_cfg_array ); constexpr uint32_t bb_p_idr_read_get ( const pin_config_t* const pin_cfg_array ); const uint32_t p_bsr; const uint32_t bsr_set_msk, bsr_reset_msk; const uint32_t p_bb_odr_read, p_bb_idr_read; }; 

About abbreviated records.


It often happens that you need to create an object configuration structure for a specific task (for example, under the ADC input). If there are a lot of such conclusions, then to write all the parameters every time is tiresome. For this you can use the template class, which will use our template class. For ADC, it will look like this:

 template < EC_PORT_NAME PORT, EC_PORT_PIN_NAME PIN_NAME > class pin_config_adc_check_param : public pin_config_check_param< PORT, PIN_NAME, EC_PIN_MODE::INPUT, EC_PIN_OUTPUT_CFG::NO_USE, EC_PIN_SPEED::LOW, EC_PIN_PULL::UP, EC_PIN_AF::NO_USE, EC_LOCKED::LOCKED, EC_PIN_STATE_AFTER_INIT::NO_USE > { public: constexpr pin_config_adc_check_param() {}; }; 

The ad in the code will take many times less space

 const pin_config_adc_check_param< EC_PORT_NAME::B, EC_PORT_PIN_NAME::PIN_1 > adc_left; 

Total


Thus, at the output, we were able to create objects at the compilation stage, which do not use RAM in any way and do not require calling the constructor in real time. At the same time we will be sure that they are initialized correctly.

In this particular case, it is difficult to make a mistake, I agree. Maybe there is no need to check here, but for example, when it comes to creating PLL configuration settings, it is already more difficult. You can not take something into account. For example, that a divider cannot be set to any value, although the input field allows it to be received, or the displayed divider receives a frequency that exceeds, or vice versa, does not reach the limits of the recommended values. In such cases, the ability to check at compile time is very helpful.

It is also worth noting that the global initialization structure created for the pin class object will not go into the main firmware file. It will be dropped by the linker as not used. Only uint32_t variables filled with a constructor and the methods actually called in the user program will go into flash.

The code in the article is part of this library. The library is still in its infancy. How will the alpha version - will be a separate article on this topic.

Special thanks to madcomaker for responding to the Toaster , who came up with an idea.

Source: https://habr.com/ru/post/331468/


All Articles