Skip to content

arch: arm: cortex_m: do not re-enable LSPEN on FP context switch (ES0182 fix) - #11

Closed
Gowtham1256 wants to merge 1 commit into
rapyuta-robotics:rr-v4.2.0from
Gowtham1256:fix/es0182-no-lspen-on-fp-context-switch
Closed

arch: arm: cortex_m: do not re-enable LSPEN on FP context switch (ES0182 fix)#11
Gowtham1256 wants to merge 1 commit into
rapyuta-robotics:rr-v4.2.0from
Gowtham1256:fix/es0182-no-lspen-on-fp-context-switch

Conversation

@Gowtham1256

Copy link
Copy Markdown

Problem

On Cortex-M4 with CONFIG_FPU_SHARING=y, the OKSbot application clears FPCCR.LSPEN at boot via a PRE_KERNEL_1 hook to force eager FPU stacking, working around Cortex-M4 errata ES0182.

However z_arm_mpu_stack_guard_and_fpu_adjust() was re-enabling LSPEN (FPU->FPCCR |= FPU_FPCCR_LSPEN_Msk) on every context switch to a K_FP_REGS thread — immediately undoing the boot-time fix.

ES0182 crash mechanism

  1. FPCCR.LSPEN = 1 (lazy stacking re-enabled by this function)
  2. Thread executes any FPU instruction → hardware defers register save to FPCAR
  3. An ISR fires before the lazy write completes (on OKSbot: 400 Hz SPI ISR from the Jetson, or CAN heartbeat timer)
  4. Hardware saves s0–s15 + fpscr to the address in FPCAR, which was set for the basic (non-FPU) exception frame — 8 bytes too early
  5. This overwrites the stacked r0/r1/r2/r3/r12/lr/**pc**/xpsr with FPU register values
  6. Exception return loads the corrupted PC → cascading INVSTATE / INVPC / MemManage faults → reboot loop

Observed crash signature

xpsr:  0x08011400   ← code-segment address (should be Thumb/ISR flags)
fpscr: 0x08061f54   ← rodata pointer (should be FP status flags)
pc:    0x4a813674   ← garbage

Seen on OKSbot B1087 (STM32F405 @ 168 MHz, jiri_board, Zephyr v4.2.0). The crash was a deterministic reboot loop starting within the first 200 ms of boot. CAN harness removal confirmed the trigger was NOT CAN — the 400 Hz SPI ISR from the Jetson host is sufficient.

Fix

Remove the single line FPU->FPCCR |= FPU_FPCCR_LSPEN_Msk; from the K_FP_REGS branch in z_arm_mpu_stack_guard_and_fpu_adjust().

With this change, once the application disables LSPEN at boot, it stays disabled (eager stacking) through every subsequent context switch. Eager stacking costs one additional ISR-entry cycle (~2.5 µs on Cortex-M4 at 168 MHz when the FPU has been used) but is unconditionally safe and eliminates the ES0182 race window entirely.

The note in the existing comment about "activate lazy stacking" is removed because we are intentionally not doing that.

Companion fix (application side)

The application (rr_oks_robot_base_controller / oksbot_base_ctrl_fw) must still provide two SYS_INIT hooks:

// PRE_KERNEL_1 — clear LSPEN before any driver enables interrupts
static int disable_fpu_lazy_stacking(void) {
    FPU->FPCCR &= ~FPU_FPCCR_LSPEN_Msk;
    return 0;
}
SYS_INIT(disable_fpu_lazy_stacking, PRE_KERNEL_1, 0);

// PRE_KERNEL_2 — tag main thread K_FP_REGS so the MPU guard is
// sized correctly from the first context switch
static int set_main_thread_fp_regs(void) {
    z_main_thread.base.user_options |= K_FP_REGS;
    return 0;
}
SYS_INIT(set_main_thread_fp_regs, PRE_KERNEL_2, 0);

Without the PRE_KERNEL_1 hook, LSPEN is still enabled during early boot (before the first K_FP_REGS context switch).

