From 6594c1b04499f2be8ca38a65e0983719be932464 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Mon, 20 Jul 2026 19:37:10 +0200 Subject: [PATCH] pureboot: harden the sim device runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cancel the GPIO bridge's cycle timers with the state they drive: avr_reset drops the TX latch, whose falling edge starts a spurious decode before bridge_reset runs, and the stale sampler then interleaves with the loader's first real answer through the shared shift state — the first post-reset replies came back corrupted and the knock retries burned the activation window into the application. Wrap the mega's registered flash ioctl to re-dispatch page erases with Z masked to the page boundary: simavr's PGERS handler erases spm_pagesize bytes from Z & ~1 (its PGWRT path masks correctly), wiping the neighbouring page when Z sits past the page start, which hardware permits (§26.8.1). Model the write-once temporary buffer in the tiny NVM module — silicon refuses a second load per word until the buffer clears, and a last-write- wins model masks real firmware bugs. Optional arguments select the reset vector (the mega's fuse profiles) and a raw flash image to resume from (power-fail tests re-enter a dumped state). Co-Authored-By: Claude Fable 5 --- test/pureboot_device.c | 93 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/test/pureboot_device.c b/test/pureboot_device.c index 245edbf..596c0e1 100644 --- a/test/pureboot_device.c +++ b/test/pureboot_device.c @@ -43,6 +43,43 @@ static const char *dump_path; static uint32_t reset_pc; static volatile sig_atomic_t reset_requested; +// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at +// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) — +// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z +// anywhere inside the page wipes half the neighbouring page in simulation +// only. Wrap the mega's registered flash ioctl and re-dispatch page erases +// with Z forced to the page boundary; everything else passes through. +static avr_flash_t *mega_flash; +static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param); + +static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param) +{ + if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) { + uint16_t z = (uint16_t)(io->avr->data[30] | (io->avr->data[31] << 8)); + uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1)); + io->avr->data[30] = (uint8_t)masked; + io->avr->data[31] = (uint8_t)(masked >> 8); + int result = mega_flash_ioctl(io, ctl, param); + io->avr->data[30] = (uint8_t)z; + io->avr->data[31] = (uint8_t)(z >> 8); + return result; + } + return mega_flash_ioctl(io, ctl, param); +} + +static void fix_mega_flash_erase(void) +{ + for (avr_io_t *io = avr->io_port; io; io = io->next) { + if (io->kind && strcmp(io->kind, "flash") == 0) { + mega_flash = (avr_flash_t *)io; + mega_flash_ioctl = io->ioctl; + io->ioctl = fixed_flash_ioctl; + return; + } + } + fprintf(stderr, "device: no flash module to fix — SPM page erases may misalign\n"); +} + static void request_reset(int sig) { (void)sig; @@ -54,6 +91,7 @@ static void request_reset(int sig) typedef struct { avr_io_t io; uint8_t buffer[128]; + uint8_t used[128]; // a buffer word loads once until erased — like silicon unsigned page; } tiny_nvm_t; @@ -71,16 +109,21 @@ static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param) uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1); if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0 unsigned offset = z & (n->page - 1) & ~1u; - n->buffer[offset] = mcu->data[0]; - n->buffer[offset + 1] = mcu->data[1]; + if (!n->used[offset]) { // first write wins until the buffer clears + n->buffer[offset] = mcu->data[0]; + n->buffer[offset + 1] = mcu->data[1]; + n->used[offset] = 1; + } } else if (command == 0x03) { // PGERS memset(mcu->flash + page_base, 0xff, n->page); } else if (command == 0x05) { // PGWRT: programming only clears bits for (unsigned i = 0; i < n->page; i++) mcu->flash[page_base + i] &= n->buffer[i]; memset(n->buffer, 0xff, n->page); + memset(n->used, 0, n->page); } else if (command == 0x11) { // CTPB memset(n->buffer, 0xff, n->page); + memset(n->used, 0, n->page); } mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly return 0; @@ -162,8 +205,14 @@ static void rx_start_next(void) // A reset abandons whatever the bridge was mid-transfer: bytes still queued // for a chip that no longer has the context to receive them meaningfully, // and a decode in progress on a TX line the reset may have already changed. +// The pending cycle timers must go with the state: avr_reset drops the TX +// output latch, whose falling edge starts a spurious decode before this +// runs, and a stale tx_sample would then interleave with the loader's first +// real answer through the shared shift state, corrupting it. static void bridge_reset(void) { + avr_cycle_timer_cancel(avr, tx_sample, NULL); + avr_cycle_timer_cancel(avr, rx_step, NULL); rx_head = rx_tail = 0; rx_active = 0; tx_active = 0; @@ -215,8 +264,14 @@ static void finish(int sig) int main(int argc, char *argv[]) { - if (argc != 8) { - fprintf(stderr, "usage: %s \n", argv[0]); + if (argc < 8 || argc > 10) { + fprintf(stderr, + "usage: %s " + " [reset_hex] [resume_flash]\n" + " reset_hex: reset vector (default: base on the mega, 0 on the tinies)\n" + " resume_flash: raw full-flash image loaded instead of the ELF — a prior\n" + " run's dump, for power-fail resume tests\n", + argv[0]); return 2; } const char *mcu_name = argv[2]; @@ -235,16 +290,27 @@ int main(int argc, char *argv[]) avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0); memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased - elf_firmware_t fw = {0}; - if (elf_read_firmware(argv[1], &fw) != 0) { - fprintf(stderr, "device: cannot read %s\n", argv[1]); - return 1; + if (argc > 9) { + // Resume: the full flash image of an interrupted prior run. + FILE *f = fopen(argv[9], "rb"); + if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) { + fprintf(stderr, "device: cannot read %s\n", argv[9]); + return 1; + } + fclose(f); + } else { + elf_firmware_t fw = {0}; + if (elf_read_firmware(argv[1], &fw) != 0) { + fprintf(stderr, "device: cannot read %s\n", argv[1]); + return 1; + } + memcpy(avr->flash + base, fw.flash, fw.flashsize); } - memcpy(avr->flash + base, fw.flash, fw.flashsize); - // The mega enters the loader in hardware (BOOTRST, not modeled); the - // tinies reset to word 0 like silicon — erased flash walks up into the - // loader, and after the host's surgery the patched vector routes there. - reset_pc = use_uart_pty ? base : 0; + // The mega enters the loader in hardware (BOOTRST, not modeled — the + // argument picks the modeled fuse's target); the tinies reset to word 0 + // like silicon — erased flash walks up into the loader, and after the + // host's surgery the patched vector routes there. + reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (use_uart_pty ? base : 0); avr->pc = reset_pc; avr->codeend = avr->flashend; @@ -258,6 +324,7 @@ int main(int argc, char *argv[]) } if (use_uart_pty) { + fix_mega_flash_erase(); // POLL_SLEEP paces an idle-polling loader in host real time (a // no-hardware CPU-saving hack); clear it so cycles run free. uint32_t flags = 0;