Articles > Electronics >

Understanding STM32 Microcontroller GPIOs

STM32F103C8-Bluepill-Development-Board
STM32F103C8 Blue Pill Development Board

When using any STM32 microcontroller, GPIOs need to be initialized before you can use them in the application program. The GPIO peripheral is configured and controlled using a set of registers. Multiple registers are assigned to each port available in an STM32 MCU. These registers can be directly accessed and programmed but it's a tedious task. To make it easier, we can use the HAL driver provided by ST or the CMSIS driver provided by ARM. This code is also generated by STM32CubeMX application as per user configuration and makes it easy to generate initialization codes. Let's see how to initialize GPIO directly using registers and do some port manipulations.

Following are the registers associated each GPIO port. Each port on STM32 can have up to 16 GPIO pins. A particular MCU can have multiple such ports.

  • GPIO port mode register (GPIOx_MODER) - 32 bits, dual bit configuration
  • GPIO port output type register (GPIOx_OTYPER) - 16 bits, single bit configuration
  • GPIO port output speed register (GPIOx_OSPEEDR) - 32 bits, dual bit
  • GPIO port pull-up/pull-down register (GPIOx_PUPDR) - 32 bits, dual bit
  • GPIO port input data register (GPIOx_IDR) - 16 bits, single bit
  • GPIO port output data register (GPIOx_ODR) - 16 bits, single bit
  • GPIO port bit set/reset register (GPIOx_BSRR) - 32 bits, single bit
  • GPIO port bit reset register (GPIOx_BRR) - 16 bits, single bit
  • GPIO port configuration lock register (GPIOx_LCKR) - 16 bits, single bit
  • GPIO alternate function low register (GPIOx_AFRL) - 32 bits, four bit configuration
  • GPIO alternate function high register (GPIOx_AFRH) - 32 bits, four bit
  • GPIO Port bit reset register (GPIOx_BRR) - 16 bits, single bit (not available in all MCUs)


In software, you can access these registers as

  • GPIOx->MODER
  • GPIOx->OTYPER
  • GPIOx->OSPEEDR
  • GPIOx->PUPDR
  • GPIOx->IDR
  • GPIOx->ODR
  • GPIOx->BSRR
  • GPIOx->BRR
  • GPIOx->LCKR
  • GPIOx->AFRL
  • GPIOx->AFRH


Where "x" cab be the port identifier starting from letter A. For example if you want to set pin 5 of port A, ie PA5, as output, you can do so by,

GPIOA->MODER |= (1U << 5);

(1U << 5) simply means "shift unsigned integer 1 to 5 positions left". This has the effect of writing 0b01 ("0b" is the prefix used to indicate a binary literal similar to 0x for hex literal) to configuration bit pair of 5th pin of port A. 0b01 means the pin will be set as an output. The |= means it's a "Read Modify Write" (RMW) access to the register, where we need to first read the register, use the value to determine a new value and write that result back to the same register. So there are three steps involved. But there's a better way to do this which will be discussed in a while.

As a programmer, you'll most likely add multiple definitions to a header file to make such operations easier, which is already done by STM developers. For example GPIOx->MODER can accept the following values,

  • GPIO_MODER_MODER5_Pos     -  the relative position of the register
  • GPIO_MODER_MODER5_Msk     -  a 0b11 binary mask pair
  • GPIO_MODER_MODER5         -  a 0b11 binary mask pair
  • GPIO_MODER_MODER5_0       -  a 0b01 binary set mask
  • GPIO_MODER_MODER5_1       -  a 0b10 binary set mask


The 5 is obviously to indicate the pin and therefore can have values from 0 to 15 for each pin of a port. The definitions of those values are,

#define GPIO_MODER_MODER5_Pos    (10U)                                 
#define GPIO_MODER_MODER5_Msk    (0x3U << GPIO_MODER_MODER5_Pos)  //0x00000C00
#define GPIO_MODER_MODER5        GPIO_MODER_MODER5_Msk
#define GPIO_MODER_MODER5_0      (0x1U << GPIO_MODER_MODER5_Pos)  //0x00000400
#define GPIO_MODER_MODER5_1      (0x2U << GPIO_MODER_MODER5_Pos)  //0x00000800

