From ec16e7c6abb6c727ea27bd8a23b4d6192668b127 Mon Sep 17 00:00:00 2001 From: R0ck Date: Sun, 25 Oct 2026 00:27:30 +0100 Subject: [PATCH] nrf52: the SoftDevice can write flash, and what it writes is kept FICR left CODEPAGESIZE and CODESIZE at zero, so the SoftDevice saw no flash and answered every sd_flash_page_erase and sd_flash_write with NRF_ERROR_FORBIDDEN (found with node.debug's GDB stub: r0 = 0xF after svc 0x28). LittleFS never formatted. Renode also rebuilt flash at every start. NVMC now writes the InternalFS region to the node's directory whenever flash goes back to read-only, and the next start of the same build loads it. Closes #565. --- CHANGELOG.md | 8 ++++ internal/firmware/emulated/emulated_node.go | 1 + internal/firmware/emulated/nrf52.go | 33 ++++++++++++++-- internal/firmware/emulated/nrf52_test.go | 38 ++++++++++++++++++ tools/renode/ficr.py | 5 +++ tools/renode/peripherals/NRF52840_NVMC.cs | 43 ++++++++++++++++++++- 6 files changed, 124 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a38e02b4..15189334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,14 @@ had changed in them - which is the gap this file exists to close. ### Fixed +- **Emulated nRF52 boards keep what they store.** The factory registers never + said how big the flash was, so the SoftDevice refused every erase and write + with `NRF_ERROR_FORBIDDEN`: the filesystem could not be formatted, and + `IdentityStore::save()` failed on every boot. And Renode rebuilt flash from + the image at every start, so nothing would have survived a restart anyway. + The filesystem's pages are now kept in the node's directory and loaded back + when the same build starts again. A preference set over the console + survives a restart. - **Adding a scheduled send no longer repeats the ones already said.** Any `schedule.add` made every one-shot send whose moment had passed go out again. diff --git a/internal/firmware/emulated/emulated_node.go b/internal/firmware/emulated/emulated_node.go index 6df13f0e..657fc974 100644 --- a/internal/firmware/emulated/emulated_node.go +++ b/internal/firmware/emulated/emulated_node.go @@ -307,6 +307,7 @@ func (e *EmulatedNode) Start(ctx context.Context, bridge string) (err error) { fmt.Sprintf("%s=%d", EnvNoiseSeed, noiseSeedFor(e.RunSeed, e.NodeName, e.BoardName, e.Position))) if e.Emulator == Renode { + e.radioEnv = append(e.radioEnv, "MESHBENCH_FLASH_KEEP="+e.keptStorage()) if err := e.startRenode(ctx, bridge); err != nil { return err } diff --git a/internal/firmware/emulated/nrf52.go b/internal/firmware/emulated/nrf52.go index 39682b6d..8f108984 100644 --- a/internal/firmware/emulated/nrf52.go +++ b/internal/firmware/emulated/nrf52.go @@ -1,6 +1,7 @@ package emulated import ( + "crypto/sha256" "fmt" "hash/fnv" "os" @@ -14,9 +15,11 @@ import ( // stands in for a flashed board. const ( nrf52FlashBytes = 1024 * 1024 - nrf52UICRBase = 0x10001000 - nrf52UICRBytes = 0x1000 - nrf52FICRBase = 0x10000000 + // nrf52StorageBase is where the Adafruit core's InternalFS starts. + nrf52StorageBase = 0xED000 + nrf52UICRBase = 0x10001000 + nrf52UICRBytes = 0x1000 + nrf52FICRBase = 0x10000000 ) // SoftDeviceHex finds the fetched SoftDevice for a version, or "" if it is not @@ -84,6 +87,13 @@ func (e *EmulatedNode) renodeFlash() (string, error) { fmt.Fprintf(&b, "sysbus LoadBinary @%s 0x%X\n", path, r.addr) } + // What the firmware stored last time, over the erased pages, when it was + // this same build: see NRF52840_NVMC.Keep. A different build is a board + // reflashed, and starts with nothing, as the QEMU boards do. + if kept := e.keptStorage(); fileExists(kept) { + fmt.Fprintf(&b, "sysbus LoadBinary @%s 0x%X\n", kept, nrf52StorageBase) + } + // SP and PC come from the vector table of whatever boots first, which is // the MBR at zero and not the application. Setting only VectorTableOffset // leaves SP uninitialised and the firmware faults within 45 instructions @@ -175,6 +185,13 @@ func (e *EmulatedNode) ficrWrites() string { regs := map[uint32]uint32{ 0x060: uint32(id), 0x064: uint32(id >> 32), + // The flash's geometry: 256 pages of 4 kB. The SoftDevice checks + // every page it is asked to erase or write against these, and with + // both reading zero there was no flash at all - every + // sd_flash_page_erase answered NRF_ERROR_FORBIDDEN, LittleFS could not + // format, and nothing a node stored survived a reboot (#565). + 0x010: 0x00001000, // CODEPAGESIZE + 0x014: 0x00000100, // CODESIZE 0x0A0: 0x00000000, // DEVICEADDRTYPE - public 0x0A4: uint32(id ^ 0xA1B2C3D4), 0x0A8: uint32(id>>32) & 0xFFFF, @@ -238,3 +255,13 @@ func SoftDeviceDir() string { } return filepath.Join(dir, "meshbench", "softdevice") } + +// keptStorage is where this node's filesystem is kept between starts, named for +// the build so another build does not inherit it. +func (e *EmulatedNode) keptStorage() string { + sum := sha256.Sum256([]byte(e.Image)) + if b, err := os.ReadFile(e.Image); err == nil { + sum = sha256.Sum256(b) + } + return filepath.Join(e.Dir, fmt.Sprintf("storage-%x.bin", sum[:6])) +} diff --git a/internal/firmware/emulated/nrf52_test.go b/internal/firmware/emulated/nrf52_test.go index 9532bea3..fc956da2 100644 --- a/internal/firmware/emulated/nrf52_test.go +++ b/internal/firmware/emulated/nrf52_test.go @@ -110,3 +110,41 @@ func TestFlashLaysTheSoftDeviceUnderTheApplication(t *testing.T) { t.Errorf("the byte at 0x10 is 0x%02X, want 0x42", got[16]) } } + +// The SoftDevice checks every flash operation against the flash's geometry, +// and with CODEPAGESIZE and CODESIZE reading zero it forbade every one (#565). +func TestFICRSaysHowBigTheFlashIs(t *testing.T) { + w := (&EmulatedNode{NodeName: "n1"}).ficrWrites() + for _, want := range []string{"0x10000010 0x00001000", "0x10000014 0x00000100"} { + if !strings.Contains(w, "sysbus WriteDoubleWord "+want) { + t.Errorf("FICR does not write %s:\n%s", want, w) + } + } +} + +// What the firmware stored goes back into flash at the next start of the same +// build, and a different build starts with nothing (#565). +func TestKeptStorageComesBackForTheSameBuild(t *testing.T) { + dir := t.TempDir() + e := &EmulatedNode{Image: writeUF2(t, 0), Dir: dir} + script, err := e.renodeFlash() + if err != nil { + t.Fatal(err) + } + if strings.Contains(script, "0xED000") { + t.Fatalf("nothing was kept, yet storage is loaded:\n%s", script) + } + if err := os.WriteFile(e.keptStorage(), []byte{1}, 0o644); err != nil { + t.Fatal(err) + } + if script, _ = e.renodeFlash(); !strings.Contains(script, e.keptStorage()+" 0xED000") { + t.Errorf("the kept storage is not loaded:\n%s", script) + } + other := &EmulatedNode{Image: writeUF2(t, 0x0), Dir: dir} + if err := os.WriteFile(other.Image, uf2blk(0, []byte{9, 9, 9, 9}), 0o644); err != nil { + t.Fatal(err) + } + if other.keptStorage() == e.keptStorage() { + t.Error("another build would inherit this one's storage") + } +} diff --git a/tools/renode/ficr.py b/tools/renode/ficr.py index 5c5da6f0..2dab97e0 100644 --- a/tools/renode/ficr.py +++ b/tools/renode/ficr.py @@ -19,6 +19,11 @@ **{0x080 + 4 * i: 0x11111111 * (i + 1) for i in range(4)}, # ER[0..3] **{0x090 + 4 * i: 0x22222222 * (i + 1) for i in range(4)}, # IR[0..3] + # Flash geometry, which the SoftDevice checks every flash operation + # against: with these zero it forbids every erase and write. + 0x010: 0x00001000, # CODEPAGESIZE — 4 kB + 0x014: 0x00000100, # CODESIZE — 256 pages + 0x0A0: 0x00000000, # DEVICEADDRTYPE — public 0x0A4: 0xA1B2C3D4, # DEVICEADDR[0] 0x0A8: 0x0000E5F6, # DEVICEADDR[1] diff --git a/tools/renode/peripherals/NRF52840_NVMC.cs b/tools/renode/peripherals/NRF52840_NVMC.cs index 1ab300bf..de105ed1 100644 --- a/tools/renode/peripherals/NRF52840_NVMC.cs +++ b/tools/renode/peripherals/NRF52840_NVMC.cs @@ -36,6 +36,7 @@ // rather than one worth pretending away. // using System; +using System.IO; using Antmicro.Renode.Core; using Antmicro.Renode.Core.Structure.Registers; using Antmicro.Renode.Logging; @@ -47,6 +48,7 @@ public class NRF52840_NVMC : BasicDoubleWordPeripheral, IKnownSize { public NRF52840_NVMC(IMachine machine) : base(machine) { + keepPath = Environment.GetEnvironmentVariable("MESHBENCH_FLASH_KEEP"); DefineRegisters(); } @@ -65,8 +67,17 @@ private void DefineRegisters() .WithReservedBits(1, 31) ; + // Flash going back to read-only is where a write or an erase has + // finished, so that is where the filesystem is kept: see Keep. Registers.Config.Define(this) - .WithEnumField(0, 2, out config, name: "WEN") + .WithEnumField(0, 2, out config, name: "WEN", + changeCallback: (was, now) => + { + if(was != ConfigMode.ReadOnly && now == ConfigMode.ReadOnly) + { + Keep(); + } + }) .WithReservedBits(2, 30) ; @@ -166,8 +177,38 @@ private void Erase(ulong start, ulong size) } } + // Keep writes the filesystem's pages to the node's own directory, so + // what the firmware stored is there when the node next starts (#565). + // Renode builds flash from the image at every start, so without this + // a board kept its preferences and identity only until it was stopped. + // The region is the Adafruit core's InternalFS on the nRF52840, which + // is where every nRF52 MeshCore build keeps its files. + private void Keep() + { + if(string.IsNullOrEmpty(keepPath)) + { + return; + } + try + { + var bytes = sysbus.ReadBytes(KeepStart, (int)KeepSize); + var tmp = keepPath + ".tmp"; + File.WriteAllBytes(tmp, bytes); + File.Copy(tmp, keepPath, true); + File.Delete(tmp); + } + catch(Exception e) + { + this.Log(LogLevel.Warning, "could not keep the filesystem at {0}: {1}", keepPath, e.Message); + } + } + + private readonly string keepPath; private IEnumRegisterField config; + private const ulong KeepStart = 0xED000; + private const ulong KeepSize = 0x7000; + private const ulong PageSize = 0x1000; private const ulong FlashStart = 0x0; private const ulong FlashSize = 0x100000;