8. Peripheral Driver Development: GPIO

1. Overview

In this course, we will develop peripheral drivers for the STM32Fxxx MCU. The driver will be developed in the following steps:

  1. Create a MCU-specific header file (stm32f446xx.h) that contains the register definitions for the peripheral.
  2. Create the following driver files:
    • gpio_driver.h and gpio_driver.c
    • i2c_driver.h and i2c_driver.c
    • spi_driver.h and spi_driver.c
    • uart_driver.h and uart_driver.c
  3. Create a sample application that uses the peripheral driver.

In this section, we will cover step 1 and 2 for the GPIO peripheral. The GPIO peripheral is a good starting point because it is simple and easy to understand.


2. Create a MCU-specific header file

Create a new STM32 project in STM32CubeIDE. In the project, create a new folder called Drivers. In the Drivers folder, create two folders called Inc and Src. In the Inc folder, create a new header file called stm32f446xx.h. Right click on the Driver folder > C/C++ Build, and then uncheck the Exclude resource from build option. This will ensure that the stm32f446xx.h file is included in the build. Apply the changes and then click on Apply and Close.

This file (stm32f446xx.h) will contain the following:

  1. Memory base addresses.
  2. Clock managment macros.
  3. IRQ definitions.
  4. Peripheral register structure definitions.
  5. Peripheral register bit definitions.
  6. Other useful configuration macros.

Here is the skeleton stm32f446xx.h file:

#ifndef INC_STM32F446XX_H_
#define INC_STM32F446XX_H_

#endif /* INC_STM32F446XX_H_ */

Now try to build the project, you will get the following error message:

../Src/main.c:20:10: fatal error: stm32f446xx.h: No such file or directory

This is because the stm32f446xx.h file is not in the include path. To fix this, right click on the project > Properties > C/C++ General > Settings > Tool Settings > MCU GCC Compiler > Include Paths. Click on the Add button and then click on the Workspace button. Select the stm32f446xx.h file and click on OK. Click on Apply and Close.

Build the project again, you will see red squiggly lines under the

#if !defined(__SOFT_FP__) && defined(__ARM_FP)
  #warning "FPU is not initialized, but the project is compiling for an FPU. Please initialize the FPU before use."
#endif

To disable compiling for FPU, go to Project > Properties > C/C++ Build > Settings > MCU Settings and for the drop down menus:

  • Floating-point unit: None
  • Floating-point ABI: Software implementation

3. Memory base addresses

You can check out the Reference Manual, Section 2.2.2 Memory map and register boundary addresses. And you will see the following memory map:

memory_map

This helps us define the memory base addresses for the various memory regions. We will define the following memory base addresses in the stm32f446xx.h file:

#ifndef INC_STM32F446XX_H_
#define INC_STM32F446XX_H_


/*
 * base addresses of Flash and SRAM memories
 */

#define FLASH_BASEADDR          0x08000000U  // Base address of the Flash memory
#define SRAM1_BASEADDR          0x20000000U  // Base address of the SRAM1 (112 KB)
#define SRAM2_BASEADDR          0x2001C000U  // Base address of the SRAM2 (16 KB)
#define ROM_BASEADDR            0x1FFF0000U  // Base address of the system memory (ROM), where the bootloader is stored
#define SRAM                    SRAM1_BASEADDR // Default SRAM base address for general access

#endif /* INC_STM32F446XX_H_ */

The SRAM addresses provided in the macros are not bit-banding addresses themselves; they are the base addresses of the SRAM regions. In STM32 microcontrollers, a portion of the SRAM address space is also mapped to the bit-band region, enabling bit-addressable access to memory, which is a separate feature. Bit-banding can simplify the code for setting or clearing individual bits in registers, like those used in GPIO, SPI, and I2C drivers. For instance, instead of reading a register, modifying the bit, and writing it back, you can directly set or clear a bit using its bit-band alias address. This atomic operation is particularly beneficial in interrupt-driven or multi-threaded environments where race conditions might occur.

People often refer to system memory as ROM in the context of microcontrollers like the STM32 because this section of memory, which is technically flash memory, is typically used to store read-only data such as the bootloader or factory settings, which are not meant to be modified during normal operation. The bootloader, for example, is a small program preloaded by the manufacturer that initializes the device and loads the main application. It is executed when the microcontroller is reset. Because this system memory is intended to be read-only during the device's normal operation, it is colloquially referred to as ROM, even though it is writable during certain update processes.


4. Bus domain base addresses

There are several bus domains in the STM32F446xx MCU. Just repeat the same process as above to define the base addresses for the bus domains:

/*
 * AHBx and APBx Bus Peripheral base addresses
 */

#define PERIPH_BASEADDR         0x40000000U
#define APB1PERIPH_BASEADDR     PERIPH_BASEADDR
#define APB2PERIPH_BASEADDR     0x40010000U
#define AHB1PERIPH_BASEADDR     0x40020000U
#define AHB2PERIPH_BASEADDR     0x50000000U

5. Peripheral Register Structure Definitions

Peripheral register structures are a convenient way to represent hardware registers of peripherals within the STM32 microcontroller. They allow for type-safe, descriptive access to registers, which improves code readability and maintainability.

#include <stdint.h>

#define __vo volatile

Here we include the stdint.h header which defines integer types with specific widths, like uint32_t, ensuring that the variable is exactly 32 bits long regardless of the platform the code is run on. The __vo macro is defined to annotate variables as volatile. The volatile keyword tells the compiler that the value of the variable may change at any time without any action being taken by the code the compiler finds nearby. This is critical for hardware register access as their values may change independently of the program flow, for example by hardware events or other parts of the system.

typedef struct
{
    __vo uint32_t MODER;    // GPIO port mode register
    __vo uint32_t OTYPER;   // GPIO port output type register
    __vo uint32_t OSPEEDR;  // GPIO port output speed register
    __vo uint32_t PUPDR;    // GPIO port pull-up/pull-down register
    __vo uint32_t IDR;      // GPIO port input data register
    __vo uint32_t ODR;      // GPIO port output data register
    __vo uint32_t BSRR;     // GPIO port bit set/reset register
    __vo uint32_t LCKR;     // GPIO port configuration lock register
    __vo uint32_t AFR[2];   // AFR[0]: GPIO alternate function low register, AFR[1]: GPIO alternate function high register
} GPIO_RegDef_t;

This structure, GPIO_RegDef_t, defines the layout of the GPIO peripheral's registers in memory. Each field in the structure corresponds to a register within the GPIO peripheral. The registers are marked as volatile to ensure that each read/write operation accesses the actual register and not a cached value. Arrays are used for registers like AFR that are logically grouped (low and high).