These values are defined in the part specific header file in the "CMSIS\Device\ST\" folder for each part. The above snippets are coped from "\CMSIS\Device\ST\STM32F4xx\Include\stm32f446xx.h".

If you've ever used bit manipulation in your programs, things would be clear by now on using those definitions accordingly. GPIO_MODER_MODER5_Pos is the relative position of a bit pair assigned to 5th pin of port A. For the first pair, the relative position would be zero. So we're cleverly using the shift operator to set the binary values in a register for each pin. Notice the relative position is defined as an unsigned int 10U ("u" suffix is to indicate a unsigned int literal) while the rest of the numbers as hex values. This is just arbitrary.

Now let's see how we can configure the register using these definitions.

To set a bit pair to 0b11 simultaneously,

GPIOA->MODER |= GPIO_MODER_MODER5;


To clear a bit pair to 0b00 simultaneously,

GPIOA->MODER &= ~(GPIO_MODER_MODER5);


To set only the first bit to 1 while the second bit is unchanged,

GPIOA->MODER |= GPIO_MODER_MODER5_0;


To clear the first bit to 0 while the second bit is unchanged,

GPIOA->MODER &= ~(GPIO_MODER_MODER5_0);


To set the second bit to 1 while the first bit is unchanged,

GPIOA->MODER |= GPIO_MODER_MODER5_1;


To clear the second bit to 0 while the first bit is unchanged,

GPIOA->MODER |= ~(GPIO_MODER_MODER5_1);


To set the bit pair to 01 simultaneously,

GPIOA->MODER |= GPIO_MODER_MODER5_0; //set first bit
GPIOA->MODER &= ~(GPIO_MODER_MODER5_1); //clear second bit


To set the bit pair to 10 simultaneously,

GPIOA->MODER &= ~(GPIO_MODER_MODER5_0); //clear first bit
GPIOA->MODER |= GPIO_MODER_MODER5_1; //set second bit


For single bit configuration registers such as OTYPER, we get only a position and a mask in the form of,

GPIO_OTYPER_OT0_Pos     (0U)
GPIO_OTYPER_OT0_Msk     (0x1U << GPIO_OTYPER_OT0_Pos)
GPIO_OTYPER_OT0          GPIO_OTYPER_OT0_Msk


Did you see the overhead of setting a binary pair to either 0b01 or 0b10 at the same time ? We needed two statements to do that. This is because we can not do simultaneous AND or OR operations on a single register. Also those two statements constitute of two RMW sequences, so in total 6 single instructions at the least. We are also not guaranteed that these six instructions will be executed in order in case an interrupt occurs or a thread or processor has access to the same memory locations we're manipulating. That's a problem and that's why we have "atomic" operations. It simplifies the RMW operations from the programmer's perspective and also ensures that the instructions will be executed in the order they're written.

There's no need to access all the registers atomically becasue they're usually set only once. In case of GPIO configuration registers of STM32, we can perform atomic write operations using the dedicated BSRR and BRR registers. BSRR is a 32-bit register where the lower 16-bits are used to set any of the 16 pins and the higher 16-bits to clear/reset any of the 16 pins of a particular IO port. The BRR register's higher 16-bits are reserved and the lower 16-bits reset or clear the 16 pins.

To set PA5 (5th pin of port A), we can do,

GPIOA->BSRR = (1U << 5);   //set the 5th bit or PA5

or 

GPIOA->BSRR = GPIO_BSRR_BS5;   //set the 5th bit or PA5


To clear PA5,

GPIOA->BSRR = (1U << 21);   //clear the 5th bit or PA5

or

GPIOA->BSRR = GPIO_BSRR_BR5;   //clear the 5th bit or PA5

where GPIO_BSRR_BS5 is a macro that sets the 5th bit of lower 16-bits and GPIO_BSRR_BR5 clears the 5th bit of higher 16-bits. Similar operations can be performed on BRR register macros such as GPIO_BRR_BR5Macro definitions like these are available for all of the GPIO registers.

Interestingly, some family of controllers such as STM32F446 do not have the BRR register but only the BSRR. The engineers might've thought it was redundant, I think.