Embedded HandbookApps
All sections › II. Core and system mechanisms
STM32F407, STM32F405 · level advanced · 7 min read
Open in the interactive handbook

7а. Exception entry from the inside: the stack frame, EXC_RETURN and the FPU

In section 7 an interrupt handler looked like an ordinary C function. That is by design: the Cortex-M core itself saves what a C function is obliged to save, and itself restores everything afterwards. This section is about what exactly happens on entry and exit: which registers go onto the stack and in what order, what the strange number in LR is, how the core chooses the stack, and why with the FPU enabled the frame is three times larger yet costs almost nothing.

Everything shown is a run in QEMU (machine netduinoplus2, Cortex-M4F core): the program _verify/exc7a/exc7a.c puts recognisable numbers into the registers, raises an interrupt through the NVIC, and an assembly handler records LR and both stack pointers; then the frame is read straight from the stack. The rules were checked against the Cortex-M4 core manual from the pack. The check is _verify/exc7a_check.py.

Terms used in the documentation

Term Meaning
stacking, stack frame saving the state on the stack, and the block saved
EXC_RETURN a special value in LR by which the core recognises an exception return
MSP, PSP the main and process stack pointers
Thread mode, Handler mode the mode of ordinary code and the mode of a handler
STKALIGN, aligner aligning the frame to 8 bytes and the padding word
lazy stacking, FPCCR.LSPEN deferred saving of the FPU registers
FPCAR the address of the space for the FPU registers in the frame
late-arriving exception a higher-priority exception that comes during stacking

7а.1 What the core puts on the stack

Before the first instruction of the handler the core itself saves eight words:

Offset from SP in the handler Word In the experiment
+0x00 R0 0x11111111
+0x04 R1 0x22222222
+0x08 R2 0x33333333
+0x0C R3 0x44444444
+0x10 R12 0xCCCCCCCC
+0x14 LR the return address of the interrupted function
+0x18 PC where to return: the next instruction of the interrupted code
+0x1C xPSR flags and exception number; bit 24 — Thumb

These are exactly the registers that a C function is not obliged to save — that is why the handler can be an ordinary function: the rest (R4–R11) the compiler will save itself if they are needed.

Where exactly we return. In the experiment the interrupt is raised by an str instruction to an NVIC register, followed by dsb and isb. In the frame PC = 0x0800011C — the address of the nop after isb: in QEMU the exception was taken exactly there.


7а.2 Alignment: 32 bytes that turned out to be 36

The core manual describes a frame of eight words — 32 bytes. The experiment measured 36. The reason is alignment: on entry the core aligns the frame to 8 bytes (the STKALIGN bit). If the stack pointer before entry was divisible by 8 only with a remainder of 4, the core inserts a padding word and marks this in bit 9 of xPSR, so as to remove it on exit.

In the experiment xPSR in the frame is 0x21000200: bit 24 (Thumb) and bit 9 — the padding was there. The ARM calling convention expects the stack to be aligned to 8 bytes, and an interrupt can come anywhere — so the core aligns it itself.

⚠️ STM32F1 does not have this alignment after reset. It has a Cortex-M3 core of revision r1p1 (section 2а), and the STKALIGN bit after reset is 0: the frame is aligned only to 4 bytes. From revision r2p0 the bit is on from the start. On F1 it is enabled in the first line of the program: SCB->CCR |= SCB_CCR_STKALIGN_Msk; — otherwise a handler working with double or 64-bit numbers will get an unaligned stack (section 7в.3).


7а.3 EXC_RETURN: the number in LR

An ordinary function call puts the return address into LR. On exception entry the core puts EXC_RETURN there — a number of the form 0xFFFFFFxx that never occurs among code addresses. When the handler executes bx lr (or pop {pc}), the core sees this number and performs an exception return: it pops the frame from the stack. The low bits tell which stack and which frame:

EXC_RETURN Return to Stack Frame In the experiment
0xFFFFFFF1 a handler (nested) MSP without FPU —
0xFFFFFFF9 the main code MSP without FPU main code on MSP
0xFFFFFFFD the main code PSP without FPU main code on PSP
0xFFFFFFE1 a handler MSP with FPU —
0xFFFFFFE9 the main code MSP with FPU after an FPU instruction
0xFFFFFFED the main code PSP with FPU —

