Skip to content

target: fix nRF54LM20A flash algorithm (HardFault/IPSR=3 in ProgramPage) - #2033

Open
cumin777 wants to merge 1 commit into
pyocd:developfrom
cumin777:fix/nrf54lm20a-flash-algo
Open

cumin777 wants to merge 1 commit into
pyocd:developfrom
cumin777:fix/nrf54lm20a-flash-algo

Conversation

@cumin777

Copy link
Copy Markdown

Fixes #2016.

Root cause

The nRF54LM20A builtin target (#1889) enabled only CONFIG.WEN in its flash algorithm and copied words straight into RRAM, with the write buffer left unconfigured (CONFIG.WRITEBUFSIZE=0, "Unbuffered") and TASKS_COMMITWRITEBUF never issued. Every CPU store to RRAM was rejected by the RRAMC (EVENTS_ACCESSERROR set, ACCESSERRORADDR = store target, precise BusFault → HardFault, IPSR=3). Erase kept working because EraseChip only pokes RRAMC task registers — exactly the "erase fine, flash faults" signature reported in #2016.

Fix

Program through the RRAMC write buffer, matching Nordic's own nRF54LM20A flash algorithm (the FLM-derived one shipped in probe-rs targets):

  1. CONFIG = WEN | WRITEBUFSIZE(32) (0x2001), wait READY
  2. word-copy the page to the destination address
  3. TASKS_COMMITWRITEBUF = 1 to flush the last partial 128-bit line, wait READY
  4. CONFIG = 0, wait READY

Pages are 4 KiB (was 4 bytes) so each ProgramPage call covers whole 128-bit RRAM codewords — with 4-byte pages the flow completed but an independent verify showed stray bytes never landed (e.g. 2 wrong bytes per flash). page_buffers/begin_stack were relocated accordingly (algo code occupies 0x004–0x0B4; buffers at 0x20000100/0x20001100, stack at 0x20003000).

The full C source of the blob (built with arm-zephyr-eabi-gcc -mcpu=cortex-m33 -mthumb -Os, freestanding, -Wl,-Ttext=0x20000004) is embedded in the commit message.

Hardware verification

Seeed XIAO nRF54LM20A, on-board CMSIS-DAP, Windows 11:

Step Result
pyocd erase --mass -t nrf54lm20a ✅
pyocd flash -t nrf54lm20a firmware_lm20a_blink.hex on the blank (mass-erased) chip — the exact repro from #2016 ✅ exit 0, programmed 24576 bytes (6 pages)
Independent openocd verify_image (different toolchain) ✅ zero byte differences
reset run ✅ image boots (LED blink demo)
Re-flash over an already-programmed chip ✅ exit 0

Before this change, the same sequence faults with target was not halted as expected after calling flash algorithm routine (IPSR=3).

For reference, the nRF54L15 (nrf54l) target uses a different controller (MRAMC) where unbuffered CPU stores are accepted, which is why L15 programs fine with an otherwise identical algorithm shape.

🤖 Generated with Claude Code

The previous nRF54LM20A flash algorithm enabled only CONFIG.WEN and copied
words straight into RRAM with no write-buffer configuration and no buffer
commit. Every CPU store to RRAM then faulted (HardFault, IPSR=3) — while
erase kept working because EraseChip only pokes RRAMC task registers — so
`pyocd flash -t nrf54lm20a` faulted at the first programming store, on any
chip state (pyocd#2016).

This blob programs through the RRAMC write buffer, matching Nordic's own
nRF54LM20A flash algorithm (the same one shipped in probe-rs targets): set
CONFIG = WEN | WRITEBUFSIZE(32) = 0x2001, copy the page, flush with
TASKS_COMMITWRITEBUF, wait READY, clear WEN. Pages are 4 KiB so each
program call covers whole 128-bit RRAM codewords (4-byte pages were observed
dropping stray bytes, e.g. 2 bytes/flash in an independent OpenOCD verify).

The previous blob's flash algorithm was byte-identical to the official one
merged in pyocd#1889 (shipped in v0.44.0), so this fix lands on top of that and
also applies to any fork/pin that carried it.

Hardware verified on a Seeed XIAO nRF54LM20A (on-board CMSIS-DAP):
- mass-erase + flash on a blank chip: exit 0, 24576 bytes programmed
- independent OpenOCD verify_image: zero byte differences
- board resets and runs the image

Built with arm-zephyr-eabi-gcc -mcpu=cortex-m33 -mthumb -Os from this C:

    #include <stdint.h>
    #define RRAMC        0x5004E000u
    #define TASKS_CWB    (*(volatile uint32_t *)(RRAMC + 0x008))
    #define READY        (*(volatile uint32_t *)(RRAMC + 0x400))
    #define CONFIG_REG   (*(volatile uint32_t *)(RRAMC + 0x500))
    #define ERASEALL     (*(volatile uint32_t *)(RRAMC + 0x540))
    #define CFG          (1u | (0x20u << 8))   /* WEN | WRITEBUFSIZE=32 */
    static void wait(void){ while((READY & 1u)==0){} }
    int Init(uint32_t a){ (void)a; wait(); return 0; }
    int UnInit(void){ return 0; }
    int EraseChip(void){ CONFIG_REG=1; wait(); ERASEALL=1; wait(); CONFIG_REG=0; return 0; }
    int EraseSector(uint32_t a){ CONFIG_REG=CFG; wait(); *(volatile uint32_t*)a=0xFFFFFFFFu; TASKS_CWB=1; wait(); CONFIG_REG=0; return 0; }
    int ProgramPage(uint32_t a, uint32_t sz, const uint32_t *s){
        CONFIG_REG=CFG; wait();
        for (uint32_t n=sz&~3u; n; n-=4) *(volatile uint32_t*)a++=*s++;
        TASKS_CWB=1; wait(); CONFIG_REG=0; return 0; }

Fixes pyocd#2016.

Co-Authored-By: Claude <noreply@anthropic.com>
cumin777 added a commit to cumin777/platform-seeedboards that referenced this pull request Sep 22, 2026
The StarSphere-1024/pyOCD@lm20_stable fork carried exactly three changes
over upstream, all now in official releases, so the fork adds nothing:
its nRF54LM20A target support was merged upstream as pyocd/pyOCD#1889
(shipped in v0.44.0, flash algorithm byte-identical), its JLink
serial-number fix was superseded by the version-aware fix in v0.45.0
(pyocd/pyOCD#1927), plus a trivial .gitignore entry.

- Pin pyocd==0.45.1 in the PlatformIO upload path and the factory_reset
  venvs (requirements.txt).
- Make _installed_pyocd_is_expected() version-aware: the old check only
  tested for the nrf54lm20a target, which the retired fork also exposes,
  so machines that had the fork installed would silently keep it. Now
  `pyocd --version` is compared against the pin first, making the switch
  self-healing; the install banner prints the version.
- Bump the factory_reset DEP_MARKER (.deps_forked_pyocd_v1 ->
  .deps_official_pyocd_v1) so cached venvs reinstall from the new pin.
- Correct the misleading comments that claimed the fork carried working
  nRF54LM20A flash support while stock pyOCD faults: the algorithms are
  byte-identical, so `pyocd flash` faults either way. The fault is
  root-caused (RRAM programmed with the RRAMC write buffer
  unconfigured, pyocd/pyOCD#2016) with a fix pending in
  pyocd/pyOCD#2033; bump this pin once that ships. Until then the LM20A
  upload path uses OpenOCD (nrf54lm20a-load); pyocd serves
  list/rtt/erase, and the default cmsis-dap upload is unaffected.

Hardware verified on a XIAO nRF54LM20A (CMSIS-DAP): pyocd 0.45.1 list /
erase --mass work; `pyocd flash` faults exactly as under the fork (zero
regression); the OpenOCD path is untouched.

Co-Authored-By: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
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