Thoughts on MCU/Embedded Register and Field Access Patterns

December 21, 2023

A common way to define and use peripheral registers in MCU/embedded applications is to define one or more properties of the peripheral, its registers and the fields of each register. For example, in CMSIS 5, SysTick peripheral is defined as:

typedef struct
{
  __IOM uint32_t CTRL;                   /*!< Offset: 0x000 (R/W)  SysTick Control and Status Register */
  __IOM uint32_t LOAD;                   /*!< Offset: 0x004 (R/W)  SysTick Reload Value Register */
  __IOM uint32_t VAL;                    /*!< Offset: 0x008 (R/W)  SysTick Current Value Register */
  __IM  uint32_t CALIB;                  /*!< Offset: 0x00C (R/ )  SysTick Calibration Register */
} SysTick_Type;

When the corresponding memory address is cast as SysTick_Type*, the registers CTRL, LOAD, VAL and CALIB can be used directly through the structure like (assuming the variable name is SYSTICK) SYSTICK->CTRL.

Then, the individual fields of registers, for example the field ENABLE of register CTRL, is defined like this:

#define SysTick_CTRL_ENABLE_Pos             0U                                            /*!< SysTick CTRL: ENABLE Position */
#define SysTick_CTRL_ENABLE_Msk            (1UL /*<< SysTick_CTRL_ENABLE_Pos*/) 

Then, the ENABLE field of CTRL register can be modified by:

  • reading the CTRL register value to a temporary variable tmp
  • clearing the ENABLE field in tmp (using the bitwise inverse of ENABLE_Msk)
  • modifying the ENABLE field in tmp (e.g. ORing with ENABLE_Msk to set it)
  • writing tmp back to the CTRL register

Similar code structure is also kept in STM32 CMSIS components. For HASH component (in STM32H563), the actual instance of the structure is created like this (in cmsis_device_h5 repository):

#define HASH_NS  ((HASH_TypeDef *) HASH_BASE_NS)

Another slightly different approach is in Apache NuttX, where the peripheral base addresses and register offsets are individually defined, like (in stm32u5 architecture):

#define STM32_LPUART1_BASE 0x46002400

and

#define STM32_USART_CR1_OFFSET 0x0000  /* Control register 1 */

#define STM32_USART1_CR1       (STM32_USART1_BASE + STM32_USART_CR1_OFFSET)

Both approaches require knowing the peripheral, register and field when writing a code. A pseudo function for field modification is basically:

MODIFY_FIELD(PERIPHERAL, REGISTER, FIELD)
e.g.
MODIFY_FIELD(USART1, USART1_CR1, USART1_CR1_UE)

in some form or other.

The definitions above are needed and these approaches are not surprising. However, the MODIFY_FIELD code is I think “over-specified”. By over-specification, I mean the FIELD is already coupled to a register, so actually the only information needed is the peripheral and the field, e.g.:

BETTER_MODIFY_FIELD(PERIPHERAL, FIELD)

PERIPHERAL is not always coupled to the FIELD or to the REGISTER, since some/most peripherals are instances of the same function (called group in SVD). For example, LPUART1, USUARTx, UARTy are all instances of USART. On the other hand, registers are actually related to (group) USART and fields are related to a register. Naturally, each peripheral instance naturally has a different register set.

Another issue with the code pattern mentioned above is that the macros are not enough for the better or simpler usage. The information which register a field belongs to is not given above. There is simply a need for a macro, something like:

#define STM32_USART_CR1_REGISTER	STM32_USART_CR1

This makes it possible to use the simpler/better modify method, like:

BETTER_MODIFY_FIELD(USART1, USART_CR1_EN)

Yet another improvement and another problem above is the naming of macros. In order to avoid name clashes:

  • peripherals might be prefixed with a constant (like STM32_)
  • registers should be prefixed with peripheral group (like USART_ or longer form STM32_USART_)
  • fields should be prefixed with register name (like USART_CR1_ or longer form STM32_USART_CR1_)

This results again an over-specification when coding, because the macro names already contain the information macros’ contain. For example:

SET_FIELD(USART1, USART_CR1_EN)

By this line, we already understand it sets the EN field of CR1 register of USART1 peripheral without knowing the actual value of USART_CR1_EN because of the name alone. However, programmatically, in C, it is not possible to use the information encoded in the name. So, there are multiple macros which makes this call possible, something like:

