0x06 GPIO driver development

0x06 GPIO driver development

Usually, library functions and APIs are provided by the vendors of MCUs for working with select onboard peripherals. This greatly lowers the learning curve, as coding it up from scratch is extremely tedious and time-consuming. Nevertheless, there is always a plus to knowing how to do stuff from first principles. For one, it gives you a very good understanding of the peripherals and what goes into writing drivers and allows you to debug code on a lower level of abstraction. Also, you may need to control the peripherals in a way not supported by the library functions, independently develop your peripheral APIs and libraries, or tailor existing ones to your specific application.

Each peripheral almost always has a set of associated registers. Peripheral registers are special memory-mapped registers used to control and configure the behaviour of peripheral devices in an MCU. They are usually located in a specific memory address range designated for peripheral devices and can be accessed using memory-mapped I/O techniques. These registers, their functions and the memory locations can be found in the MCU's reference manual. The subsequent development is based on the STM32F4xx family of MCU. The methods developed are generic and easily extensible to other families.

Procedures

  1. Create a new µvison project. Make sure to specify the appropriate target board.

  2. Add boilerplate code, ::CMSIS:CORE and ::Device:Startup software components to your project and create a main.c file.

  3. Download the reference manual of your MCU from the vendor's site. We will be using this a lot. All addresses used here are taken from the specifications of the manual.

  4. This section aims to configure the second pin in port B, PB2 as an output to drive an LED, so we will attach an LED bulb to this pin and turn it on.

GPIO: Minimal Configuration

To configure a GPIO, we need to write at least three registers.

  • Register that controls the CLK source to the bus the port is hooked up unto. By default, CLKs to buses unto which peripherals are hooked up is turned off to conserve energy.

  • Register that controls the mode of the pin. The pin can be configured as INPUT, OUTPUT, ALTERNATE functions etc.

  • Register that writes (in the case of OUTPUT mode) or reads the pin(in the case of INPUT mode).

Configuring the PB2 of the STM32F429xx as an output pin to drive an LED.

  • Clock control is accomplished using the Reset and Clock Control registers(RCC). To find out the bus we need to enable its CLK, we examine the schematics of the MCU to find out which bus connects the peripheral of interest. In our case, the GPIOx is hooked to the AHB1 bus. So we enable the corresponding clock to GPIOB in the RCC_AHB1ENR register.

  • Next is configuring the mode of the pin. In the STM32F4xx family, this is set in the MODER register. Each pin's mode is represented by two bits. 00: Input (reset state), 01: General purpose output mode, 10: Alternate function mode and 11: Analog mode.

  • Finally, we write to or read from the ODR (Output Data Register) or IDR (Input Data Register). If the pin is set up as an input in the MODER register, we read the IDR to find what signal is applied on the pin, and in the same way, we write to the ODR to set a state for the pin when it is configured as an input.

//main.c file

//Address of RCC in the memory space
#define RCC_BASE_ADDR      0x40023800UL  
//Address of GPIOB in the memory space
#define GPIOB_BASE_ADDR    0x40020400UL
#define RCC_AHB1ENR_ADDR   ((volatile uint32_t *)(RCC_BASE_ADDR + 0x30UL ))
//Address of MODER register in the memory space
#define GPIOB_MODER_ADDR   ((volatile uint32_t *)(GPIOB_BASE_ADDR + 0x00UL ))
//Address of ODR register in the memory space
#define GPIOB_ODR_ADDR     ((volatile uint32_t *)(GPIOB_BASE_ADDR + 0x14UL ))         

int main(){
    //Enable CLK to GPIOB
    *RCC_AHB1ENR_ADDR |= 1UL<<1;  
    //CLR and SET PB2 output mode. 01=> General Purpose Output
    *GPIOB_MODER_ADDR &= ~(3UL<<4);
    *GPIOB_MODER_ADDR |= 1UL<<4;
    //Write HIGH to PB2 ODR
    *GPIOB_ODR_ADDR |= 1UL<<2;
    return 0;
}

GPIO: Configuring all necessary registers

