
WHY DO WE NEED TO DEFINE REGISTERS ADDRESSES?
In programming microcontrollers for different type of duties such as: timing, communicating with other units, or sampling an analogue signal; we use peripherals like: ADCs, timers, communication peripherals (SPI, I2C, USART etc). These peripherals have different type of registers such as: data registers, status registers and control registers. The usage of the word “register” may seem confusing because it is used also for CPU registers like: R0-R12, PC and LR. However, here “register” means addresses of peripherals in memory mapped IO.
HOW TO DO IT?
There are different methods of defining peripheral’s addresses and they depend on the programming language we use. That’s why we use different methods in different programming languages. Although, in this article I would like to focus only on defining an address of periperal in C programming language. Moreover, in C programming language, there are various ways to do this. We will examine each of them, so that you can later choose the one which is suitable for you.
1. BY CREATING A POINTER
I consider that this method is not efficient. However, this example will help us understand the principles of the methodology.
volatile uint8_t* register_name = (uint8_t*)register_adress;
In order to use register, you call *register_name pointer.
Let’s take a look at an example in which we will define register’s addresses of GPIO ports of Atmega328p microcontroller from AVR family.
//definition of registers
volatile uint8_t* PORTB = (uint8_t*)0x25;
volatile uint8_t* DDRB = (uint8_t*)0x24;
volatile uint8_t* PINB = (uint8_t*)0x23;
volatile uint8_t* PORTC = (uint8_t*)0x28;
volatile uint8_t* DDRC = (uint8_t*)0x27;
volatile uint8_t* PINC = (uint8_t*)0x26;
volatile uint8_t* PORTD = (uint8_t*)0x2B;
volatile uint8_t* DDRD = (uint8_t*)0x2A;
volatile uint8_t* PIND = (uint8_t*)0x29;
// Using registers
*PORTB |= (1<<1); // set first bit of PORTB register
*PORTB &= ~(1<<2); // reset second bit of PORTB register
I would like to point out that in this example it is necessary to pay attention to the type of pointer. I used uint8_t and it works for 8 bit microcontrollers. However, if you use a 32 bit microcontoller like STM32 you have to use uint32_t as your pointer variable type. I would recommend searching more about stdint library and memory organisation of your microcontroller.
BY USING PRE-PROCESSORS
Although this method looks more efficient than first method , It requires to define preprocessor diretive for each registers of peripherals as first method.
#define register_name (*((volatile uint8_t) register_adress))
In this method it is enough to call register_name to use it. It will be understandable with an example.
// Definition of registers
#define PORTB (*((volatile uint8_t) 0x25))
#define DDRB (*((volatile uint8_t) 0x24))
#define PINB (*((volatile uint8_t) 0x23))
#define PORTC (*((volatile uint8_t) 0x28))
#define DDRC (*((volatile uint8_t) 0x27))
#define PINC (*((volatile uint8_t) 0x26))
#define PORTD (*((volatile uint8_t) 0x2B))
#define DDRD (*((volatile uint8_t) 0x2A))
#define PIND (*((volatile uint8_t) 0x29))
// Usage of registers
PORTB |= (1<<1); // set first bit of PORTB register
DDRB &= ~(1<<2); // reset second bit of PORTB register
Personally, I do not prefer to use these last two methods. The reason why is that while using them we need to define registers one by one. Moreover, we have to do it for each register of each peripherals (even if they are identical, like numbers of GPIO ports).
BY USING STRUCTS
In my opinion, this is the most efficient way to define registers’ addresses of peripherals. We do not need to define it for each register of a peripheral. It is enough to define first member of struct. Compiler will appoint remaining by incrementing address instead of us. Another andvantage of this method appears while using multiple number of identical peripherals. For instance, you may have 4 GPIO ports in your microcontroller. This method allows you to define all registers with a few lines of code by creating a new member from the same struct.
typedef struct
{
uint32_t register1_name; // first register, offset: 0x00
uint32_t register2_name; // second register, offset: 0x04
uint32_t register3_name; // third register, offset: 0x08
} Peripheral_type;
volatile Peripheral_type* peripheral_name = (Peripheral_type*)adress_of_firts_register;
In this example, I used uint32_t as pointer type because I assumed this microcontroller is 32 bit. As it is written on comment lines address values are incremented by 4 (it should be 1 in 8 bit microcontrollers) and compiler does this job for us. It is enough to define register1 address then other registers will be located in the memory sequaciously by compiler. The question is why do they have sequacious addresses? The Answer is easy, microcontroller vendors design hardwares in this way 🙂

Here we can see the same example but using last method:
//Definition of Registers
typedef struct
{
uint8_t PIN; // first register, offset: 0x00
uint8_t DDR; // second register, offset: 0x01
uint8_t PORT; // third register, offset: 0x02
} IO_type;
volatile IO_type* GPIOB = (IO_type*)0x23;
volatile IO_type* GPIOC = (IO_type*)0x26;
volatile IO_type* GPIOD = (IO_type*)0x29;
//Usage of Registers
*GPIOB->PORT |= (1<<1); // set first bit of register
*GPIOB->PORT &= ~(1<<2); // reset second bit of register
It is possible also to combine second and third method:
typedef struct
{
uint32_t register1_name; // first register, offset: 0x00
uint32_t register2_name; // second register, offset: 0x04
uint32_t register3_name; // third register, offset: 0x08
} Peripheral_type;
#define Peripheral_name ((volatile Peripheral_type*) adress_of_firts_register)
Peripheral_name->registername |= (1<<1); //set first bit
Peripheral_name->registername &= ~(1<<2); //reset second bit