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).

Peripheral unit registers are mapped into the system memory space.
Fig. 1 Memory mapped peripheral unit

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.

CFGR/W0x00Configuration register
STATR0x04Status register
DATAR/W0x08Data register (transmit and receive)
Table 1. I2C register overview
7:4SLV_ADDRConfigure the slave address
3TIMEOUT_ENEnable time-out
2RXTX_SELSelect transfer type: transmit or receive
1MS_MODE_SELSelect master or slave mode of operation
0I2C_ENEnable the I2C module
Table 2. CFG register bit fields
7BUS_BUSYI2C bus is busy
6TX_FIFO_EMPTYTransmit FIFO is empty
5RX_FIFO_FULLReceive FIFO is full
1ARB_LOSTArbitration Lost
0TX_ERRORTransmit Error
Table 3. STAT register bit fields

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

Was this article helpful?