Other registers are used to configure the GPIO pins. In the above examples, we did not write them as their default values work for our application. The general procedure for configuring a GPIO pin is as follows:

  • Enable the clock to the bus that connects GPIOx in the RCC_AHB1ENR register.

  • Set the mode of the pin in the MODER register.

  • Set the output type in the OTYPER register. This can be 0: Output push-pull (reset state), 1: Output open-drain.

  • Set the output speed in the OSPEEDR register. 00: Low speed 01: Medium speed 10: High speed 11: Very high speed.

  • Configure the I/O pull-up, pull-down in the PUPDR register. 00: No pull-up, pull-down, 01: Pull-up, 10: Pull-down, 11: Reserved.

  • Finally, write to the corresponding bit of the ODR to set a state for the pin, 0: LOW, 1: HIGH. Similarly, read the IDR's corresponding bit to determine the pin's state if it is configured as an input in the MODER register.

//main.c

#define RCC_BASE_ADDR      0x40023800UL
#define GPIOB_BASE_ADDR    0x40020400UL
#define RCC_AHB1ENR_ADDR   ((volatile uint32_t *)(RCC_BASE_ADDR + 0x30UL ))
#define GPIOB_MODER_ADDR   ((volatile uint32_t *)(GPIOB_BASE_ADDR + 0x00UL ))
#define GPIOB_OTYPER_ADDR  ((volatile uint32_t *)(GPIOB_BASE_ADDR + 0x04UL ))
#define GPIOB_OSPEEDR_ADDR ((volatile uint32_t *)(GPIOB_BASE_ADDR + 0x08UL ))
#define GPIOB_PUPDR_ADDR   ((volatile uint32_t *)(GPIOB_BASE_ADDR + 0x0CUL ))
#define GPIOB_ODR_ADDR     ((volatile uint32_t *)(GPIOB_BASE_ADDR + 0x14UL ))

int main(){
    //Enable CLK to GPIOB
    *RCC_AHB1ENR_ADDR |= 1UL<<1;
    //CLR and SET PB2 output mode. 01=> General Purpose Output
    *GPIOB_MODER_ADDR &= ~(3UL<<4);
    *GPIOB_MODER_ADDR |= 1UL<<4;
    //Written to Config O/P type, => Set push-pull(0)
    *GPIOB_OTYPER_ADDR &= ~(1UL<<2);
    //Written to Config O/P speed, => Set low speed(00)
    *GPIOB_OSPEEDR_ADDR &= ~(3UL<<4);
    //Config I/O pull-up/down, => Set no pull-up, no pull down(00)
    *GPIOB_PUPDR_ADDR &= ~(3UL<<4);
    //Write HIGH or LOW to O/P => Set HIGH(1)
    *GPIOB_ODR_ADDR |= 1UL<<2;
    return 0;

}

GPIO: A modular way of writing the driver

In the above developments, we defined a peripheral base address, added an offset, cast it to a pointer and assigned it to a macro. We dereference the macro and operate on the register to access the memory location referenced by this macro. The problem with this approach is that for every peripheral register, we need to look up the offset address and repeat the procedure defined above. This is not scalable as the number of lookups increases with the number of registers associated with a peripheral. Also, it does not make use of the fact that peripherals have the same set of registers(e.g. GPIOx all have the same set of registers).

