This page teaches the M-profile world of Course 2’s STM32L476 systematically, always in contrast with the A-profile machine you now know: the register file and instruction set, the fixed memory map, the exception model that makes an MCU an MCU, the FPU’s stacking behavior, how C and assembly meet in firmware, why every RTOS is built on the same two exceptions, and the DSP extensions that CMSIS-DSP turns into filter kernels. No board required anywhere — the arm-none-eabi cross-toolchain compiles and disassembles it all on the Mac.
1 · Two profiles, one architecture family
A-profile (your Mac, Pi 5, Jetson)
M-profile (STM32’s Cortex-M4)
ISA
A64 (+A32/T32 on some)
T32/Thumb-2 only — there is no ARM64 here
Architecture
ARMv8-A+
ARMv7E-M (the E = DSP extensions)
Registers
x0–x30, SP, PSTATE
R0–R12, SP (two of them), LR=R14, PC=R15, xPSR
Memory
Virtual, MMU, deep caches
Fixed physical map, MPU at most, flash + SRAM
Pipeline
Wide, out-of-order, predicted
3-stage, in-order
Interrupts
OS-mediated, exception levels
NVIC wired into the core — the defining feature
Privilege
EL0/EL1/…
Handler vs. Thread mode; privileged vs. unprivileged
Purpose
Throughput
Determinism — bounded, analyzable latency
The Cortex-M4 runs the T32 (Thumb-2) instruction set: a mix of 16- and 32-bit encodings, chosen for code density (flash is the budget). Consequences you’ll see in every disassembly: mixed instruction widths down the listing; UAL syntax nearly identical to A32; short IT (if-then) blocks instead of A32’s predicate-everything; and bit 0 of every code address is 1 (the historical Thumb interworking bit) — function pointers and vector-table entries are all odd. Registers are 32-bit R0–R15, with PC a real, visible register again (unlike A64).
The Exercise 6.1 experience, summarized: the same C dot product becomes ~2× fewer code bytes on T32 (density) but each instruction does less and there’s no vector unit to hide latency — the two ISAs are optimizing different objective functions.
2 · The programmer’s model: registers and modes
R0–R12 general purpose; R13 = SP — banked: MSP (main, used by handlers and boot) vs. PSP (process — what an RTOS gives tasks); R14 = LR; R15 = PC.
xPSR is three overlaid views: APSR (the NZCV+Q flags — Q is the sticky saturation flag, §9), IPSR (which exception is running — 0 in thread mode), EPSR (the Thumb bit, IT state).
Mask registers:PRIMASK (the one Module 8’s critical sections toggle — masks all configurable-priority interrupts), BASEPRI (mask below a priority threshold — how an RTOS masks some interrupts while leaving, say, the motor-control ISR live), FAULTMASK.
CONTROL selects MSP/PSP and privilege in thread mode — the two bits an RTOS flips when it launches a task.
Calling convention: AAPCS32 — arguments in R0–R3, result in R0, R4–R11 callee-saved, R12 scratch, 8-byte stack alignment at public interfaces. Exercise 6.2’s table against AAPCS64 writes itself from this line.
3 · The fixed memory map
Every Cortex-M device lays out the same 4 GB — this table is the address-decoding hardware, and it’s why bare-metal C can #define peripheral addresses portably across the whole family:
Region
Range
Contents (STM32L476 flavor)
Code
0x0000 0000 – 0x1FFF FFFF
Flash aliased at 0 (vector table lives here), 1 MB flash at 0x0800 0000
SRAM
0x2000 0000 – 0x3FFF FFFF
96 KB SRAM1 + 32 KB SRAM2
Peripheral
0x4000 0000 – 0x5FFF FFFF
APB/AHB peripherals — every GPIOA->… from Module 8
External
0x6000 0000 – 0x9FFF FFFF
FMC/QSPI-mapped memories
System / PPB
0xE000 0000 – 0xE00F FFFF
The core’s own registers: NVIC, SysTick, SCB, DWT (DWT->CYCCNT!), FPU control
Two M-profile specials: bit-banding — the SRAM and peripheral regions each have an alias region (0x2200 0000, 0x4200 0000) where one word aliases one bit, making single-bit writes atomic without RMW (neat; in practice BSRR-style registers and masks won, but you’ll meet it in older code); and the MPU — not an MMU: no translation, just ~8 regions of access permissions (null-trap, stack guards, RTOS task isolation). No page tables, no TLB, no swap: Module 2 §6’s contrast, now official.
4 · The exception model — the heart of the machine
This is the machinery that makes Course 2’s ADC/DMA/timer firmware possible, and it’s ruthlessly elegant.
The vector table is an array of addresses (not instructions) at address 0: entry 0 is the initial MSP value, entry 1 the reset handler, then NMI, HardFault, …, SysTick, then one entry per peripheral IRQ (82 on the L476). At reset the hardware loads SP from entry 0 and PC from entry 1 — that is boot. Exercise 6.4’s annotation of the Course 2 startup file starts exactly here (and Module 8 §6 explained the KEEP that protects it from the linker).
Automatic stacking. When an exception fires, the hardware pushes the caller-saved AAPCS32 set onto the current stack — no compiler-visible prologue needed:
← stacked frame (basic, 8 words) →
higher │ xPSR │ PC │ LR │ R12 │ R3 │ R2 │ R1 │ R0 │ ← SP after entry
…and simultaneously loads LR with EXC_RETURN — a magic value (0xFFFF FFF1/F9/FD/E1/E9/ED) encoding which stack and which frame type to unwind. The handler is then just a normal C function honoring AAPCS32; returning branches to EXC_RETURN, and the hardware pops the frame. This is why an ISR in STM32 firmware is a plain void ADC_IRQHandler(void) with no attributes — the hardware did the ABI’s caller-saved half, the compiler does the callee-saved half. Exercise 6.3 draws this frame by hand, with and without the FPU extension (§6).
Latency and its optimizations: entry is ~12 cycles, deterministic. Tail-chaining: back-to-back exceptions skip the pop/re-push — six-ish cycles between handlers. Late-arrival: a higher-priority exception arriving during stacking wins the vector fetch without restacking. These aren’t trivia — they’re why interrupt-driven DSP at audio rates is comfortable on a 80 MHz core.
Priorities and the NVIC: each interrupt has a programmable priority (STM32: 4 bits → 16 levels, lower number = more urgent, optionally split into preempt/sub groups). Higher priority preempts a running handler — nested interrupts are the default reality, which is exactly why Module 8 §7’s “ISR-shared state” discipline talked about which contexts can preempt which. NVIC_EnableIRQ, NVIC_SetPriority (CMSIS) are thin wrappers over PPB registers from §3.
Faults (Exercise 7.1 territory in Course 2, theory here): HardFault plus the configurable MemManage / BusFault / UsageFault, with cause registers — CFSR (what kind), BFAR/MMFAR (what address, when valid). A fault handler that dumps the stacked frame (the hardware already saved PC and xPSR for you!) plus CFSR turns “it crashed” into “invalid store to 0x0000 0004 from the instruction at PC=0x0800 1F22” — the frame-chain walking of Exercise 4.5, Cortex-M edition.
5 · SysTick, SVC, PendSV — why every RTOS looks the same
Three system exceptions exist specifically to build operating systems on:
SysTick — a 24-bit down-counter in the core (not a vendor peripheral): the OS timebase, firing the scheduler tick.
SVC — a synchronous exception raised by the svc instruction: the system-call gate (FreeRTOS uses it exactly once, to start the scheduler; other RTOSes use it as the full API trap).
PendSV — an exception with no hardware source at all: software sets a pend bit, and it fires when nothing more urgent is running. Set to the lowest priority, it becomes the perfect context-switch point — it can never preempt a real ISR, and any ISR that wakes a task just pends it and returns.
sequenceDiagram participant T1 as Task A (PSP) participant ST as SysTick / any ISR participant PSV as PendSV (lowest prio) participant T2 as Task B (PSP') T1->>ST: tick fires (hardware stacks R0–R3,R12,LR,PC,xPSR on PSP) ST->>ST: scheduler: B should run; pend PendSV ST-->>PSV: return → tail-chain into PendSV PSV->>PSV: push R4–R11 on PSP, save PSP into TCB(A) PSV->>PSV: load PSP' from TCB(B), pop R4–R11 PSV-->>T2: EXC_RETURN → hardware pops B's frame
sequenceDiagram
participant T1 as Task A (PSP)
participant ST as SysTick / any ISR
participant PSV as PendSV (lowest prio)
participant T2 as Task B (PSP')
T1->>ST: tick fires (hardware stacks R0–R3,R12,LR,PC,xPSR on PSP)
ST->>ST: scheduler: B should run; pend PendSV
ST-->>PSV: return → tail-chain into PendSV
PSV->>PSV: push R4–R11 on PSP, save PSP into TCB(A)
PSV->>PSV: load PSP' from TCB(B), pop R4–R11
PSV-->>T2: EXC_RETURN → hardware pops B's frame
Read that diagram twice and FreeRTOS’s port.c becomes documentation instead of magic: the hardware stacks half the context automatically (§4), PendSV’s handler stacks the other half (R4–R11), swaps PSP, and unstacks in reverse. Tasks live on their own PSP stacks; the kernel and ISRs use MSP. That’s the entire trick, on every Cortex-M RTOS ever shipped — and it’s why Course 2’s Module 7 FreeRTOS labs configure PendSV/SysTick priorities the way they do. (Mastering the FreeRTOS Real-Time Kernel covers the API side; its interrupt-management chapter pairs perfectly with this section.)
6 · The FPU and lazy stacking
The M4F’s FPU adds S0–S31 (single precision only — Module 7 §4.5’s -Wdouble-promotion warning is this fact wearing a compiler flag). It’s off at reset — the startup code enables it via CPACR (that once-mysterious line in Exercise 6.4’s annotation).
The exception-model interaction is the clever part: a floating-point-using task would need 17 more registers stacked per interrupt (S0–S15 + FPSCR: the 26-word extended frame). Lazy stacking (default): the hardware reserves the space but skips the copy, arming a trigger; only if the handler itself touches the FPU do the FP registers actually spill. ISRs that stay integer-only — most — pay nothing. This is why Course 2’s “keep ISRs short and integer” advice has a microarchitectural teeth behind it, and it’s the “with FPU” half of Exercise 6.3’s diagram.
7 · Firmware in C: what the compiler and startup code arrange
The full boot story, assembled from pieces you now own: vector table (§4) → reset handler → .data copy and .bss zero (Module 8 §6’s linker diagram) → FPU enable (§6) → clocks → main. The C runtime is those ~40 lines of startup assembly — Exercise 6.4 annotates every one against this page.
Mixed C/assembly conventions in firmware mirror Module 4’s rules with AAPCS32 numbers (§2): ISRs are plain C functions (§4 explains why); genuinely-assembly routines (a PendSV handler, a cycle-exact bit-bang) export AAPCS32-clean symbols; CMSIS intrinsics (__disable_irq, __DMB, __get_PRIMASK, __LDREXW/__STREXW) wrap the instructions C can’t spell — you met them all in Modules 7–8.
Cycle counting on-target: DWT->CYCCNT (§3’s PPB) is the M-profile mach_absolute_time — Course 2’s benchmarking backbone.
8 · A64 ↔︎ T32: the mental mapping table
The translation dictionary for reading both worlds fluently (Exercise 6.1/6.2 fill in the measured columns):
Concept
A64 (Mac)
T32 (STM32)
Args / result
x0–x7 / x0
R0–R3 / R0
Callee-saved
x19–x28, x29, x30
R4–R11, LR discipline
Frame push
stp x29, x30, [sp, #-16]!
push {r4-r7, lr} (16-bit encoding!)
Return
ret (via x30)
bx lr / pop {…, pc}
Zero register
xzr
none — movs r0, #0
Conditional without branch
csel
it eq + conditional instr
Wide constant
movz/movk
movw/movt
PC-relative data
adrp + @PAGEOFF
ldr r0, =label (literal pool)
Multiply-accumulate
madd
mla / smlal
Stack alignment
16 bytes
8 bytes
The literal pool row deserves a note: T32 keeps large constants in the instruction stream (after functions), loaded PC-relative — you’ll see .word islands in every firmware disassembly where A64 would have movk chains. Different densities, same problem (Module 2 §1: fixed-width encodings can’t hold constants).
9 · The DSP extensions and CMSIS-DSP
ARMv7E-M’s “E” gives the M4 single-cycle MAC and SIMD-within-a-register: treating one 32-bit register as 2×16-bit (or 4×8-bit) lanes —
SMLAD — dual 16×16 multiply, both products added to an accumulator, one cycle: two Q15 MAC taps at once.
SMLALD — same, into a 64-bit accumulator: the overflow-safe Q15 dot-product engine.
QADD/QSUB/QADD16 — saturating add/subtract (the Q flag in xPSR records that saturation happened — sticky, so you check once per block).
SMMUL — 32×32 returning the high 32: §1’s Q31 primitive.
This is Module 5 §3–4 shrunk to 32-bit registers: where NEON has v0.4h lanes and SQDMULH, the M4 packs two halfwords per GP register and calls SMLAD — same algebra, quarter the width, zero extra register file. CMSIS-DSP (arm_dot_prod_q15, arm_fir_q15, arm_biquad_…) is the vendor-maintained library that writes these instructions so you don’t: Exercise 6.5 reads its dot-product inner loop against your Module 5 NEON version, and the mapping is one-to-one by design. That library is literally the code Course 2’s filter labs call per sample.
10 · The capstone: One Algorithm, Two ARMs
Everything this course built converges in Exercise 6.6’s write-up. The comparison skeleton, as prompts: encodings (fixed 32 vs. mixed 16/32 — code bytes measured in 6.1) · register file (31×64 vs. 13×32 — spills counted) · pipeline (out-of-order + predicted vs. 3-stage in-order — Exercise 2.5/2.6’s effects present vs. absent) · memory (three cache levels vs. flash wait-states + SRAM determinism) · FP/SIMD (NEON lanes vs. packed-register SIMD; lazy stacking as the interrupt tax) · interrupt model (OS-mediated vs. NVIC-in-core, 12-cycle entry) · and the judgment paragraph: which DSP work belongs on which core and what evidence from this course says so — the question Course 2’s Module 8.5 benchmark then answers with a stopwatch.
Reference shelf: Yiu 3–8, 10.1–10.4, 12, 13, 20–22 remains the encyclopedic treatment behind every section here (the book copy in Apple Books); the STM32L476 reference manual for the device-specific columns; Mastering the FreeRTOS Real-Time Kernel (v1.1, the free PDF) for §5’s API-side continuation into Course 2 Module 7; Plantz 21 (exceptions/interrupts, A-profile flavored) as contrast reading.