Embedded Systems · Microcontrollers

How a Microcontroller Memory Map Assigns Meaning to Every Address

How fixed address regions are decoded on a microcontroller, why peripherals sit at constant addresses, and what the bus does with an address that nothing claims.

The core places an address on its bus and expects an answer. Nothing in those thirty-two bits says what kind of thing lives at the location; the meaning comes from a decoder that watches a subset of the address lines and selects exactly one slave for the cycle. On a microcontroller that decoder is fixed in silicon, and the partition it implements is as much part of the programming interface as the instruction set. Code that assumes a peripheral is at one address will run on a device with the same map and fault on a device with a different one.

The coarse partition the architecture fixes

ARMv7-M divides the 4 GiB space into regions with different access rules, and every implementation inherits them:

RangeRegionTypical contents
0x0000_0000 – 0x1FFF_FFFFCodeflash, or a boot ROM aliased over it
0x2000_0000 – 0x3FFF_FFFFSRAMdata, stack, heap
0x4000_0000 – 0x5FFF_FFFFPeripheralvendor peripheral registers
0x6000_0000 – 0x9FFF_FFFFExternal RAMmemory-mapped SDRAM or PSRAM
0xA000_0000 – 0xDFFF_FFFFExternal deviceoff-chip peripherals
0xE000_0000 – 0xE00F_FFFFPrivate peripheral busNVIC, SysTick, debug blocks
0xE010_0000 – 0xFFFF_FFFFVendorimplementation-specific system blocks

The classification matters because it carries attributes, not just addresses. Code and SRAM regions are normal memory: independent accesses need not be performed in program order, and adjacent writes may be merged into a wider transfer. Peripheral space is device memory: accesses to it are performed in program order relative to each other, must be sized and aligned as the instruction specifies, and cannot be repeated or merged. That difference is why a write to a peripheral register is never combined with a neighbouring write by the bus interface, while two writes to adjacent SRAM words may be.

The private peripheral bus is 1 MiB, and its position is not negotiable. The interrupt controller’s registers, the system timer, and the debug components sit there because the core itself is the master of that bus; a vendor cannot move them without changing the core.

Two further regions exist on some implementations and are not part of the architectural partition. A vendor may expose an alias window in which a word corresponds to a single bit of an underlying region, which is the mechanism described in the bit-banding article; the alias consumes address space proportional to thirty-two times the region it shadows. Separately, the optional protection unit described in the MPU article does not add addresses but attaches permissions to ranges of the ones that exist.

What the decoder actually looks at

A slave is selected by a comparator on the upper address bits. A peripheral occupying 1 KiB needs ten bits of offset inside itself and therefore compares only the twenty-two bits above that. The comparison is exact, so a 1 KiB block answers only within its own window; the interesting failures come from decoders that compare fewer bits than the address width. A bridge that decodes only the low sixteen bits of the address makes its peripheral appear at every 64 KiB multiple of its base, and a write through any of those aliases reaches the same register. Parts that do this usually document the mirroring in the memory map chapter, because a stray pointer that lands on a mirror is indistinguishable from one that lands on the real thing.

Mirroring has two practical effects. First, an out-of-range offset inside a peripheral’s allocated window does not necessarily fault; it may land in a neighbouring register, because the decoder never distinguished the two. Second, a pointer arithmetic bug that adds a large multiple of the block size can silently reach a valid register instead of an invalid address, which is a much harder failure to find than a bus fault.

Inside a peripheral, the register file is usually sparser than the address window. Reserved offsets read as zero and ignore writes. This is deliberate: a mis-addressed write is absorbed rather than faulting, because faulting on every unimplemented offset would make the device brittle against future register additions and would turn a harmless stray store into a crash. The cost is that a wrong offset frequently produces no error at all, only a peripheral that never starts.

The vector table as the map’s first customer

The map’s most consequential client is the reset sequence. On ARMv7-M the core reads the first two words of the code region: the initial main stack pointer from offset 0x0, and the reset handler address from offset 0x4. It loads the stack pointer before executing a single instruction, which is why the first word of a firmware image is a data value rather than code.

Everything after that is the exception vector table, one word per exception, sixteen system entries followed by the vendor’s interrupts. The table’s position is a boot-time decision. Many devices implement a remap control that aliases the bootloader’s region, or a small SRAM, over address zero so that a firmware update can run from a different physical memory without moving the table. Later, the table can be relocated anywhere in the code region by writing the vector table offset register, which is the mechanism a bootloader uses to hand control to an application image at a non-zero base.