#define GPIOA           ((GPIO_RegDef_t*)GPIOA_BASEADDR)
#define GPIOB           ((GPIO_RegDef_t*)GPIOB_BASEADDR)
#define GPIOC           ((GPIO_RegDef_t*)GPIOC_BASEADDR)
// ... Additional GPIO ports as needed

These macros define pointers to GPIO_RegDef_t structures, with the base address for each GPIO port. This allows the programmer to access the GPIO registers using descriptive names such as GPIOA->MODER to access the mode register of port A. The cast to (GPIO_RegDef_t*) is a typecast, which tells the compiler that the numerical address should be treated as a pointer to a GPIO_RegDef_t structure.

By using these definitions, you can write code that manipulates the GPIO registers by name, which is far more readable and less error-prone than using raw numerical addresses. It's a common practice in embedded systems programming, particularly in situations where you want to abstract away the hardware specifics and make the code more portable.


6. RCC Register Structure Definition

The RCC (Reset and Clock Control) peripheral in STM32 microcontrollers is critical for managing the clock system. It controls the clocks of the MCU core and the peripherals. The register structure RCC_RegDef_t defines a C structure that mirrors the layout of the RCC registers in the STM32's memory map, facilitating code that can manage the RCC hardware registers directly in a type-safe manner.

/*
 * peripheral register definition structure for RCC
 */

typedef struct
{
  __vo uint32_t CR;            /*!< Control Register: This register allows control of the microcontroller's internal oscillators like the HSI (High-Speed Internal) and HSE (High-Speed External) oscillators, as well as the PLL (Phase-Locked Loop) and CSS (Clock Security System). Address offset: 0x00 */
  __vo uint32_t PLLCFGR;       /*!< PLL Configuration Register: Configures the main PLL which is used to generate the system clock. This includes settings for the PLL multipliers and dividers. Address offset: 0x04 */
  ...
  __vo uint32_t AHB1ENR;       /*!< AHB1 Peripheral Clock Enable Register: Used to enable the clock for peripherals connected to the AHB1 bus. Address offset: 0x30 */
  __vo uint32_t AHB2ENR;       /*!< AHB2 Peripheral Clock Enable Register: Used to enable the clock for peripherals connected to the AHB2 bus. Address offset: 0x34 */
  __vo uint32_t AHB3ENR;       /*!< AHB3 Peripheral Clock Enable Register: Used to enable the clock for peripherals connected to the AHB3 bus. Address offset: 0x38 */
  uint32_t      RESERVED2;     /*!< Reserved: This space is reserved and should not be used. Address offset: 0x3C */
  __vo uint32_t APB1ENR;       /*!< APB1 Peripheral Clock Enable Register: Used to enable the clock for peripherals connected to the APB1 bus. Address offset: 0x40 */
  __vo uint32_t APB2ENR;       /*!< APB2 Peripheral Clock Enable Register: Used to enable the clock for peripherals connected to the APB2 bus. Address offset: 0x44 */
  uint32_t      RESERVED3[2];  /*!< Reserved: These spaces are reserved and should not be used. Address offsets: 0x48-0x4C */
  ...

} RCC_RegDef_t;

The CR and PLLCFGR fields in the structure allow control over the internal and external oscillators and the main PLL, which are fundamental for setting up the microcontroller's clock tree. The AHB1ENR, AHB2ENR, and AHB3ENR registers, specifically, are used to enable or disable the clock supply to the peripherals connected to the AHB1, AHB2, and AHB3 buses respectively. Disabling the clock for a peripheral that's not in use can save power, while enabling it is necessary for the peripheral to operate.

#define RCC                ((RCC_RegDef_t*)RCC_BASEADDR)

This macro defines a pointer to RCC_RegDef_t, casting the base address of the RCC hardware registers (RCC_BASEADDR) to a pointer to RCC_RegDef_t. This allows you to use the RCC macro to access the RCC registers directly by name (like RCC->CR or RCC->AHB1ENR) in your code.

