← Back to Embedded C series
🔌
Basics
Basics · Prereq: lesson 3

04. Pointers · volatile · Memory-Mapped I/O

In embedded, a register is just a specific memory address (e.g. STM32 GPIOC->ODR is 0x4001100C). Write to that address and a pin moves; read it and you get the pin state — that's memory-mapped I/O (MMIO). We practice pointers (to handle addresses) and volatile (to safely read hardware-changed values) with PC gcc. The (*(volatile uint32_t *)addr) pattern is the foundation of STM32 control from lesson 14.

pointervolatileMMIOregisterCMSISmemory map
Duration
~1.5 hours
Level
📊 Beginner
Prerequisite
🎯 Lesson 3
OUTCOME
Understand that a register is "memory at a fixed address" and use the (*(volatile uint32_t *)ADDR) pattern and a struct+pointer (CMSIS style) to read/write that address safely.

What you'll learn

  • 1Use a pointer's address-of (&) and dereference (*) correctly
  • 2Understand registers as fixed-address memory and explain MMIO
  • 3Read/write a specific address with the (*(volatile uint32_t *)ADDR) pattern
  • 4Explain why volatile is needed (prevents the compiler from skipping reads)
  • 5Understand grouping peripherals as a struct+pointer (CMSIS style)

Introduction

Lesson 3 manipulated register bits. But where is that register? In embedded, registers live at specific addresses in the memory map. Handling addresses needs pointers; safely reading hardware-changed values needs volatile.

Key concepts

1) Pointer — a variable holding an address

c
uint8_t  x = 10;
uint8_t *p = &x;   /* p is x's address */
*p = 20;           /* dereference: write 20 to where p points (x) → x==20 */

p is an address (where), *p is the content there (what). Not confusing the two is the key.

2) MMIO — register = address

STM32F103 e.g.AddressMeaning
RCC->APB2ENR0x40021018peripheral clock enable
GPIOC->CRH0x40011004port C high pin config
GPIOC->ODR0x4001100Cport C output data
c
#define GPIOC_ODR (*(volatile uint32_t *)0x4001100CUL)
GPIOC_ODR |= (1u << 13);   /* PC13 output High */

3) volatile — "really read it every time"

When the compiler thinks a value can't change, it skips memory reads and caches in a CPU register. But hardware registers change on their own.

c
while (!(UART_SR & TXE)) { }   /* wait until TXE becomes 1 */
⚠️

If UART_SR isn't volatile, the compiler may loop on a once-read value forever. volatile is effectively mandatory on MMIO pointers and ISR-shared variables.

4) Grouping registers as a struct (CMSIS)

c
typedef struct {
    volatile uint32_t MODER;   /* +0x00 */
    volatile uint32_t IDR;     /* +0x04 */
    volatile uint32_t ODR;     /* +0x08 */
} gpio_t;
#define GPIOC ((gpio_t *)0x40011000UL)
GPIOC->ODR |= (1u << 13);     /* base + 0x08 */

Member order is the offset (+0,+4,+8…), so GPIOC->ODR, RCC->APB2ENR notation from lesson 13 all follow this pattern.

Core example

PC has no address like 0x40011000, so we make a variable a "fake register" and practice the same pointer pattern. The mechanism is identical.

c
#include <stdio.h>
#include <stdint.h>
#define BIT(n) (1u << (n))

int main(void)
{
    volatile uint32_t fake_reg = 0;       /* really a register at a fixed address */
    volatile uint32_t *reg = &fake_reg;   /* pointer to that address */
    *reg |= BIT(3);                       /* set bit3 */
    *reg |= BIT(17);                      /* set bit17 */
    *reg &= ~BIT(3);                      /* clear bit3 */
    printf("reg=0x%08X bit17=%s\n", (unsigned)*reg,
           ((*reg >> 17) & 1u) ? "ON" : "OFF");
    return 0;
}
// reg=0x00020000 bit17=ON

Common mistakes

Q. while (!(SR & TXE)); loops forever.

A. If SR isn't volatile, the compiler optimizes "doesn't change in the loop, read once". Mark the register pointer/macro volatile to re-read the real address each time.

Q. Accessing (uint32_t *)0x40011000 on PC crashes (segfault).

A. That address is an STM32-only peripheral; it doesn't exist in PC memory. On PC, put a variable's address in the pointer to practice the pattern; access the real address on the lesson-14 simulator/board.

Q. Is writing 0x40011000 alone enough for an address?

A. Suffix 32-bit address constants with UL (0x40011000UL) and always cast to a pointer type: (volatile uint32_t *)0x40011000UL.

Summary

  • Embedded registers are memory at fixed addresses; access is pointer dereference
  • MMIO idiom: (*(volatile uint32_t *)ADDR) to read/write a specific address
  • volatile re-reads hardware-changed values instead of caching — mandatory on registers
  • Peripherals grouped as struct+pointer read cleanly as GPIO->ODR (CMSIS)
  • To change a variable outside a function, pass its address (pointer), not the value

Exercises

  1. Use a volatile pointer on a fake register to set/clear bit5·bit12 and print the result in hex
  2. Build a MODER/IDR/ODR struct and confirm offsets are +0/+4/+8
  3. Zero a caller's variable via clear_flag(uint8_t *reg)
Example code / lecture materials

All lecture materials and example code (with homework and answers) are openly available on GitHub.

View on GitHub ↗