5.5.1 · D4Embedded Systems & Real-Time Software

Exercises — Microcontroller architecture — ARM Cortex-M series (M0, M3, M4, M7)

3,772 words17 min readBack to topic

Before we start, let us re-earn the three formulas we will lean on, so no symbol appears unexplained.

The picture below shows exactly why the pipeline-fill formula is and not — read it before Level 2.

Figure — Microcontroller architecture — ARM Cortex-M series (M0, M3, M4, M7)

Level 1 — Recognition

Recall Solution 1.1

Straight from the family table:

  • M0+ stages, no FPU.
  • M3 stages, no FPU.
  • M4 stages, FPU optional (only on the "M4F" variant), and if present it is single-precision only.
  • M7 stages (dual-issue), FPU optional, and can be single + double precision.

Why "by default" matters: even on an M4F/M7 the FPU is off at reset — you must set the CP10/CP11 bits in CPACR first (see Exercise 5.2).

Recall Solution 1.2

The NVIC (Nested Vectored Interrupt Controller). On an interrupt it automatically:

  1. Pushes 8 registers (R0–R3, R12, LR, PC, xPSR) onto the current stack, and
  2. Looks up the handler address in the vector table and jumps straight to it.

Both happen in a fixed, known number of cycles — that fixed-ness (not zero, but knowable) is what makes timing deterministic. See Interrupts and the NVIC.


Level 2 — Application

Recall Solution 2.1

What/why: use the pipeline-fill formula because we count cycles for a straight run of instructions. The pipe costs 1 extra cycle to fill (only 1 stage beyond the first), then 50 instructions stream out one per cycle.

Recall Solution 2.2

Cycles: cycles. Time: . Since , that is . Why divide by 168: each cycle lasts , and we have 12 of them.

Recall Solution 2.3

The key insight: across iterations the pipe stays full, so you pay the fill cost once, not per loop. Not — that would wrongly re-fill the pipe every pass. The back-edge edge case, stated plainly: the loop's conditional branch is already inside the 10 instructions (it is counted in ), and because we assume perfect branch prediction it costs its normal one cycle with no flush. If, instead, the back-edge mispredicted even once per pass, you would add flush cycles per pass, giving cycles. So loop overhead is either "free-ish" (predicted) or a per-pass tax (mispredicted) — never silently ignored.

Recall Solution 2.4

Cycles cycles (round up — you can't run a partial cycle). Why divide: IPC is instructions per cycle, so cycles = instructions ÷ IPC.


Level 3 — Analysis

Figure — Microcontroller architecture — ARM Cortex-M series (M0, M3, M4, M7)
Recall Solution 3.1
  • (a) M3: cycles/sample.
  • (b) M4 MLA: cycles/sample.
  • (c) M4 SMLAD: two MACs per instruction cycles/sample.
  • Speedup M3 → SMLAD: — and this is per MHz, before the M4's higher clock helps further.

Why SIMD halves it: one 32-bit instruction operates on two packed 16-bit lanes at once (the blue and yellow lanes in the figure above). See Fixed-point vs floating-point DSP.

Recall Solution 3.2
  • M3 soft-float: cycles.
  • M4F hardware: cycles.
  • Ratio: faster.
  • double trap: the M4 FPU is single-precision only, so every double op silently falls back to software emulation — you're back near the 200-cycle regime. Use float literals (3.14f) and sinf, not sin.
Recall Solution 3.3

Since , that is . Why each term is bounded: detect is one sync stage, stack is a fixed 12-cycle push, and the blocking term is bounded because you know the longest higher-priority handler. Determinism = every term knowable, not zero.


Level 4 — Synthesis

Recall Solution 4.1

Cycles per sample: cycles. Deadline per sample: . Required clock: we need cycles to fit in , so So even a modest ~2 MHz clock technically meets it — meaning the FIR is not the bottleneck; interrupts and other tasks dominate. (Real designs still pick ~100 MHz for headroom against jitter and other tasks. See RTOS task scheduling and context switching.)

Recall Solution 4.2

Cycles/second in switching: cycles/s. Cycles/second available: . Fraction: . Tiny — which is why the auto-stacking NVIC + two-stack (MSP/PSP) design is so valued: switching overhead stays negligible. See RTOS task scheduling and context switching.

Recall Solution 4.3
  • FFT / image stream → cacheable main memory. It benefits from cache hits and its timing only needs to be good on average, not worst-case-guaranteed.
  • Servo loop code + data → TCM (tightly-coupled memory). TCM has single-cycle, fixed access with no cache misses, so its worst-case timing is bounded — exactly what a hard 10 µs deadline needs.

Why not cache the servo? Because a cache miss makes access time data-dependent: a hit is fast, but a miss stalls the core for many cycles while it fetches from slow main memory. That variability means you can no longer prove the servo finishes within 10 µs in the worst case — and hard-real-time is judged on the worst case, never the average. TCM removes the gamble entirely: every access is a fixed single cycle, so the deadline is provable. Rule of thumb: determinism > average speed for anything with a hard deadline. See Cache vs TCM in real-time systems.


Level 5 — Mastery

Recall Solution 5.1

Design A: single-issue ⇒ IPC , so cycles. Design B: , so cycles cycles. Speedup: . Notice the win splits into two independent factors: clock () and IPC (), and . Depth alone bought the clock; dual-issue bought the extra 1.35.

Recall Solution 5.2

Cause: the FPU is a coprocessor (CP10/CP11) that is disabled at reset. Your compiled code emitted real hardware FP instructions (because of -mfloat-abi=hard), but with the coprocessor disabled those instructions are undefined and trap as a HardFault. Fix: in SystemInit (startup) set the CP10/CP11 access bits in the CPACR register — commonly: SCB->CPACR |= (0xF << 20); (grants full access to CP10 and CP11), then a DSB/ISB barrier. No arithmetic here, but it's the single most common "my float crashes" mystery.

Recall Solution 5.3

Does the stacking get wasted? No. Cortex-M uses late-arrival: when a higher-priority IRQ appears during the stacking already in progress, the CPU reuses that same register push and simply vectors to the higher-priority handler — it does not push a second time. So we count the stacking once, not twice.

Walk the timeline in order:

  • detect the pending interrupt: cycles.
  • stack the 8 registers (shared, done exactly once thanks to late-arrival): cycles.
  • high-priority handler body runs to completion: cycles.
  • exception-return / unstack path before the servo body starts: cycles.

Since , that is .

The mastery point: the naive answer double-counts the push as cycles. Late-arrival (and its sibling tail-chaining) is exactly the hardware mechanism that removes that duplicate 12-cycle push, keeping worst-case latency bounded and knowable even under nested preemption. See Interrupts and the NVIC.


Recall Self-test checklist (reveal after finishing)

Pipeline fill paid once per run? ::: Yes — add a single time, not per loop (assuming no branch flushes). IPC formula direction? ::: Cycles = Instructions ÷ IPC (IPC is instructions per cycle). double on M4F? ::: Software emulation — slow. Single-precision float only in hardware. Cycles → time? ::: Divide cycles by clock in MHz to get µs; multiply µs by 1000 to read ns. Nested IRQ stacking counted how many times? ::: Once (tail-chain / late-arrival reuse), not per level. What breaks the ideal ? ::: Branch mispredictions, data/memory stalls, exceptions — each adds a re-fill/bubble penalty.