Testing

  • Before: B1087 rebooted every ~200 ms in a deterministic loop; xpsr/fpscr contained code-segment addresses.
  • After: B1087 ran continuously for 7+ minutes with no faults; all eight threads (MOTION_CTRL, CAN_TX, ROBOT_DATA_LINK, DRIVE_TRAIN_CTRL, SENSOR_BAR, IMU, DISTANCE_SENSOR, WDT_FEEDER) healthy.
  • Board: jiri_board (STM32F405RGTx, 168 MHz, 128 KB RAM, CONFIG_FPU=y, CONFIG_FPU_SHARING=y, CONFIG_HW_STACK_PROTECTION=y)

References

  • ST Cortex-M4 Errata ES0182 — "Lazy state preservation might prevent a correct FPU context saving"
  • Zephyr upstream issue #108793

On Cortex-M4 with CONFIG_FPU_SHARING=y, Zephyr's PRE_KERNEL_1 hook
(in the OKSbot application) clears FPCCR.LSPEN at boot to force eager
FPU stacking, working around Cortex-M4 errata ES0182.

However z_arm_mpu_stack_guard_and_fpu_adjust() was unconditionally
re-enabling LSPEN (FPU->FPCCR |= FPU_FPCCR_LSPEN_Msk) on every
context switch to a K_FP_REGS thread, immediately undoing the
boot-time fix.

ES0182 root cause:
  - FPCCR.LSPEN=1 (lazy stacking enabled)
  - Thread executes any FPU instruction → LSPEN triggers deferred save
  - A nested ISR fires before the lazy-stacking write completes
  - Hardware saves s0-s15/fpscr to FPCAR, which points at the wrong
    exception-frame offset (basic frame sized, not extended)
  - Stacked r0/r1/r2/r3/r12/lr/pc/xpsr are overwritten with FP
    register values
  - Exception return loads a corrupted PC → cascading
    INVSTATE / INVPC / MemManage faults → reboot loop

Observed on OKSbot (STM32F405 @ 168 MHz, jiri_board) where a 400 Hz
SPI ISR from the Jetson fired during a LOG_ERR call in the fault
handler. Crash signature: code-segment addresses appearing in
xpsr/fpscr exception-frame slots, PC pointing to garbage.

Fix: remove the FPU->FPCCR |= FPU_FPCCR_LSPEN_Msk line from the
K_FP_REGS branch of z_arm_mpu_stack_guard_and_fpu_adjust(). LSPEN
is kept at 0 (eager stacking) permanently. Eager stacking costs one
extra exception-entry cycle (~2.5 µs on Cortex-M4 at 168 MHz when
the FPU has been used) but is unconditionally safe and eliminates
the ES0182 race entirely.

The companion app-side fix (PRE_KERNEL_1 clearing LSPEN, plus a
PRE_KERNEL_2 hook tagging z_main_thread with K_FP_REGS) remains
necessary to keep LSPEN=0 from early boot through to the first
context switch.

Relates to Zephyr upstream issue zephyrproject-rtos#108793.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@Gowtham1256

Copy link
Copy Markdown
Author

Closing in favour of #10, which is a cleaner fix for the same root cause.

Why #10 is better than this PR:

This PR keeps LSPEN=0 permanently (eager stacking) by removing FPU->FPCCR |= FPU_FPCCR_LSPEN_Msk from z_arm_mpu_stack_guard_and_fpu_adjust(). It works, but it incurs ~2.5 µs extra overhead on every ISR entry when the FPU has been used, and it requires an app-side PRE_KERNEL_1 hook to keep LSPEN disabled.

PR #10 fixes the root cause at the right layer — z_setup_new_thread() in kernel/thread.c forces K_FP_REGS on all threads at creation time when CONFIG_FPU && CONFIG_FPU_SHARING. This ensures arch_new_thread() sizes the MPU guard correctly from the start, closing the ES0182 race window without touching LSPEN at all. No performance cost, no app-side workaround needed, and it covers all threads (main, sysworkq, idle) in one place. Matches the approach in upstream zephyrproject-rtos#110300.

@Gowtham1256 Gowtham1256 closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant