The peripheral access layer is a layer of the firmware that provides access to the registers of a microcontroller’s peripheral units. It has to be easy to use, well structured, and efficient. This layer is part of the software packages every microcontroller vendor provides for free with their Software Development Kits (SDK) and Integrated Development Environments (IDE). There are various approaches to defining a peripheral access layer, in this article, we will present a simple example complying with the CMSIS standard that is used for ARM Cortex-based microcontrollers.
Memory Access Introduction
One of the biggest benefits of using C language in embedded systems is the capability to directly access memory locations using pointers. This is very important as the peripheral units (e.g SPI, ADC, I2C, GPIO, etc.) used in microcontrollers are memory-mapped. This means that accessing them is actually an operation of accessing addresses from the system memory space (see Fig.1).

Accessing a register of a peripheral unit is achieved by creating a pointer to its address and then dereferencing the pointer so we can access the actual value at that address. Example:
uint32_t temp;
temp = *((volatile uint32_t*) 0x80000000); /* Read the value of register with address 0x80000000 */
*((volatile uint32_t*) 0x80000008) = 0x55AA0011; /* Write value to register with address 0x80000008 */
The volatile
keyword is used because the value at that address can change without the intervention of the code. The peripheral unit can update it at any time. By declaring a pointer to a volatile
variable, we indicate to the compiler that it should not perform any optimizations on the usage of this variable.
Hardcoding the addresses of all registers in our code, as shown above, will make it very hard to read and maintain. That style is not recommended. We can make it a little bit more user-friendly and increase the abstraction with the use of macros. Example:
#define CFG_REG (*((volatile uint32_t*) 0x80000000))
#define DATA_REG (*((volatile uint32_t*) 0x80000008))
uint32_t temp;
temp = CFG_REG; /* Read the value of the register at address 0x80000000 */
DATA_REG = = 0x55AA0011; /* Write value to the register at address 0x80000008 */
We can improve the readability of our code even further with the introduction of the peripheral access layer.
Peripheral Access Layer Example
We will use an I2C peripheral for the examples. For keeping things simple and easy to follow, we will not define the peripheral access layer for all of its registers. The subset of registers we will use is shown in the tables below.
Name | Access | Offset | Description |
---|---|---|---|
CFG | R/W | 0x00 | Configuration register |
STAT | R | 0x04 | Status register |
DATA | R/W | 0x08 | Data register (transmit and receive) |
… | … | … | … |
Bit | Symbol | Description |
---|---|---|
31:4 | N/A | Reserved |
7:4 | SLV_ADDR | Configure the slave address |
3 | TIMEOUT_EN | Enable time-out |
2 | RXTX_SEL | Select transfer type: transmit or receive |
1 | MS_MODE_SEL | Select master or slave mode of operation |
0 | I2C_EN | Enable the I2C module |
Bit | Symbol | Description |
---|---|---|
31:8 | N/A | Reserved |
7 | BUS_BUSY | I2C bus is busy |
6 | TX_FIFO_EMPTY | Transmit FIFO is empty |
5 | RX_FIFO_FULL | Receive FIFO is full |
4:2 | N/A | Reserved |
1 | ARB_LOST | Arbitration Lost |
0 | TX_ERROR | Transmit Error |
Register Layout Definition Without Bit Fields
The first step is defining a structure that will contain the registers of our peripheral unit. Each member of the structure is a peripheral’s register. The position and the name of members(registers) are taken from the specification. In the example below, we can see how the registers from Table. 1 are placed in the structure. The bit fields with this approach are manipulated using macros (as shown later in the article).
#define __IO volatile /* Defines 'read / write' permissions */
#define __I volatile const /* Defines 'read only' permissions */
#define __O volatile /* Defines 'write only' permissions */
/** I2C - Register Layout Typedef */
typedef struct {
__IO uint32_t CFG; /* Configuration for shared functions, offset: 0x0 */
__I uint32_t STAT; /* Status register for Master, Slave, offset: 0x4 */
__IO uint32_t DATA; /* Interrupt Enable Set and read register., offset: 0x8 */
} I2C_Type;
Register Layout Definition with Bit Fields
An alternative approach to the one shown in the previous example is to define a structure that contains not only the peripheral registers but also the bit fields. This can be done using unions the following way:
typedef struct {
union {
unsigned long LONG; /* Use for acccesing the whole 32bits */
struct {
unsigned short H;
unsigned short L;
} WORD; /* Use for accessing high and low 16bits */
struct {
unsigned char Reserved:24;
unsigned char SLV_ADDR:4;
unsigned char TIMEOUT_EN:1;
unsigned char RXTX_SEL:1;
unsigned char MS_MODE_SEL:1;
unsigned char I2C_EN:1;
} BIT; /* Use for accessing individual bit fields of the register */
} CFG; /* Register name */
union {
/* Here definition for the STAT register can be placed */
} STAT; /* Register name */
/* The rest of the I2C registers are placed here just like the example above */
} I2C_Bitfields_type
Using bit fields can make the code more readable, however, it has some drawbacks. C language bit fields have compiler dependent implementation and that can cause issues with creating portable code.
Mapping an Instance
Once we have defined a structure for our peripheral we need to map it to the correct system memory address. Each peripheral is mapped starting from a specific address (Base Address). This address is specified in the device’s documentation.
/* I2C - Peripheral instance base addresses */
/** Peripheral I2C base address */
#define I2C_BASE (0x80000000u)
/** Peripheral I2C base pointer */
#define I2C ((I2C_Type *)I2C_BASE)
Accessing the Registers of the Peripheral
Accessing each register is done by accessing the members of the structure we defined. Remember that there is a pointer to a structure of type I2C_Type that points to where the actual peripheral is memory-mapped. Accessing the registers based on the definition we already made can be done the following way:
/* Write a value to the CFG register of the I2C peripheral*/
I2C->CFG = 0x00000052;
/* Read the CFG register */
uint32_t temp;
temp = I2C->CFG;
The final step is implementing a way for manipulating the bit fields of the registers. This is done using macros. For every, register we need to define the position of each bit field and create a mask for it. Example:
/* Position and Mask definition for bit fields of CFG register */
#define I2C_CFG_I2C_EN_Pos (0U)
#define I2C_CFG_I2C_EN_Msk (0x1UL << I2C_CFG_I2C_EN_Pos)
#define I2C_CFG_SLV_ADDR_Pos (4U)
#define I2C_CFG_SLV_ADDR_Msk (0xfUL << I2C_CFG_SLV_ADDR_Pos )
Using those definitions and bit wise operations, we can manipulate every bit field of a register.
#define _FLD2VAL(field, value) (((uint32_t)(value) & field ## _Msk) >> field ## _Pos)
#define _VAL2FLD(field, value) (((uint32_t)(value) << field ## _Pos) & field ## _Msk)
The macro _VAL2FLD
will put a specific value in the position of the bit field.
uint32_t reg_value;
reg_value = I2C->CFG; /* Read current value of the register */
reg_value &= ~I2C_CFG_SLV_ADDR_Msk; /* Clear bits to be written to */
reg_value |= _VAL2FLD(I2C_CFG_SLV_ADDR,5); /* Set a value in the bit field*/
I2C->CFG = reg_value; /* Write the new value in the registers */
The macro _FLD2VAL
will read the value of a specific bit field.
uint32_t temp;
/* Get the value of I2C_EN bit field from CFG register of the I2C peripheral */
temp = _FLD2VAL(I2C_CFG_I2C_EN, I2C->CFG);
Leave A Comment