(RCC_AHB1ENR The figure (which I assume is a screenshot from the reference manual) shows the RCC_AHB1ENR register's bit layout. Each bit corresponds to a peripheral connected to the AHB1 bus. Setting a bit to 1 enables the clock for the corresponding peripheral. For example, setting bit 0 to 1 enables the clock for GPIO port A.

/*
 * Clock Enable Macros for GPIOx peripherals
 */

#define GPIOA_PCLK_EN()     (RCC->AHB1ENR |= (1 << 0))
#define GPIOB_PCLK_EN()     (RCC->AHB1ENR |= (1 << 1))
...
#define GPIOH_PCLK_EN()     (RCC->AHB1ENR |= (1 << 7))
#define GPIOI_PCLK_EN()     (RCC->AHB1ENR |= (1 << 8))

These macros are convenience functions that enable the peripheral clock for the GPIO ports by setting the corresponding bit in the AHB1ENR register. For instance, GPIOA_PCLK_EN() sets bit 0 of the AHB1ENR register to 1, enabling the clock for GPIO port A. Using bitwise OR |=, ensures that only the targeted bit is changed without affecting the rest of the register.

By using these macros, you can cleanly enable the clocks for the GPIO peripherals when needed, improving code readability and avoiding magic numbers in your code. Each macro corresponds to enabling the clock for a specific GPIO port, from GPIOA to GPIOI. These macros are especially useful during the initialization phase of your program, where you're setting up various peripherals to be used by your application.

We can also define macros to disable the clock for the GPIO peripherals:

/*
 * Clock Disable Macros for GPIOx peripherals
 */

#define GPIOA_PCLK_DI()     (RCC->AHB1ENR &= ~(1 << 0))
#define GPIOB_PCLK_DI()     (RCC->AHB1ENR &= ~(1 << 1))
...
#define GPIOI_PCLK_DI()     (RCC->AHB1ENR &= ~(1 << 8))

7. Create the GPIO driver files

Create stm32f446xx_gpio_driver.h and stm32f446xx_gpio_driver.c files in the Drivers folder. In the stm32f446xx_gpio_driver.h file, include the stm32f446xx.h file and define the GPIO pin configuration structure:

We can declare our first API function in the stm32f446xx_gpio_driver.h file:

/*
 * Peripheral Clock setup
 */
void GPIO_PeriClockControl(GPIO_RegDef_t *pGPIOx, uint8_t EnorDi);

In stm32f446xx_gpio_driver.c, include the stm32f446xx_gpio_driver.h file and define the GPIO_PeriClockControl() function:

void GPIO_PeriClockControl(GPIO_RegDef_t *pGPIOx, uint8_t EnorDi)
{
    if(EnorDi == ENABLE)
    {
        if(pGPIOx == GPIOA)
        {
            GPIOA_PCLK_EN();
        }
        else if (pGPIOx == GPIOB)
        {
            GPIOB_PCLK_EN();
        }
        ...
        else if (pGPIOx == GPIOI)
        {
            GPIOI_PCLK_EN();
        }
    }
    else
    {
        if(pGPIOx == GPIOA)
        {
            GPIOA_PCLK_DI();
        }
        else if (pGPIOx == GPIOB)
        {
            GPIOB_PCLK_DI();
        }
        ...
        else if (pGPIOx == GPIOI)
        {
            GPIOI_PCLK_DI();
        }
    }
}

The function above is pretty self-explanatory. It enables or disables the peripheral clock for the given GPIO port.


8. GPIO Initialization and De-initialization

Now we can declare the GPIO initialization and de-initialization functions in the stm32f446xx_gpio_driver.h file. We need two extra structures for this:

GPIO Pin Configuration Structure

typedef struct
{
    uint8_t GPIO_PinNumber;       // Specifies the GPIO pin number (0 to 15).
    uint8_t GPIO_PinMode;         // Specifies the operating mode for the selected pin (e.g., input, output, alt function, or analog).
    uint8_t GPIO_PinSpeed;        // Specifies the speed for the GPIO pin (e.g., low, medium, high speed).
    uint8_t GPIO_PinPuPdControl;  // Specifies the pull-up or pull-down activation for the pin.
    uint8_t GPIO_PinOPType;       // Specifies the output type (e.g., push-pull or open-drain).
    uint8_t GPIO_PinAltFunMode;   // Specifies the alternate function mode (when the mode is set to alt function).
} GPIO_PinConfig_t;

This structure GPIO_PinConfig_t is used to configure the characteristics of a single GPIO pin. Each field in the structure represents a specific configuration aspect of the GPIO pin, like its number, mode, speed, pull-up/pull-down settings, output type, and alternate function if applicable.

GPIO Handle Structure

typedef struct
{
    GPIO_RegDef_t *pGPIOx;        // Holds the base address of the GPIO port to which the pin belongs.
    GPIO_PinConfig_t GPIO_PinConfig; // Holds GPIO pin configuration settings.
} GPIO_Handle_t;

GPIO_Handle_t is a handle structure for a GPIO pin. It contains a pointer pGPIOx to the GPIO port to which the pin belongs and a GPIO_PinConfig_t structure that holds the configuration settings for that specific pin.

GPIO Initialization and De-initialization Functions

void GPIO_Init(GPIO_Handle_t *pGPIOHandle);
void GPIO_DeInit(GPIO_RegDef_t *pGPIOx);

These functions are used for initializing and de-initializing the GPIO pins. GPIO_Init sets up a GPIO pin based on the configuration specified in GPIO_Handle_t, and GPIO_DeInit resets the GPIO port registers to their default reset values.

Implementation of GPIO_Init Function

void GPIO_Init(GPIO_Handle_t *pGPIOHandle)
{
    uint32_t temp = 0; // temp. register

    // enable the peripheral clock
    GPIO_PeriClockControl(pGPIOHandle->pGPIOx, ENABLE);

    // 1. configure the mode of gpio pin
    if (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode <= GPIO_MODE_ANALOG)
    {
        // the non interrupt mode
        temp = (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber));
        pGPIOHandle->pGPIOx->MODER &= ~(0x3 << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber)); // clearing
        pGPIOHandle->pGPIOx->MODER |= temp;                                                       // setting
    }
    else
    {
        // Interrupt mode (code later)
    }

    // 2. configure the speed
    temp = (pGPIOHandle->GPIO_PinConfig.GPIO_PinSpeed << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber));
    pGPIOHandle->pGPIOx->OSPEEDR &= ~(0x3 << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber)); // clearing
    pGPIOHandle->pGPIOx->OSPEEDR |= temp;

    // 3. configure the pupd settings
    temp = (pGPIOHandle->GPIO_PinConfig.GPIO_PinPuPdControl << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber));
    pGPIOHandle->pGPIOx->PUPDR &= ~(0x3 << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber)); // clearing
    pGPIOHandle->pGPIOx->PUPDR |= temp;

    // 4. configure the optype
    temp = (pGPIOHandle->GPIO_PinConfig.GPIO_PinOPType << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
    pGPIOHandle->pGPIOx->OTYPER &= ~(0x1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber); // clearing
    pGPIOHandle->pGPIOx->OTYPER |= temp;

    // 5. configure the alt functionality
    if (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode == GPIO_MODE_ALTFN)
    {
        // configure the alt function registers.
        uint8_t temp1, temp2;

        temp1 = pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber / 8;
        temp2 = pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber % 8;
        pGPIOHandle->pGPIOx->AFR[temp1] &= ~(0xF << (4 * temp2)); // clearing
        pGPIOHandle->pGPIOx->AFR[temp1] |= (pGPIOHandle->GPIO_PinConfig.GPIO_PinAltFunMode << (4 * temp2));
    }
}

In the GPIO_Init function, the configuration process involves several steps:

  • Enable Peripheral Clock: First, the clock for the GPIO port is enabled.

  • Configure Pin Mode: The mode of the GPIO pin is configured. If it's a non-interrupt mode (normal operation modes), the function sets up the MODER register accordingly. For interrupt modes (like falling edge, rising edge, or both), the function configures the EXTI registers for external interrupt functionality.

  • Configure Speed: The speed of the GPIO pin is set using the OSPEEDR register.

  • Configure Pull-up/Pull-down Settings: The PUPDR register is set to enable either pull-up or pull-down resistors as per the configuration.

  • Configure Output Type: The OTYPER register is configured to set the output type to either push-pull or open-drain.

  • Configure Alternate Functionality: If the pin mode is an alternate function, the AFR register is set up to define which alternate function is used.

This function encapsulates all the necessary steps to initialize a GPIO pin based on the user-defined configuration, making it easier to reuse and maintain the code for different GPIO initialization scenarios in the STM32 microcontroller.

Implementation of GPIO_DeInit Function

We can go back to stm32f446.h and define the GPIOx_REG_RESET() macros:

/*
 *  Macros to reset GPIOx peripherals
 */
