Inside the MCU: Architecture, Memory, and the Execution Model
Inside the MCU: Architecture, Memory, and the Execution Model
What actually happens in the first microseconds after power stabilizes, before your first line of C runs? If your mental model of the silicon ends at "the CPU fetches instructions and writes to RAM," you will eventually spend days chasing bugs that are one-paragraph explanations of architecture. This chapter opens the hardware layer the rest of the book rests on. By the end of it, you will be able to read a memory map, predict where the linker will place a given symbol, explain why an apparently valid access faults at runtime, and reason precisely about how a Cortex-M core turns your compiled machine code into electrical activity on the bus matrix.
We will travel bottom-up: from power-on reset to the vector table, then into the register file and privilege model, then into the 4 GB memory map and its aliases, then into the execution pipeline and its interaction with flash wait states, and finally into the linker map file and a debugging case study that ties everything together. Each step builds the next. If you are already comfortable with Cortex-M basics, skim the early sections and pay attention to the map-file walkthrough and the case study — those are the parts engineers return to most.
1.1 The First Microsecond: From Reset Vector to Your First C Instruction
Press the reset button (or watch the brown-out detector release), and three things happen almost simultaneously. The clock system starts ticking once the oscillator stabilizes. The core samples its boot configuration pins. And a hidden piece of hardware — built into every Cortex-M core — pulls the initial stack pointer and the reset vector from a fixed location in memory.
That fixed location is the vector table. On every Cortex-M part, the first four bytes at the beginning of the code region contain the initial main stack pointer (MSP), and the next four bytes contain the address of the reset handler. There is no instruction at address zero. The core reads those eight bytes as data, loads MSP from the first word, loads the program counter (PC) from the second, and begins executing your reset handler. From this point on, every cycle belongs to your code.
A typical startup file looks like this:
```c /* Stack size and heap size defined in the linker script */ extern uint32_t _estack; extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss;
void Reset_Handler(void) { /* Copy initialized data from flash to SRAM */ uint32_t *src = &_sidata; uint32_t *dst = &_sdata; while (dst < &_edata) { *dst++ = *src++; } /* Zero the BSS section */ dst = &_sbss; while (dst < &_ebss) { *dst++ = 0; } /* Run C++ static constructors (in C++ only) */ /* __libc_init_array(); */ /* Hand control to main */ main(); /* If main returns, halt cleanly */ while (1) { __WFI(); } } ```
What looks like ordinary C is actually doing a great deal of architectural work. The references to `_sidata`, `_sdata`, `_edata`, `_sbss`, and `_ebss` are not variables — they are symbols exported by the linker, and they are the exact addresses where initialized data lives in flash, where it must be copied to in SRAM, and where zero-initialized data begins and ends. The fact that this routine runs from reset means that whatever the linker placed in the vector table at address `0x00000000` is now in charge of your silicon. A mistake here is a bricked board, not a crashed application.
The vector table itself is an array of 32-bit words, one entry per exception and interrupt source. Its layout is fixed by ARM and is one of the few absolute contracts you have with the silicon:
| Offset | Contents | |---|---| | `0x00` | Initial Main Stack Pointer (MSP) | | `0x04` | Reset vector (address of `Reset_Handler`) | | `0x08` | NMI vector | | `0x0C` | HardFault vector | | `...` | (Other system exceptions) | | `0x3C` | SVCall vector | | `...` | (More system exceptions) | | `0x40 + 4·N` | First external interrupt (IRQ0) | | `...` | (One entry per IRQ) |
The table does not contain instructions — it contains the addresses where execution should jump when each event fires. This is why the vector table is often described as a "jump table for exceptions." We will return to this idea in Chapter 2, where the NVIC actually uses it; for now, the important point is that this table must live at address `0x00000000` of the code region by default, or at the address pointed to by the Vector Table Offset Register (VTOR) if you have remapped it. If the linker places it anywhere else, the core cannot find the reset handler and your board is dead.
A common debugging habit worth forming early: when a board refuses to start, the first thing to check is the first 16 bytes of flash. If the initial SP looks suspicious (zero, or some obviously wrong value like `0xDEADBEEF`), the linker script or startup file is wrong. If the initial SP looks plausible but the reset vector points into the middle of an unrelated function, the linker emitted a table for a different entry point. These are not the kinds of bugs a debugger helps with; you need to look at the bytes on the wire.
1.2 Cortex-M Core Architecture: Registers, Modes, and Privilege
Once the reset handler is executing, the core is in Handler mode at privileged level, using MSP as its stack pointer. To write serious firmware you need to internalize what those four nouns mean and how they interact.
The Register File
Cortex-M cores expose a uniform register file to the programmer:
- **R0–R12**: General-purpose 32-bit registers. R0–R7 are the "low registers," accessible to every Thumb instruction including the 16-bit ones; R8–R12 are "high registers," reachable only by 32-bit instructions. Compilers do most of their work in R0–R7.
- **R13 (SP)**: The stack pointer. There are two physical SPs, MSP and PSP. Only one is visible at a time, selected by the CONTROL register. MSP is used in Handler mode and after reset; PSP is used by user threads in Thread mode when the OS asks for it.
- **R14 (LR)**: The link register, where the return address is stored on subroutine call.
- **R15 (PC)**: The program counter. Reading PC returns the current address plus 4 on most parts (the offset accounts for the pipeline).
- **xPSR**: A combined program-status register that internally breaks into three bytes: APSR (flags from arithmetic), IPSR (which exception number is running), and EPSR (execution state, including the Thumb bit and ICI/IT state). Software usually reads them via MRS into a general register.
On top of these, three special-purpose registers control exception masking:
- **PRIMASK**: A 1-bit interrupt mask. Setting it to 1 blocks all exceptions except NMI and HardFault. The `cpsid i` instruction sets it.
- **BASEPRI**: A multi-bit priority mask. Setting it to a nonzero value blocks all exceptions whose priority is numerically equal to or lower (less urgent) than that value. This is the cleanest way to disable a specific subset of interrupts.
- **FAULTMASK**: A 1-bit mask that, when set, elevates priority to HardFault level. It is rarely used and considered dangerous outside of OS context switches.
Operating Modes and Privilege Levels
Cortex-M parts define two operating modes:
- **Thread mode**: The mode in which normal application code runs. It can be either privileged or unprivileged.
- **Handler mode**: The mode entered automatically when an exception is taken. It is always privileged.
Two privilege levels apply in Thread mode: privileged, which has full access to memory and special registers, and unprivileged, which cannot access most System Control Space registers and may be denied access to memory regions by the MPU. Switching from privileged to unprivileged is done by writing to the CONTROL register; switching back requires an exception to be taken (because unprivileged code cannot write CONTROL[0]).
A practical consequence: if your bootloader or RTOS want