diff --git a/qemu/py32f071.c b/qemu/py32f071.c index 9145fd2..8247796 100644 --- a/qemu/py32f071.c +++ b/qemu/py32f071.c @@ -986,6 +986,9 @@ OBJECT_DECLARE_SIMPLE_TYPE(PY25Q16State, PY25Q16) #define PY25Q16_SIZE (2 * MiB) +/* Page-program buffer size. Programming wraps within a page; see PY25Q16_CMD_PP. */ +#define PY25Q16_PAGE_SIZE 0x100 + enum { PY25Q16_CMD_NONE = 0, 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->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; case PY25Q16_CMD_SE: diff --git a/tools/test_flash_persist.py b/tools/test_flash_persist.py index 8870291..cd315b4 100755 --- a/tools/test_flash_persist.py +++ b/tools/test_flash_persist.py @@ -43,11 +43,20 @@ QMP = "/tmp/uvk5-persist-test.sock" # 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 # 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 = [ - ("mr/vfo attrs", 0x008100, 0x20), # 1024 MR + 7 VFO attributes, 2 bytes each - ("settings", 0x00A100, 0x20), # the settings block + ("mr/vfo attrs", 0x008000, 0x20), # 1024 MR + 7 VFO attributes, 2 bytes each + ("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: def __init__(self, path): @@ -128,7 +137,8 @@ def sha(path): def snapshot(path): 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(): @@ -182,6 +192,17 @@ def main(): else: 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. print("\nbooting again from the same file") proc, qmp = boot(image)