0x07 GPIO driver development: Based on Device Peripheral Access Layer

0x07 GPIO driver development: Based on Device Peripheral Access Layer

Notice that in the previous developments, macros were used to store the base address of the peripherals. To access each register, we add an offset to the base address(the offset of each register with respect to the peripheral address in the address space of the MCU is usually found in the reference manual), cast it to a pointer of volatile uint32_t type and assigned it to a macro. There is one obvious shortcoming of this approach; portability. The peripheral addresses of MCUs in the even same family (say STM32Fxxxx) vary. So our code breaks when the target platform is not STM32F429xx. Also, looking up each address and offset is time-consuming and error-prone.

MCU vendors usually provide a Device Peripheral Access Layer Header File. This file contains data structures and the address mapping for all peripherals, registers declarations, bits definition and macros to access the peripheral’s registers hardware. Instead of dealing with numeric addresses, we use macros and data structures to access the peripherals efficiently. For the STM32F429xx series, STM32 provides stm32f429xx.h, a header file containing all the necessary macros and data structures to work with the onboard peripherals.

This section aims to develop digital I/O functions for working with GPIO. Reading and writing pins is generally a common operation in any MCU programming, so we write generic functions to simplify this interaction.

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. You must add the corresponding DPAL header file to your project to use the macros and structures defined in the peripheral access layer interface. The MCU vendor usually provides this. In this case, it is stm32f429xx.h.

  4. Add a GPIO_drivers.h file. This will contain our function prototypes and macro definitions and edit it as follows:

//GPIO_drivers.h

#include <stdint.h>
#define IN   0b00
#define OUT  0b01
#define HIGH 0b01
#define LOW  0b00

/*
@ brief: Writes STATE to the designated PIN, in the designated PORT
@ return: void
@ params: 
PIN: pin to write.
PORT: port to write. Default is 'A'
STATE: HIGH or LOW. 
*/
void digitalWrite(int PIN, uint32_t STATE, char PORT = 'A');

/*
@ brief: Reads STATE to the designated PIN, in the designated PORT 
@ return: int denoting STATE of PIN
@ params
PIN: pin to read
PORT: port to read. Default is 'A'
*/
int digitalRead(int PIN, char PORT = 'A');

/*
@ brief: Configures PIN the specified PORT
@ return: void
@ params
PIN: pin to conigure
MODER: mode of PIN => OUT, IN, ALT or ANLG. 
PORT: port to configure. Default is 'A'
OTYPER: output type. Default is Output push-pull(0)
OSPEEDR: output speed. Default is low-speed(00)
PUPDR: pull-up/down: Default is no pull-up, no pull-down(00)
*/    
void pinMode(int PIN, uint32_t MODER, char PORT = 'A', uint32_t OTYPER = 0b00, uint32_t OSPEEDR = 0b00, uint32_t PUPDR = 0b00);

The implementation of the above header file is done in C++. This is because C does not support passing default parameters. We will heavily rely on this concept when implementing the functions to allow for flexibility in configuring the ports using the I/O functions. Add a GPIO_drivers.cpp file and edit as shown below.

//GPIO_drivers.cpp
//Implementation of the  pinMode() function
void pinMode(int PIN, uint32_t MODER, char PORT = 'A', uint32_t OTYPER = 0x00, uint32_t OSPEEDR = 0x00, uint32_t PUPDR = 0x00){
    RCC->AHB1ENR |= 1UL<<(PORT - 'A');
    switch(PORT){
        case 'A':
            GPIOA->MODER   &= ~(3UL<<(2*PIN));
            GPIOA->MODER   |= MODER<<(2*PIN);
            GPIOA->OTYPER  &= ~(1UL<<PIN);
            GPIOA->OTYPER  |= OTYPER<<PIN;
            GPIOA->OSPEEDR &= ~(3UL<<(2*PIN));
            GPIOA->OSPEEDR |= OSPEEDR<<(2*PIN);
            GPIOA->PUPDR   &= ~(3UL<<(2*PIN));
            GPIOA->PUPDR   |= PUPDR<<(2*PIN); break;
        case 'B':
            GPIOB->MODER   &= ~(3UL<<(2*PIN));
            GPIOB->MODER   |= MODER<<(2*PIN);
            GPIOB->OTYPER  &= ~(1UL<<PIN);
            GPIOB->OTYPER  |= OTYPER<<PIN;
            GPIOB->OSPEEDR &= ~(3UL<<(2*PIN));
            GPIOB->OSPEEDR |= OSPEEDR<<(2*PIN);
            GPIOB->PUPDR   &= ~(3UL<<(2*PIN));
            GPIOB->PUPDR   |= PUPDR<<(2*PIN); break;
        case 'C':
            GPIOC->MODER   &= ~(3UL<<(2*PIN));
            GPIOC->MODER   |= MODER<<(2*PIN);
            GPIOC->OTYPER  &= ~(1UL<<PIN);
            GPIOC->OTYPER  |= OTYPER<<PIN;
            GPIOC->OSPEEDR &= ~(3UL<<(2*PIN));
            GPIOC->OSPEEDR |= OSPEEDR<<(2*PIN);
            GPIOC->PUPDR   &= ~(3UL<<(2*PIN));
            GPIOC->PUPDR   |= PUPDR<<(2*PIN); break;
        default:                               break;
    }
}

