gwordal

Lesson 2 of 5 · 22 min

GPIO registers

On an Arduino, digitalWrite(13, HIGH) looks like one action. On the STM32 it is three separate facts you must establish yourself: the port has a clock, the pin is an output, and the output is high. Each one is a write to a memory-mapped register, and understanding them removes the mystery from every HAL call you will ever read. The target is the NUCLEO-L476RG, whose green user LED LD2 is wired to pin PA5 (port A, pin 5).

Why a peripheral needs a clock

To save power, every peripheral on an STM32 is clock-gated: after reset almost all of them are switched off. If you write to the GPIOA registers before enabling its clock, the write is silently ignored and reads return zero. Nothing crashes, nothing blinks, and the bug is maddening.

The enable bits live in the RCC (Reset and Clock Control) block. On the L476, GPIO ports hang off the AHB2 bus, so the bit is in RCC->AHB2ENR: bit 0 is GPIOAEN, bit 1 GPIOBEN, bit 2 GPIOCEN, and so on. The reference manual also recommends reading the register back after setting the bit, which gives the clock a couple of cycles to propagate before the first access.

The registers you need

Each GPIO port is a block of registers starting at its base address. GPIOA is at 0x4800 0000. The offsets below are the same for every port on the L4 family.

RegisterOffsetPurpose
MODER0x00Mode of each pin, 2 bits per pin: 00 input, 01 output, 10 alternate function, 11 analog
OTYPER0x04Push-pull (0) or open-drain (1), 1 bit per pin
OSPEEDR0x08Edge speed, 2 bits per pin
PUPDR0x0CPull-up or pull-down, 2 bits per pin
IDR0x10Read the pin level
ODR0x14Output latch
BSRR0x18Atomic set (bits 0 to 15) and reset (bits 16 to 31)

Because MODER uses 2 bits per pin, pin n owns bits 2n and 2n+1. For PA5 that is bits 11 and 10.

Worked example: bit manipulation with masks

We want PA5 as an output without disturbing any other pin. The reset value of GPIOA MODER is 0xABFF FFFF: most pins are analog (11), but PA13, PA14 and PA15 are already set up for debug and must not change. The recipe is always clear the field, then set the new value.

  1. Field position: 2 x 5 = 10, so the mask is 0x3 << 10 = 0x0000 0C00.
  2. Invert it to get a clearing mask: ~0x0C00 = 0xFFFF F3FF.
  3. Clear: 0xABFF FFFF AND 0xFFFF F3FF = 0xABFF F3FF. Bits 11 and 10 are now 00.
  4. Set output (01): 1 << 10 = 0x0000 0400. OR it in: 0xABFF F3FF OR 0x0400 = 0xABFF F7FF.

Only bit 11 changed (1 to 0), exactly as intended. In C that is a single expression:

GPIOA->MODER = (GPIOA->MODER & ~(3u << 10)) | (1u << 10);

Simply writing GPIOA->MODER |= (1u << 10) would be wrong: bit 11 stays 1, so the pin would become 11, analog, and never drive anything. This is the most common beginner mistake with 2-bit fields.

Blink with direct register writes

#include "stm32l476xx.h"

static void delay(volatile uint32_t n) {
  while (n--) {}                              // crude busy wait
}

int main(void) {
  RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;        // 1. clock for port A
  (void)RCC->AHB2ENR;                         // read back to let it settle

  GPIOA->MODER &= ~GPIO_MODER_MODE5_Msk;      // 2. clear PA5 mode field
  GPIOA->MODER |= GPIO_MODER_MODE5_0;         //    01 = general purpose output

  for (;;) {
    GPIOA->BSRR = (1u << 5);                  // 3. PA5 high, LED on
    delay(300000);
    GPIOA->BSRR = (1u << (5 + 16));           //    PA5 low, LED off
    delay(300000);
  }
}

The volatile on the counter stops the compiler from deleting the empty loop. After reset the L476 runs from the 4 MHz MSI oscillator, and one pass of that loop costs roughly 5 to 8 cycles, so 300000 iterations take about 0.4 to 0.6 s. It is deliberately imprecise; the next lesson replaces it with a hardware timer.

Why BSRR instead of ODR

You could write GPIOA->ODR ^= (1u << 5). That compiles to read, modify, write. If an interrupt fires between the read and the write and changes another pin on the same port, your write puts the old value back and the interrupt's change is lost. BSRR avoids this because writing a 1 affects only that pin and writing a 0 does nothing, so it is a single atomic store. Bits 16 to 31 reset the pins: to set PA5 and clear PA6 in one write, use (1u << 5) | (1u << (6 + 16)) = 0x0040 0020. Both pins change in the same bus cycle.

The same thing with HAL

HAL_Init();
SystemClock_Config();                         // generated by CubeMX
__HAL_RCC_GPIOA_CLK_ENABLE();                 // same RCC bit as above

GPIO_InitTypeDef g = {0};
g.Pin   = GPIO_PIN_5;
g.Mode  = GPIO_MODE_OUTPUT_PP;                // MODER 01, OTYPER 0
g.Pull  = GPIO_NOPULL;                        // PUPDR 00
g.Speed = GPIO_SPEED_FREQ_LOW;                // OSPEEDR 00
HAL_GPIO_Init(GPIOA, &g);

for (;;) {
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
  HAL_Delay(500);                             // SysTick based, accurate
}

HAL does exactly the same register writes, plus argument handling that works for every pin and every family. The price is code size and a few dozen cycles per call, which is irrelevant for an LED and noticeable for a bit-banged protocol running at megahertz.

Check yourself

You want PA5 as output but PA5 currently reads 11 in its MODER field. Which statement is correct?

Check yourself

What is the BSRR value that sets PA3 and resets PA7 in one write?