STM32N6: XSPI2 Abort() from memory-mapped never clears BUSY — only when the session was actively used for Direct-XIP instruction fetch
Hi,
Maybe someone has encountered a similar problem. I've been working on it for a few days now and unfortunately with no results, so I'm looking for help here. Maybe I'm doing something wrong.
Board / setup
ST NUCLEO-N657X0-Q (reference board). MCU: STM32N657X0HxQ (VFBGA264). On-board external NOR: MX25UM51245G in Octal DTR mode on XSPI2 (external loader: MX25UM51245G_STM32N6570-NUCLEO.stldr). FSBL's XSPI init/memory-mapped-enable code is based on the XSPI_NOR_MemoryMapped_DTR reference example for this board.
Boot chain: FSBL (runs from AXISRAM2, never itself XIP) → MCUboot (Direct-XIP + Direct-XIP-Revert, single image, ECDSA P-256) → verifies and jumps into the Application, which executes Direct-XIP directly from the same XSPI2 memory-mapped NOR (no copy to RAM for the app itself).
Goal
From within the running Application (while it is executing Direct-XIP from XSPI2), temporarily exit memory-mapped mode, write to the flash (initially just the MCUboot trailer image_ok flag, eventually firmware-update writes to the secondary slot), then re-enter memory-mapped mode and resume execution. The code that does this (FlashAgent_*) copies itself plus the entire compiled stm32n6xx_hal_xspi.c object into RAM via a linker section (.ramfunc, >RAM AT>ROM) before ever touching the peripheral, so nothing needed for the abort/write/re-enable runs from the flash that's being manipulated.
Symptom
The abort sequence (SET_BIT(CR,ABORT) → wait TC → clear TC → wait BUSY==0 → clear FMODE) — i.e. exactly what HAL_XSPI_Abort() does — gets stuck waiting for BUSY==0. TC sets correctly every time. BUSY never clears, and it doesn't matter how long we wait (tested up to 50M busy-wait iterations ≈ 2s @ 600 MHz CPU clock, no different from the ~5M/0.2s baseline).
Register snapshot at the point of the stuck wait (identical, bit-for-bit modulo clock scaling, across dozens of runs):
SR = 0x00000C20 (BUSY=1, FLEVEL=12 @ prescaler=3; FLEVEL=10 @ prescaler=7)
CR = 0x36400301 (FMODE=3 unchanged - expected, only cleared after BUSY=0)
AR = ~0x001274xx (offset near the end of the compiled Appli image)
IR = 0x0000EE11 (the Octal DTR memory-mapped read opcode)
FLEVEL is frozen from the very first poll iteration — sampled at iter 1, 1000, 100000, 1000000, 2500000, always identical. It's not draining slowly and it's not being refilled either; it's simply static.
Critical comparison — the same abort sequence works fine elsewhere on the same board
- FSBL's own
XSPI_NOR_Write()/XSPI_NOR_Erase()(plainHAL_XSPI_Abort(), no tricks) reliably works — confirmed indirectly every boot, since MCUboot'sboot_select_or_erase()writescopy_done/magicto the trailer via this exact path before jumping to the app. - ST's own reference example (
XSPI_NOR_MemoryMapped_DTR) also only ever callsHAL_XSPI_Abort()from FSBL, before jumping into a Direct-XIP application — never from within actively-executing Direct-XIP code. - I reviewed every other XSPI example in
STM32CubeN6for this MCU (AutoPolling_DTR,ReadWhileWrite_DTR,XSPIM_SwappedMode,PSRAM_MemoryMapped) — none of them demonstrate an application aborting memory-mapped mode to write back to the same flash it is currently executing from.ReadWhileWrite_DTRlooked promising by name, but it's the flash chip's own indirect-mode dual-bank read-while-write feature — no memory-mapped/XIP involved at all.
The only structural difference I can find between "works" (FSBL, ST's example) and "hangs" (my case) is: in the failing case, the memory-mapped session had just been used to fetch and execute CPU instructions (Direct-XIP) in the moments immediately before the abort — not just plain data reads.
Mitigations tried (all software-side, all on real hardware) — every one produced a bit-identical result unless noted:
| # | Change | Result |
|---|---|---|
| 1 | Manual FIFO drain (while FLEVEL!=0: read DR) in the busy-wait loop | No effect (with or without) |
| 2 | Forced NOPREF_AXI=1 via direct register write after HAL_XSPI_MemoryMapped() (found/fixed a real HAL bug: HAL_XSPI_MemoryMapped() silently skips writing NOPREF_AXI when NoPrefetchData==DISABLE) | Fixed the config bug, no effect on the hang |
| 3 | ClockPrescaler 3→7 (37.5 MHz→18.75 MHz) | No effect on the hang (FLEVEL scaled 12→10 as expected from timing, BUSY still stuck); also reduced boot-time read reliability at that speed |
| 4 | CLEAR_BIT(CR,EN) then SET_BIT(CR,EN) as an alternative documented escape (RM0486 §28.4.12: "BUSY does not fall until there is an abort, or the peripheral is disabled") | Caused a full, unrecoverable AXI bus hang (matches ES0620 erratum "Possible deadlock when a request arrives during the disabling process" — clearing EN while BUSY is still set) |
| 5 | Explicit busy-wait delay (~20k cycles) between __ISB() and SET_BIT(CR,ABORT) | No effect |
| 6 | Disabled Cortex-M55 L1 I-Cache (SCB_DisableICache()) before the RAM relocation — confirmed via SCB->CCR readback that IC was ON by default (no SCB_EnableICache() anywhere in the codebase, so presumably enabled by BootROM) | No effect |
| 7 | SCB_CleanInvalidateDCache_by_Addr() for the target region + SCB_InvalidateICache() immediately before the abort | No effect |
| 8 | Removed a printf()/puts() call immediately before the ROM→RAM call transition (confirmed via objdump that the linker inserts a veneer, LDR PC,[PC], to bridge the ~0x3C000000-byte gap between the ROM caller and the RAM callee) | No effect |
| 9 | ChipSelectHighTimeCycle 1→8, then 1→60 (of max 64), alone and combined with #1 | No effect |
| 10 | Verified via nm/objdump on the linked ELF that every function on the failing path (Abort, WaitFlagTimeout, debug helpers, and even HAL_XSPI_Command/Transmit/MemoryMapped) is correctly linked into RAM (0x34xxxxxx) — ruling out "code partially still executing from flash" | Confirmed all in RAM |
Questions
- Is this a known limitation/errata of the XSPI Octal DTR memory-mapped abort sequence specifically when the session was used for live CPU instruction fetch (XIP) rather than plain data reads? I couldn't find anything in ES0620 (device errata) that matches this specific symptom (only the EN-disable deadlock, which is a consequence of my workaround attempt, not the root cause).
- Is there a documented, additional step beyond the standard
HAL_XSPI_Abort()sequence (CR_ABORT→ waitTC→ waitBUSY=0→ clearFMODE) required specifically to recover from aborting a Direct-XIP session, that isn't covered in RM0486 §28.4.12 or in any of the reference examples? - Has anyone gotten a self-write-while-XIP pattern (write to the same external NOR an application is executing Direct-XIP from) working on STM32N6 + Octal DTR at all? Or is the intended/supported architecture always "writes happen from a context that is not itself executing XIP from that flash" (i.e. bootloader-mediated, as in all the reference examples)?
