Microcontrollers & architecture 3 min read

The memory map

On a microcontroller, everything the CPU can reach, from Flash and SRAM to every peripheral, has an address on one long number line. The memory map tells you which range is which, so the same kind of pointer can read a variable or switch on an LED.

A typical Arm Cortex-M memory map. From 0x0000_0000: Flash with the vector table and program. From 0x2000_0000: SRAM for the stack, heap and global variables. From 0x4000_0000: peripheral registers such as GPIO, UART and timers. From 0x6000_0000: optional external memory. From 0xE000_0000 to 0xFFFF_FFFF: core system peripherals such as the NVIC, SysTick and debug.
High addresses at the top, as reference manuals usually draw it.

One address space

A Cortex-M uses 32-bit addresses, which gives a 4 GB space. Arm fixes what each region is for, so the layout is the same from one vendor to the next. A real chip fills only a sliver of it. An STM32F103C8, for example, has 64 KB of Flash at 0x0800_0000 and 20 KB of SRAM at 0x2000_0000. Reading an address where nothing is fitted triggers a fault.

Region by region

Where you meet it

The linker script tells the toolchain where Flash and RAM are, so it can place code and data:

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

And because peripherals are just addresses, this line drives pin PA5 high on an STM32F103, where the GPIOA output register sits at 0x4001_080C:

*(volatile uint32_t *)0x4001080C |= (1u << 5);

The volatile keyword tells the compiler that every access matters, so it never caches or skips one. Vendor headers wrap these addresses in readable names like GPIOA->ODR.