Skip to main content
Associate
August 3, 2026
Solved

STM32N6: XSPI2 Abort() from memory-mapped never clears BUSY — only when the session was actively used for Direct-XIP instruction fetch

  • August 3, 2026
  • 1 reply
  • 131 views

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 

  1. FSBL's own XSPI_NOR_Write()/XSPI_NOR_Erase() (plain HAL_XSPI_Abort(), no tricks) reliably works — confirmed indirectly every boot, since MCUboot's boot_select_or_erase() writes copy_done/magic to the trailer via this exact path before jumping to the app.
  2. ST's own reference example (XSPI_NOR_MemoryMapped_DTR) also only ever calls HAL_XSPI_Abort() from FSBL, before jumping into a Direct-XIP application — never from within actively-executing Direct-XIP code.
  3. I reviewed every other XSPI example in STM32CubeN6 for 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_DTR looked 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

  1. 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).
  2. Is there a documented, additional step beyond the standard HAL_XSPI_Abort() sequence (CR_ABORT → wait TC → wait BUSY=0 → clear FMODE) 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?
  3. 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)?
Best answer by Mariusz Razik

Our own translation unit's string literals were NOT covered by this rule. Functions marked __attribute__((section(".RamFunc"))) get their CODE relocated correctly, but GCC does NOT propagate that section attribute to string literals those functions reference — those land in the default .rodata output section, i.e. back in flash/ROM. Since we used debug logging (const char* literals) inside the abort/BUSY-wait loop, every debug print during "wait for BUSY=0" was a fresh read from the flash address space we were trying to quiesce — and because FMODE isn't cleared until the abort sequence completes, those reads kept re-triggering peripheral activity through the still-configured memory-mapped session. That looked exactly like "BUSY never clears no matter what we try."

The fix was one line added to the same .ramfunc section:


*flash_agent.c.obj(.rodata .rodata.*)
i.e. explicitly pull our own object file's rodata into RAM too, the same way the HAL driver's rodata already was. After this, HAL_XSPI_Abort() started succeeding immediately, on the first attempt, zero BUSY-wait iterations — after dozens of hardware-level hypotheses (DMA, cache, DTR/DQS protocol, prescaler, EN toggle, RCC reset, CS-high timing, etc.) had gone nowhere.

1 reply

Mariusz RazikAuthorBest answer
Associate
August 4, 2026

Our own translation unit's string literals were NOT covered by this rule. Functions marked __attribute__((section(".RamFunc"))) get their CODE relocated correctly, but GCC does NOT propagate that section attribute to string literals those functions reference — those land in the default .rodata output section, i.e. back in flash/ROM. Since we used debug logging (const char* literals) inside the abort/BUSY-wait loop, every debug print during "wait for BUSY=0" was a fresh read from the flash address space we were trying to quiesce — and because FMODE isn't cleared until the abort sequence completes, those reads kept re-triggering peripheral activity through the still-configured memory-mapped session. That looked exactly like "BUSY never clears no matter what we try."

The fix was one line added to the same .ramfunc section:


*flash_agent.c.obj(.rodata .rodata.*)
i.e. explicitly pull our own object file's rodata into RAM too, the same way the HAL driver's rodata already was. After this, HAL_XSPI_Abort() started succeeding immediately, on the first attempt, zero BUSY-wait iterations — after dozens of hardware-level hypotheses (DMA, cache, DTR/DQS protocol, prescaler, EN toggle, RCC reset, CS-high timing, etc.) had gone nowhere.