pureboot_device and the tsb device, C until now, rewritten in C++23 with every modeled behavior intact — the PGERS Z-mask and m48-discard ioctl wraps, the GPIO bridge's timing and pacing, the tiny NVM's write-once buffer, pin ownership, and the PB_PTY/TSB_PTY lines the harnesses parse. The one linkage fact worth a comment: simavr's parts headers (uart_pty.h) carry no C++ guards where its core headers do, so those includes sit in an extern "C" block. Warning-clean at -Wall -Wextra on the build line; the full protocol suites on all four sim-driven chips prove the conversion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
127 lines
4.2 KiB
C++
127 lines
4.2 KiB
C++
// 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
|
|
// 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
|
|
// 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 <csignal>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <print>
|
|
|
|
#include <unistd.h>
|
|
|
|
// The parts headers (uart_pty.h) carry no C++ linkage guards of their own,
|
|
// unlike simavr's core headers — the block covers both harmlessly.
|
|
extern "C" {
|
|
#include "avr_uart.h"
|
|
#include "sim_avr.h"
|
|
#include "sim_elf.h"
|
|
#include "uart_pty.h"
|
|
}
|
|
|
|
namespace {
|
|
|
|
avr_t *avr;
|
|
uart_pty_t uart_pty;
|
|
const char *dump_path;
|
|
|
|
[[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);
|
|
_exit(0);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
if (argc < 3) {
|
|
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
|
|
return 2;
|
|
}
|
|
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");
|
|
if (!avr) {
|
|
std::println(stderr, "device: no ATmega328P core");
|
|
return 1;
|
|
}
|
|
avr_init(avr);
|
|
avr->frequency = 16000000;
|
|
// Real flash powers up erased (0xff); the app region must look erased
|
|
// before the bootloader programs it.
|
|
std::memset(avr->flash, 0xff, avr->flashend + 1);
|
|
|
|
// 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
|
|
// section base ourselves and enter there (BOOTRST is not modelled).
|
|
elf_firmware_t fw{};
|
|
if (elf_read_firmware(argv[1], &fw) != 0) {
|
|
std::println(stderr, "device: cannot read {}", argv[1]);
|
|
return 1;
|
|
}
|
|
// An image that runs past flash end cannot execute on hardware, and a
|
|
// naive copy of it would smash the heap beyond avr->flash — after which
|
|
// the simulation misbehaves in ways that point everywhere but here.
|
|
// Refuse it loudly instead.
|
|
if (boot_base + fw.flashsize > avr->flashend + 1) {
|
|
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
|
|
fw.flashsize, boot_base, avr->flashend);
|
|
return 1;
|
|
}
|
|
std::memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
|
|
avr->pc = boot_base;
|
|
avr->codeend = avr->flashend;
|
|
|
|
// Optional: seed the config page (one page below the boot section) with a
|
|
// hex byte string, so the password gate and emergency erase can be tested.
|
|
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
|
|
const char *cfg = std::getenv("TSB_CONFIG");
|
|
if (cfg) {
|
|
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) {
|
|
char b[3] = {cfg[i], cfg[i + 1], 0};
|
|
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(b, nullptr, 16));
|
|
}
|
|
}
|
|
|
|
// POLL_SLEEP makes simavr usleep(1) on every status-register read while the
|
|
// UART is idle — a host-CPU-saving hack that models no hardware and paces a
|
|
// tight-polling loader (one that releases TX between bytes, as one-wire does)
|
|
// in real time, distorting protocol timing. Clear it so the loader runs at
|
|
// true cycle speed.
|
|
std::uint32_t uflags = 0;
|
|
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
|
|
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
|
|
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
|
|
|
|
uart_pty_init(avr, &uart_pty);
|
|
uart_pty_connect(&uart_pty, '0');
|
|
std::println("TSB_PTY {}", uart_pty.pty.slavename);
|
|
std::fflush(stdout);
|
|
|
|
std::signal(SIGTERM, finish);
|
|
std::signal(SIGINT, finish);
|
|
|
|
for (;;) {
|
|
int state = avr_run(avr);
|
|
if (state == cpu_Done || state == cpu_Crashed)
|
|
break;
|
|
}
|
|
finish(0);
|
|
}
|