#define GPIOA_REG_RESET()       do{ (RCC->AHB1RSTR |= (1 << 0)); (RCC->AHB1RSTR &= ~(1 << 0)); }while(0)
#define GPIOB_REG_RESET()       do{ (RCC->AHB1RSTR |= (1 << 1)); (RCC->AHB1RSTR &= ~(1 << 1)); }while(0)
...
#define GPIOI_REG_RESET()       do{ (RCC->AHB1RSTR |= (1 << 8)); (RCC->AHB1RSTR &= ~(1 << 8)); }while(0)

Now we can implement the GPIO_DeInit() function:

void GPIO_DeInit(GPIO_RegDef_t *pGPIOx)
{
    if(pGPIOx == GPIOA)
    {
        GPIOA_REG_RESET();
    }else if (pGPIOx == GPIOB)
    {
        GPIOB_REG_RESET();
    }...
    }else if (pGPIOx == GPIOI)
    {
        GPIOI_REG_RESET();
    }
}

9. GPIO Read Functions

The GPIO read functions are used to read the state of GPIO pins and ports. These functions are crucial for interacting with external devices or checking the status of buttons, switches, and sensors.

uint8_t GPIO_ReadFromInputPin(GPIO_RegDef_t *pGPIOx, uint8_t PinNumber);
uint16_t GPIO_ReadFromInputPort(GPIO_RegDef_t *pGPIOx);

These function prototypes declare two types of read operations: one for reading the state of a single pin and the other for reading the entire GPIO port.

Reading from an Input Pin

uint8_t GPIO_ReadFromInputPin(GPIO_RegDef_t *pGPIOx, uint8_t PinNumber)
{
   uint8_t value;
   value = (uint8_t)((pGPIOx->IDR  >> PinNumber) & 0x00000001);
   return value;
}

In GPIO_ReadFromInputPin, the state of a specific pin is read. This function takes two parameters: a pointer to the GPIO port (pGPIOx) and the pin number (PinNumber).

  • The uint8_t type is used for the PinNumber and return value to signify that the pin numbers and their states are within the 8-bit range.
  • The function reads the input data register (IDR) of the specified GPIO port and shifts it right by PinNumber bits. This operation brings the desired bit (representing the state of the pin) to the least significant bit position.
  • Then, it performs a bitwise AND with 0x00000001 to isolate this least significant bit.
  • The result is cast to uint8_t and returned, representing the state of the specified pin (either 0 or 1).

Reading from an Input Port

uint16_t GPIO_ReadFromInputPort(GPIO_RegDef_t *pGPIOx)
{
    uint16_t value;
    value = (uint16_t)pGPIOx->IDR;
    return value;
}

In GPIO_ReadFromInputPort, the state of an entire GPIO port is read. This is useful when you want to check the state of all pins of a GPIO port simultaneously.

  • The function takes a pointer to the GPIO port (pGPIOx) as its parameter.
  • The uint16_t type is used for the return value because a GPIO port can have up to 16 pins, and the state of each pin corresponds to one bit in the 16-bit wide input data register (IDR).
  • The function reads the IDR register value of the specified GPIO port and casts it to uint16_t to match the return type of the function.
  • This 16-bit value is returned, with each bit representing the state of the corresponding pin in the GPIO port.

10. GPIO Write Functions

The GPIO write functions are essential for controlling the state of GPIO pins and ports. These functions enable setting, resetting, or toggling the GPIO pins, which is crucial for interfacing with LEDs, motors, and other output devices.

Writing to an Output Pin

void GPIO_WriteToOutputPin(GPIO_RegDef_t *pGPIOx, uint8_t PinNumber, uint8_t Value)
{
    if (Value == GPIO_PIN_SET)
    {
        // write 1 to the output data register at the bit field corresponding to the pin number
        pGPIOx->ODR |= (1 << PinNumber);
    }
    else
    {
        // write 0
        pGPIOx->ODR &= ~(1 << PinNumber);
    }
}

In GPIO_WriteToOutputPin, a specific GPIO pin's state is set or cleared. The function takes three parameters: a pointer to the GPIO port (pGPIOx), the pin number (PinNumber), and the value to set (Value).

  • Writing 1 or 0 to a specific pin is achieved by manipulating the Output Data Register (ODR) of the GPIO port.
  • If Value is GPIO_PIN_SET, the function sets the corresponding bit in the ODR using the bitwise OR operation (|). This operation sets the specified pin while leaving the states of other pins unchanged.
  • If Value is not GPIO_PIN_SET, the function clears the corresponding bit using the bitwise AND operation with the complement of the bitmask (& ~). This clears the specified pin without affecting other pins.

Writing to an Output Port

void GPIO_WriteToOutputPort(GPIO_RegDef_t *pGPIOx, uint16_t Value)
{
    pGPIOx->ODR = Value;
}

In GPIO_WriteToOutputPort, the entire GPIO port is set to a specific state. This is useful for changing the state of all pins in a GPIO port simultaneously.

  • The function takes a pointer to the GPIO port (pGPIOx) and a 16-bit Value that represents the desired state for all the pins.
  • The uint16_t type is used for Value since a GPIO port can have up to 16 pins, and each bit in the Value corresponds to the state of one pin.
  • The function directly assigns the Value to the ODR register of the GPIO port, setting or clearing all pins at once.

Toggling an Output Pin

void GPIO_ToggleOutputPin(GPIO_RegDef_t *pGPIOx, uint8_t PinNumber)
{
    pGPIOx->ODR ^= (1 << PinNumber);
}

In GPIO_ToggleOutputPin, a specific GPIO pin's state is toggled (if it was set, it is cleared and vice versa).

  • The function takes a pointer to the GPIO port (pGPIOx) and the pin number (PinNumber).
  • The toggling is achieved using the bitwise XOR operation (^). This operation flips the state of the specified pin, leaving the states of other pins unchanged.

11. Exercise: Blinking an LED

In this exercise, we're going to make an LED blink using an STM32F446 microcontroller. According to the user manual (UM1724), the user LED on the board is connected to GPIO pin PA5. The code provided demonstrates how to control the LED using the GPIO interface. Let's break down the code and its functionality:

Code Explanation

  1. Include the Header File:
#include "stm32f446xx.h"

This includes the necessary definitions and function prototypes that we just wrote in the previous sections, including the GPIO peripheral register structure definitions, macros, and function prototypes.

  1. Delay Function:
void delay(void)
{
    for (uint32_t i = 0; i < 500000; i++)
        ;
}

A simple delay function is implemented using a for loop. This creates a blocking delay, which is used later to maintain the LED state (either ON or OFF) for a visible duration.

  1. Main Function - Setup and Loop:
