C · 2026-06-16 · embedded · register · MMIO · STM32 · GPIO

Reading & writing MCU registers (C)

A peripheral register is just a fixed memory address. You access it through a volatile pointer so the compiler emits a real load or store every time instead of caching the value (see using volatile correctly). What you may do to it depends on its access type — and the reference manual spells this out for every field. Taking the STM32H563 GPIO port as the example (RM0481, §13.4; base address from the memory-map table):

#include <stdint.h>

/* STM32H563 GPIOA on AHB2, non-secure alias (secure alias is 0x5202_0000).
   Base from RM0481 memory map; offsets from RM0481 section 13.4. The access
   type is baked into each pointer so the compiler enforces it for us:
     rw -> volatile          ro -> const volatile          wo -> volatile      */
#define GPIOA_BASE   0x42020000u

#define GPIOA_MODER  (*(volatile uint32_t *)       (GPIOA_BASE + 0x00u))  /* rw */
#define GPIOA_IDR    (*(const volatile uint32_t *) (GPIOA_BASE + 0x10u))  /* ro */
#define GPIOA_ODR    (*(volatile uint32_t *)       (GPIOA_BASE + 0x14u))  /* rw */
#define GPIOA_BSRR   (*(volatile uint32_t *)       (GPIOA_BASE + 0x18u))  /* wo */

/*******************************************************************************
 * Function Name  : gpio_set_output
 * Description    : Configure a pin as output in the read-write MODER (2 bits/pin).
 * Input          : pin - pin number 0..15.
 * Output         : None.
 * Return         : None.
 *******************************************************************************/
void gpio_set_output(uint32_t pin)
{
  uint32_t moder = GPIOA_MODER;        /* read  */

  moder &= ~(0x3u << (pin * 2u));      /* modify: clear the 2-bit field */
  moder |=  (0x1u << (pin * 2u));      /* 01 = general-purpose output   */

  GPIOA_MODER = moder;                 /* write */
}

/*******************************************************************************
 * Function Name  : gpio_read_pin
 * Description    : Read one input pin from the read-only IDR.
 * Input          : pin - pin number 0..15.
 * Output         : None.
 * Return         : 1 if the pin is high, 0 if low.
 *******************************************************************************/
uint32_t gpio_read_pin(uint32_t pin)
{
  return (GPIOA_IDR >> pin) & 1u;
}

/*******************************************************************************
 * Function Name  : gpio_write_pin
 * Description    : Drive a pin atomically through the write-only BSRR.
 * Input          : pin - pin number 0..15; level - 0 or 1.
 * Output         : None.
 * Return         : None.
 *******************************************************************************/
void gpio_write_pin(uint32_t pin, uint32_t level)
{
  if (level != 0u)
  {
    GPIOA_BSRR = (1u << pin);          /* BSy[15:0]: set this pin   */
  }
  else
  {
    GPIOA_BSRR = (1u << (pin + 16u));  /* BRy[31:16]: reset this pin */
  }
}

read-only vs read-write vs write-only

The reference manual tags every bitfield r, rw or w, and they are not interchangeable:

  • IDR is read-only (r). It mirrors the live state of the input pins; writing it does nothing. Modelling it as const volatile turns a stray GPIOA_IDR = x; into a compile error instead of a silent no-op — the type system carries the datasheet’s intent.
  • MODER / ODR are read-write (rw). You can read them back, so configuration is a read-modify-write: read, change the field, write. Fine when nothing else touches the register concurrently.
  • BSRR is write-only (w). The low half sets pins, the high half resets them, and a read returns 0x0000. Because a single write to BSRR changes only the pins you name — no read-modify-write — it is atomic against an ISR driving a different pin on the same port. That is why gpio_write_pin uses BSRR, not ODR ^= ... (see atomic writes).

Before any of this works

  • Enable the port’s clock first: set GPIOAEN in RCC_AHB2ENR, otherwise every register reads back as zero and writes are dropped.
  • A single aligned 32-bit register access is already atomic on the core; the hazard is only the read-modify-write on rw registers — prefer BSRR/BRR for single-bit changes.
  • With TrustZone enabled, use the secure alias (0x5202_0000); the CMSIS device header picks the right one for you, which is why production code uses GPIOA->ODR rather than a hand-rolled address.