Cortex-M Memory Map and Bus Architecture: Code, SRAM and Peripherals

On a Cortex-M microcontroller everything is an address. Flash, RAM, GPIO ports, timers and even the interrupt controller are all reached by reading and writing 32-bit locations in a single 4 GB address space. ARM fixes the broad layout of this space for every Cortex-M chip, and vendors place their memories and peripherals inside the regions ARM defines. Once you understand this map, linker scripts, pointer-based register access and fault addresses all make sense.

The Fixed Cortex-M Memory Map

The 4 GB space is split into regions of 512 MB or 1 GB. Each region has default attributes: whether code can execute from it, and whether accesses may be cached or buffered.

Address rangeRegionTypical contentsExecutable
0x00000000 to 0x1FFFFFFFCode (512 MB)On-chip flash, boot ROM, sometimes a small SRAMYes
0x20000000 to 0x3FFFFFFFSRAM (512 MB)On-chip SRAM; first 1 MB bit-band region on M3/M4Yes
0x40000000 to 0x5FFFFFFFPeripheral (512 MB)Vendor peripherals on AHB and APB busesNo
0x60000000 to 0x9FFFFFFFExternal RAM (1 GB)External memory controllers, SDRAMYes
0xA0000000 to 0xDFFFFFFFExternal device (1 GB)External peripheralsNo
0xE0000000 to 0xE00FFFFFPrivate Peripheral BusNVIC, SysTick, SCB, debug unitsNo
0xE0100000 to 0xFFFFFFFFVendor specificVendor-defined system areasNo

The Private Peripheral Bus is identical on every Cortex-M chip because it belongs to the core, not to the vendor. The System Control Space starts at 0xE000E000: SysTick registers are at 0xE000E010, the NVIC enable registers start at 0xE000E100, and the System Control Block, which holds CPUID, the vector table offset and fault status registers, starts at 0xE000ED00. That is why the same CMSIS core header works for parts from different vendors.

Buses Behind the Map

Cortex-M3 and M4 use a Harvard style bus interface. The I-Code bus fetches instructions and the D-Code bus reads literal data and constants from the Code region. The System bus handles everything from 0x20000000 upward: SRAM, peripherals and external memory. The PPB connects the core to its internal system components. Because instruction fetch and data access can happen on different buses at the same time, a loop running from flash while reading SRAM does not stall on bus contention.

Cortex-M0 and M0+ have a single AHB-Lite bus for everything, which keeps them small. Cortex-M7 replaces these with a 64-bit AXI master, tightly coupled memory (TCM) interfaces and caches.

Outside the core, the vendor connects the buses through an AHB bus matrix to memories, DMA controllers and high-speed peripherals. Slower peripherals such as UARTs and I2C sit behind AHB-to-APB bridges. APB accesses take extra cycles, and each bridge often has its own clock divider, which is why a peripheral's clock can be lower than the CPU clock.

Example: LPC1768 Layout

The NXP LPC1768 (Cortex-M3, up to 100 MHz) shows how a vendor fills the map. It has 512 KB of flash and 64 KB of SRAM split into blocks that sit on different bus matrix ports, so the CPU and DMA can use separate blocks at the same time.

BlockStart addressSizeNotes
On-chip flash0x00000000512 KBVector table at the start
Local SRAM0x1000000032 KBFastest RAM for the CPU, in the Code region
AHB SRAM bank 00x2007C00016 KBGood for DMA and Ethernet buffers
AHB SRAM bank 10x2008000016 KBGood for USB buffers
GPIO0x2009C00016 KBFast GPIO on AHB
APB0 peripherals0x40000000512 KBTimers, UART0, UART1, I2C, ADC
APB1 peripherals0x40080000512 KBSSP, DAC, UART2, UART3, system control

A linker script describes these physical memories so that the linker places code in flash and variables in RAM.

Linker script fragment for the LPC1768 (arm-none-eabi-ld)
MEMORY
{
  FLASH   (rx)  : ORIGIN = 0x00000000, LENGTH = 512K
  RAM     (rwx) : ORIGIN = 0x10000000, LENGTH = 32K
  AHBRAM  (rwx) : ORIGIN = 0x2007C000, LENGTH = 32K
}

SECTIONS
{
  .text : { KEEP(*(.isr_vector)) *(.text*) *(.rodata*) } > FLASH
  .data : { *(.data*) } > RAM AT > FLASH
  .bss  : { *(.bss*) *(COMMON) } > RAM
  .dma_buffers (NOLOAD) : { *(.dma_buffers) } > AHBRAM
}

Accessing Addresses from C

Any register is just a volatile pointer to a fixed address. The program below reads the CPUID register at 0xE000ED00, which every Cortex-M core implements, and decodes which core it is running on. It then places a buffer in a specific RAM block using a section attribute that matches the linker script above.

cpuid.c: decode the CPUID register and place a buffer in AHB SRAM
#include <stdint.h>

#define SCB_CPUID   (*(volatile const uint32_t *)0xE000ED00u)

/* DMA buffer forced into AHB SRAM by the linker script */
__attribute__((section(".dma_buffers")))
static uint8_t dma_rx[1024];

const char *core_from_cpuid(uint32_t cpuid)
{
    uint32_t implementer = (cpuid >> 24) & 0xFF;   /* 0x41 = ARM */
    uint32_t partno      = (cpuid >> 4) & 0xFFF;

    if (implementer != 0x41)
        return "not an ARM-designed core";

    switch (partno) {
    case 0xC20: return "Cortex-M0";
    case 0xC60: return "Cortex-M0+";
    case 0xC23: return "Cortex-M3";
    case 0xC24: return "Cortex-M4";
    case 0xC27: return "Cortex-M7";
    default:    return "other Cortex-M";
    }
}

volatile const char *core_name;     /* inspect in the debugger */
volatile uint32_t   cpuid_raw;
volatile uint32_t   buf_addr;

int main(void)
{
    cpuid_raw = SCB_CPUID;
    core_name = core_from_cpuid(cpuid_raw);
    buf_addr  = (uint32_t)dma_rx;   /* 0x2007C000 or nearby */

    while (1)
        ;
}

On an LPC1768 the watch window shows Cortex-M3, and buf_addr falls inside the AHB SRAM range. The volatile keyword on the register pointer is essential: it forces the compiler to perform every access exactly as written instead of caching the value in a register or removing a write it thinks is unused.

Common Mistakes

  • Declaring the RAM region in the linker script larger than the physical memory; the stack then grows into nothing and the first function call faults.
  • Forgetting volatile on register pointers; optimised builds skip status polling or merge writes.
  • Putting DMA buffers in a RAM block the DMA controller cannot reach on that chip's bus matrix.
  • Assuming peripheral addresses are the same across vendors; only the core region at 0xE0000000 is common.
  • Accessing a peripheral before enabling its clock or power bit; on many parts the read returns zero or causes a bus fault.

Summary

Every Cortex-M chip shares one 4 GB map: code from 0x00000000, SRAM from 0x20000000, peripherals from 0x40000000, external memory above that, and core system registers at 0xE0000000. Cortex-M3 and M4 fetch instructions and data over separate buses, while the M0 family uses a single bus. Vendors fill the regions with their own memories and peripherals, which you describe in the linker script and access through volatile pointers.