From cbe6acff13fe5815e31f68e620a60cb94eb51f88 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Mon, 20 Jul 2026 10:45:19 +0200 Subject: [PATCH] pureboot: review-pass fixes to the host tool and device runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pureboot.py: reject an empty image file with a clear error instead of an IndexError deep in the vector-surgery planner; tighten the erase docstring (order is irrelevant there — every target byte is the same value, unlike a real flash where page 0 must go last). pureboot_device.c: the GPIO bridge's bit_cycles used plain truncating division where the firmware computes its own bit period with round-to-nearest (uart.hpp: (Clock.hz + Baud.bd/2)/Baud.bd) — one cycle off per bit on both tinies, harmless in practice but needless drift against a firmware built to a different constant. Matched exactly. Also clear the queued-bytes/decode-in-progress bridge state on the test-only reset signal, so a future reset-mid-transfer scenario can't feed a freshly reset chip bytes queued for its previous life. Co-Authored-By: Claude Fable 5 --- pureboot/pureboot.py | 7 +++++-- test/pureboot_device.c | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pureboot/pureboot.py b/pureboot/pureboot.py index 98befab..441164c 100644 --- a/pureboot/pureboot.py +++ b/pureboot/pureboot.py @@ -193,6 +193,8 @@ def load_image(path): """Raw binary, or Intel HEX by extension (.hex/.ihx/.ihex).""" data = open(path, "rb").read() if not path.lower().endswith((".hex", ".ihx", ".ihex")): + if not data: + raise Error(f"{path}: empty image") return data memory = {} for number, line in enumerate(data.decode("ascii", "replace").splitlines(), 1): @@ -284,8 +286,9 @@ def covered(pages, skip_blank): def op_erase_flash(loader): - """0xff over the whole application area, page 0 first — an interrupted - erase leaves the entry word blank and the chip still boots the loader.""" + """0xff over the whole application area. Order does not matter here — + every target byte is the same value — and a blank word 0 still falls + through to the loader, so an interruption is harmless.""" blank = bytes([0xFF] * loader.info.page) for address in range(0, loader.info.base, loader.info.page): loader.write_page(address, blank) diff --git a/test/pureboot_device.c b/test/pureboot_device.c index d14352e..245edbf 100644 --- a/test/pureboot_device.c +++ b/test/pureboot_device.c @@ -159,6 +159,18 @@ static void rx_start_next(void) avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL); } +// 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. +static void bridge_reset(void) +{ + rx_head = rx_tail = 0; + rx_active = 0; + tx_active = 0; + tx_level = 1; + avr_raise_irq(rx_pin, 1); // idle line +} + static void poll_pty(void) { uint8_t chunk[256]; @@ -262,7 +274,7 @@ int main(int argc, char *argv[]) nvm.io.ioctl = nvm_ioctl; avr_register_io(avr, &nvm.io); - bit_cycles = avr->frequency / baud; + bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 0); avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL); avr_raise_irq(rx_pin, 1); // idle line @@ -297,6 +309,8 @@ int main(int argc, char *argv[]) avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags); flags &= ~AVR_UART_FLAG_POLL_SLEEP; avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags); + } else { + bridge_reset(); } } if (!use_uart_pty && ++since_poll >= 2000) {