The table is read by hardware, not by code, so its alignment requirement is architectural: the base must sit on a natural boundary at least as large as the table, with a floor of 128 bytes. A part with 128 exception entries needs a 512-byte-aligned base, and a part with 200 needs the next power of two above 800 bytes. A relocated table placed at a non-conforming address faults on the first interrupt, and the fault appears at the vector fetch rather than at anything the code did wrong.

Unmapped addresses and the two kinds of bus fault

An access to an address no slave claims produces an error response from the bus, and the core turns that into a BusFault. The distinction that matters is precision.

A precise bus fault is reported on the instruction that caused it, with the stacked return address pointing at that instruction, so a handler can identify the offending store and the register that held the address. A store to a peripheral can be buffered, though, and the error may only come back after the store has retired and several more instructions have executed. That is an imprecise fault: the stacked PC points somewhere after the culprit, and the address is not recoverable from the fault status registers. Imprecise faults are typically configured to escalate to a hard fault because there is nothing useful to do with them beyond resetting.

Reads are easier. A load cannot retire until its data returns, so a read from an unmapped address is nearly always precise, and the fault address register holds the exact location. This asymmetry is a practical reason to probe a peripheral with a read before writing to it, and a reason why a “write and hope” initialisation sequence can fail in a way that leaves no diagnostic.

Device memory also rejects unaligned access. A halfword store to an odd address inside the peripheral region raises a fault rather than being split into two byte accesses the way it would in SRAM. Register definitions are therefore laid out so that every member is naturally aligned, and a packed struct used as a register overlay is a defect waiting for the first unaligned member.

Why nothing is relocated

A general-purpose system places peripherals at addresses chosen by an enumerating bus, and drivers learn those addresses at run time. A microcontroller does the opposite: the addresses are constants in a header file, and the compiler folds them into instructions as immediates or literal-pool loads.

The reason is that there is no translation layer to manage. Without an MMU there is no virtual address, no page table, and no TLB to miss; a load’s address goes to the bus unchanged. The savings are real and specific: no page-table walks, no TLB maintenance on context switch, and a memory access latency that is a fixed function of the slave’s own timing rather than of a translation cache’s state. The cost is that two firmware images built for different parts of the same family may differ only in their address constants, and a binary cannot be moved between them.

The linker script is where the software half of the map is declared. It restates the regions the hardware decoder implements, because the linker must place code and initialised data at addresses the bus will actually answer:

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x00000000, LENGTH = 512K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
  .text : { *(.text*) *(.rodata*) } > FLASH
  .data : { *(.data*) } > RAM AT > FLASH
  .bss  : { *(.bss*) *(COMMON) } > RAM
}

The flash origin here is the architectural one; many vendors offset it — a common choice places program flash at 0x0800_0000 and keeps a system area at zero — which is why the value must come from the part’s own map rather than from a generic template.

The .data line encodes a second fact about the map: initialised variables have two addresses. Their load address is in flash, where the linker stores the initial values, and their run address is in SRAM. Startup code copies between them before main runs, and .bss is zeroed rather than copied because the C standard requires static storage to start at zero and storing a region of zeros in flash would waste the space. A linker script whose LENGTH exceeds the physical memory produces a binary that links cleanly and faults on the first write past the end of the SRAM the decoder actually implements, which is why the lengths must come from the device’s data sheet rather than from a template.

The map is the contract between three parties that never meet: the silicon vendor’s decoder, the linker script, and the register definitions in the header. When they disagree, the failure appears as a peripheral that does not respond, a fault at an address that looks correct in the source, or a variable that changes value for no visible reason. Checking the three against each other is the first step in bringing up any unfamiliar part, and the rest of the peripheral behaviour described in the clock-gating article only becomes observable once the map itself is right.

References

  1. ARMv7-M Architecture Reference Manual (ARM DDI 0403) — Arm Limited
  2. Cortex-M3 Technical Reference Manual (ARM DDI 0337) — Arm Limited
  3. The Definitive Guide to Arm Cortex-M3 and Cortex-M4 Processors — Joseph Yiu, Newnes
  4. Computer Organization and Design: The Hardware/Software Interface — Patterson & Hennessy, Morgan Kaufmann