3 Commits

Author SHA1 Message Date
a3ea099105 pureboot: the detail reaches become the library's own API
The three things this repo took from under libavr's counter are now over
it, and the local copies fold away. The USART release on a software
link's pins is the library's init contract (its guard here becomes a
deletion, byte-identical images held by the gate); the WDRF routing test
is power::peek_reset_cause().watchdog instead of a hand lookup of the
flag's register; the tsb tiers' baud arithmetic is the public solver.
libavr pin advances over those three additions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:12:35 +02:00
c535c4756c pureboot: the activation countdown in the narrowest type that holds it
The fixed-baud window counted down a uint32 where almost every window fits
24 bits; the countdown now takes avr::uint24_t when the poll budget allows
(the autobaud budget's own choice), uint32 past 16.7M polls — four bytes
off every fixed-baud image on every chip, the full suites green on the
changed window. The README size table is refreshed — its autobaud column
had also gone stale by the no-assembly pass's measurement-loop win, which
nothing gated: sizes.py check-readme now runs as the gate's final stage,
where every tree is freshly built and the table can actually be held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:47:29 +02:00
321ff8a4ee test: the device runners speak the C++ the rest of the repo does
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>
2026-07-28 13:59:03 +02:00
12 changed files with 245 additions and 249 deletions

View File

@@ -22,15 +22,17 @@ if(PROJECT_IS_TOP_LEVEL)
# The behavioral tests drive the real wire protocols over a simavr pty
# (as the host tools do) and actually flash the device. The runners are
# host programs built at configure time against libsimavr; if they or
# Python are missing, only the size tests run.
find_program(_host_cc NAMES cc gcc)
# host programs built at configure time against libsimavr (C++23 — what
# the distribution's compiler speaks in full); if they or Python are
# missing, only the size tests run.
find_program(_host_cxx NAMES c++ g++)
find_package(Python3 COMPONENTS Interpreter)
if(_host_cc AND Python3_FOUND)
if(_host_cxx AND Python3_FOUND)
set(PB_DEVICE ${CMAKE_BINARY_DIR}/pureboot_device)
execute_process(
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.c
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
-I/usr/include/simavr -I/usr/include/simavr/parts
-o ${PB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pureboot_device.cpp
-lsimavr -lsimavrparts -lelf -lutil
RESULT_VARIABLE _pbdev_res ERROR_VARIABLE _pbdev_err)
if(NOT _pbdev_res EQUAL 0)
@@ -40,8 +42,9 @@ if(PROJECT_IS_TOP_LEVEL)
if(LIBAVR_MCU STREQUAL "atmega328p")
set(TSB_DEVICE ${CMAKE_BINARY_DIR}/tsb_device)
execute_process(
COMMAND ${_host_cc} -O2 -I/usr/include/simavr -I/usr/include/simavr/parts
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.c
COMMAND ${_host_cxx} -std=c++23 -Wall -Wextra -O2
-I/usr/include/simavr -I/usr/include/simavr/parts
-o ${TSB_DEVICE} ${CMAKE_CURRENT_SOURCE_DIR}/test/device.cpp
-lsimavr -lsimavrparts -lelf
RESULT_VARIABLE _dev_res ERROR_VARIABLE _dev_err)
if(NOT _dev_res EQUAL 0)

View File

@@ -2,7 +2,7 @@
`master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln`
builds the loaders from the same sources Ninja does, to a **byte-identical
`.text`** — 404 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
`.text`** — 400 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in
its 512-byte section. CMake remains the build system; the solution is here so the
port opens in Studio as its predecessor did.

2
libavr

Submodule libavr updated: 26b80e262d...911a87538f

View File

@@ -28,21 +28,21 @@ it carries the calibration machinery and no clock at all.
| Chip | Flash | Loader at | Link | Stock | Autobaud |
|---|---|---|---|---|---|
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 394 B | 464 B |
| ATtiny25 † | 2 KiB | 0x0600 | software | 398 B | 468 B |
| ATtiny45 † | 4 KiB | 0x0e00 | software | 402 B | 472 B |
| ATtiny85 † | 8 KiB | 0x1e00 | software | 402 B | 472 B |
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 364 B | 478 B |
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 366 B | 482 B |
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 366 B | 482 B |
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 392 B | 468 B |
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 402 B | 478 B |
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 404 B | 482 B |
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 404 B | 482 B |
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 404 B | 482 B |
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 404 B | 482 B |
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 398 B | 476 B |
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 424 B | 502 B |
| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 390 B | 460 B |
| ATtiny25 † | 2 KiB | 0x0600 | software | 394 B | 464 B |
| ATtiny45 † | 4 KiB | 0x0e00 | software | 398 B | 468 B |
| ATtiny85 † | 8 KiB | 0x1e00 | software | 398 B | 468 B |
| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 360 B | 474 B |
| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 362 B | 480 B |
| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 362 B | 480 B |
| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 388 B | 464 B |
| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 398 B | 474 B |
| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B |
| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B |
| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B |
| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B |
| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 394 B | 474 B |
| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 420 B | 500 B |
† No hardware boot section: the host patches the reset vector, and the budget
is 510 bytes, since the slot's last word is the trampoline.
@@ -52,7 +52,7 @@ USART's own pins with the `OSCCAL` trim baked, 510 of its 512 — they alone
carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud
alone carries the calibration loop, a bit-banged link on a USART's pins alone
has to release it (below), and the trim adds its one register write. Without
the trim that build is 504; on the default pins, 502. The flash bank riding
the trim that build is 504; on the default pins, 500. The flash bank riding
in a transfer's selector byte keeps even those chips' addressing the same
16-bit form every other chip uses, which is why they are no longer the
outlier they were.
@@ -551,7 +551,7 @@ Per chip preset, `ctest` runs:
(`tools/make_presets.py --check`), so a hand edit or a generator change
cannot drift the pair apart;
- `pureboot.protocol` — end to end against a simavr device
(`test/pureboot_device.c`: a hardware USART as a pty, or a cycle-timed
(`test/pureboot_device.cpp`: a hardware USART as a pty, or a cycle-timed
GPIO⇄pty bridge for a software-UART build, plus the SPM/NVM module simavr's
tiny cores lack) driven by the real host tool through knock-from-reset,
program + verify of both memories, session reconnect, an external reset

View File

@@ -38,13 +38,6 @@ using dev = avr::device<{.clock = avr::hertz_t{PUREBOOT_CLOCK_HZ}}>;
constexpr avr::baud_t wire_baud{PUREBOOT_BAUD};
#endif
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
consteval std::int16_t wdrf_field()
{
auto reg = std::string_view{avr::hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
return avr::hw::db.field_index(reg, "WDRF");
}
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
// Chips without a hardware boot section — the tinies and the m48s, whose SPM
// runs from anywhere (Atmel-8271 §26) — keep the application's relocated
@@ -174,27 +167,6 @@ constexpr char usart_digit = '0' + PUREBOOT_USART;
constexpr char usart_digit = '0';
#endif
// Release a hardware USART the application may have left enabled onto a
// bit-banged link's pins. A software transmitter drives its TX pin through the
// port register, but while that USART's TXEN is set the USART owns the pin and
// the port write does nothing — the loader would receive and obey yet never
// answer. Writing UCSRnB zero hands the pin back to the port. Guarded on the
// pin actually being a USART's TXD, so a link on non-USART pins emits nothing.
template <char Inst, avr::io::pin Tx>
[[gnu::always_inline]] inline void release_usart_on()
{
if constexpr (avr::uart::has_usart<Inst>())
if constexpr (avr::uart::detail::usart_pin<Inst>("TXD") == Tx)
avr::hw::reg_impl<avr::uart::detail::ureg<Inst, "UCSR#B">()>::write(0);
}
template <avr::io::pin Tx>
[[gnu::always_inline]] inline void release_usarts_on()
{
release_usart_on<'0', Tx>();
release_usart_on<'1', Tx>();
}
template <avr::hertz_t C, avr::baud_t B>
struct hardware_link {
using uart = avr::uart::usart<usart_digit, C, {.baud = B, .max_baud_error = 2.5_pct}>;
@@ -241,7 +213,6 @@ struct software_link {
static void init()
{
avr::init<rx_t, tx_t>();
release_usarts_on<avr::PUREBOOT_TX>();
}
static bool pending()
@@ -275,7 +246,6 @@ struct autobaud_link {
static void init()
{
avr::init<uart>();
release_usarts_on<avr::PUREBOOT_TX>();
}
static std::uint8_t rx()
@@ -351,9 +321,14 @@ consteval std::uint32_t window_polls()
return timeout_seconds * static_cast<std::uint32_t>(dev::clock.hz / link::poll_cycles);
}
// The countdown in the narrowest type that holds it: a fourth byte would
// cost a wider decrement chain at every poll for range most windows never
// use (the autobaud budget makes the same choice).
using window_t = std::conditional_t<window_polls() <= 0xffffff, avr::uint24_t, std::uint32_t>;
bool pending_before_deadline()
{
std::uint32_t polls = window_polls();
window_t polls = window_polls();
do {
if (link::pending())
return true;
@@ -493,7 +468,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at)
#endif
// A watchdog reset belongs to the application, whose watchdog stays forced
// on until it clears WDRF — no activation window in its way.
if (avr::hw::field_impl<wdrf_field()>::test())
if (avr::power::peek_reset_cause().watchdog)
run_app();
link::init();

View File

@@ -7,62 +7,71 @@
// 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 <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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"
}
static avr_t *avr;
static uart_pty_t uart_pty;
static const char *dump_path;
namespace {
static void finish(int sig)
avr_t *avr;
uart_pty_t uart_pty;
const char *dump_path;
[[noreturn]] void finish(int)
{
(void)sig;
if (dump_path) {
FILE *f = fopen(dump_path, "wb");
std::FILE *f = std::fopen(dump_path, "wb");
if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(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) {
fprintf(stderr, "usage: %s <tsb.elf> <boot_base_hex> [flash_dump.bin]\n", argv[0]);
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
return 2;
}
uint32_t boot_base = (uint32_t)strtoul(argv[2], NULL, 0);
dump_path = argc >= 4 ? argv[3] : NULL;
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) {
fprintf(stderr, "device: no ATmega328P core\n");
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.
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
// 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 = {0};
elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) {
fprintf(stderr, "device: cannot read %s\n", argv[1]);
std::println(stderr, "device: cannot read {}", argv[1]);
return 1;
}
// An image that runs past flash end cannot execute on hardware, and a
@@ -70,23 +79,23 @@ int main(int argc, char *argv[])
// the simulation misbehaves in ways that point everywhere but here.
// Refuse it loudly instead.
if (boot_base + fw.flashsize > avr->flashend + 1) {
fprintf(stderr, "device: %u B at 0x%x runs past flash end 0x%x — image does not fit its slot\n",
(unsigned)fw.flashsize, boot_base, avr->flashend);
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;
}
memcpy(avr->flash + boot_base, fw.flash, fw.flashsize);
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 = getenv("TSB_CONFIG");
const char *cfg = std::getenv("TSB_CONFIG");
if (cfg) {
uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
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] = (uint8_t)strtoul(b, NULL, 16);
avr->flash[app_end + i / 2] = static_cast<std::uint8_t>(std::strtoul(b, nullptr, 16));
}
}
@@ -95,18 +104,18 @@ int main(int argc, char *argv[])
// 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.
uint32_t uflags = 0;
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');
printf("TSB_PTY %s\n", uart_pty.pty.slavename);
fflush(stdout);
std::println("TSB_PTY {}", uart_pty.pty.slavename);
std::fflush(stdout);
signal(SIGTERM, finish);
signal(SIGINT, finish);
std::signal(SIGTERM, finish);
std::signal(SIGINT, finish);
for (;;) {
int state = avr_run(avr);
@@ -114,5 +123,4 @@ int main(int argc, char *argv[])
break;
}
finish(0);
return 0;
}

View File

@@ -10,7 +10,7 @@ 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).
that the mute could not happen here at all (test/pureboot_device.cpp).
Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
<baud> <app_bin> <tool_py> <workdir> <link>

View File

@@ -23,16 +23,22 @@
//
// 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 <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <print>
#include <string_view>
#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>
// 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_eeprom.h"
#include "avr_flash.h"
#include "avr_ioport.h"
@@ -41,31 +47,35 @@
#include "sim_elf.h"
#include "sim_io.h"
#include "uart_pty.h"
}
static avr_t *avr;
static uart_pty_t uart_pty;
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;
namespace {
static int parse_link(const char *spec)
avr_t *avr;
uart_pty_t uart_pty;
bool link_software;
char uart_digit = '0';
char sw_rx_port = 'B', sw_tx_port = 'B';
int sw_rx_bit = 0, sw_tx_bit = 1;
char sw_tx_owner = 0; // the USART whose TXD the software link sits on
const char *dump_path;
std::uint32_t reset_pc;
volatile std::sig_atomic_t reset_requested;
int parse_link(std::string_view spec)
{
if (strcmp(spec, "usart0") == 0 || strcmp(spec, "usart1") == 0) {
link_software = 0;
if (spec == "usart0" || spec == "usart1") {
link_software = false;
uart_digit = spec[5];
return 0;
}
if (strncmp(spec, "sw", 2) == 0) {
link_software = 1;
if (spec[2] == '\0')
if (spec.starts_with("sw")) {
link_software = true;
if (spec.size() == 2)
return 0;
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);
int fields =
std::sscanf(spec.data() + 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;
@@ -87,19 +97,19 @@ static int parse_link(const char *spec)
// 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);
avr_flash_t *mega_flash;
int (*mega_flash_ioctl)(avr_io_t *io, std::uint32_t ctl, void *param);
static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
int fixed_flash_ioctl(avr_io_t *io, std::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);
auto z = static_cast<std::uint16_t>(io->avr->data[30] | (io->avr->data[31] << 8));
auto masked = static_cast<std::uint16_t>(z & ~(mega_flash->spm_pagesize - 1));
io->avr->data[30] = static_cast<std::uint8_t>(masked);
io->avr->data[31] = static_cast<std::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);
io->avr->data[30] = static_cast<std::uint8_t>(z);
io->avr->data[31] = static_cast<std::uint8_t>(z >> 8);
return result;
}
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) &&
@@ -114,46 +124,44 @@ static int fixed_flash_ioctl(avr_io_t *io, uint32_t ctl, void *param)
return mega_flash_ioctl(io, ctl, param);
}
static void fix_mega_flash_erase(void)
void fix_mega_flash_erase()
{
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;
if (io->kind && std::string_view{io->kind} == "flash") {
mega_flash = reinterpret_cast<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");
std::println(stderr, "device: no flash module to fix — SPM page erases may misalign");
}
static void request_reset(int sig)
void request_reset(int)
{
(void)sig;
reset_requested = 1;
}
// ------------------------------------------------------------- tiny NVM ---
typedef struct {
struct tiny_nvm_t {
avr_io_t io;
uint8_t buffer[128];
uint8_t used[128]; // a buffer word loads once until erased — like silicon
std::uint8_t buffer[128];
std::uint8_t used[128]; // a buffer word loads once until erased — like silicon
unsigned page;
} tiny_nvm_t;
};
static tiny_nvm_t nvm;
tiny_nvm_t nvm;
static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
{
(void)param;
if (ctl != AVR_IOCTL_FLASH_SPM)
return -1;
tiny_nvm_t *n = (tiny_nvm_t *)io;
auto *n = reinterpret_cast<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);
std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
auto z = static_cast<std::uint16_t>(mcu->data[30] | (mcu->data[31] << 8));
std::uint32_t page_base = static_cast<std::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
@@ -162,46 +170,44 @@ static int nvm_ioctl(avr_io_t *io, uint32_t ctl, void *param)
n->used[offset] = 1;
}
} else if (command == 0x03) { // PGERS
memset(mcu->flash + page_base, 0xff, n->page);
std::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);
std::memset(n->buffer, 0xff, n->page);
std::memset(n->used, 0, n->page);
} else if (command == 0x11) { // CTPB
memset(n->buffer, 0xff, n->page);
memset(n->used, 0, n->page);
std::memset(n->buffer, 0xff, n->page);
std::memset(n->used, 0, n->page);
}
mcu->data[0x57] &= (uint8_t)~0x1f; // the operation completes instantly
mcu->data[0x57] &= static_cast<std::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;
int pty_master = -1;
avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
avr_cycle_count_t bit_cycles;
static int tx_level = 1, tx_active, tx_bit;
static uint8_t tx_shift;
int tx_level = 1, tx_active, tx_bit;
std::uint8_t tx_shift;
static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *param)
avr_cycle_count_t tx_sample(avr_t *, avr_cycle_count_t when, void *)
{
(void)mcu;
(void)param;
if (tx_bit < 8) {
tx_shift = (uint8_t)((tx_shift >> 1) | (tx_level ? 0x80 : 0));
tx_shift = static_cast<std::uint8_t>((tx_shift >> 1) | (tx_level ? 0x80 : 0));
if (++tx_bit < 8)
return when + bit_cycles;
/* The byte is delivered at the stop bit's sampling point (9.5 bit
* times), where a hardware receiver raises its RXC not sooner: a
* host answering before the stop bit would put its start bit on the
* wire while the device is still driving, which the device,
* transmitting, is not watching for. */
// The byte is delivered at the stop bit's sampling point (9.5 bit
// times), where a hardware receiver raises its RXC — not sooner: a
// host answering before the stop bit would put its start bit on the
// wire while the device is still driving, which the device,
// transmitting, is not watching for.
return when + bit_cycles;
}
if (write(pty_master, &tx_shift, 1) != 1)
fprintf(stderr, "device: pty write lost a byte\n");
std::println(stderr, "device: pty write lost a byte");
tx_active = 0;
return 0;
}
@@ -213,9 +219,9 @@ static avr_cycle_count_t tx_sample(avr_t *mcu, avr_cycle_count_t when, void *par
// 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;
avr_uart_t *tx_owner;
static int tx_pin_taken(void)
bool tx_pin_taken()
{
return tx_owner && avr_regbit_get(avr, tx_owner->txen);
}
@@ -225,27 +231,26 @@ static int tx_pin_taken(void)
// 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)
void reset_tx_owner()
{
if (tx_owner)
avr_regbit_clear(avr, tx_owner->txen);
}
static void find_tx_owner(void)
void find_tx_owner()
{
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;
if (io->kind && std::string_view{io->kind} == "uart" &&
reinterpret_cast<avr_uart_t *>(io)->name == sw_tx_owner) {
tx_owner = reinterpret_cast<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);
std::println(stderr, "device: no USART{} to own the software link's TX pin", sw_tx_owner);
}
static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
void tx_hook(avr_irq_t *, std::uint32_t value, void *)
{
(void)irq;
(void)param;
if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere
tx_level = 1;
return;
@@ -254,22 +259,20 @@ static void tx_hook(avr_irq_t *irq, uint32_t value, void *param)
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);
avr_cycle_timer_register(avr, bit_cycles + bit_cycles / 2, tx_sample, nullptr);
}
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;
std::uint8_t rx_queue[8192];
unsigned rx_head, rx_tail; // ring: head = next to send
int rx_active, rx_bit;
std::uint8_t rx_byte;
static void rx_start_next(void);
void rx_start_next();
static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param)
avr_cycle_count_t rx_step(avr_t *, avr_cycle_count_t when, void *)
{
(void)mcu;
(void)param;
if (rx_bit < 8) {
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
rx_bit++;
@@ -285,7 +288,7 @@ static avr_cycle_count_t rx_step(avr_t *mcu, avr_cycle_count_t when, void *param
return 0;
}
static void rx_start_next(void)
void rx_start_next()
{
if (rx_active || rx_head == rx_tail)
return;
@@ -294,7 +297,7 @@ static void rx_start_next(void)
rx_active = 1;
rx_bit = 0;
avr_raise_irq(rx_pin, 0); // start bit
avr_cycle_timer_register(avr, bit_cycles, rx_step, NULL);
avr_cycle_timer_register(avr, bit_cycles, rx_step, nullptr);
}
// A reset abandons whatever the bridge was mid-transfer: bytes still queued
@@ -304,10 +307,10 @@ static void rx_start_next(void)
// 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)
void bridge_reset()
{
avr_cycle_timer_cancel(avr, tx_sample, NULL);
avr_cycle_timer_cancel(avr, rx_step, NULL);
avr_cycle_timer_cancel(avr, tx_sample, nullptr);
avr_cycle_timer_cancel(avr, rx_step, nullptr);
rx_head = rx_tail = 0;
rx_active = 0;
tx_active = 0;
@@ -315,9 +318,9 @@ static void bridge_reset(void)
avr_raise_irq(rx_pin, 1); // idle line
}
static void poll_pty(void)
void poll_pty()
{
uint8_t chunk[256];
std::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);
@@ -332,23 +335,22 @@ static void poll_pty(void)
// ------------------------------------------------------------------ main ---
static void finish(int sig)
[[noreturn]] void finish(int)
{
(void)sig;
if (dump_path) {
FILE *f = fopen(dump_path, "wb");
std::FILE *f = std::fopen(dump_path, "wb");
if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(f);
std::fwrite(avr->flash, 1, avr->flashend + 1, f);
std::fclose(f);
}
avr_eeprom_desc_t ee = {.ee = NULL, .offset = 0, .size = 0};
avr_eeprom_desc_t ee = {.ee = nullptr, .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");
std::snprintf(path, sizeof(path), "%s.eeprom", dump_path);
f = std::fopen(path, "wb");
if (f) {
fwrite(ee.ee, 1, ee.size, f);
fclose(f);
std::fwrite(ee.ee, 1, ee.size, f);
std::fclose(f);
}
}
}
@@ -357,85 +359,87 @@ static void finish(int sig)
_exit(0);
}
} // namespace
int main(int argc, char *argv[])
{
int link_given = 0;
bool link_given = false;
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
if (opt != 'l' || parse_link(optarg) != 0) {
fprintf(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)\n");
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
return 2;
}
link_given = 1;
link_given = true;
}
int args = argc - optind;
if (args < 7 || args > 9) {
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[@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",
argv[0]);
std::print(stderr,
"usage: {} [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
" [reset_hex] [resume_flash]\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",
argv[0]);
return 2;
}
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
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]);
const std::string_view mcu_name = argv[2];
auto base = static_cast<std::uint32_t>(std::strtoul(argv[4], nullptr, 0));
auto page = static_cast<unsigned>(std::atoi(argv[5]));
auto baud = static_cast<unsigned>(std::atoi(argv[6]));
dump_path = argv[7];
int is_mega = strncmp(mcu_name, "atmega", 6) == 0;
const bool is_mega = mcu_name.starts_with("atmega");
if (!link_given)
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1
avr = avr_make_mcu_by_name(mcu_name);
avr = avr_make_mcu_by_name(mcu_name.data());
if (!avr) {
fprintf(stderr, "device: no %s core\n", mcu_name);
std::println(stderr, "device: no {} core", 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
avr->frequency = static_cast<std::uint32_t>(std::strtoul(argv[3], nullptr, 0));
std::memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
if (args > 8) {
// 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]);
std::FILE *f = std::fopen(argv[9], "rb");
if (!f || std::fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
std::println(stderr, "device: cannot read {}", argv[9]);
return 1;
}
fclose(f);
std::fclose(f);
} else {
elf_firmware_t fw = {0};
elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) {
fprintf(stderr, "device: cannot read %s\n", argv[1]);
std::println(stderr, "device: cannot read {}", argv[1]);
return 1;
}
// An image past flash end would smash the simulator's heap and turn
// into phantom peripheral behavior (lessons: believe the size gate
// first) — refuse it loudly instead.
if (base + fw.flashsize > avr->flashend + 1) {
fprintf(stderr, "device: %u B at 0x%x runs past flash end 0x%x — image does not fit its slot\n",
(unsigned)fw.flashsize, base, avr->flashend);
std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
fw.flashsize, base, avr->flashend);
return 1;
}
memcpy(avr->flash + base, fw.flash, fw.flashsize);
std::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 = is_mega && strncmp(mcu_name, "atmega48", 8) != 0;
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0);
const bool boot_section = is_mega && !mcu_name.starts_with("atmega48");
reset_pc = args > 7 ? static_cast<std::uint32_t>(std::strtoul(argv[8], nullptr, 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));
std::uint8_t blank[1024];
std::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;
@@ -449,7 +453,7 @@ int main(int argc, char *argv[])
fix_mega_flash_erase();
} else {
nvm.page = page;
memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
std::memset(nvm.buffer, 0xff, sizeof(nvm.buffer));
nvm.io.kind = "tiny_nvm";
nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io);
@@ -458,37 +462,38 @@ int main(int argc, char *argv[])
if (!link_software) {
// 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;
std::uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, uart_digit);
printf("PB_PTY %s\n", uart_pty.pty.slavename);
std::println("PB_PTY {}", 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);
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), static_cast<unsigned>(sw_rx_bit));
avr_irq_register_notify(
avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_tx_port), static_cast<unsigned>(sw_tx_bit)), tx_hook,
nullptr);
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");
if (openpty(&pty_master, &slave, nullptr, &raw, nullptr) != 0) {
std::println(stderr, "device: openpty failed");
return 1;
}
fcntl(pty_master, F_SETFL, O_NONBLOCK);
printf("PB_PTY %s\n", ttyname(slave));
std::println("PB_PTY {}", ttyname(slave));
}
fflush(stdout);
std::fflush(stdout);
signal(SIGTERM, finish);
signal(SIGINT, finish);
signal(SIGUSR1, request_reset); // an external reset line, for the tests
std::signal(SIGTERM, finish);
std::signal(SIGINT, finish);
std::signal(SIGUSR1, request_reset); // an external reset line, for the tests
long since_poll = 0;
for (;;) {
@@ -500,7 +505,7 @@ int main(int argc, char *argv[])
avr_reset(avr);
avr->pc = reset_pc;
if (!link_software) { // reset restores the pacing hack; re-clear it
uint32_t flags = 0;
std::uint32_t flags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
@@ -524,5 +529,4 @@ int main(int argc, char *argv[])
}
}
finish(0);
return 0;
}

