pureboot: review-pass fixes to the host tool and device runner

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 10:45:19 +02:00
parent 9145d1ec22
commit cbe6acff13
2 changed files with 20 additions and 3 deletions

View File

@@ -193,6 +193,8 @@ def load_image(path):
"""Raw binary, or Intel HEX by extension (.hex/.ihx/.ihex).""" """Raw binary, or Intel HEX by extension (.hex/.ihx/.ihex)."""
data = open(path, "rb").read() data = open(path, "rb").read()
if not path.lower().endswith((".hex", ".ihx", ".ihex")): if not path.lower().endswith((".hex", ".ihx", ".ihex")):
if not data:
raise Error(f"{path}: empty image")
return data return data
memory = {} memory = {}
for number, line in enumerate(data.decode("ascii", "replace").splitlines(), 1): 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): def op_erase_flash(loader):
"""0xff over the whole application area, page 0 first — an interrupted """0xff over the whole application area. Order does not matter here —
erase leaves the entry word blank and the chip still boots the loader.""" 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) blank = bytes([0xFF] * loader.info.page)
for address in range(0, loader.info.base, loader.info.page): for address in range(0, loader.info.base, loader.info.page):
loader.write_page(address, blank) loader.write_page(address, blank)

View File

@@ -159,6 +159,18 @@ static void rx_start_next(void)
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL); 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) static void poll_pty(void)
{ {
uint8_t chunk[256]; uint8_t chunk[256];
@@ -262,7 +274,7 @@ int main(int argc, char *argv[])
nvm.io.ioctl = nvm_ioctl; nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io); 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); 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_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), 1), tx_hook, NULL);
avr_raise_irq(rx_pin, 1); // idle line 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); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP; flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &flags);
} else {
bridge_reset();
} }
} }
if (!use_uart_pty && ++since_poll >= 2000) { if (!use_uart_pty && ++since_poll >= 2000) {