Files
bootloader/test/pureboot_device.c
BlackMark 9cbe2d682d pureboot: every libavr chip — 37 loaders, the m48 class, the 644 geometry
The chip table becomes family blocks covering all 37 targets. The m48s
are a new deployment class: no boot section, so the tiny profile spoken
over the hardware USART — host-patched reset vector, trampoline
hand-over, a 510-byte budget (474 B built), no fuse preflight — while
their RWWSRE store stays the buffer discard (Atmel-8271 §26.2); the
device keys the patch flag and the CPU-halt waits on the curated
boot-section capability and the discard on the RWWSRE bit itself. The
644s' 64 KiB is exactly the 16-bit byte space: plain LPM, byte wire
addresses, 498 B in a 512-byte slot — and their 1 KiB minimum boot
section holds the resident and staging slots together, so self-update
needs no fuse step (the update test's slot pick now keys word-flash on
base >= 64 KiB; base + slot merely touching the boundary stays
byte-addressed). The 1284 joins the 1284P's word-addressed 1 KiB slot at
558 B. BOOT_FUSE gains every boot-sectioned family's ladder and fuse
byte; the planner exercises them all. The sim scaffolding keys
patch-vector-ness instead of the atmega name prefix, the fixture app
picks its clock by family (the tiny25/45/13 builds surfaced the 16 MHz
fallthrough as garbled banners), and the runner's wrapped flash ioctl
performs the m48 discard simavr's no-RWW cores turn into a stray buffer
fill. Sizes across the fleet: 466-504 B megas, 474 B m48s, 498 B 644s,
488-502 B tinies, 558 B 1284s — every chip passing
size/pi/planner/protocol/reloc/update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 18:53:04 +02:00

409 lines
13 KiB
C