We use a switch statement in the above implementation to configure the specified port. The relatively large default parameter list enables us to configure the registers when we don't want to use their default values. Notice that we only went up to port C in the implementation, as the extension to other ports is trivial. To configure a pin using this function, we include the header file GPIO_drivers and call the function as follows:

#include "GPIO_drivers.h"

int main(){
//Configure PA3 as INPUT pin
pinMode(3,OUT);
//Configure PB2 as IN pin
pinMode(3,IN,'B');
return 0;
}

In the same way, we implement two other digital I/O functions to write and read GPIO pins. Below is the implementation of the digitalWrite() function.

//GPIO_drivers.cpp
//Implementation of the  digitalWrite() function

void digitalWrite(int PIN, uint32_t STATE, char PORT = 'A'){
    switch(PORT){
    case 'A': GPIOA->ODR &= ~(1UL<<PIN); GPIOA->ODR |= STATE<<PIN; break;
    case 'B': GPIOB->ODR &= ~(1UL<<PIN); GPIOB->ODR |= STATE<<PIN; break;
    case 'C': GPIOC->ODR &= ~(1UL<<PIN); GPIOC->ODR |= STATE<<PIN; break;
    case 'D': GPIOD->ODR &= ~(1UL<<PIN); GPIOD->ODR |= STATE<<PIN; break;
    case 'E': GPIOE->ODR &= ~(1UL<<PIN); GPIOE->ODR |= STATE<<PIN; break;
    case 'F': GPIOF->ODR &= ~(1UL<<PIN); GPIOF->ODR |= STATE<<PIN; break;
    case 'G': GPIOG->ODR &= ~(1UL<<PIN); GPIOG->ODR |= STATE<<PIN; break;
    case 'H': GPIOH->ODR &= ~(1UL<<PIN); GPIOH->ODR |= STATE<<PIN; break;
    case 'I': GPIOI->ODR &= ~(1UL<<PIN); GPIOI->ODR |= STATE<<PIN; break;
    case 'J': GPIOJ->ODR &= ~(1UL<<PIN); GPIOJ->ODR |= STATE<<PIN; break;
    case 'K': GPIOK->ODR &= ~(1UL<<PIN); GPIOK->ODR |= STATE<<PIN; break;
    default:                                                       break;

    }
}

From the description of this function in the header file, it writes a given state to the specified port. When the port is not specified, it is implicitly assumed to be port A. To use this function in our project, we include the header file GPIO_drivers.h and call the functions as follows:

#include "GPIO_drivers.h"

int main(){
//Turn on LED connected to PA3
pinMode(3,OUT);           //Configure pin as OUTPUT
digitalWrite(3,HIGH);     //Write HIGH to the pin
//Turn off LED connected to PB2
pinMode(2,OUT);        
digitalWrite(2,LOW,'B');
return 0;
}

Finally, we write the digitalRead() function to read digital signals applied at the GPIO pins. Below is the implementation.

int digitalRead(uint32_t PIN, char PORT = 'A'){
    switch(PORT){
        case 'A': return ((GPIOA->IDR & 1<<PIN) >0);
        case 'B': return ((GPIOB->IDR & 1<<PIN) >0);
        case 'C': return ((GPIOC->IDR & 1<<PIN) >0);
        case 'D': return ((GPIOD->IDR & 1<<PIN) >0);
        case 'E': return ((GPIOE->IDR & 1<<PIN) >0);
        case 'F': return ((GPIOF->IDR & 1<<PIN) >0);
        case 'G': return ((GPIOG->IDR & 1<<PIN) >0);
        case 'H': return ((GPIOH->IDR & 1<<PIN) >0);
        case 'I': return ((GPIOI->IDR & 1<<PIN) >0);
        case 'J': return ((GPIOJ->IDR & 1<<PIN) >0);
        case 'K': return ((GPIOK->IDR & 1<<PIN) >0);
        default:  return 0;            
    }
}

A typical call of this function looks like this:

#include "GPIO_drivers.h"

int main(){
//Read pin PA3
pinMode(3,OUT);                     //Configure pin as OUTPUT
int pinA3_status = digitalRead(3);  //Read value of pin

//Read pin PB2
pinMode(2,OUT);        
int pinB2_status = digitalRead(2,'B');
return 0;
}

GPIO digital I/O operations are a fairly common operation in embedded systems development. The functions we developed in this section greatly simplify the otherwise somewhat tedious tasks of writing a driver for each pin we wish to interact with. Also, since the development is based on a standard header file, it is portable within the STM32Fxxxx family.

Further Reading