int main(void)
{
    GPIO_Handle_t GpioLed;
    ...
}

The main function is where the program starts executing. It sets up the GPIO pin connected to the LED and then enters an infinite loop to toggle the LED.

  1. Configure GPIO for LED:

  2. GPIO Handle and Port Definition:

GPIO_Handle_t GpioLed;
GPIO_RegDef_t *pGPIO_LED = GPIOA;

GpioLed is an instance of the GPIO_Handle_t structure, and pGPIO_LED is a pointer to GPIOA, the GPIO port where the LED is connected (PA5).

  • Pin Configuration:
GpioLed.pGPIOx = pGPIO_LED;
GpioLed.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_NO_5;
GpioLed.GPIO_PinConfig.GPIO_PinMode = GPIO_MODE_OUT;
GpioLed.GPIO_PinConfig.GPIO_PinSpeed = GPIO_SPEED_FAST;
GpioLed.GPIO_PinConfig.GPIO_PinOPType = GPIO_OP_TYPE_PP;
GpioLed.GPIO_PinConfig.GPIO_PinPuPdControl = GPIO_NO_PUPD;

The GPIO pin (PA5) is configured as an output with a fast speed and push-pull mode. The push-pull mode (GPIO_OP_TYPE_PP) is essential for driving the LED, as it allows the pin to source and sink current. Open-drain mode (GPIO_OP_TYPE_OD) is not suitable for this purpose since it only sinks current, and the LED would require an external pull-up resistor to operate.

  1. Initialize GPIO and Clock:
GPIO_PeriClockControl(pGPIO_LED, ENABLE);
GPIO_Init(&GpioLed);

GPIO_PeriClockControl enables the clock for the GPIO port, which is necessary for the GPIO peripheral to operate. GPIO_Init initializes the GPIO pin with the configuration specified in GpioLed.

For those familiar with C++ and its class structure, this C-based code might seem a bit different. In C++, you often encapsulate functionality within classes, utilizing methods and properties. However, in C, especially in embedded systems programming, we separate the data and function. This approach is more in line with procedural programming, unlike the object-oriented approach of C++ where data and methods are encapsulated together in classes.

  1. LED Blinking Loop:
while (1)
{
    GPIO_ToggleOutputPin(pGPIO_LED, GPIO_PIN_NO_5);
    delay();
}

In the infinite while loop, the LED is toggled on and off using GPIO_ToggleOutputPin. The delay function is called after each toggle to keep the LED in its current state (on or off) for a period, making the blinking visible.


12. Exercise: Toggle an LED using a Button (without interrupts)

#include "stm32f446xx.h"

void delay(void)
{
    for (uint32_t i = 0; i < 50000; i++)
        ;
}

int main(void)
{
    // LED configuration (same as previous exercise)
    ...

    // Button configuration
    GPIO_Handle_t GpioBtn;

    GpioBtn.pGPIOx = GPIOC;
    GpioBtn.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_NO_13;
    GpioBtn.GPIO_PinConfig.GPIO_PinMode = GPIO_MODE_IN;
    GpioBtn.GPIO_PinConfig.GPIO_PinSpeed = GPIO_SPEED_FAST;
    GpioBtn.GPIO_PinConfig.GPIO_PinPuPdControl = GPIO_PIN_PU; // Enable pull-up if the button is active low

    GPIO_PeriClockControl(GPIOC, ENABLE);
    GPIO_Init(&GpioBtn);

    while (1)
    {
        // Check if the button is pressed
        if (GPIO_ReadFromInputPin(GPIOC, GPIO_PIN_NO_13) == 0) // Button is pressed
        {
            // Delay for debounce (optional)
            delay();

            // Toggle the LED
            GPIO_ToggleOutputPin(pGPIO_LED, pinNumber);

            // Wait until the button is released
            while (GPIO_ReadFromInputPin(GPIOC, GPIO_PIN_NO_13) == 0)
                ;
        }
    }
    return 0;
}

13. Interrupts

In an STM32 microcontroller, using interrupts instead of polling for a button press involves the interplay of several components: the Nested Vectored Interrupt Controller (NVIC), the System Configuration Controller (SYSCFG), and the External Interrupt/Event Controller (EXTI). Let's break down the role of each in handling a button press interrupt:

1. External Interrupt/Event Controller (EXTI)

  • Role: EXTI manages external interrupts. It can be configured to trigger an interrupt based on a signal edge (rising, falling, or both) on a specific GPIO pin.
  • Application: When the button is pressed, if it's connected to a GPIO pin configured with EXTI, it can trigger an interrupt. You need to configure the EXTI line corresponding to the GPIO pin connected to the button (e.g., EXTI13 if the button is connected to PC13).

We can add the following structure definition to stm32f446xx.h:

typedef struct
{
    __vo uint32_t IMR;   /*!< Interrupt Mask Register: Used to enable or disable the interrupts. A bit set to 1 enables the interrupt, and 0 disables it. Address offset: 0x00 */
    __vo uint32_t EMR;   /*!< Event Mask Register: Used to enable or disable events. A bit set to 1 enables the event, and 0 disables it. Events are similar to interrupts but don't result in the processor being halted. Address offset: 0x04 */
    __vo uint32_t RTSR;  /*!< Rising Trigger Selection Register: Configures interrupt lines to be sensitive to rising edges. Setting a bit in this register selects the corresponding line for rising edge detection. Address offset: 0x08 */
    __vo uint32_t FTSR;  /*!< Falling Trigger Selection Register: Configures interrupt lines to be sensitive to falling edges. Setting a bit in this register selects the corresponding line for falling edge detection. Address offset: 0x0C */
    __vo uint32_t SWIER; /*!< Software Interrupt Event Register: Allows software to generate an interrupt or event by writing to this register. Setting a bit here sets the corresponding interrupt/event line to pending. Address offset: 0x10 */
    __vo uint32_t PR;    /*!< Pending Register: Indicates whether a selected line has a pending interrupt. Reading this register returns the pending bit. Writing a 1 to a bit in this register clears the corresponding interrupt pending bit. Address offset: 0x14 */

} EXTI_RegDef_t;

2. System Configuration Controller (SYSCFG)

  • Role: SYSCFG is used to route the external interrupt line from the GPIO pin to the EXTI controller. Since multiple GPIO pins can be connected to the same EXTI line (e.g., PA0, PB0, PC0, etc., all map to EXTI0), SYSCFG configures which GPIO port (A, B, C, etc.) will be connected to a specific EXTI line.
  • Application: If your button is connected to PC13, you need to use SYSCFG to connect EXTI line 13 to GPIO port C.

