pureboot: the pin axis, and the mute it was hiding
Pins move the image for exactly one reason — a bit-banged link on a USART's own
pins has to release that USART — and the matrix said outright that they were no
axis, so the tightest configuration in the space was one nothing built. Not
subtly, either: the 1284's slot ends at flash end, so that build does not merely
exceed the size test's limit, it fails to link. pureboot_{sw,autobaud}_on_usart
{0,1} are gate points in both matrix modes now, and the exhaustive sweep carries
the pins across its whole cross product. The hand-measured table is the gate's
output: 506 B of 512 for the 1284 autobaud on USART0's pins, 504 on USART1's.
pureboot.mute drives the defect itself — an application hands over with USART0
still enabled and the loader on those pins must still answer. Reaching that
needed the runner to know an enabled USART owns its TxD, which simavr does not
model at all: it wires a USART through IRQs and never takes the pin from the
port. It also brings UCSRnB up with TXEN already set where silicon clears the
register, so the runner restores the reset value for the USART it models — the
mute must come from the application, not from power-on. The fixture stays
silent, since nothing is listening on the USART it brings up.
test_handshake.py, written where no gate could run it, is pureboot.handshake.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
// reset reaches those loaders through the patched vector (or the runner
|
||||
// models BOOTRST), so the application owes them nothing.
|
||||
//
|
||||
// PUREBOOT_HANDOVER drops the listening and jumps straight in, leaving the
|
||||
// USART enabled behind it — the hand-over state a loader bit-banging on that
|
||||
// USART's own pins has to survive.
|
||||
//
|
||||
// The fixture speaks the deployment its loader was built for: the same
|
||||
// PUREBOOT_* defines configure it, and without them it assumes the stock
|
||||
// deployment (the crystal/RC clock table below, the chip's natural link).
|
||||
@@ -64,16 +68,29 @@ struct link {
|
||||
{
|
||||
tx_t::write(static_cast<std::uint8_t>(c));
|
||||
}
|
||||
// The loader sits in the top slot — 512 bytes on every chip. The jump
|
||||
// takes a word address, which is what makes the >64 KiB chips' entry
|
||||
// reachable through a 16-bit pointer at all.
|
||||
static void enter_loader()
|
||||
{
|
||||
constexpr std::uint32_t slot = 512;
|
||||
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
||||
}
|
||||
|
||||
[[noreturn]] static void idle()
|
||||
{
|
||||
// 'L' hands back to the loader in the top slot — 512 bytes on every
|
||||
// chip. The jump takes a word address, which is what makes the
|
||||
// >64 KiB chips' entry reachable through a 16-bit pointer at all.
|
||||
constexpr std::uint32_t slot = 512;
|
||||
#if defined(PUREBOOT_HANDOVER)
|
||||
// Hand back at once, with this USART still enabled — the state that
|
||||
// leaves a bit-banged loader on its pins mute unless the loader
|
||||
// releases it. Unconditional because there is no command wire to
|
||||
// wait on: that loader's link is the pins, not this peripheral.
|
||||
enter_loader();
|
||||
__builtin_unreachable();
|
||||
#else
|
||||
for (;;) {
|
||||
auto command = tx_t::read_blocking();
|
||||
if (command == 'L')
|
||||
reinterpret_cast<void (*)()>(static_cast<std::uint16_t>((avr::hw::db.mem.flash_size - slot) / 2))();
|
||||
enter_loader();
|
||||
// 'D' leaves every word of the SPM page buffer dirty, so that a
|
||||
// following 'L' enters the loader with the buffer it never clears.
|
||||
if (command == 'D') {
|
||||
@@ -82,6 +99,7 @@ struct link {
|
||||
tx('D');
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,8 +127,13 @@ struct link<C, false> {
|
||||
int main()
|
||||
{
|
||||
avr::init<typename link<dev::clock>::tx_t>();
|
||||
#if !defined(PUREBOOT_HANDOVER)
|
||||
link<dev::clock>::tx('A');
|
||||
link<dev::clock>::tx('P');
|
||||
link<dev::clock>::tx('P');
|
||||
#endif
|
||||
// The hand-over fixture stays silent: nothing is listening on the USART it
|
||||
// brings up — the loader it hands to speaks those pins directly — so its
|
||||
// banner would be a write into a peer that does not exist.
|
||||
link<dev::clock>::idle();
|
||||
}
|
||||
|
||||
73
test/pbmute.py
Normal file
73
test/pbmute.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hand-over with a USART left enabled on the loader's own pins.
|
||||
|
||||
A software or autobaud link deployed on a USART's TxD is mute if an
|
||||
application hands over with that USART still enabled: TXEN keeps the USART
|
||||
owning the pin, so the bit-banged transmitter's port writes go nowhere and the
|
||||
loader receives and obeys while answering nothing. The link's init releases it.
|
||||
|
||||
The state is reached the way silicon reaches it — an application that sets up
|
||||
its USART and jumps in with no reset between, so nothing clears UCSRnB for it.
|
||||
The pin ownership itself is modelled by the device runner: simavr wires a
|
||||
USART through IRQs alone and never takes the pin from the port, so without
|
||||
that the mute could not happen here at all (test/pureboot_device.c).
|
||||
|
||||
Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <app_bin> <tool_py> <workdir> <link>
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(f"FAIL: {message}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir, link = sys.argv[1:]
|
||||
page, baud = int(page), int(baud)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
import pureboot as pb
|
||||
|
||||
if "@" not in link:
|
||||
fail(f"the link {link} names no owning USART — nothing would be under test")
|
||||
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
dump = os.path.join(workdir, "dump.bin")
|
||||
|
||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, link=link)
|
||||
try:
|
||||
port = pb.Port(device.pty, baud)
|
||||
loader = pb.Loader(port)
|
||||
loader.connect(25)
|
||||
resident = loader.info.version
|
||||
|
||||
# Install the fixture and let it take over. It brings up the USART
|
||||
# that owns these pins and jumps straight back in.
|
||||
pb.op_flash(loader, app_bin, erase=False, verify=True)
|
||||
loader.run_application()
|
||||
|
||||
# The loader is running again with that USART enabled behind it. Only
|
||||
# the release makes it audible; without it the connect times out.
|
||||
loader = pb.Loader(port)
|
||||
try:
|
||||
loader.connect(25)
|
||||
except pb.Error as error:
|
||||
fail(f"the loader never answered after the hand-over — the USART still owns its TX pin ({error})")
|
||||
if loader.info.version != resident:
|
||||
fail(f"identity changed across the hand-over: {resident} then {loader.info.version}")
|
||||
|
||||
# Answering is not enough: it has to still be a working loader.
|
||||
pb.verify_pages(loader, pb.plan_flash(open(app_bin, "rb").read(), loader.info))
|
||||
port.close()
|
||||
finally:
|
||||
device.stop()
|
||||
print("pbmute: a loader on a USART's own pins answers after a hand-over that left it enabled")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -10,7 +10,9 @@
|
||||
//
|
||||
// The link follows the chip's natural default (USART0 on the megas, the
|
||||
// software UART on PB0/PB1 elsewhere) unless -l overrides it: `-l usart1`
|
||||
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins.
|
||||
// for the second instance, `-l sw:B5,B1` for a software build's RX,TX pins,
|
||||
// and `-l sw:D0,D1@0` where those pins are a USART's own — see the pin
|
||||
// ownership the bridge models below.
|
||||
//
|
||||
// simavr's tiny cores decode the SPM opcode but attach no NVM module — SPM
|
||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||
@@ -46,6 +48,7 @@ static int link_software;
|
||||
static char uart_digit = '0';
|
||||
static char sw_rx_port = 'B', sw_tx_port = 'B';
|
||||
static int sw_rx_bit = 0, sw_tx_bit = 1;
|
||||
static char sw_tx_owner = 0; // the USART whose TXD the software link sits on
|
||||
static const char *dump_path;
|
||||
static uint32_t reset_pc;
|
||||
static volatile sig_atomic_t reset_requested;
|
||||
@@ -61,8 +64,12 @@ static int parse_link(const char *spec)
|
||||
link_software = 1;
|
||||
if (spec[2] == '\0')
|
||||
return 0;
|
||||
if (sscanf(spec + 2, ":%c%d,%c%d", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit) == 4)
|
||||
char owner = 0;
|
||||
int fields = sscanf(spec + 2, ":%c%d,%c%d@%c", &sw_rx_port, &sw_rx_bit, &sw_tx_port, &sw_tx_bit, &owner);
|
||||
if (fields == 4 || fields == 5) {
|
||||
sw_tx_owner = owner;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -198,10 +205,50 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A USART owns its TxD pin whenever its transmitter is enabled, and the port
|
||||
// register cannot drive it (§20.2 / Atmel-8271 §19.2) — which is why a
|
||||
// bit-banged link deployed on those pins is mute until it clears UCSRnB.
|
||||
// simavr wires a USART entirely through IRQs and never touches the port pin
|
||||
// model, so the ownership does not exist there and the mute cannot happen:
|
||||
// supply it, or the very state this models is untestable. The link spec's
|
||||
// trailing @n names the USART; without one the pins are nobody's.
|
||||
static avr_uart_t *tx_owner;
|
||||
|
||||
static int tx_pin_taken(void)
|
||||
{
|
||||
return tx_owner && avr_regbit_get(avr, tx_owner->txen);
|
||||
}
|
||||
|
||||
// simavr leaves TXEN set in UCSRnB out of reset, where silicon clears the
|
||||
// whole register (§20.11.3) — which would hand the pin to a USART no code has
|
||||
// enabled, making a freshly reset chip mute for reasons hardware does not
|
||||
// have. Reset it the way the datasheet does, so the ownership starts from
|
||||
// nobody's and only an application that really enables the USART takes it.
|
||||
static void reset_tx_owner(void)
|
||||
{
|
||||
if (tx_owner)
|
||||
avr_regbit_clear(avr, tx_owner->txen);
|
||||
}
|
||||
|
||||
static void find_tx_owner(void)
|
||||
{
|
||||
for (avr_io_t *io = avr->io_port; io; io = io->next)
|
||||
if (io->kind && strcmp(io->kind, "uart") == 0 && ((avr_uart_t *)io)->name == sw_tx_owner) {
|
||||
tx_owner = (avr_uart_t *)io;
|
||||
reset_tx_owner();
|
||||
return;
|
||||
}
|
||||
fprintf(stderr, "device: no USART%c to own the software link's TX pin\n", sw_tx_owner);
|
||||
}
|
||||
|
||||
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
|
||||
{
|
||||
(void)irq;
|
||||
(void)param;
|
||||
if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere
|
||||
tx_level = 1;
|
||||
return;
|
||||
}
|
||||
int level = value & 1;
|
||||
if (!tx_active && tx_level == 1 && level == 0) { // start edge
|
||||
tx_active = 1;
|
||||
@@ -324,7 +371,8 @@ int main(int argc, char *argv[])
|
||||
fprintf(stderr,
|
||||
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||
" [reset_hex] [resume_flash]\n"
|
||||
" -l link: usart0 | usart1 | sw[:B0,B1] (RX,TX); default: the chip's own\n"
|
||||
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
||||
" them); default: the chip's own\n"
|
||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
||||
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
|
||||
" run's dump, for power-fail resume tests\n",
|
||||
@@ -410,6 +458,8 @@ int main(int argc, char *argv[])
|
||||
printf("PB_PTY %s\n", uart_pty.pty.slavename);
|
||||
} else {
|
||||
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
|
||||
if (sw_tx_owner)
|
||||
find_tx_owner();
|
||||
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit);
|
||||
avr_irq_register_notify(avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), (unsigned)sw_tx_bit), tx_hook,
|
||||
NULL);
|
||||
@@ -447,6 +497,7 @@ int main(int argc, char *argv[])
|
||||
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
|
||||
} else {
|
||||
bridge_reset();
|
||||
reset_tx_owner();
|
||||
}
|
||||
}
|
||||
if (link_software && ++since_poll >= 2000) {
|
||||
|
||||
@@ -8,7 +8,8 @@ this, ~60 reboots/s of UART-reset garbage in which a stray 0x2b reads as a
|
||||
prompt — otherwise spins the tool forever. Regression for that hang, plus a
|
||||
control that a well-behaved loader still connects.
|
||||
|
||||
Stdlib only; run with `python test/test_handshake.py`. Not yet wired into ctest.
|
||||
Stdlib only, no device: host-tool logic, so it runs on every chip's preset
|
||||
beside pureboot.planner.
|
||||
"""
|
||||
import importlib.util
|
||||
import pathlib
|
||||
|
||||
Reference in New Issue
Block a user