// simavr "device" for the pureboot protocol tests, all three chips. Loads
// the boot-linked ELF at the loader base, starts execution there (BOOTRST /
// the patched vector are not what is under test), and exposes the loader's
// serial link as a pty for the real host tool:
//
// - Megas: the hardware USART through simavr's uart_pty.
// - Tinies: an 8N1 bridge between a pty and the GPIO software UART
// (drives PB0, the loader's RX; decodes PB1, its TX), timed against the
// simulated cycle counter.
//
// 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
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
// implements buffer fill, page erase, page write, and CTPB, completing
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
// 'F' command answers with flash bytes — the tests assert transport only.
//
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
// ground-truth cross-check against what the host read back.
#include <fcntl.h>
#include <pty.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include "avr_eeprom.h"
#include "avr_flash.h"
#include "avr_ioport.h"
#include "avr_uart.h"
#include "sim_avr.h"
#include "sim_elf.h"
#include "sim_io.h"
#include "uart_pty.h"
static avr_t *avr;
static uart_pty_t uart_pty;
static int use_uart_pty;
static const char *dump_path;
static uint32_t reset_pc;
static volatile sig_atomic_t reset_requested;
// simavr 1.6's avr_flash PGERS handler erases spm_pagesize bytes starting at
// Z & ~1 instead of the page containing Z (its PGWRT path masks correctly) —
// hardware ignores the in-page bits (§26.8.1), so an erase issued with Z
// anywhere inside the page wipes half the neighbouring page in simulation
// only. Wrap the mega's registered flash ioctl and re-dispatch page erases
// with Z forced to the page boundary; everything else passes through.
//
// A second gap on the boot-section-less m48s: their RWWSRE bit is the
// temporary-buffer discard (Atmel-8271 §26.2/§26.3.1), but the stock model
// gates its RWWSRE branch on AVR_SELFPROG_HAVE_RWW — absent on the m48
// core — so the discard store falls through into the buffer-fill branch and
// plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
// here instead.
static avr_flash_t *mega_flash;
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param);
static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
{
if (ctl == AVR_IOCTL_FLASH_SPM && avr_regbit_get(io->avr, mega_flash->pgers)) {
uint16_t z = (uint16_t)(io->avr->data[30] | (io->avr->data[31] << 8));
uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1));
io->avr->data[30] = (uint8_t)masked;
io->avr->data[31] = (uint8_t)(masked >> 8);
int result = mega_flash_ioctl(io, ctl, param);
io->avr->data[30] = (uint8_t)z;
io->avr->data[31] = (uint8_t)(z >> 8);
return result;
}
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
(io->avr->data[mega_flash->r_spm] & 0x11) == 0x11) { // RWWSRE|SELFPRGEN: the m48 buffer discard
for (int i = 0; i < mega_flash->spm_pagesize / 2; i++) {
mega_flash->tmppage[i] = 0xffff;
mega_flash->tmppage_used[i] = 0;
}
avr_regbit_clear(io->avr, mega_flash->selfprgen);
return 0;
}
return mega_flash_ioctl(io, ctl, param);
}
static void fix_mega_flash_erase(void)
{
for (avr_io_t *io = avr->io_port; io; io = io->next) {
if (io->kind && strcmp(io->kind, "flash") == 0) {
mega_flash = (avr_flash_t *)io;
mega_flash_ioctl = io->ioctl;
io->ioctl = fixed_flash_ioctl;
return;
}
}
fprintf(stderr, "device: no flash module to fix — SPM page erases may misalign\n");
}
static void request_reset(int sig)
{
(void)sig;
reset_requested = 1;
}
// ------------------------------------------------------------- tiny NVM ---
typedef struct {
avr_io_t io;
uint8_t buffer[128];
uint8_t used[128]; // a buffer word loads once until erased — like silicon
unsigned page;
} tiny_nvm_t;
static tiny_nvm_t nvm;
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
{
(void)param;
if (ctl != AVR_IOCTL_FLASH_SPM)
return -1;
tiny_nvm_t *n = (tiny_nvm_t *)io;
avr_t *mcu = io->avr;
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8));
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1);
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
unsigned offset = z & (n->page - 1) & ~1u;
if (!n->used[offset]) { // first write wins until the buffer clears
n->buffer[offset] = mcu->data[0];
n->buffer[offset + 1] = mcu->data[1];
n->used[offset] = 1;
}
} else if (command == 0x03) { // PGERS
memset(mcu->flash + page_base, 0xff, n->page);
} else if (command == 0x05) { // PGWRT: programming only clears bits
for (unsigned i = 0; i < n->page; i++)
mcu->flash[page_base + i] &= n->buffer[i];
memset(n->buffer, 0xff, n->page);
memset(n->used, 0, n->page);
} else if (command == 0x11) { // CTPB
memset(n->buffer, 0xff, n->page);
memset(n->used, 0, n->page);
}
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly
return 0;
}
// ----------------------------------------------------------- GPIO bridge ---
static int pty_master = -1;
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
static avr_cycle_count_t bit_cycles;
static int tx_level = 1, tx_active, tx_bit;
static uint8_t tx_shift;
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param)
{
(void)mcu;
(void)param;
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
if (++tx_bit < 8)
return when + bit_cycles;
if (write(pty_master, &tx_shift, 1) != 1)
fprintf(stderr, "device: pty write lost a byte\n");
tx_active = 0;
return 0;
}
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
{
(void)irq;
(void)param;
int level = value & 1;
if (!tx_active && tx_level == 1 && level == 0) { // start edge
tx_active = 1;
tx_bit = 0;
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, NULL);
}
tx_level = level;
}
static uint8_t rx_queue[8192];
static unsigned rx_head, rx_tail; // ring: head = next to send
static int rx_active, rx_bit;
static uint8_t rx_byte;
static void rx_start_next(void);
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param)
{
(void)mcu;
(void)param;
if (rx_bit < 8) {
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
rx_bit++;
return when + bit_cycles;
}
if (rx_bit == 8) { // stop bit, plus one idle bit of margin
avr_raise_irq(rx_pin, 1);
rx_bit++;
return when + 2 * bit_cycles;
}
rx_active = 0;
rx_start_next();
return 0;
}
static void rx_start_next(void)
{
if (rx_active || rx_head == rx_tail)
return;
rx_byte = rx_queue[rx_head];
rx_head = (rx_head + 1) % sizeof(rx_queue);
rx_active = 1;
rx_bit = 0;
avr_raise_irq(rx_pin, 0); // start bit
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.
// The pending cycle timers must go with the state: avr_reset drops the TX
// output latch, whose falling edge starts a spurious decode before this
// runs, and a stale tx_sample would then interleave with the loader's first
// real answer through the shared shift state, corrupting it.
static void bridge_reset(void)
{
avr_cycle_timer_cancel(avr, tx_sample, NULL);
avr_cycle_timer_cancel(avr, rx_step, NULL);
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];
ssize_t got = read(pty_master, chunk, sizeof(chunk));
for (ssize_t i = 0; i < got; i++) {
unsigned next = (rx_tail + 1) % sizeof(rx_queue);
if (next == rx_head)
break; // full: the host will retry on timeout
rx_queue[rx_tail] = chunk[i];
rx_tail = next;
}
if (got > 0)
rx_start_next();
}
// ------------------------------------------------------------------ main ---
static void finish(int sig)
{
(void)sig;
if (dump_path) {
FILE *f = fopen(dump_path, "wb");
if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(f);
}
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0};
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
char path[512];
snprintf(path, sizeof(path), "%s.eeprom", dump_path);
f = fopen(path, "wb");
if (f) {
fwrite(ee.ee, 1, ee.size, f);
fclose(f);
}
}
}
if (use_uart_pty)
uart_pty_stop(&uart_pty);
_exit(0);
}
int main(int argc, char *argv[])
{
if (argc < 8 || argc > 10) {
fprintf(stderr,
"usage: %s <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
" [reset_hex] [resume_flash]\n"
" reset_hex: reset vector (default: base on the mega, 0 on the tinies)\n"
" resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
" run's dump, for power-fail resume tests\n",
argv[0]);
return 2;
}
const char *mcu_name = argv[2];
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0);
unsigned page = (unsigned)atoi(argv[5]);
unsigned baud = (unsigned)atoi(argv[6]);
dump_path = argv[7];
use_uart_pty = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART
avr = avr_make_mcu_by_name(mcu_name);
if (!avr) {
fprintf(stderr, "device: no %s core\n", mcu_name);
return 1;
}
avr_init(avr);
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0);
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
if (argc > 9) {
// Resume: the full flash image of an interrupted prior run.
FILE *f = fopen(argv[9], "rb");
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
fprintf(stderr, "device: cannot read %s\n", argv[9]);
return 1;
}
fclose(f);
} else {
elf_firmware_t fw = {0};
if (elf_read_firmware(argv[1], &fw) != 0) {
fprintf(stderr, "device: cannot read %s\n", argv[1]);
return 1;
}
memcpy(avr->flash + base, fw.flash, fw.flashsize);
}
// The boot-sectioned megas enter the loader in hardware (BOOTRST, not
// modeled — the argument picks the modeled fuse's target); the tinies
// and the boot-section-less m48s reset to word 0 like silicon — erased
// flash walks up into the loader, and after the host's surgery the
// patched vector routes there.
int boot_section = use_uart_pty && strncmp(mcu_name, "atmega48", 8) != 0;
reset_pc = argc > 8 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
avr->pc = reset_pc;
avr->codeend = avr->flashend;
// Erased EEPROM, as hardware powers up (simavr zeroes it).
uint8_t blank[1024];
memset(blank, 0xff, sizeof(blank));
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0};
if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
seed.ee = blank;
avr_ioctl(avr, AVR_IOCTL_EEPROM_SET, &seed);
}
if (use_uart_pty) {
fix_mega_flash_erase();
// POLL_SLEEP paces an idle-polling loader in host real time (a
// no-hardware CPU-saving hack); clear it so cycles run free.
uint32_t flags = 0;
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);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0');
printf("PB_PTY %s\n", uart_pty.pty.slavename);
} else {
nvm.page = page;
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
nvm.io.kind = "tiny_nvm";
nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io);
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
int slave;
struct termios raw;
cfmakeraw(&raw);
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) {
fprintf(stderr, "device: openpty failed\n");
return 1;
}
fcntl(pty_master, F_SETFL, O_NONBLOCK);
printf("PB_PTY %s\n", ttyname(slave));
}
fflush(stdout);
signal(SIGTERM, finish);
signal(SIGINT, finish);
signal(SIGUSR1, request_reset); // an external reset line, for the tests
long since_poll = 0;
for (;;) {
int state = avr_run(avr);
if (state == cpu_Done || state == cpu_Crashed)
break;
if (reset_requested) {
reset_requested = 0;
avr_reset(avr);
avr->pc = reset_pc;
if (use_uart_pty) { // reset restores the pacing hack; re-clear it
uint32_t flags = 0;
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) {
since_poll = 0;
poll_pty();
}
}
finish(0);
return 0;
}