We can add the following structure definition to stm32f446xx.h:

typedef struct
{
    __vo uint32_t MEMRMP;    /*!< Memory Remap Register: Used to remap the memory accessible at address 0x0000 0000. This can be used to remap system flash memory, system memory, or FSMC (Flexible Static Memory Controller) bank 1 to the base address. Address offset: 0x00 */
    __vo uint32_t PMC;       /*!< Peripheral Mode Configuration Register: Used to configure certain parameters related to power management, like ADC control in low-power modes. Address offset: 0x04 */
    __vo uint32_t EXTICR[4]; /*!< External Interrupt Configuration Registers: Configures the sources of external interrupts on the GPIO pins. EXTICR[0] to EXTICR[3] correspond to GPIO port selection for EXTI lines 0 to 15. Address offset: 0x08-0x14 */
    uint32_t RESERVED1[2];   /*!< Reserved: These spaces are reserved and should not be used. Address offsets: 0x18-0x1C */
    __vo uint32_t CMPCR;     /*!< Compensation Cell Control Register: Controls I/O compensation cell parameters. The I/O compensation cell is used to compensate for I/O delay in high-frequency operation. Address offset: 0x20 */
    uint32_t RESERVED2[2];   /*!< Reserved: These spaces are reserved and should not be used. Address offsets: 0x24-0x28 */
    __vo uint32_t CFGR;      /*!< Configuration Register: Used to configure various features and functions of the system, like clock output, packet error checking, and more. Address offset: 0x2C */

} SYSCFG_RegDef_t;

3. Nested Vectored Interrupt Controller (NVIC)

  • Role: NVIC is responsible for handling the prioritization and execution of interrupts. It manages the interrupt vectors and processes the interrupt requests from various peripherals, including EXTI.
  • Application: After configuring EXTI and SYSCFG, you need to enable the corresponding interrupt line (EXTI line 13 for PC13) in the NVIC and set its priority. When the button is pressed and the EXTI line is triggered, NVIC will handle the interrupt and run the interrupt service routine (ISR) you define.

We can add the following macros to stm32f446xx.h:

/**********************************START:Processor Specific Details **********************************/
/*
 * ARM Cortex Mx Processor NVIC ISERx register Addresses
 */

#define NVIC_ISER0 ((__vo uint32_t *)0xE000E100)
#define NVIC_ISER1 ((__vo uint32_t *)0xE000E104)
#define NVIC_ISER2 ((__vo uint32_t *)0xE000E108)
#define NVIC_ISER3 ((__vo uint32_t *)0xE000E10c)

/*
 * ARM Cortex Mx Processor NVIC ICERx register Addresses
 */
#define NVIC_ICER0 ((__vo uint32_t *)0XE000E180)
#define NVIC_ICER1 ((__vo uint32_t *)0XE000E184)
#define NVIC_ICER2 ((__vo uint32_t *)0XE000E188)
#define NVIC_ICER3 ((__vo uint32_t *)0XE000E18C)

/*
 * ARM Cortex Mx Processor Priority Register Address Calculation
 */
#define NVIC_PR_BASE_ADDR ((__vo uint32_t *)0xE000E400)

/*
 * ARM Cortex Mx Processor number of priority bits implemented in Priority Register
 */
#define NO_PR_BITS_IMPLEMENTED 4

/*
 * macros for all the possible priority levels
 */
#define NVIC_IRQ_PRI0 0
#define NVIC_IRQ_PRI15 15

Putting it All Together

In your application, to use an interrupt for a button press:

  1. Configure the GPIO Pin: Set the GPIO pin where the button is connected as an input with an internal pull-up or pull-down resistor, depending on your button's wiring.

  2. Configure EXTI: Map the GPIO pin to the corresponding EXTI line and configure the trigger condition (rising/falling/both edges).

  3. Configure SYSCFG: Connect the EXTI line to the correct GPIO port using the SYSCFG_EXTICR register.

  4. Configure NVIC: Enable the interrupt for the EXTI line in NVIC and set its priority.

  5. Implement ISR: Write the Interrupt Service Routine for the EXTI line. This routine will be executed when the button is pressed.

interrupt_components

GPIO Initialization with Interrupt

