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:
IDRis read-only (r). It mirrors the live state of the input pins; writing it does nothing. Modelling it asconst volatileturns a strayGPIOA_IDR = x;into a compile error instead of a silent no-op — the type system carries the datasheet’s intent.MODER/ODRare 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.BSRRis write-only (w). The low half sets pins, the high half resets them, and a read returns0x0000. Because a single write toBSRRchanges 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 whygpio_write_pinusesBSRR, notODR ^= ...(see atomic writes).
Before any of this works
- Enable the port’s clock first: set
GPIOAENinRCC_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
rwregisters — preferBSRR/BRRfor 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 usesGPIOA->ODRrather than a hand-rolled address.