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 range | Region | Typical contents | Executable |
|---|---|---|---|
| 0x00000000 to 0x1FFFFFFF | Code (512 MB) | On-chip flash, boot ROM, sometimes a small SRAM | Yes |
| 0x20000000 to 0x3FFFFFFF | SRAM (512 MB) | On-chip SRAM; first 1 MB bit-band region on M3/M4 | Yes |
| 0x40000000 to 0x5FFFFFFF | Peripheral (512 MB) | Vendor peripherals on AHB and APB buses | No |
| 0x60000000 to 0x9FFFFFFF | External RAM (1 GB) | External memory controllers, SDRAM | Yes |
| 0xA0000000 to 0xDFFFFFFF | External device (1 GB) | External peripherals | No |
| 0xE0000000 to 0xE00FFFFF | Private Peripheral Bus | NVIC, SysTick, SCB, debug units | No |
| 0xE0100000 to 0xFFFFFFFF | Vendor specific | Vendor-defined system areas | No |
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.
| Block | Start address | Size | Notes |
|---|---|---|---|
| On-chip flash | 0x00000000 | 512 KB | Vector table at the start |
| Local SRAM | 0x10000000 | 32 KB | Fastest RAM for the CPU, in the Code region |
| AHB SRAM bank 0 | 0x2007C000 | 16 KB | Good for DMA and Ethernet buffers |
| AHB SRAM bank 1 | 0x20080000 | 16 KB | Good for USB buffers |
| GPIO | 0x2009C000 | 16 KB | Fast GPIO on AHB |
| APB0 peripherals | 0x40000000 | 512 KB | Timers, UART0, UART1, I2C, ADC |
| APB1 peripherals | 0x40080000 | 512 KB | SSP, DAC, UART2, UART3, system control |
A linker script describes these physical memories so that the linker places code in flash and variables in RAM.
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.
#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.