Wrap page-program writes within their 256-byte page

Real SPI NOR latches only the low address bits into its page buffer, so a program
burst that runs past the page boundary continues at the start of the same page. The
model incremented the address straight through instead.

Consequence: the firmware issues a 512-byte burst at 0x008F00 inside a single CS
assertion (measured -- the CS never drops mid-burst), which spilled into 0x009000.
That is the per-band VFO frequency area in eeprom_compat.c's map, so stored
frequencies were zeroed. RADIO_ConfigureChannel only substitutes the band's lower
limit when it reads 0xFFFFFFFF, so a stored 0 was taken literally and clamped to
BX4819_band1_lower -- which is why every typed frequency reverted to 18.000 MHz.

Verified: writes now align to sectors (0x8000-0x9000 and 0xA000-0xB000) and an
instrumented build records zero stores into 0x9000-0x90D6, where before it was
overwritten on every boot.

test_flash_persist.py had encoded the bug in its expectations: it watched 0x008100
and 0x00A100, which were only ever written *because* of the missing wrap. Those move
to 0x008000/0x00A000, and a MUST_NOT_CHANGE guard on the frequency area now fails if
a write spills there again.

keypad_test.py still passes.
This commit is contained in:
mckero committed 2026-08-28 14:41:20 +01:00
1 parent 0b879227e5
commit da1ad7e1e6
2 files changed
+46 -4

No files matched your search

+22 -1
View File
@@ -986,6 +986,9 @@ OBJECT_DECLARE_SIMPLE_TYPE(PY25Q16State, PY25Q16)
#define PY25Q16_SIZE (2 * MiB) #define PY25Q16_SIZE (2 * MiB)
/* Page-program buffer size. Programming wraps within a page; see PY25Q16_CMD_PP. */
#define PY25Q16_PAGE_SIZE 0x100
enum { enum {
PY25Q16_CMD_NONE = 0, PY25Q16_CMD_NONE = 0,
PY25Q16_CMD_READ = 0x03, PY25Q16_CMD_READ = 0x03,
@@ -1067,7 +1070,25 @@ static uint8_t py25q16_xfer(void *opaque, uint8_t out)
s->data[s->addr % PY25Q16_SIZE] &= out; s->data[s->addr % PY25Q16_SIZE] &= out;
s->dirty = true; s->dirty = true;
} }
s->addr++; /*
* Page program wraps within its 256-byte page: a burst that runs past the
* page boundary continues at the start of the same page rather than
* spilling into the next one. Real SPI NOR works this way because the
* chip latches only the low address bits into its page buffer.
*
* Without this the model let one transaction walk straight through, and a
* 512-byte burst at 0x008F00 overwrote 0x009000 -- which is the VFO
* frequency area in eeprom_compat.c's map. The stored frequency became
* zero, RADIO_ConfigureChannel only substitutes the band's lower limit for
* 0xFFFFFFFF, so the frequency was taken as 0 and clamped to
* BX4819_band1_lower. That is why a typed frequency always reverted to
* 18 MHz.
*
* Measured: the firmware really does send 512 bytes inside a single CS
* assertion here, so the wrap has to be modelled rather than assumed away.
*/
s->addr = (s->addr & ~(PY25Q16_PAGE_SIZE - 1))
| ((s->addr + 1) & (PY25Q16_PAGE_SIZE - 1));
return 0xff; return 0xff;
case PY25Q16_CMD_SE: case PY25Q16_CMD_SE:
+24 -3
View File
@@ -43,11 +43,20 @@ QMP = "/tmp/uvk5-persist-test.sock"
# guessed. The mapping is in App/driver/eeprom_compat.c: these are *flash* addresses, # guessed. The mapping is in App/driver/eeprom_compat.c: these are *flash* addresses,
# not the EEPROM addresses the settings code uses, and an earlier version of this test # not the EEPROM addresses the settings code uses, and an earlier version of this test
# watched EEPROM offsets by mistake and reported "same" for everything. # watched EEPROM offsets by mistake and reported "same" for everything.
#
# These were 0x008100 and 0x00A100 while page-program wrapping was missing from the
# model: writes ran past the page boundary and landed a page high. With wrapping in
# place the firmware's writes align to the sector, as they do on real hardware.
WATCH = [ WATCH = [
("mr/vfo attrs", 0x008100, 0x20), # 1024 MR + 7 VFO attributes, 2 bytes each ("mr/vfo attrs", 0x008000, 0x20), # 1024 MR + 7 VFO attributes, 2 bytes each
("settings", 0x00A100, 0x20), # the settings block ("settings", 0x00A000, 0x20), # the settings block
] ]
# Must stay untouched: this is where per-band VFO frequencies live. The missing page
# wrap let a 512-byte burst at 0x008F00 spill into it, zeroing stored frequencies so
# every typed frequency reverted to BX4819_band1_lower (18 MHz).
MUST_NOT_CHANGE = [("vfo frequencies", 0x009000, 0xD6)]
class Qmp: class Qmp:
def __init__(self, path): def __init__(self, path):
@@ -128,7 +137,8 @@ def sha(path):
def snapshot(path): def snapshot(path):
data = open(path, "rb").read() data = open(path, "rb").read()
return {name: data[off:off + length] for name, off, length in WATCH} regions = WATCH + MUST_NOT_CHANGE
return {name: data[off:off + length] for name, off, length in regions}
def main(): def main():
@@ -182,6 +192,17 @@ def main():
else: else:
print("PASS the settings and channel regions were written") print("PASS the settings and channel regions were written")
# The frequency area must be left alone. A page-program burst that fails to
# wrap spills into it and zeroes stored frequencies, which is what made a
# typed frequency always revert to 18 MHz.
spilled = [n for n, _, _ in MUST_NOT_CHANGE if after[n] != before[n]]
if spilled:
failures.append(
f"{', '.join(spilled)} was overwritten -- a write spilled past a "
"page boundary into the VFO frequency area")
else:
print("PASS the VFO frequency area was not overwritten")
# A second boot must see what the first one left behind. # A second boot must see what the first one left behind.
print("\nbooting again from the same file") print("\nbooting again from the same file")
proc, qmp = boot(image) proc, qmp = boot(image)