When using any STM32 microcontrollers, 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 (Hardware Abstraction Layer) driver provided by ST or the CMSIS (Cortex Microcontroller Software Interface Standard) 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 with each GPIO port. Each port on STM32 can have up to 16 GPIO pins. A particular MCU variant can have multiple such ports.
|Register||Description||Size in bits||Configuration Length in bits|
||GPIO port mode register||32||2|
||GPIO port output type register||16||1|
||GPIO port output speed register||32||2|
||GPIO port pull-up/pull-down register||32||2|
||GPIO port input data register||16||1|
||GPIO port output data register||16||1|
||GPIO port bit set/reset register||32||1|
||GPIO port bit reset register||16||1|
||GPIO port configuration lock register||16||1|
||GPIO alternate function low register||32||4|
||GPIO alternate function high register||32||4|
The configuration length is simply the number of bits required to configure the state of a single pin. For 16-bit registers, it is usually a single bit and two bits for 32-bit registers. In software, you can access these registers as,
Where "x" can 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
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
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 copied 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
GPIOA->MODER |= GPIO_MODER_MODER5;
To clear a bit pair to
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 bitGPIOA->MODER &= ~(GPIO_MODER_MODER5_1); //clear second bit
To set the bit pair to 10 simultaneously,
GPIOA->MODER &= ~(GPIO_MODER_MODER5_0); //clear first bitGPIOA->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
0b10 at the same time ? We needed two statements to do that. This is because we can not do simultaneous
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 because 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
GPIOA->BSRR = GPIO_BSRR_BS5; //set the 5th bit or PA5
To clear PA5,
GPIOA->BSRR = (1U << 21); //clear the 5th bit or PA5
GPIOA->BSRR = GPIO_BSRR_BR5; //clear the 5th bit or PA5
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_BR5. Macro 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 have thought it was redundant, I think.