A better way is to define a data structure that organizes its members linearly in memory (since the peripherals' registers are organised this way) and then arrange the registers accordingly. One such data structure in C is the struct. This method allows us to pass or return a whole set of peripheral registers to or from a function without using a prohibitive amount of macros. This technique is illustrated below.

//Define a struct and arrange the members as the preipheral registers
//The size of <data type> shouldmatch the size of the target register
typedef struct{
<data type> perpheral_reg1;
<data type> perpheral_reg2;
         ...
<data type> perpheral_regN;

}Peripheral_TypeDef;

Now we define a user-defined type of type Peripheral_TypeDef, cast it to a pointer and assign it to a macro as follows:

#define GPIOx ((Peripheral_TypeDef*)(base_addr_of_peripheral))

The individual registers can then be assessed by

GPIOx->perpheral_reg1 = ...
GPIOx->perpheral_regN = ...

GPIO: Re-writing the driver using structs

To apply this approach in our development, add peripheral_registers.h file in your project and make a struct with the peripheral registers arranged in order.

// peripheral_registers.h

#include <stdint.h>
/** 
  * @brief General Purpose I/O
  */
typedef struct{
        volatile uint32_t GPIOx_MODER;                //0x00
        volatile uint32_t GPIOx_OTYPER;               //0x04
        volatile uint32_t GPIOx_OSPEEDR;              //0x08
        volatile uint32_t GPIOx_PUPDR;                //0x0C
        volatile uint32_t GPIOx_IDR;                  //0x10
        volatile uint32_t GPIOx_ODR;                  //0x14                 
        volatile uint32_t GPIOx_BSRR;                 //0x18
        volatile uint32_t GPIOx_LCKR;                 //0x1C
        volatile uint32_t GPIOx_AFRL;                 //0x20
        volatile uint32_t GPIOx_AFRH;                 //0x24    
    }GPIO_RegDef;
/** 
  * @Reset and clock control
  */
typedef struct{
    volatile uint32_t RCC_CR;                       //0x00
    volatile uint32_t RCC_PLLCFGR;                  //0x04
    volatile uint32_t RCC_CFGR;                     //0x08
    volatile uint32_t RCC_CIR;                      //0x0C
    volatile uint32_t RCC_AHB1RSTR;                 //0X10
    volatile uint32_t RCC_AHB2RSTR;                 //0X14
    volatile uint32_t RCC_AHB3RSTR;                 //0X18
    volatile uint32_t RESERVED0;                    //0X1C
    volatile uint32_t RCC_APB1RSTR;                 //0x20
    volatile uint32_t RCC_APB2RSTR;                 //0x24
    volatile uint32_t RESERVED1[2];                 //0x28
    volatile uint32_t RCC_AHB1ENR;                  //0x30
    volatile uint32_t RCC_AHB2ENR;                  //0x34
    volatile uint32_t RCC_AHB3ENR;                  //0x38
    volatile uint32_t RESERVED3;                    //0x3C
    volatile uint32_t RCC_APB1ENR;                  //0x40
    volatile uint32_t RCC_APB2ENR;                  //0x44
    volatile uint32_t RESERVED4[2];                 //0x48
    volatile uint32_t RCC_AHB1LPENR;                //0x50
    volatile uint32_t RCC_AHB2LPENR;                //0x54
    volatile uint32_t RCC_AHB3LPENR;                //0x58
    volatile uint32_t RESERVED6;                    //0x5C
    volatile uint32_t RCC_APB1LPENR;                //0x60
    volatile uint32_t RCC_APB2LPENR;                //0x64
    volatile uint32_t RESERVED7[2];                 //0x68
    volatile uint32_t RCC_BDCR;                     //0x70
    volatile uint32_t RCC_CSR;                      //0x74
    volatile uint32_t RESERVED9[2];                 //0x78
    volatile uint32_t RCC_SSCGR;                    //0x80
    volatile uint32_t RCC_PLLI2SCFGR;               //0x84
    volatile uint32_t RCC_PLLSAICFGR;               //0x88
    volatile uint32_t RCC_DCKCFGR;                    //0x8C

}RCC_TypeDef;

Now, cast the base addresses of the peripherals to the user-defined types GPIO_Typdef and RCC_TypeDef and assign them to macros.

//Cast base addresses of RCC and GPIOB to the corresponding data structures

#define RCC ((RCC_TypeDef*)0x40023800UL)
#define GPIOB ((GPIO_TypeDef*)0x40020400UL)

Finally, include the header file, peripheral_register.h, in the main function and configure the GPIO as done previously.

#include "peripheral_registers.h"

int main(){
    RCC->AHB1ENR |= 1UL<<1;
    GPIOB->MODER &= ~(3UL<<4); 
    GPIOB->MODER |= 1UL<<4;
    GPIOB->ODR   |= 1UL<<2;
}

Further Reading