test: the simavr device stops dumping a flash image nobody reads

Every protocol run wrote the simulated flash to /tmp/tsb_dump.bin on exit -
outside the repository, from four tests that run in parallel into one file - for
a cross-check no test made. The dump goes, and the device's clock and page size
are named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BlackMarkandClaude Opus 5.5 committed 2026-10-23 21:41:55 +02:00
1 parent aafaf25a47
commit bc671618a7
2 files changed
+20 -31

No files matched your search

+17 -26
View File
@@ -1,12 +1,8 @@
// simavr "device" for the TSB bootloader: load the boot-linked ELF into the // simavr "device" for the TSB bootloader: load the boot-linked ELF into the
// ATmega328P boot section, enter it (BOOTRST is not modelled, so we set PC to // ATmega328P boot section, enter it (BOOTRST is not modelled, so PC is set to
// the boot base, exactly as simavr's own board_simduino does), and expose // the boot base, exactly as simavr's own board_simduino does), and expose
// UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then // UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then
// speaks the TSB protocol over that pty and actually flashes the device. // speaks the TSB protocol over that pty and actually flashes the device.
//
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM)
// we dump the flash image to a file for a ground-truth cross-check against
// what the client read back through the bootloader.
#include <array> #include <array>
#include <csignal> #include <csignal>
#include <cstdint> #include <cstdint>
@@ -28,19 +24,16 @@ extern "C" {
namespace { namespace {
// What the loaders are built for: tsb/*.cpp's device clock and the chip's SPM
// page, which is also the config page sitting directly below the boot section.
constexpr std::uint32_t clock_hz = 16'000'000;
constexpr std::uint32_t page_bytes = 128;
avr_t *avr; avr_t *avr;
uart_pty_t uart_pty; uart_pty_t uart_pty;
const char *dump_path;
[[noreturn]] void finish(int) [[noreturn]] void finish(int)
{ {
if (dump_path) {
std::FILE *f = std::fopen(dump_path, "wb");
if (f) {
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
std::fclose(f);
}
}
uart_pty_stop(&uart_pty); uart_pty_stop(&uart_pty);
_exit(0); _exit(0);
} }
@@ -49,12 +42,11 @@ const char *dump_path;
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
if (argc < 3) { if (argc != 3) {
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]); std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex>", argv[0]);
return 2; return 2;
} }
auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0)); const auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
dump_path = argc >= 4 ? argv[3] : nullptr;
avr = avr_make_mcu_by_name("atmega328p"); avr = avr_make_mcu_by_name("atmega328p");
if (!avr) { if (!avr) {
@@ -62,14 +54,14 @@ int main(int argc, char *argv[])
return 1; return 1;
} }
avr_init(avr); avr_init(avr);
avr->frequency = 16000000; avr->frequency = clock_hz;
// Real flash powers up erased (0xff); the app region must look erased // Real flash powers up erased (0xff); the app region must look erased
// before the bootloader programs it. // before the bootloader programs it.
std::memset(avr->flash, 0xff, avr->flashend + 1); std::memset(avr->flash, 0xff, avr->flashend + 1);
// simavr's ELF loader flattens the flash base to 0 (it expects an app at // simavr's ELF loader flattens the flash base to 0 (it expects an app at
// 0x0), but it hands back the boot code in fw.flash; place it at the boot // 0x0), but it hands back the boot code in fw.flash, so it is placed at the
// section base ourselves and enter there (BOOTRST is not modelled). // boot section base by hand.
elf_firmware_t fw{}; elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) { if (elf_read_firmware(argv[1], &fw) != 0) {
std::println(stderr, "device: cannot read {}", argv[1]); std::println(stderr, "device: cannot read {}", argv[1]);
@@ -88,12 +80,11 @@ int main(int argc, char *argv[])
avr->pc = boot_base; avr->pc = boot_base;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
// Optional: seed the config page (one page below the boot section) with a // Optional: seed the config page with a hex byte string, so the password
// hex byte string, so the password gate and emergency erase can be tested. // gate and emergency erase can be tested.
// Layout: [appjump lo][appjump hi][timeout][password...][0xff]. // Layout: [appjump lo][appjump hi][timeout][password...][0xff].
const char *cfg = std::getenv("TSB_CONFIG"); if (const char *cfg = std::getenv("TSB_CONFIG")) {
if (cfg) { const auto app_end = boot_base - page_bytes;
std::uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) { for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
const std::array pair{cfg[i], cfg[i + 1], '\0'}; const std::array pair{cfg[i], cfg[i + 1], '\0'};
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16)); avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(pair.data(), nullptr, 16));
@@ -119,7 +110,7 @@ int main(int argc, char *argv[])
std::signal(SIGINT, finish); std::signal(SIGINT, finish);
for (;;) { for (;;) {
int state = avr_run(avr); const auto state = avr_run(avr);
if (state == cpu_Done || state == cpu_Crashed) { if (state == cpu_Done || state == cpu_Crashed) {
break; break;
} }
+3 -5
View File
@@ -23,14 +23,13 @@ class Device:
page (via the device's TSB_CONFIG hook) so the password gate and emergency page (via the device's TSB_CONFIG hook) so the password gate and emergency
erase are exercisable.""" erase are exercisable."""
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", config=None): def __init__(self, binary, elf, boot_base, config=None):
env = dict(os.environ) env = dict(os.environ)
if config is not None: if config is not None:
env["TSB_CONFIG"] = config env["TSB_CONFIG"] = config
self.proc = subprocess.Popen( self.proc = subprocess.Popen(
[binary, elf, boot_base, dump], [binary, elf, boot_base],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env) stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
self.dump = dump
self.pty = None self.pty = None
deadline = time.time() + 5 deadline = time.time() + 5
while time.time() < deadline: while time.time() < deadline:
@@ -166,7 +165,6 @@ class Host:
self.s.write(bytes([CONFIRM])) self.s.write(bytes([CONFIRM]))
self._expect(CONFIRM, "emergency mainloop ready") self._expect(CONFIRM, "emergency mainloop ready")
# A wrong password byte hangs the loader, still draining the line. Two # A wrong password byte hangs the loader, still draining the line. Two
# things must not happen: it must not activate, and it must not fall # things must not happen: it must not activate, and it must not fall
# through to the emergency erase - a byte the gate has already refused # through to the emergency erase - a byte the gate has already refused
@@ -236,7 +234,7 @@ def scenario_emergency(host):
def scenario_wrong_password(host): def scenario_wrong_password(host):
"""A wrong password byte neither activates the loader nor opens the """A wrong password byte neither activates the loader nor opens the
emergency erase behind it - the oracle carries a dedicated fix for the emergency erase behind it - the oracle carries a dedicated fix for the
second, and nothing here exercised either half.""" second."""
check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply") check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply")
check(host.say(0x00) == b"", "a 0 byte after it does not request the erase") check(host.say(0x00) == b"", "a 0 byte after it does not request the erase")
check(host.say(CONFIRM) == b"", "and neither does a confirm") check(host.say(CONFIRM) == b"", "and neither does a confirm")