#define USART1_BASE  0x...

#define USART_CR1_OFFSET <reg_offset>

#define USART_CR1_EN_POS ...
#define USART_CR1_EN_MASK ...

#define USART_CR1_REG_OFFSET <reg_offset>

So the actual register address can be found from USART1_BASE and USART_CR1_REG_OFFSET. Since it is already known beforehand that the group of USART1 is USART, by having an extra macro:

#define USART1_GROUP USART

the call can be further reduced to:

SET_FIELD(USART1, CR1_EN)

This is I think the most simple version. Nothing is repeated, all static knowledge is stored in the macros (such as the group name of USART1, register where CR1_EN belongs to, and addresses, offsets, positions and masks). The names used in the code in methods like SET_FIELD is simpler, while the actual names are still preserved to avoid name clashes.

The macro implementations of these methods (e.g. SET_FIELD) require a slightly more advanced macro knowledge, since it involves token pasting. Simply wrapping each macro functions resolves the potential issue with token pasting.

An example implementation is below:

#include <stdio.h>
#include <stdint.h>

#define USART1_BASE		    0x1234000
#define USART1_GROUP		USART

#define USART_CR1_OFFSET	0x10

#define USART_CR1_EN_REG	CR1
#define USART_CR1_EN_POS	2UL

#define USART_CR1_EN_MASK	    0x00000002
#define USART_CR1_EN_CLEAR_MASK	0xFFFFFFFD

#define PERIPHERAL_BASE(peripheral)	peripheral ## _BASE

#define PERIPHERAL_GROUP_EX(peripheral) peripheral ## _GROUP
#define PERIPHERAL_GROUP(peripheral) PERIPHERAL_GROUP_EX(peripheral)

#define REGISTER_OFFSET_EX(group, reg) group ## _ ## reg ## _OFFSET
#define REGISTER_OFFSET(group, reg) REGISTER_OFFSET_EX(group, reg)

#define FIELD_REGISTER_EX(group, field) group ## _ ## field ## _REG
#define FIELD_REGISTER(group, field) FIELD_REGISTER_EX(group, field)

#define FIELD_POS_EX(group, field) group ## _ ## field ## _POS
#define FIELD_POS(group, field) FIELD_POS_EX(group, field)

#define FIELD_MASK_EX(group, field) group ## _ ## field ## _MASK
#define FIELD_MASK(group, field) FIELD_MASK_EX(group, field)

#define FIELD_CLEAR_MASK_EX(group, field) group ## _ ## field ## _CLEAR_MASK
#define FIELD_CLEAR_MASK(group, field) FIELD_CLEAR_MASK_EX(group, field)

#define SET_REGISTER(peripheral, reg, value) \
	set_register(PERIPHERAL_BASE(peripheral), \
		  REGISTER_OFFSET(PERIPHERAL_GROUP(peripheral), reg), \
		  value)

#define SET_FIELD(peripheral, field, value) \
	set_field(PERIPHERAL_BASE(peripheral), \
		  REGISTER_OFFSET(PERIPHERAL_GROUP(peripheral), \
			          FIELD_REGISTER(PERIPHERAL_GROUP(peripheral), \
					         field)), \
		  FIELD_POS(PERIPHERAL_GROUP(peripheral), field), \
		  FIELD_CLEAR_MASK(PERIPHERAL_GROUP(peripheral), field), \
		  value)

void set_register(
	uintptr_t base_addr, 
	uint32_t reg_offset,
	uint32_t value)
{
	printf("0x%08lX %u\n", 
			base_addr + reg_offset, 
			value);
	/*
	write_register(base_addr + reg_offset, value)
	*/
}

void set_field(
	uintptr_t base_addr, 
	uint32_t reg_offset,
	uint32_t field_pos,
	uint32_t field_clear_mask,
	uint32_t value)
{
	printf("0x%08lX %u %u %u\n", 
			base_addr + reg_offset, 
			field_pos,
			field_clear_mask,
			value);
	/*
	uint32_t v = read_register(base_addr + reg_offset);
	v &= field_clear_mask;
	v |= (value << field_pos);
	write_register(base_addr + reg_offset, v)
	*/
}

int main(void)
{
  SET_REGISTER(USART1, CR1, 1);
  SET_FIELD(USART1, CR1_EN, 1);
}