Bare Metal STM32 - GPIO Bare Metal STM32 - GPIO

Bare Metal STM32 - GPIO

Peeling back the layers of abstraction, we have bare-metal programming for microcontrollers. One thing I think many students don’t learn during university is how to actually use official documentation (e.g. datasheets). So I wanted to create a short guide on how I go about performing common tasks using bare-metal programming on the STM32F411. I’ll be using STM32CubeIDE, but feel free to use any IDE you like.

Setup

We will reference three pieces of documentation:

  1. STM32F411 Nucleo User Manual
  2. STM32F411 Datasheet
  3. STM32F411 Reference Manual

I will also be including these headers into my project.

  • CMSIS (Common Microcontroller Software Interface Standard) headers provided by ARM.
  • stm32f411xe.h header provided by ST

ARM designs the CPU architecture and licenses it to manufacturers like STMicroelectronics to build chips around. The CMSIS headers define the Cortex-M4 core, and they’re wired up to the STM32F411’s peripherals via stm32f4xx.h, which is what we’ll actually be including.

To include the headers into the project:

Right-click your project → Properties → C/C++ Build → Settings → MCU GCC Compiler → Include Paths. Hit the ”+” icon and add the paths

So in my case I would be adding the following paths:

  • Headers/CMSIS and HEADERS/ST

Blinking an LED

The User Manual tells us that the green user LED (LD2) is connected to PA5 (Port A, Pin 5):

User LD2: the green LED is a user LED connected to … STM32 I/O PA5 (pin 21) …

Enabling Clock Access to Port A

Before we can control any peripheral, we need to enable its clock. Section 5.3.2 of the Reference Manual explains that peripheral clocks are gated off by default to save power, so we need to explicitly turn them on.

Peripheral Clock Gating

Next, we need to figure out which clock register to use: RCC_AHB1ENR or RCC_AHB2ENR. Looking at Figure 3 (STM32F411xC/xE block diagram) in the Datasheet, we can see that GPIO Port A sits on the AHB1 bus (Advanced High-performance Bus), so we’ll be using RCC_AHB1ENR.

Block Diagram

The Reference Manual covers this register in section 7.3.10 - RCC AHB1 peripheral clock enable register (RCC_AHB1ENR). To enable the clock for Port A, we set bit 0 to 1:

Clock Enable Register

#define GPIOAEN (1U<<0)
RCC->AHB1ENR |= GPIOAEN;

Configuring Pin 5 as Output

GPIO ports have at least two registers: a direction register and a data register. The direction register configures pins as input or output; the data register reads from or writes to them.

Section 8.4.1 - GPIO Port Mode Register (GPIOx_MODER) in the Reference Manual is what we want. Each pin gets a 2-bit field, and the formula for finding pin y’s bits is 2y and 2y+1. For pin 5, that’s bits 10 and 11. The encoding for general-purpose output is 01, so right to left, bit 10 = 1 and bit 11 = 0:

Port Mode Register

GPIOA->MODER |= (1U<<10);
GPIOA->MODER &= ~(1U<<11);

Toggling LED

Now that pin direction is set, we write to the data register. Section 8.4.6 - GPIO Port Output Data Register (GPIOx_ODR) in the Reference Manual covers this. We can toggle pin 5 by XOR-ing its bit on every iteration:

Port Output Data Register

We can toggle Pin5 by inverting the bit every time:

#define PIN5 (1U<<5)
GPIOA->ODR ^= PIN5;

Putting it all together

main.c
#include "stm32f4xx.h"
#define GPIOAEN (1U<<0)
#define PIN5 (1U<<5)
int main(void)
{
RCC->AHB1ENR |= GPIOAEN;
GPIOA->MODER |= (1U<<10);
GPIOA->MODER &= ~(1U<<11);
while(1)
{
GPIOA->ODR ^= PIN5;
// short delay
for(int i=0; i<100000; i++){};
}
}

Bonus: using the BSRR

The bit set/reset register (BSRR) is used to give you atomic control over ODR pins. It’s a 32 bit register split into two halves. The the lower half is to set pins 0-15, and the upper half is to reset pins 0-15. The key advantage over the regular ODR pins is atomicity. When you do ODR ^ PIN5 that performs a read, modify, and a write. If an interrupt fires during this process, it could corrupt the state. With BSRR, the hardware handles it. Section 8.4.7. in the Reference Manual describes this register.

BSRR

There is only a slight change to the previous program:

#include "stm32f4xx.h"
#define GPIOAEN (1U<<0)
#define PIN5_SET (1U<<5)
#define PIN5_RESET (1U<<21)
int main(void)
{
RCC->AHB1ENR |= GPIOAEN;
GPIOA->MODER |= (1U<<10);
GPIOA->MODER &= ~(1U<<11);
while(1)
{
GPIOA->BSRR = PIN5_SET;
for(int i=0; i<100000; i++){};
GPIOA->BSRR = PIN5_RESET;
for(int i=0; i<100000; i++){};
}
}

Using a button as an input

Found once again in the Reference Sheet, the input register is very simple:

GPIO Input Data Register

Suppose we connect a button to pin 13. Then we can read it’s state like so:

#define BTN_PIN (1U<<13)
if (GPIOC->IDR & BTN_PIN)

Our final program now looks like:

#include "stm32f4xx.h"
#define GPIOAEN (1U<<0)
#define GPIOCEN (2U<<0)
#define PIN5_SET (1U<<5)
#define PIN5_RESET (1U<<21)
#define BTN_PIN (1U<<13)
int main(void)
{
// Enable clock access to GPIOs
RCC->AHB1ENR |= GPIOAEN;
RCC->AHB1ENR |= GPIOCEN;
// Set PA5 as output pin (LED)
GPIOA->MODER |= (1U<<10);
GPIOA->MODER &= ~(1U<<11);
// Set PC13 as input pin (Button)
GPIOC->MODER &= ~(1U<<26);
GPIOC->MODER &= ~(1U<<27);
while(1)
{
// Check if the button is pressed
if (GPIOC->IDR & BTN_PIN)
{
GPIOA->BSRR = PIN5_SET; // Turn on LED
}
else
{
GPIOA->BSRR = PIN5_RESET; // Turn off LED
}
}
}

← Back to blog