The experiment obtained three of the six values itself: main code on MSP — 0xFFFFFFF9; switching the main code to the process stack (CONTROL.SPSEL = 1) — 0xFFFFFFFD, and the frame went onto PSP; executing one FPU instruction — 0xFFFFFFE9.

The low byte of EXC_RETURN bit by bit: bit 4 — frame without or with FPU, bit 3 — return to the main code or to a handler, bit 2 — stack PSP or MSP; six values from 0xFFFFFFF1 to 0xFFFFFFED laid out by these bits, three of them obtained in the experiment — F9 on MSP, FD after switching to PSP, E9 after an FPU instruction

Hence the practical rule: a handler must not corrupt LR if it exits through bx lr. The compiler knows this; assembly handlers do not. And the same number helps when analysing a fault (section 35): bit 2 of EXC_RETURN tells which stack to take the frame from.

Why two stacks. The kernel and interrupts always run on MSP. An RTOS (section 24) switches tasks to PSP — each has its own stack — and an interrupt puts the frame on the stack of the interrupted task, while the handler itself runs on MSP. A context switch is a substitution of PSP between entry and exit.


7а.4 With the FPU: 108 bytes that cost almost nothing

The exception frame without and with the FPU: eight words, S0–S15 and FPSCR, padding; lazy stacking

If the main code used the FPU, the frame needs space for its registers too: S0–S15, FPSCR and one reserved word — 18 more words, 26 words = 104 bytes in total. The experiment measured 108 — the same 4 bytes of padding.

How the core knows the FPU was used: the very first FPU instruction sets the FPCA bit in CONTROL — the experiment read CONTROL = 0x00000004 right after vmov. Without it the frame is the ordinary one, 32 bytes, even if the FPU is enabled.

Lazy stacking. Saving 17 registers on every interrupt is expensive, and most often the handler does not touch the FPU. So by default (FPCCR = 0xC0000000: bits ASPEN and LSPEN) the core only reserves space in the frame and remembers its address in FPCAR; the registers themselves are written there only if the handler executes an FPU instruction.

The experiment showed this. The memory below the stack was filled beforehand with the number 0xDEADBEEF:

Mode S0 in the frame S15 in the frame FPSCR in the frame
lazy (LSPEN = 1, default) 0xDEADBEEF — not written 0xDEADBEEF 0xDEADBEEF
immediate (LSPEN = 0) 0x3F800000 = 1.0 0x40000000 = 2.0 0x00000000

With lazy stacking the space is there, but there is no write: the handler did not use the FPU. FPCAR = 0x2001FF90, the stack pointer in the handler is 0x2001FF70: the difference is 0x20 = 32 bytes, that is, the space for S0 starts right after the eight words.

The practical outcome: the FPU in the main code makes interrupts hardly more expensive, as long as the handlers themselves do without floating point. Do one float multiplication in a handler — and the core will write the 17 registers.


7а.5 Tail-chaining and late-arriving exceptions

Two mechanisms from the core manual that save cycles:

The emulator does not count cycles honestly, so the numbers come from the core description: exception entry on Cortex-M3/M4 takes 12 cycles (on M0 — 16, section 7.1), and tail-chaining 6 cycles instead of an exit and a new entry; Flash wait states add their own. An important consequence: several interrupts in a row cost less than the same number separately, — and handler latency must be measured under load, not one at a time.


7а.6 Pitfalls

An assembly handler returned "to the wrong place". LR was corrupted — it holds EXC_RETURN, not an address.

HardFault analysis gives garbage. The frame was taken from MSP, but the exception came from a task on PSP. Look at bit 2 of EXC_RETURN.

The frame "shifted" by a word. The padding was not accounted for: bit 9 of xPSR in the frame.

On F1 printf("%f") in an interrupt sometimes prints garbage. The r1p1 core: STKALIGN is off after reset, the frame is aligned only to 4 bytes. Enable the bit at the start of the program (7а.2).

Interrupts slowed down after enabling the FPU. The handler (or a function it calls) uses float — lazy stacking fires on every interrupt. Move the calculations out of the handler.

Lazy stacking was disabled "for reliability". Now 17 registers are written on every interrupt, even if the handler does not need the FPU.

The stack overflowed only with the FPU. A frame of 104–108 bytes instead of 32–36 — at every nesting level.


7а.7 Summary

Next: section 7б "Cortex-M assembly: Thumb-2 through the eyes of C".

Open in the interactive handbook