void GPIO_Init(GPIO_Handle_t *pGPIOHandle)
{
    uint32_t temp = 0; // Temporary register for configurations

    // Enable the peripheral clock for the GPIO port
    GPIO_PeriClockControl(pGPIOHandle->pGPIOx, ENABLE);

    // Configure the mode of GPIO pin
    if (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode <= GPIO_MODE_ANALOG)
    {
        // Non-interrupt mode: Input, Output, Alternate Function, or Analog
        temp = (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber));
        pGPIOHandle->pGPIOx->MODER &= ~(0x3 << (2 * pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber)); // Clearing the mode bits
        pGPIOHandle->pGPIOx->MODER |= temp; // Setting the mode bits
    }
    else
    {
        // Interrupt mode configuration
        if (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode == GPIO_MODE_IT_FT)
        {
            // Configure the FTSR for falling edge trigger
            EXTI->FTSR |= (1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
            // Clear the corresponding RTSR bit
            EXTI->RTSR &= ~(1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
        }
        else if (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode == GPIO_MODE_IT_RT)
        {
            // Configure the RTSR for rising edge trigger
            EXTI->RTSR |= (1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
            // Clear the corresponding FTSR bit
            EXTI->FTSR &= ~(1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
        }
        else if (pGPIOHandle->GPIO_PinConfig.GPIO_PinMode == GPIO_MODE_IT_RFT)
        {
            // Configure both FTSR and RTSR for both rising and falling edge trigger
            EXTI->RTSR |= (1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
            EXTI->FTSR |= (1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
        }

        // Configure the GPIO port selection in SYSCFG_EXTICR for interrupt routing
        uint8_t temp1 = pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber / 4;
        uint8_t temp2 = pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber % 4;
        uint8_t portcode = GPIO_BASEADDR_TO_CODE(pGPIOHandle->pGPIOx);
        SYSCFG_PCLK_EN(); // Enable SYSCFG clock
        SYSCFG->EXTICR[temp1] &= ~(0xF << (temp2 * 4)); // Clearing existing port selection
        SYSCFG->EXTICR[temp1] |= (portcode << (temp2 * 4)); // Setting port selection

        // Enable the EXTI interrupt delivery using IMR
        EXTI->IMR |= (1 << pGPIOHandle->GPIO_PinConfig.GPIO_PinNumber);
    }

    // Rest of the code to configure other GPIO pin settings (like speed, output type, pull-up/pull-down) would go here...
}

, where

#define GPIO_BASEADDR_TO_CODE(x) ((x == GPIOA) ? 0 : (x == GPIOB) ? 1 \
                                                 : (x == GPIOC)   ? 2 \
                                                 : (x == GPIOD)   ? 3 \
                                                 : (x == GPIOE)   ? 4 \
                                                 : (x == GPIOF)   ? 5 \
                                                 : (x == GPIOG)   ? 6 \
                                                 : (x == GPIOH)   ? 7 \
                                                 : (x == GPIOI)   ? 8 \
                                                                  : 0)

The code above sets up the EXTI line for the specified GPIO pin to trigger an interrupt based on different conditions (falling edge, rising edge, or both). Here's a breakdown of what the code is doing:

  1. Configuring Trigger Conditions for Interrupts:

    • The code checks the GPIO_PinConfig.GPIO_PinMode field of the pGPIOHandle structure to determine how the pin should trigger an interrupt.
      • If GPIO_MODE_IT_FT is selected, it configures the EXTI line to trigger an interrupt on a falling edge (transition from high to low). This is done by setting the corresponding bit in the Falling Trigger Selection Register (FTSR) and clearing the same bit in the Rising Trigger Selection Register (RTSR).
      • If GPIO_MODE_IT_RT is selected, it does the opposite, configuring for a rising edge trigger by setting the corresponding bit in the RTSR and clearing it in the FTSR.
      • If GPIO_MODE_IT_RFT is selected, it configures the EXTI line to trigger on both rising and falling edges by setting the corresponding bits in both the RTSR and FTSR.
  2. Setting up the SYSCFG External Interrupt Configuration Register (EXTICR):

    • The code calculates which EXTICR register to use and which bit field within that register based on the GPIO pin number. This is necessary to route the EXTI line to the correct GPIO port.
    • It then enables the SYSCFG peripheral clock and writes the port code to the relevant field in the appropriate EXTICR register. This step is crucial as EXTI lines can be mapped to multiple GPIO ports, and this configuration tells the EXTI controller which port to monitor for the specified line.
    • Each SYSCFG_EXTICRx register has 4 EXTIx. For example, to get the pin PC13, we need to go to EXTI4 (13 / 4 = 3, and EXTI numbering starts from 1) and set the 4th to 7th pins to 0010 (because PA is 0000, PB is 0001, etc.).

SYSCFG_EXTICRx

  1. Enabling the EXTI Line for Interrupt Generation:
    • Finally, it enables the configured EXTI line in the Interrupt Mask Register (IMR) to allow the EXTI controller to send interrupt requests to the Nested Vectored Interrupt Controller (NVIC).

In summary, this code sets up a specific GPIO pin to generate interrupts on the configured edge(s). It involves configuring the EXTI line for the desired trigger condition, setting up SYSCFG to route the EXTI line to the correct GPIO port, and enabling the EXTI line in the IMR for interrupt generation. This approach is commonly used in embedded systems to react to a button presses.

In addition, let's also define 3 IRQ helper functions in stm32f446xx_gpio_driver.h:

/*
 * IRQ Configuration and ISR handling
 */
void GPIO_IRQInterruptConfig(uint8_t IRQNumber, uint8_t EnorDi);
void GPIO_IRQPriorityConfig(uint8_t IRQNumber, uint32_t IRQPriority);
void GPIO_IRQHandling(uint8_t PinNumber);

These functions are part of an abstraction layer that simplifies managing interrupts in STM32 microcontrollers. They handle the configuration and management of the interrupt request (IRQ) lines, their priorities, and the handling of the interrupts.

NVIC Initialization

/**
 * @brief  Configures the interrupt for a given IRQ number
 * @param  IRQNumber: IRQ number of the interrupt
 * @param  EnorDi: ENABLE or DISABLE macro for the interrupt
 * @retval None
 */
void GPIO_IRQInterruptConfig(uint8_t IRQNumber, uint8_t EnorDi)
{
    if (EnorDi == ENABLE)
    {
        // Enable the interrupt for the specified IRQ number
        if (IRQNumber <= 31)
        {
            // Program ISER0 register to enable the interrupt in NVIC for IRQ numbers 0 to 31
            *NVIC_ISER0 |= (1 << IRQNumber);
        }
        else if (IRQNumber > 31 && IRQNumber < 64) // IRQ numbers 32 to 63
        {
            // Program ISER1 register to enable the interrupt in NVIC for IRQ numbers 32 to 63
            *NVIC_ISER1 |= (1 << (IRQNumber % 32));
        }
        else if (IRQNumber >= 64 && IRQNumber < 96)
        {
            // Program ISER2 register to enable the interrupt in NVIC for IRQ numbers 64 to 95
            *NVIC_ISER2 |= (1 << (IRQNumber % 64));
        }
    }
    else
    {
        // Disable the interrupt for the specified IRQ number
        if (IRQNumber <= 31)
        {
            // Program ICER0 register to disable the interrupt in NVIC for IRQ numbers 0 to 31
            *NVIC_ICER0 |= (1 << IRQNumber);
        }
        else if (IRQNumber > 31 && IRQNumber < 64)
        {
            // Program ICER1 register to disable the interrupt in NVIC for IRQ numbers 32 to 63
            *NVIC_ICER1 |= (1 << (IRQNumber % 32));
        }
        else if (IRQNumber >= 64 && IRQNumber < 96)
        {
            // Program ICER2 register to disable the interrupt in NVIC for IRQ numbers 64 to 95
            *NVIC_ICER2 |= (1 << (IRQNumber % 64));
        }
        // We can add more, but there aren't any IRQ numbers above 95 in STM32F446xx
    }
}

  • Purpose: Sets the priority for a given IRQ number.
  • Implementation:
    • The NVIC's Interrupt Priority Registers (IPR) are used to set the priority of each IRQ line.
    • The IRQ number is divided by 4 to find the IPR register (iprx), as each IPR register holds the priority for 4 different IRQ lines.
    • The position within the register (iprx_section) is determined by taking the remainder of the IRQ number divided by 4.
    • NO_PR_BITS_IMPLEMENTED represents the number of priority bits implemented. The priority field is left-aligned, so the shift amount is calculated accordingly.
    • The priority is set by shifting it to the correct position in the register and writing it to the appropriate IPR register.

Configuring IRQ Priority

/**
 * @brief  Configures the priority for a given IRQ number
 * @param  IRQNumber: IRQ number of the interrupt
 * @param  IRQPriority: Priority to be set
 * @retval None
 */
void GPIO_IRQPriorityConfig(uint8_t IRQNumber, uint32_t IRQPriority)
{
    // First, identify the interrupt priority register (IPR) for the given IRQ number
    uint8_t iprx = IRQNumber / 4;           // Each IPR register contains priority fields for 4 IRQs
    uint8_t iprx_section = IRQNumber % 4;   // Identify which section (0-3) within the IPR register is relevant

    // Calculate the bit position for setting the priority in the IPR register
    // Each section in an IPR register has 8 bits, and only 'NO_PR_BITS_IMPLEMENTED' bits are used for priority
    uint8_t shift_amount = (8 * iprx_section) + (8 - NO_PR_BITS_IMPLEMENTED);

    // Modify only the relevant bits for the IRQ priority in the targeted IPR register
    // The address is offset by 'iprx' to point to the correct IPR register, and the priority is shifted into position
    *(NVIC_PR_BASE_ADDR + iprx) |= (IRQPriority << shift_amount);
    // Note that pointer arithmetic is error-prone. Because `NVIC_PR_BASE_ADDR` is `uint32_t`, every increment will add 4 bytes, rather than 1 byte, even though `iprx` is `uint8_t`.
}
  • Purpose: Sets the priority for a given IRQ number.
  • Implementation:
    • The NVIC's Interrupt Priority Registers (IPR) are used to set the priority of each IRQ line.
    • The IRQ number is divided by 4 to find the IPR register (iprx), as each IPR register holds the priority for 4 different IRQ lines.
    • The position within the register (iprx_section) is determined by taking the remainder of the IRQ number divided by 4.
    • NO_PR_BITS_IMPLEMENTED represents the number of priority bits implemented. The priority field is left-aligned, so the shift amount is calculated accordingly.
    • The priority is set by shifting it to the correct position in the register and writing it to the appropriate IPR register.

GPIO Interrupt Handling

/**
 * @brief  Handles the interrupt for a specified GPIO pin. You need to call this function from your ISR.
 * @param  PinNumber: The GPIO pin number whose interrupt needs to be handled
 * @retval None
 */
void GPIO_IRQHandling(uint8_t PinNumber)
{
    // Check if the interrupt pending bit is set for the specified pin in the EXTI Pending Register (PR)
    if (EXTI->PR & (1 << PinNumber))
    {
        // Clear the interrupt pending bit for the specified pin
        // Writing 1 to the specific bit in the EXTI PR register clears the pending bit for that interrupt
        EXTI->PR |= (1 << PinNumber);
    }
}
  • Purpose: Clears the interrupt pending bit for a specific EXTI line. Because every application has a different ISR, this function should be called from the ISR to clear the interrupt pending bit for the EXTI line that triggered the interrupt. You should define the ISR for the EXTI line in your application using the same name as the IRQ handler function (e.g., EXTI0_IRQHandler for EXTI line 0), and then call this function in it. The naming can be found in the startup file (startup_stm32f446xx.s).
  • Implementation:
    • The EXTI Pending Register (PR) is checked to determine if the specified pin number's interrupt is pending.
    • If it is, the corresponding bit in the EXTI PR register is cleared by writing 1 to it. This is a common practice in STM32 MCUs where writing 1 to a bit in the interrupt pending register clears the interrupt.

14. Exercise: Toggle an LED using a Button (with interrupts)

// Include the header files for the MCU and standard functions
#include "stm32f446xx.h"
#include <string.h>

// Define a delay function for debouncing
void delay(void)
{
    for (uint32_t i = 0; i < 50000; i++)
        ;
}

int main(void)
{
    // Initialize a GPIO handle for the LED with zeros
    GPIO_Handle_t GpioLed;
    memset(&GpioLed, 0, sizeof(GpioLed)); // Clear all the configuration fields

    // Specify the GPIO port and pin for the LED
    GPIO_RegDef_t *pGPIO_LED = GPIOA;
    uint8_t pinNumber = GPIO_PIN_NO_5;

    // Configure the LED pin as output with fast speed and push-pull mode
    GpioLed.pGPIOx = pGPIO_LED;
    GpioLed.GPIO_PinConfig.GPIO_PinNumber = pinNumber;
    GpioLed.GPIO_PinConfig.GPIO_PinMode = GPIO_MODE_OUT;
    GpioLed.GPIO_PinConfig.GPIO_PinSpeed = GPIO_SPEED_FAST;
    GpioLed.GPIO_PinConfig.GPIO_PinOPType = GPIO_OP_TYPE_PP;
    GpioLed.GPIO_PinConfig.GPIO_PinPuPdControl = GPIO_NO_PUPD;

    // Enable the peripheral clock for the LED's GPIO port and initialize the LED pin
    GPIO_PeriClockControl(pGPIO_LED, ENABLE);
    GPIO_Init(&GpioLed);

    // Initialize a GPIO handle for the button with zeros
    GPIO_Handle_t GpioBtn;
    memset(&GpioBtn, 0, sizeof(GpioBtn)); // Clear all the configuration fields

    // Configure the button GPIO pin for interrupt generation on a falling edge with pull-up
    GpioBtn.pGPIOx = GPIOC;
    GpioBtn.GPIO_PinConfig.GPIO_PinNumber = GPIO_PIN_NO_13;
    GpioBtn.GPIO_PinConfig.GPIO_PinMode = GPIO_MODE_IT_FT;
    GpioBtn.GPIO_PinConfig.GPIO_PinSpeed = GPIO_SPEED_FAST;
    GpioBtn.GPIO_PinConfig.GPIO_PinPuPdControl = GPIO_PIN_PU; // Enable pull-up if the button is active low

    // Enable the peripheral clock for the button's GPIO port and initialize the button pin
    GPIO_PeriClockControl(GPIOC, ENABLE);
    GPIO_Init(&GpioBtn);

    // Configure the interrupt request for the button pin and set its priority
    GPIO_IRQInterruptConfig(IRQ_NO_EXTI15_10, ENABLE);
    GPIO_IRQPriorityConfig(IRQ_NO_EXTI15_10, NVIC_IRQ_PRI15);

    // Enter an infinite loop to wait for the interrupt to occur
    while(1);

    return 0;
}

// Define the IRQ handler for the EXTI line connected to the button
void EXTI15_10_IRQHandler(void)
{
    delay(); // Debounce the button press
    GPIO_IRQHandling(GPIO_PIN_NO_13); // Clear the interrupt pending bit for the button's EXTI line
    GPIO_ToggleOutputPin(GPIOA, GPIO_PIN_NO_5); // Toggle the LED's state
}