View File

@@ -36,4 +36,10 @@ if ((full)); then
done
fi
# Every tree is freshly built now — the one moment the README's size table
# can be held to what the images measure (a per-preset ctest sees only its
# own chip; the table needs all of them, and ungated it drifts: a
# common-code shave moves every row at once with nothing over budget).
python3 tools/sizes.py check-readme
echo "check: every chip green"

View File

@@ -65,7 +65,7 @@ constexpr std::uint8_t comm_window = 200;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
// The 16-byte device-info block, streamed out on activation.
// clang-format off

View File

@@ -200,7 +200,7 @@ extern "C" [[noreturn]] void tsb_app(); // the application's reset vector: --def
// only the divisor low byte and U2X0 need a store. The solver still does
// the datasheet work; the asserts pin the reset-state assumptions.
{
constexpr auto sol = avr::uart::detail::solve_baud(dev::clock, 115200_Bd);
constexpr auto sol = avr::uart::solve_baud(dev::clock, 115200_Bd);
static_assert(sol.u2x && sol.ubrr < 256, "lean bring-up writes UBRR0L only, with U2X0");
avr::hw::reg<"UBRR0">::write(static_cast<std::uint8_t>(sol.ubrr));
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1));

View File

@@ -65,7 +65,7 @@ constexpr std::uint8_t comm_window = 200;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
constexpr auto baud = avr::uart::solve_baud(16_MHz, 115200_Bd);
// The 16-byte device-info block, streamed out on activation.
// clang-format off