Part 2 · C Meets the Metal35 min read
The Cortex-M Architecture
Every STM32F407 peripheral is ST’s design, but the processor core that runs your code is Arm’s, and it behaves the same on every Cortex-M4 from every vendor. That is good news: what you learn here transfers to nRF52, Kinetis, SAM, and hundreds of other parts. This lesson covers the core itself: its registers and special registers, its execution modes, exactly what the hardware does when an interrupt arrives and when the handler returns, how the NVIC decides which interrupt wins, when memory barriers are required, and how the FPU fits in. These mechanisms explain every later topic, from interrupt latency to RTOS context switches to fault analysis, so expect to come back to this page.
Core registers
Section titled “Core registers”You have already met the general-purpose registers in Reading what the compiler wrote. Here is the complete set, including the registers that only mrs and msr can reach:
| Register | Width | Purpose |
|---|---|---|
r0-r12 | 32 | General purpose (r0-r3, r12 are scratch across calls) |
sp (r13) | 32 | Stack pointer: one of two physical registers, MSP or PSP |
lr (r14) | 32 | Return address, or an EXC_RETURN value inside a handler |
pc (r15) | 32 | Program counter |
xPSR | 32 | Program status: APSR, IPSR, and EPSR combined |
PRIMASK | 1 | When 1, masks every exception with configurable priority |
FAULTMASK | 1 | When 1, masks everything except NMI |
BASEPRI | 8 | Masks exceptions with priority value greater than or equal to it (0 = off) |
CONTROL | 3 | Stack selection, privilege, FPU context active |
xPSR: three registers in one
Section titled “xPSR: three registers in one”xPSR is the combined view of three status registers that occupy different bits of the same word:
31 30 29 28 27 26 25 24 23 20 19 16 15 10 9 8 0+--+--+--+--+--+-----+--+--------+------+--------+--+-----------+|N |Z |C |V |Q |IT |T | |GE |IT | |ISR number |+--+--+--+--+--+-----+--+--------+------+--------+--+-----------+ \____ APSR ___/ EPSR APSR EPSR IPSR- APSR (application): the condition flags
N,Z,C,V, the saturation flagQ, and on the M4 theGEflags used by SIMD instructions. - IPSR (interrupt): the number of the exception currently being handled, 0 in Thread mode. You used it in Startup code and the vector table to identify an unexpected interrupt.
- EPSR (execution): the Thumb bit
T, always 1 on a running Cortex-M, and the IT/ICI bits that record the state of an IT block or an interrupted multi-register load. You cannot read EPSR directly; it reads as zero throughmrs.
The mask registers
Section titled “The mask registers”PRIMASK, FAULTMASK, and BASEPRI raise the execution priority of the core so that some exceptions cannot pre-empt it:
PRIMASK = 1blocks every exception whose priority is configurable, which is everything except Reset, NMI, and HardFault.cpsid isets it,cpsie iclears it. This is the classic “disable interrupts”.FAULTMASK = 1additionally blocks HardFault, leaving only NMI. The core clears it automatically on return from any exception except NMI. It is used in fault handlers and rarely elsewhere.BASEPRI = nblocks only exceptions whose priority value isnor greater, that is, equally or less urgent. Writing 0 disables it. It lets you build critical sections that keep high-priority interrupts running, which is how FreeRTOS implements its critical sections. Like the NVIC priority registers, only the top 4 bits are implemented on the STM32F4, so priority level 5 is written as0x50.
#include <stdint.h>#include "stm32f407_regs.h"
/* Block interrupts with priority 5..15; priorities 0..4 still run. */static inline void basepri_set(uint32_t prio){ uint32_t value = prio << (8u - NVIC_PRIO_BITS); __asm volatile ("msr basepri, %0" : : "r" (value) : "memory");}Critical sections, and which mask to use when, are covered in Interrupts done right.
CONTROL
Section titled “CONTROL”| Bit | Name | Meaning |
|---|---|---|
| 0 | nPRIV | Thread mode privilege: 0 privileged, 1 unprivileged |
| 1 | SPSEL | Thread mode stack: 0 uses MSP, 1 uses PSP |
| 2 | FPCA | Set by hardware when the current context has used the FPU |
All three bits are 0 at reset. Handler mode ignores nPRIV and SPSEL: handlers always run privileged on MSP.
Thumb-only, and what Thumb-2 means here
Section titled “Thumb-only, and what Thumb-2 means here”Classic Arm cores switch between the 32-bit Arm and 16-bit Thumb instruction sets. Cortex-M cores implement only Thumb, which is why every code address has bit 0 set and why the T bit must always be 1. The M3, M4, M7, and M33 implement Thumb-2, the mixed 16/32-bit encoding that covers nearly everything a 32-bit instruction set can do. The M0, M0+, and M23 implement a much smaller subset, mostly 16-bit encodings, which is why the same C code is larger and slower on them.
Modes, privilege, and the two stacks
Section titled “Modes, privilege, and the two stacks”The core is always in one of two modes:
- Thread mode: normal code, including
main. Entered at reset. - Handler mode: exception handlers. Entered automatically on exception entry and left on exception return.
Independently, code runs privileged or unprivileged. Handler mode is always privileged. Thread mode is privileged unless CONTROL.nPRIV is set. Unprivileged code cannot change mask registers, cannot write most system registers (NVIC, SCB, SysTick), and is subject to MPU restrictions. Once Thread mode drops privilege, it cannot raise it again by itself; it must ask a handler, usually through the svc instruction, which is how an RTOS implements system calls.
There are two physical stack pointers. MSP (main stack pointer) is loaded from the vector table at reset and is always used in Handler mode. PSP (process stack pointer) is used in Thread mode when CONTROL.SPSEL is 1. Bare-metal firmware typically runs everything on MSP. An RTOS gives each task its own stack and points PSP at the current task’s stack, while all interrupt handlers share MSP. That split means a task’s stack only needs room for that task plus one exception frame, not for every possible nesting of handlers.
Switching Thread mode to PSP looks like this:
#include <stdint.h>
__attribute__((noreturn))void start_on_psp(void (*entry)(void), uint32_t *stack_top){ __asm volatile ( "msr psp, %0 \n" /* PSP = top of the new stack */ "movs r0, #2 \n" "msr control, r0 \n" /* SPSEL = 1: Thread mode now uses PSP */ "isb \n" /* later instructions see the new SP */ "bx %1 \n" /* jump to entry, never come back */ : : "r" (stack_top), "r" (entry) : "r0", "memory"); __builtin_unreachable();}
static uint32_t app_stack[512] __attribute__((aligned(8)));
static void app_main(void){ for (;;) { }}
int main(void){ start_on_psp(app_main, &app_stack[512]);}The isb after writing CONTROL is required: it guarantees that every instruction after it uses the new stack pointer. The function never returns because its caller’s stack frame is on MSP, and after the switch every push and pop in Thread mode uses PSP; returning would pop garbage. That is also why real RTOSes start their first task from a carefully written assembly sequence, which RTOS fundamentals: tasks and the context switch builds.
The exception model
Section titled “The exception model”An exception is anything that makes the core stop what it is doing and run a handler: a device interrupt, SysTick, a fault, an svc instruction. All of them go through the same mechanism.
Numbers and priorities
Section titled “Numbers and priorities”Each exception has a number (its index in the vector table, from Startup code and the vector table) and a priority. Lower priority values are more urgent. Reset (-3), NMI (-2), and HardFault (-1) have fixed negative priorities that nothing else can match. All others are configurable from 0 to 255 in principle; the STM32F4 implements 4 bits, so there are 16 levels, 0 to 15, stored in the top nibble of each priority byte.
An exception pre-empts the running code if its priority is more urgent than the current execution priority. The current execution priority is the priority of the active handler (or “lowest possible” in Thread mode), raised further by PRIMASK, FAULTMASK, or BASEPRI if they are set. An exception that cannot pre-empt stays pending until it can.
Exception entry
Section titled “Exception entry”When an exception is accepted, the hardware does the following with no software involvement:
- Stacking. It pushes eight registers onto the current stack (
MSPorPSP, whichever was in use):r0,r1,r2,r3,r12,lr, the return address, andxPSR. - Vector fetch. In parallel, it reads the handler address from the vector table at
VTOR+ 4 × exception number. - State update. It switches to Handler mode and
MSP, writes the exception number into IPSR, and loadslrwith a specialEXC_RETURNvalue. - It starts executing the handler.
On a Cortex-M4 with zero-wait-state memory this takes 12 cycles from the interrupt being signalled to the first handler instruction. Flash wait states and bus contention add to it.
The stack frame after entry, without FPU context:
higher addresses +--------------------+ | (alignment pad) | only if SP was not 8-byte aligned +--------------------+ SP + 0x1C | xPSR | bit 9 = 1 if the pad word was added SP + 0x18 | return address | where execution resumes SP + 0x14 | LR | the interrupted code's LR SP + 0x10 | R12 | SP + 0x0C | R3 | SP + 0x08 | R2 | SP + 0x04 | R1 | SP + 0x00 | R0 | <- SP inside the handler +--------------------+ lower addressesThese are exactly the registers the calling convention allows a function to clobber. The handler is compiled as an ordinary C function, so it saves r4-r11 itself if it uses them, just like any function. That is the design decision that lets Cortex-M handlers be plain C functions with no special attribute: the hardware does the caller’s half of the calling convention, the compiler does the callee’s half.
Two details of the frame matter later. The stacked return address is the address of the instruction that will run when the handler returns; for a precise fault, it is the address of the faulting instruction, which is how Watchdogs, faults, and recovering from them finds the line that crashed. And the frame is always 8-byte aligned (CCR.STKALIGN is set by default on the M4): if sp was only 4-byte aligned, the hardware inserts a padding word first and records it in bit 9 of the stacked xPSR, so exception return can remove it.
EXC_RETURN
Section titled “EXC_RETURN”Inside a handler, lr does not hold a return address. It holds a value of the form 0xFFFFFFxx that cannot be a valid code address, so when the handler executes bx lr (or pop {..., pc}), the core recognises it and performs an exception return instead of a branch. The low bits describe the frame to restore:
EXC_RETURN | Return to | Stack | Frame |
|---|---|---|---|
0xFFFFFFF1 | Handler mode (a nested exception) | MSP | Basic, 8 words |
0xFFFFFFF9 | Thread mode | MSP | Basic |
0xFFFFFFFD | Thread mode | PSP | Basic |
0xFFFFFFE1 | Handler mode | MSP | Extended, with FPU registers |
0xFFFFFFE9 | Thread mode | MSP | Extended |
0xFFFFFFED | Thread mode | PSP | Extended |
Bit 2 selects the stack (1 = PSP), bit 3 the mode (1 = Thread), bit 4 the frame type (0 = extended FPU frame). A fault handler reads bit 2 to find out which stack the faulting code’s frame is on; an RTOS context switch reads bit 4 to know whether it must also save the upper FPU registers. In the blinky project, a breakpoint in SysTick_Handler shows lr = 0xfffffff9: Thread mode, main stack, no FPU context.
Exception return
Section titled “Exception return”On exception return the core pops the frame from the stack EXC_RETURN names, restores xPSR (and with it the mode and IPSR), and resumes at the stacked return address. If the handler corrupted the stacked values, for example through a local buffer overflow that ran into the frame, the return goes somewhere random. Because return is driven by the frame, a handler can also change where execution resumes by editing the stacked pc, a trick fault handlers and RTOSes use deliberately.
Tail chaining, late arrival, and nesting
Section titled “Tail chaining, late arrival, and nesting”The core avoids work it does not need:
Tail chaining: IRQ B becomes pending while IRQ A's handler runs (B cannot pre-empt A)
Thread |stack|----- A handler -----| 6 |----- B handler -----|unstack| Thread ^ no unstack/restack between A and B
Late arrival: IRQ H (urgent) arrives while IRQ L's frame is being stacked
Thread |stack...|-- H handler --| 6 |-- L handler --|unstack| Thread ^ L triggered stacking; the vector fetch switches to H- Tail chaining. When a handler returns and another exception is pending that can run, the core skips the unstack/re-stack pair and goes straight to the next handler, in about 6 cycles instead of 24. The frame on the stack is the same in both cases, so there is nothing to redo.
- Late arrival. If a more urgent exception arrives while the core is still stacking for a less urgent one, it uses the same frame but fetches the urgent vector instead. The less urgent handler then runs by tail chaining afterwards.
- Pop pre-emption. If an exception arrives while the core is unstacking, it abandons the unstacking and tail-chains into the new handler.
- Nesting. If a more urgent exception arrives while a handler is running, it pre-empts the handler: a new frame is pushed on
MSPandlrin the nested handler is0xFFFFFFF1. Each nesting level costs at least 32 bytes of main stack (104 with FPU context), which is why stack budgets must account for the deepest possible nesting.
The NVIC
Section titled “The NVIC”The Nested Vectored Interrupt Controller is the part of the core that manages device interrupts: it tracks which are enabled, pending, and active, holds their priorities, and feeds the most urgent one to the core. Its registers are at the same addresses on every Cortex-M4:
| Register | Address | One bit (or byte) per IRQ | Write semantics |
|---|---|---|---|
ISER0-ISER7 | 0xE000E100 | Enable | Write 1 to enable, 0 has no effect |
ICER0-ICER7 | 0xE000E180 | Enable | Write 1 to disable |
ISPR0-ISPR7 | 0xE000E200 | Pending | Write 1 to set pending |
ICPR0-ICPR7 | 0xE000E280 | Pending | Write 1 to clear pending |
IABR0-IABR7 | 0xE000E300 | Active | Read-only |
IPR0-IPR59 | 0xE000E400 | Priority byte | Read/write, byte accessible |
STIR | 0xE000EF00 | Write an IRQ number to set it pending |
IRQ n uses bit n % 32 of word n / 32 in the bit registers, so the STM32F407’s 82 IRQs use words 0 to 2. The set/clear register pairs mean you never need a read-modify-write: NVIC->ISER[1] = 1u << 6; enables IRQ 38 (USART2) without any risk of disturbing the others. The nvic_enable_irq() helper in stm32f407_regs.h is exactly that.
A device interrupt reaches your handler only if all of these hold: the peripheral has its own interrupt enable bit set and its event flag raised, the IRQ is enabled in the NVIC, its priority beats the current execution priority, and PRIMASK is clear. Pending state is separate from the peripheral flag: an IRQ stays pending in the NVIC until its handler starts, and a peripheral flag that is still set when the handler returns makes the IRQ pending again immediately.
Priorities and the 4 implemented bits
Section titled “Priorities and the 4 implemented bits”Each IRQ has an 8-bit priority field, but the STM32F4 implements only bits 7:4. The unimplemented low bits read as zero and ignore writes:
NVIC->IPR[28] = 5u; /* bug: reads back as 0x00, the MOST urgent level */NVIC->IPR[28] = 5u << 4; /* correct: priority level 5, byte value 0x50 */The first line is a very common bug when porting code from a part with a different number of priority bits. nvic_set_priority() in the course header shifts for you, and CMSIS’s NVIC_SetPriority() does the same using __NVIC_PRIO_BITS.
Priority grouping: AIRCR.PRIGROUP
Section titled “Priority grouping: AIRCR.PRIGROUP”The priority byte can be split into a group priority (also called pre-emption priority), which decides whether one exception can interrupt another, and a subpriority, which only breaks ties between pending exceptions with the same group priority. The split point is the 3-bit PRIGROUP field in SCB->AIRCR (bits 10:8). With 4 implemented bits:
PRIGROUP | Group bits | Subpriority bits | Pre-emption levels | Sub-levels |
|---|---|---|---|---|
| 0 to 3 | 4 (bits 7:4) | 0 | 16 | 1 |
| 4 | 3 (bits 7:5) | 1 (bit 4) | 8 | 2 |
| 5 | 2 (bits 7:6) | 2 | 4 | 4 |
| 6 | 1 (bit 7) | 3 | 2 | 8 |
| 7 | 0 | 4 | 1 | 16 |
The reset value 0 already gives 16 pre-emption levels and no subpriority on the STM32F4, which is what most projects want. (ST’s HAL calls the equivalent setting NVIC_PRIORITYGROUP_4, which writes PRIGROUP = 3.) If two pending exceptions have equal group priority and equal subpriority, the lower exception number wins. Writes to AIRCR are ignored unless the top halfword is the key 0x05FA:
SCB->AIRCR = SCB_AIRCR_VECTKEY | (3u << SCB_AIRCR_PRIGROUP_Pos);System exception priorities
Section titled “System exception priorities”The configurable system exceptions take their priorities from the System Handler Priority Registers, one byte each, with the same 4 implemented bits:
| Register | Byte 0 (bits 7:0) | Byte 1 | Byte 2 | Byte 3 (bits 31:24) |
|---|---|---|---|---|
SHPR1 (0xE000ED18) | MemManage | BusFault | UsageFault | |
SHPR2 (0xE000ED1C) | SVCall | |||
SHPR3 (0xE000ED20) | Debug Monitor | PendSV | SysTick |
Giving SysTick the least urgent level, for example:
SCB->SHPR3 = (SCB->SHPR3 & ~(0xFFu << 24)) | ((15u << 4) << 24);MemManage, BusFault, and UsageFault are disabled at reset, and their faults escalate to HardFault. Setting the enable bits in SCB->SHCSR (SCB_SHCSR_MEMFAULTENA, BUSFAULTENA, USGFAULTENA) gives each its own handler, which makes faults much easier to diagnose.
Seeing pre-emption happen
Section titled “Seeing pre-emption happen”You can watch the priority rules without any peripheral by borrowing two unused IRQs and triggering them from software through STIR. TIM6 and TIM7 are not used in this project, so their vectors (IRQ 54 and 55) are free:
#include <stdint.h>#include "stm32f407_regs.h"
#define HIGH_IRQn 54u /* TIM6_DAC vector, borrowed */#define LOW_IRQn 55u /* TIM7 vector, borrowed */
void TIM6_DAC_IRQHandler(void);void TIM7_IRQHandler(void);
static volatile uint32_t g_trace[4];static volatile uint32_t g_trace_len;
static void trace(uint32_t id){ if (g_trace_len < 4u) { g_trace[g_trace_len] = id; g_trace_len = g_trace_len + 1u; }}
void TIM6_DAC_IRQHandler(void) /* priority 4: more urgent */{ trace(2u);}
void TIM7_IRQHandler(void) /* priority 12: less urgent */{ trace(1u); NVIC->STIR = HIGH_IRQn; /* pend the urgent one */ __asm volatile ("dsb\n\tisb" : : : "memory"); trace(3u);}
int main(void){ nvic_set_priority(HIGH_IRQn, 4u); nvic_set_priority(LOW_IRQn, 12u); nvic_enable_irq(HIGH_IRQn); nvic_enable_irq(LOW_IRQn);
NVIC->STIR = LOW_IRQn; /* start the experiment */ __asm volatile ("dsb\n\tisb" : : : "memory");
for (;;) { }}Run it and inspect g_trace in gdb: it holds 1, 2, 3. The low-priority handler records 1, pends the high-priority IRQ, and is pre-empted on the spot; the urgent handler records 2 and returns; the low-priority handler resumes and records 3. The dsb and isb after the STIR write make sure the pend has taken effect and the pipeline has been refilled before trace(3u) starts, so the pre-emption happens exactly at that point. Swap the two priorities and the trace becomes 1, 3, 2: the second IRQ cannot pre-empt, stays pending, and runs by tail chaining when the first returns.
SysTick and the SCB
Section titled “SysTick and the SCB”SysTick is a 24-bit down-counter in the core, so every Cortex-M4 has the same system timer at 0xE000E010. You configured it in Blinky from nothing: LOAD holds the reload value (period minus one, at most 0xFFFFFF), VAL the current count (any write clears it), and CTRL the enable, interrupt enable, clock source, and COUNTFLAG bits. With CLKSOURCE = 1 it counts the processor clock; with 0 it counts an implementation-defined reference clock, which on the STM32F4 is the AHB clock divided by 8. Because it is part of the core, an RTOS can rely on it on any Cortex-M chip, which is exactly why they all use it for their tick.
The System Control Block at 0xE000ED00 holds the core’s configuration and status registers. You have now used most of them:
| Register | Use |
|---|---|
CPUID | Identifies the core: 0x410FC241 on the STM32F407 means Arm, Cortex-M4 (part 0xC24), revision r0p1 |
ICSR | Pend PendSV (PENDSVSET, bit 28) or SysTick; read the active and pending exception numbers |
VTOR | Vector table address |
AIRCR | Priority grouping, system reset request; writes need the 0x05FA key |
SCR | Sleep behaviour (SLEEPDEEP, sleep-on-exit) |
CCR | Traps for unaligned access and divide by zero, stack alignment |
SHPR1-SHPR3 | System exception priorities |
SHCSR | Enable MemManage/BusFault/UsageFault handlers, see which are active |
CFSR, HFSR, MMFAR, BFAR | Fault status and fault addresses |
CPACR | FPU access control |
ICSR.PENDSVSET deserves a mention: writing it pends PendSV, an exception that does nothing but run its handler when nothing more urgent is active. Given the least urgent priority, it is the standard way to defer work out of an interrupt, and it is the exception every Cortex-M RTOS uses to switch tasks.
Memory types and default attributes
Section titled “Memory types and default attributes”The address map from Memory-mapped registers in C also defines how the core treats each region. Without an MPU configured, the default attributes are:
| Region | Range | Memory type | Executable |
|---|---|---|---|
| Code | 0x00000000-0x1FFFFFFF | Normal | Yes |
| SRAM | 0x20000000-0x3FFFFFFF | Normal | Yes |
| Peripheral | 0x40000000-0x5FFFFFFF | Device | No (XN) |
| External RAM | 0x60000000-0x9FFFFFFF | Normal | Yes |
| External device | 0xA0000000-0xDFFFFFFF | Device | No |
| Private Peripheral Bus | 0xE0000000-0xE00FFFFF | Strongly-ordered | No |
| Vendor system | 0xE0100000-0xFFFFFFFF | Device | No |
Normal memory may have accesses merged or buffered. Device memory accesses happen in program order relative to each other and are never speculative, but writes may be buffered. Strongly-ordered accesses are not buffered at all. Executing from a region marked XN (“execute never”) faults, so a function pointer that has wandered into peripheral space gives a MemManage or HardFault rather than running garbage.
The Cortex-M4 has no data or instruction cache. The STM32F4’s ART accelerator is a flash prefetch buffer and small cache built by ST into the flash interface, outside the core; it is invisible to software apart from the speed-up and is configured in FLASH_ACR. Cortex-M7 parts do have core caches, and that changes the rules for DMA, as DMA explains.
What the M4 does have is a write buffer: a store to a bufferable region can complete from the core’s point of view before it reaches the peripheral. Usually invisible, it explains two later surprises: a BusFault reported as “imprecise” with a stacked pc past the offending store, and an interrupt that fires twice because the store clearing its flag had not landed when the handler returned.
Memory barriers: DMB, DSB, ISB
Section titled “Memory barriers: DMB, DSB, ISB”Barriers control ordering at the hardware level:
DMB(data memory barrier): memory accesses before it are observed before memory accesses after it. Ordering only; execution continues.DSB(data synchronization barrier): no instruction after it executes until every memory access before it has completed. Stronger thanDMB.ISB(instruction synchronization barrier): flushes the pipeline, so instructions after it are fetched again and see the effect of everything before it, such as a new stack pointer, privilege level, or FPU access setting.
The canonical uses:
| Situation | Barrier | Why |
|---|---|---|
After writing VTOR | DSB then ISB | Exceptions taken afterwards must use the new table |
After enabling the FPU in CPACR | DSB then ISB | The next instruction may be a floating-point instruction |
After writing CONTROL (stack or privilege change) | ISB | Following instructions must use the new sp or privilege |
Before WFI | DSB | Pending writes (for example, clearing a flag) complete before sleeping |
| After enabling the MPU | DSB then ISB | New memory permissions apply to the following code |
| At the end of an ISR that cleared a peripheral flag | DSB, or read the register back | The clear reaches the peripheral before the exception returns |
| Between writing a buffer and starting DMA on it | DMB (and cache maintenance on M7) | The DMA engine must see the completed data |
The Cortex-M3 and M4 have short, simple pipelines, and Arm’s own programming guide notes that several of these barriers are not strictly needed on them. Write them anyway: they cost a few cycles, they document intent, and the same code will run on an M7 or M33 where they are required. volatile, by contrast, constrains only the compiler; it emits no barrier instruction. The full story of compiler versus hardware ordering is in Concurrency, atomics, and memory ordering.
CMSIS-Core names for all of this
Section titled “CMSIS-Core names for all of this”When you use vendor code you will see these operations through Arm’s CMSIS-Core functions rather than raw inline assembly. They are thin wrappers over exactly what this lesson described:
| CMSIS-Core | Does |
|---|---|
__disable_irq() / __enable_irq() | cpsid i / cpsie i |
__get_PRIMASK() / __set_PRIMASK(x) | mrs / msr PRIMASK |
__get_BASEPRI() / __set_BASEPRI(x) | mrs / msr BASEPRI |
__get_MSP(), __set_MSP(x), __get_PSP(), __set_PSP(x) | Read and write the two stack pointers |
__get_CONTROL() / __set_CONTROL(x) | CONTROL (recent CMSIS versions add the ISB inside __set_CONTROL) |
__DSB(), __ISB(), __DMB() | Barrier instructions |
__WFI(), __WFE(), __NOP() | Sleep and no-op instructions |
NVIC_EnableIRQ(n) / NVIC_DisableIRQ(n) | Write ISER / ICER (CMSIS 5 maps these to __NVIC_EnableIRQ) |
NVIC_SetPriority(n, p) | Priority byte in IPR, or SHPR for negative (system) numbers |
NVIC_SetPriorityGrouping(g) | AIRCR.PRIGROUP with the key |
SysTick_Config(ticks) | Sets LOAD, clears VAL, gives SysTick the least urgent priority, enables it |
Differences across the Cortex-M family
Section titled “Differences across the Cortex-M family”| Core | Architecture | FPU | MPU | Caches | TrustZone | Bit-banding | Priority bits |
|---|---|---|---|---|---|---|---|
| M0 | Armv6-M | No | No | No | No | No | 2 |
| M0+ | Armv6-M | No | Optional | No | No | No | 2 |
| M3 | Armv7-M | No | Optional | No | No | Optional | 3 to 8 |
| M4 | Armv7E-M | Optional, single | Optional | No | No | Optional | 3 to 8 |
| M7 | Armv7E-M | Optional, single or double | Optional | Optional I and D | No | No | 3 to 8 |
| M23 | Armv8-M Baseline | No | Optional | No | Optional | No | 2 |
| M33 | Armv8-M Mainline | Optional, single | Optional | No | Optional | No | 3 to 8 |
Other differences you will trip over: the M0 and M0+ have no hardware divide, no BASEPRI, no FAULTMASK, and no exclusive load/store instructions, so their critical sections and atomics must disable interrupts entirely; the M0 has no VTOR (optional on the M0+); the M4, M7, and optionally M33 add DSP and SIMD instructions; the M7’s caches make DMA buffer handling a cache-maintenance problem; and Armv8-M adds stack limit registers (MSPLIM, PSPLIM) and a reworked MPU. “Optional” means the chip vendor decides: the STM32F407 has the FPU, the MPU, and bit-banding.
The FPU
Section titled “The FPU”The STM32F407’s core is an M4F: it includes the FPv4-SP floating-point unit, single precision only.
- Registers. 32 single-precision registers
s0-s31(also viewable as 16 double-word registersd0-d15for loads, stores, and moves) and the status and control registerFPSCR, which holds comparison flags, rounding mode, flush-to-zero and default-NaN controls, and sticky exception flags (invalid operation, divide by zero, overflow, underflow, inexact, input denormal). - Enabling. The FPU is off at reset. Grant access with
CPACRbits 23:20 =0xF, followed bydsbandisb, as the startup code in Startup code and the vector table does. A floating-point instruction executed before that raises a UsageFault with theNOCPflag. - ABI.
-mfloat-abi=softnever uses the FPU and calls library routines for all float math.softfpuses FPU instructions but passes float arguments in core registers, so it can link with soft-float code.hardpasses float arguments and results ins0-s15, the fastest option and the one this course uses. Objects built withhardcannot be linked withsoftorsoftfpobjects; the linker refuses with an error about VFP register arguments. - Double precision is software. Any
doubleoperation compiles to a call such as__aeabi_dmul, tens to hundreds of cycles instead of one. An unsuffixed literal like0.5is adouble, sox * 0.5withfloat xsilently promotes to double. Write0.5f, and use-Wdouble-promotionto catch the rest.
Lazy stacking
Section titled “Lazy stacking”With the FPU in use, an exception must preserve s0-s15 and FPSCR as well, since the handler’s C code may use them. That makes the frame 26 words (104 bytes) instead of 8, the extended frame with EXC_RETURN bit 4 clear:
SP + 0x64 | reserved | SP + 0x60 | FPSCR | SP + 0x5C | S15 | | ... | SP + 0x20 | S0 | SP + 0x1C | xPSR | the basic 8-word frame, unchanged, | ... | sits at the bottom SP + 0x00 | R0 |Writing 18 extra words on every interrupt would hurt latency, so by default the M4 uses lazy stacking (FPCCR at 0xE000EF34, bits ASPEN and LSPEN, both set at reset). The hardware tracks whether the interrupted context has used the FPU (CONTROL.FPCA). If it has, the core reserves space for the FPU registers in the frame but does not write them, and records the reserved address. Only if the handler itself executes a floating-point instruction does the core pause and store them into the reserved space. Handlers that never touch floats pay for the extra stack space but not for the extra memory writes, and entry latency stays at 12 cycles. If the interrupted context has not used the FPU at all, the frame is the basic 8 words.
The practical consequences: keep floating-point math out of time-critical ISRs, budget 104 bytes per nesting level for stacks that may hold FPU frames, and remember that an RTOS context switch must save s16-s31 itself for tasks that use the FPU, which it knows from bit 4 of EXC_RETURN.
Common mistakes
Section titled “Common mistakes”- Writing a priority value without shifting it into the implemented bits, so priority 5 becomes 0, the most urgent. Fix: shift by
8 - NVIC_PRIO_BITS, or use a helper that does. - Writing
AIRCRwithout the0x05FAkey, so the write is silently ignored (no reset, no priority grouping change). Fix: always OR inSCB_AIRCR_VECTKEY. - Disabling an IRQ with
NVIC->ISER[n] &= ~bit. Writing 0 toISERdoes nothing, and the read-modify-write can enable others by accident. Fix: write 1 to the bit inICER. - Changing
CONTROLto switch stacks inside a function that then returns. Fix: switch stacks only in code that never returns to a caller on the old stack, followed byisb. - Using
double(or unsuffixed literals) on the M4F and getting slow software math. Fix:floatwithfsuffixes, and-Wdouble-promotion. - Assuming same-priority interrupts can nest. They cannot; the second waits. Fix: give genuinely urgent work a more urgent group priority.
- Forgetting that a fault in MemManage/BusFault/UsageFault escalates to HardFault when those handlers are disabled. Fix: enable them in
SHCSRearly in development.
Exercises
Section titled “Exercises”- Read
SCB->CPUIDin gdb withp/x *(uint32_t *)0xE000ED00. Decode the implementer (bits 31:24), variant (23:20), part number (15:4), and revision (3:0). Expected on the STM32F407:0x410fc241, Arm, Cortex-M4, r0p1. - Build
preempt.cand confirmg_traceis1, 2, 3. Then predict and verify the trace for: priorities swapped; both priorities equal; and the high-priority IRQ disabled in the NVIC before it is pended (what is ing_trace, and what doesNVIC->ISPR[1]show afterwards?). - Set a breakpoint in
TIM6_DAC_IRQHandlerwhile runningpreempt.c. Inspect$lr, thenx/8xw $sp. Explain whylris0xfffffff1here, and find the stacked return address: it should point insideTIM7_IRQHandler, just after the barrier instructions. Useinfo symbolon it. - Add a floating-point computation to
main(with the FPU enabled), run it once, and then let SysTick fire. ComparelrinSysTick_Handlerbefore and after the first floating-point instruction executes inmain. Expected:0xfffffff9before,0xffffffe9after, becauseCONTROL.FPCAbecame 1. - Use
start_on_pspto run the blinky main loop on a 2 KB process stack. InSysTick_Handler, check thatlris0xfffffffd, and comparep/x $mspandp/x $pspin gdb. Then fill the process stack with a pattern (0xDEADBEEF) before starting, let it run for a while, and count how many words were overwritten: that is your thread’s measured stack usage.
Check yourself
Section titled “Check yourself”Question 1:Inside an interrupt handler, lr reads 0xFFFFFFFD. What does that tell you?
Bit 2 = 1 selects PSP, bit 3 = 1 means Thread mode, and bit 4 = 1 means a basic frame. 0xFFFFFFED would indicate an extended FPU frame.
Question 2:On the STM32F407, BASEPRI is set to 0x60. Which interrupts can still pre-empt?
BASEPRI blocks exceptions whose priority value is greater than or equal to it, so only more urgent levels (smaller numbers) get through.
Question 3:A ported driver does NVIC->IPR[38] = 3; intending a low-urgency priority. What priority does USART2 actually get?
Only bits 7:4 are implemented. Writing 3 sets only unimplemented bits, so the priority byte reads back as 0x00.
Question 4:IRQ A's handler is running when IRQ B (same group priority) becomes pending. What happens when A's handler returns?
Equal group priority cannot pre-empt, so B stays pending. On A's return the core skips the unstack/restack pair and goes directly to B.
Question 5:Code writes CONTROL to switch Thread mode onto PSP. Which barrier must follow, and why?
CONTROL is a core register, not memory. ISB flushes the pipeline so later instructions observe the change.
Question 6:A filter on the M4F runs far slower than expected. The disassembly shows calls to __aeabi_dmul. What is the most likely cause?
The FPv4-SP unit handles only single precision. double operations, including those created by promotion from float * 0.5, become library calls.
Key takeaways
Section titled “Key takeaways”- The Cortex-M4 has
r0-r12, two stack pointers (MSPfor handlers and by default for everything,PSPoptionally for Thread mode),lr,pc,xPSR, and the mask registersPRIMASK,FAULTMASK,BASEPRIplusCONTROL. - Exception entry automatically stacks
r0-r3,r12,lr, return address, andxPSR(pluss0-s15andFPSCRwhen the FPU is in use), so handlers are plain C functions. lrin a handler holdsEXC_RETURN; its bits say which mode, stack, and frame type to return to.- Lower priority numbers are more urgent; the STM32F4 implements 4 bits, so priorities are written shifted left by 4. Equal group priorities never pre-empt each other.
- Tail chaining and late arrival keep back-to-back interrupts cheap; nesting costs at least 32 bytes of main stack per level.
DSBwaits for memory accesses to complete,ISBrefetches instructions,DMBorders accesses. Use them afterVTOR,CPACR,CONTROL, and MPU changes and beforeWFI, even where the M4 would tolerate their absence.- The M4F FPU is single precision only;
doubleis software, and lazy stacking keeps FPU context from slowing interrupt entry.
Next: Clocks, reset, and timing, where the STM32F407 is taken from its 16 MHz reset clock to 168 MHz.