Memory map: everything in an MCU has an address
Code is in Flash, variables are in SRAM, and peripheral registers sit at fixed addresses too. How to read the memory map in a datasheet.
A 32-bit Cortex-M has a 4 GB address space from 0x0000_0000 to 0xFFFF_FFFF. Real memory occupies only part of it, but each region has an assigned use.
| Address | Region | STM32 example |
|---|---|---|
| 0x0800_0000 | Code (Flash) | Program and const data |
| 0x2000_0000 | SRAM | Global variables, stack, heap |
| 0x4000_0000 | Peripherals | GPIO, timer and UART registers |
| 0xE000_0000 | Core peripherals | NVIC, SysTick, debug |
Reading the datasheet
Find "GPIOA 0x4002_0000 to 0x4002_03FF" in the memory-map table of the reference manual, find "ODR offset 0x14" in the register table of that chapter, and the address of GPIOA_ODR is 0x4002_0014. GPIOA->ODR in the CMSIS headers does this calculation for you.
Using it for debugging
When a hard fault occurs, a faulting address of 0x2000_xxxx is RAM and 0x4xxx_xxxx is a peripheral access. Accessing the registers of a peripheral whose clock is off causes a bus error, and the address alone lets you guess the cause.
In a picture: the address space on one line
These large divisions are the same on every Cortex-M. What goes at which address inside them is decided by the chip maker and written in the "Memory map" table of the datasheet.
How a register address is formed
peripheral base address + register offset = register address
GPIOA 0x4800_0000 + ODR 0x14 = 0x4800_0014
The vendor header file expresses this calculation as a struct.
typedef struct {
volatile uint32_t MODER; // +0x00
volatile uint32_t OTYPER; // +0x04
volatile uint32_t OSPEEDR; // +0x08
volatile uint32_t PUPDR; // +0x0C
volatile uint32_t IDR; // +0x10
volatile uint32_t ODR; // +0x14
} GPIO_TypeDef;
#define GPIOA ((GPIO_TypeDef *)0x48000000u)
Write GPIOA->ODR and the compiler adds 0x14 to the base address and accesses it. The order and size of the struct members have to match the offsets in the reference manual exactly.
Where the program is placed
| Section | Contents | Where it goes |
|---|---|---|
.text |
Code | Flash |
.rodata |
const data, strings |
Flash |
.data |
Global variables with an initial value | RAM (initial values kept in Flash) |
.bss |
Global variables that start at 0 | RAM |
| Stack, heap | Space used at run time | RAM |
.data takes up both Flash and RAM. At power-up the startup code copies the initial values kept in Flash to RAM, fills .bss with 0 and then calls main. That process is why global variables have their values from the very start.
Reading the map file
The .map file produced by the build lists the address and size of every function and variable.
- When Flash or RAM runs short: sort by size and find what is taking the space.
- When a hard fault occurs: find which function the faulting address is in.
- When a variable gets corrupted: suspect the array placed just before it.
Bit-banding and aliases
Some Cortex-M3/M4 parts have a region where each bit of RAM and of the peripherals is accessed at its own address. One write changes a single bit, so there is no problem of an interrupt cutting in during a read-modify-write. Not every chip has it, so check the datasheet.
Problems that come up in practice
- Accessing a reserved region: reading an address that is not in the map causes a bus fault.
- A peripheral whose clock is off: the address is right, yet reads return 0 and writes do not take effect.
- It stops working after changing the chip: even in the same family, peripheral base addresses can differ. Switch to the header file for that chip.
- Bootloader and application addresses overlap: the Flash start address in the linker script and the vector-table location have to be moved together.