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>
This commit is contained in:
2026-07-28 13:58:49 +02:00
parent 531ae6c8dc
commit 321ff8a4ee
5 changed files with 211 additions and 196 deletions

View File

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

View File

@@ -551,7 +551,7 @@ Per chip preset, `ctest` runs:
(`tools/make_presets.py --check`), so a hand edit or a generator change (`tools/make_presets.py --check`), so a hand edit or a generator change
cannot drift the pair apart; cannot drift the pair apart;
- `pureboot.protocol` — end to end against a simavr device - `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 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, tiny cores lack) driven by the real host tool through knock-from-reset,
program + verify of both memories, session reconnect, an external reset program + verify of both memories, session reconnect, an external reset

View File

@@ -7,62 +7,71 @@
// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM) // 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 // we dump the flash image to a file for a ground-truth cross-check against
// what the client read back through the bootloader. // what the client read back through the bootloader.
#include <signal.h> #include <csignal>
#include <stdint.h> #include <cstdint>
#include <stdio.h> #include <cstdio>
#include <stdlib.h> #include <cstdlib>
#include <string.h> #include <cstring>
#include <print>
#include <unistd.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_uart.h" #include "avr_uart.h"
#include "sim_avr.h" #include "sim_avr.h"
#include "sim_elf.h" #include "sim_elf.h"
#include "uart_pty.h" #include "uart_pty.h"
}
static avr_t *avr; namespace {
static uart_pty_t uart_pty;
static const char *dump_path;
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) { if (dump_path) {
FILE *f = fopen(dump_path, "wb"); std::FILE *f = std::fopen(dump_path, "wb");
if (f) { if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f); std::fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(f); std::fclose(f);
} }
} }
uart_pty_stop(&uart_pty); uart_pty_stop(&uart_pty);
_exit(0); _exit(0);
} }
} // namespace
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
if (argc < 3) { 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; return 2;
} }
uint32_t boot_base = (uint32_t)strtoul(argv[2], NULL, 0); auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
dump_path = argc >= 4 ? argv[3] : NULL; 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) {
fprintf(stderr, "device: no ATmega328P core\n"); std::println(stderr, "device: no ATmega328P core");
return 1; return 1;
} }
avr_init(avr); avr_init(avr);
avr->frequency = 16000000; avr->frequency = 16000000;
// 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.
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; place it at the boot
// section base ourselves and enter there (BOOTRST is not modelled). // 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) { 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; return 1;
} }
// An image that runs past flash end cannot execute on hardware, and a // 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. // the simulation misbehaves in ways that point everywhere but here.
// Refuse it loudly instead. // Refuse it loudly instead.
if (boot_base + fw.flashsize > avr->flashend + 1) { 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", std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
(unsigned)fw.flashsize, boot_base, avr->flashend); fw.flashsize, boot_base, avr->flashend);
return 1; 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->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 (one page below the boot section) with a
// hex byte string, so the password gate and emergency erase can be tested. // hex byte string, so the password 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 = getenv("TSB_CONFIG"); const char *cfg = std::getenv("TSB_CONFIG");
if (cfg) { 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) { for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
char b[3] = {cfg[i], cfg[i + 1], 0}; 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) // 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 // in real time, distorting protocol timing. Clear it so the loader runs at
// true cycle speed. // true cycle speed.
uint32_t uflags = 0; std::uint32_t uflags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
uflags &= ~AVR_UART_FLAG_POLL_SLEEP; uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
uart_pty_init(avr, &uart_pty); uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0'); uart_pty_connect(&uart_pty, '0');
printf("TSB_PTY %s\n", uart_pty.pty.slavename); std::println("TSB_PTY {}", uart_pty.pty.slavename);
fflush(stdout); std::fflush(stdout);
signal(SIGTERM, finish); std::signal(SIGTERM, finish);
signal(SIGINT, finish); std::signal(SIGINT, finish);
for (;;) { for (;;) {
int state = avr_run(avr); int state = avr_run(avr);
@@ -114,5 +123,4 @@ int main(int argc, char *argv[])
break; break;
} }
finish(0); 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. 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 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 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> Usage: pbmute.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
<baud> <app_bin> <tool_py> <workdir> <link> <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 // On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
// ground-truth cross-check against what the host read back. // 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 <fcntl.h>
#include <pty.h> #include <pty.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h> #include <termios.h>
#include <unistd.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_eeprom.h"
#include "avr_flash.h" #include "avr_flash.h"
#include "avr_ioport.h" #include "avr_ioport.h"
@@ -41,31 +47,35 @@
#include "sim_elf.h" #include "sim_elf.h"
#include "sim_io.h" #include "sim_io.h"
#include "uart_pty.h" #include "uart_pty.h"
}
static avr_t *avr; namespace {
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;
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) { if (spec == "usart0" || spec == "usart1") {
link_software = 0; link_software = false;
uart_digit = spec[5]; uart_digit = spec[5];
return 0; return 0;
} }
if (strncmp(spec, "sw", 2) == 0) { if (spec.starts_with("sw")) {
link_software = 1; link_software = true;
if (spec[2] == '\0') if (spec.size() == 2)
return 0; return 0;
char owner = 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) { if (fields == 4 || fields == 5) {
sw_tx_owner = owner; sw_tx_owner = owner;
return 0; 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 // 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 // plants whatever Z/R1:R0 happen to hold. Perform the silicon's discard
// here instead. // here instead.
static avr_flash_t *mega_flash; avr_flash_t *mega_flash;
static int (*mega_flash_ioctl)(avr_io_t *io, uint32_t ctl, void *param); 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)) { 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)); auto z = static_cast<std::uint16_t>(io->avr->data[30] | (io->avr->data[31] << 8));
uint16_t masked = (uint16_t)(z & ~(mega_flash->spm_pagesize - 1)); auto masked = static_cast<std::uint16_t>(z & ~(mega_flash->spm_pagesize - 1));
io->avr->data[30] = (uint8_t)masked; io->avr->data[30] = static_cast<std::uint8_t>(masked);
io->avr->data[31] = (uint8_t)(masked >> 8); io->avr->data[31] = static_cast<std::uint8_t>(masked >> 8);
int result = mega_flash_ioctl(io, ctl, param); int result = mega_flash_ioctl(io, ctl, param);
io->avr->data[30] = (uint8_t)z; io->avr->data[30] = static_cast<std::uint8_t>(z);
io->avr->data[31] = (uint8_t)(z >> 8); io->avr->data[31] = static_cast<std::uint8_t>(z >> 8);
return result; return result;
} }
if (ctl == AVR_IOCTL_FLASH_SPM && !(mega_flash->flags & AVR_SELFPROG_HAVE_RWW) && 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); 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) { for (avr_io_t *io = avr->io_port; io; io = io->next) {
if (io->kind && strcmp(io->kind, "flash") == 0) { if (io->kind && std::string_view{io->kind} == "flash") {
mega_flash = (avr_flash_t *)io; mega_flash = reinterpret_cast<avr_flash_t *>(io);
mega_flash_ioctl = io->ioctl; mega_flash_ioctl = io->ioctl;
io->ioctl = fixed_flash_ioctl; io->ioctl = fixed_flash_ioctl;
return; 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; reset_requested = 1;
} }
// ------------------------------------------------------------- tiny NVM --- // ------------------------------------------------------------- tiny NVM ---
typedef struct { struct tiny_nvm_t {
avr_io_t io; avr_io_t io;
uint8_t buffer[128]; std::uint8_t buffer[128];
uint8_t used[128]; // a buffer word loads once until erased — like silicon std::uint8_t used[128]; // a buffer word loads once until erased — like silicon
unsigned page; 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) if (ctl != AVR_IOCTL_FLASH_SPM)
return -1; return -1;
tiny_nvm_t *n = (tiny_nvm_t *)io; auto *n = reinterpret_cast<tiny_nvm_t *>(io);
avr_t *mcu = io->avr; avr_t *mcu = io->avr;
uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
uint16_t z = (uint16_t)(mcu->data[30] | (mcu->data[31] << 8)); auto z = static_cast<std::uint16_t>(mcu->data[30] | (mcu->data[31] << 8));
uint32_t page_base = (uint32_t)(z & ~(n->page - 1)) % (mcu->flashend + 1); 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 if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
unsigned offset = z & (n->page - 1) & ~1u; unsigned offset = z & (n->page - 1) & ~1u;
if (!n->used[offset]) { // first write wins until the buffer clears 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; n->used[offset] = 1;
} }
} else if (command == 0x03) { // PGERS } 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 } else if (command == 0x05) { // PGWRT: programming only clears bits
for (unsigned i = 0; i < n->page; i++) for (unsigned i = 0; i < n->page; i++)
mcu->flash[page_base + i] &= n->buffer[i]; mcu->flash[page_base + i] &= n->buffer[i];
memset(n->buffer, 0xff, n->page); std::memset(n->buffer, 0xff, n->page);
memset(n->used, 0, n->page); std::memset(n->used, 0, n->page);
} else if (command == 0x11) { // CTPB } else if (command == 0x11) { // CTPB
memset(n->buffer, 0xff, n->page); std::memset(n->buffer, 0xff, n->page);
memset(n->used, 0, 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; return 0;
} }
// ----------------------------------------------------------- GPIO bridge --- // ----------------------------------------------------------- GPIO bridge ---
static int pty_master = -1; int pty_master = -1;
static avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty avr_irq_t *rx_pin; // the loader's RX (PB0), driven from the pty
static avr_cycle_count_t bit_cycles; avr_cycle_count_t bit_cycles;
static int tx_level = 1, tx_active, tx_bit; int tx_level = 1, tx_active, tx_bit;
static uint8_t tx_shift; 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) { 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) if (++tx_bit < 8)
return when + bit_cycles; return when + bit_cycles;
/* The byte is delivered at the stop bit's sampling point (9.5 bit // 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 // times), where a hardware receiver raises its RXC — not sooner: a
* host answering before the stop bit would put its start bit on the // host answering before the stop bit would put its start bit on the
* wire while the device is still driving, which the device, // wire while the device is still driving, which the device,
* transmitting, is not watching for. */ // transmitting, is not watching for.
return when + bit_cycles; return when + bit_cycles;
} }
if (write(pty_master, &tx_shift, 1) != 1) 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; tx_active = 0;
return 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: // 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 // 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. // 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); 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 // 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 // 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. // 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) if (tx_owner)
avr_regbit_clear(avr, tx_owner->txen); 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) 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) { if (io->kind && std::string_view{io->kind} == "uart" &&
tx_owner = (avr_uart_t *)io; reinterpret_cast<avr_uart_t *>(io)->name == sw_tx_owner) {
tx_owner = reinterpret_cast<avr_uart_t *>(io);
reset_tx_owner(); reset_tx_owner();
return; 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 if (tx_pin_taken()) { // the USART holds the line; the port write goes nowhere
tx_level = 1; tx_level = 1;
return; 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 if (!tx_active && tx_level == 1 && level == 0) { // start edge
tx_active = 1; tx_active = 1;
tx_bit = 0; 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; tx_level = level;
} }
static uint8_t rx_queue[8192]; std::uint8_t rx_queue[8192];
static unsigned rx_head, rx_tail; // ring: head = next to send unsigned rx_head, rx_tail; // ring: head = next to send
static int rx_active, rx_bit; int rx_active, rx_bit;
static uint8_t rx_byte; 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) { if (rx_bit < 8) {
avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1); avr_raise_irq(rx_pin, (rx_byte >> rx_bit) & 1);
rx_bit++; 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; return 0;
} }
static void rx_start_next(void) void rx_start_next()
{ {
if (rx_active || rx_head == rx_tail) if (rx_active || rx_head == rx_tail)
return; return;
@@ -294,7 +297,7 @@ static void rx_start_next(void)
rx_active = 1; rx_active = 1;
rx_bit = 0; rx_bit = 0;
avr_raise_irq(rx_pin, 0); // start bit 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 // 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 // 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 // runs, and a stale tx_sample would then interleave with the loader's first
// real answer through the shared shift state, corrupting it. // 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, tx_sample, nullptr);
avr_cycle_timer_cancel(avr, rx_step, NULL); avr_cycle_timer_cancel(avr, rx_step, nullptr);
rx_head = rx_tail = 0; rx_head = rx_tail = 0;
rx_active = 0; rx_active = 0;
tx_active = 0; tx_active = 0;
@@ -315,9 +318,9 @@ static void bridge_reset(void)
avr_raise_irq(rx_pin, 1); // idle line 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)); ssize_t got = read(pty_master, chunk, sizeof(chunk));
for (ssize_t i = 0; i < got; i++) { for (ssize_t i = 0; i < got; i++) {
unsigned next = (rx_tail + 1) % sizeof(rx_queue); unsigned next = (rx_tail + 1) % sizeof(rx_queue);
@@ -332,23 +335,22 @@ static void poll_pty(void)
// ------------------------------------------------------------------ main --- // ------------------------------------------------------------------ main ---
static void finish(int sig) [[noreturn]] void finish(int)
{ {
(void)sig;
if (dump_path) { if (dump_path) {
FILE *f = fopen(dump_path, "wb"); std::FILE *f = std::fopen(dump_path, "wb");
if (f) { if (f) {
fwrite(avr->flash, 1, avr->flashend + 1, f); std::fwrite(avr->flash, 1, avr->flashend + 1, f);
fclose(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) { if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &ee) == 0 && ee.ee && ee.size) {
char path[512]; char path[512];
snprintf(path, sizeof(path), "%s.eeprom", dump_path); std::snprintf(path, sizeof(path), "%s.eeprom", dump_path);
f = fopen(path, "wb"); f = std::fopen(path, "wb");
if (f) { if (f) {
fwrite(ee.ee, 1, ee.size, f); std::fwrite(ee.ee, 1, ee.size, f);
fclose(f); std::fclose(f);
} }
} }
} }
@@ -357,85 +359,87 @@ static void finish(int sig)
_exit(0); _exit(0);
} }
} // namespace
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
int link_given = 0; bool link_given = false;
for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) { for (int opt; (opt = getopt(argc, argv, "l:")) != -1;) {
if (opt != 'l' || parse_link(optarg) != 0) { 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; return 2;
} }
link_given = 1; link_given = true;
} }
int args = argc - optind; int args = argc - optind;
if (args < 7 || args > 9) { if (args < 7 || args > 9) {
fprintf(stderr, std::print(stderr,
"usage: %s [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>" "usage: {} [-l link] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
" [reset_hex] [resume_flash]\n" " [reset_hex] [resume_flash]\n"
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n" " -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
" them); default: the chip's own\n" " them); default: the chip's own\n"
" reset_hex: reset vector (default: base with a boot section, else 0)\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" " resume_flash: raw full-flash image loaded instead of the ELF — a prior\n"
" run's dump, for power-fail resume tests\n", " run's dump, for power-fail resume tests\n",
argv[0]); argv[0]);
return 2; return 2;
} }
argv += optind - 1; // argv[1] is the ELF again, whatever was parsed argv += optind - 1; // argv[1] is the ELF again, whatever was parsed
const char *mcu_name = argv[2]; const std::string_view mcu_name = argv[2];
uint32_t base = (uint32_t)strtoul(argv[4], NULL, 0); auto base = static_cast<std::uint32_t>(std::strtoul(argv[4], nullptr, 0));
unsigned page = (unsigned)atoi(argv[5]); auto page = static_cast<unsigned>(std::atoi(argv[5]));
unsigned baud = (unsigned)atoi(argv[6]); auto baud = static_cast<unsigned>(std::atoi(argv[6]));
dump_path = argv[7]; 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) if (!link_given)
link_software = !is_mega; // the chips' natural links: USART0, or PB0/PB1 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) { if (!avr) {
fprintf(stderr, "device: no %s core\n", mcu_name); std::println(stderr, "device: no {} core", mcu_name);
return 1; return 1;
} }
avr_init(avr); avr_init(avr);
avr->frequency = (uint32_t)strtoul(argv[3], NULL, 0); avr->frequency = static_cast<std::uint32_t>(std::strtoul(argv[3], nullptr, 0));
memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased std::memset(avr->flash, 0xff, avr->flashend + 1); // real flash powers up erased
if (args > 8) { if (args > 8) {
// Resume: the full flash image of an interrupted prior run. // Resume: the full flash image of an interrupted prior run.
FILE *f = fopen(argv[9], "rb"); std::FILE *f = std::fopen(argv[9], "rb");
if (!f || fread(avr->flash, 1, avr->flashend + 1, f) == 0) { if (!f || std::fread(avr->flash, 1, avr->flashend + 1, f) == 0) {
fprintf(stderr, "device: cannot read %s\n", argv[9]); std::println(stderr, "device: cannot read {}", argv[9]);
return 1; return 1;
} }
fclose(f); std::fclose(f);
} else { } else {
elf_firmware_t fw = {0}; elf_firmware_t fw{};
if (elf_read_firmware(argv[1], &fw) != 0) { 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; return 1;
} }
// An image past flash end would smash the simulator's heap and turn // An image past flash end would smash the simulator's heap and turn
// into phantom peripheral behavior (lessons: believe the size gate // into phantom peripheral behavior (lessons: believe the size gate
// first) — refuse it loudly instead. // first) — refuse it loudly instead.
if (base + fw.flashsize > avr->flashend + 1) { 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", std::println(stderr, "device: {} B at {:#x} runs past flash end {:#x} — image does not fit its slot",
(unsigned)fw.flashsize, base, avr->flashend); fw.flashsize, base, avr->flashend);
return 1; 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 // The boot-sectioned megas enter the loader in hardware (BOOTRST, not
// modeled — the argument picks the modeled fuse's target); the tinies // modeled — the argument picks the modeled fuse's target); the tinies
// and the boot-section-less m48s reset to word 0 like silicon — erased // 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 // flash walks up into the loader, and after the host's surgery the
// patched vector routes there. // patched vector routes there.
int boot_section = is_mega && strncmp(mcu_name, "atmega48", 8) != 0; const bool boot_section = is_mega && !mcu_name.starts_with("atmega48");
reset_pc = args > 7 ? (uint32_t)strtoul(argv[8], NULL, 0) : (boot_section ? base : 0); reset_pc = args > 7 ? static_cast<std::uint32_t>(std::strtoul(argv[8], nullptr, 0)) : (boot_section ? base : 0);
avr->pc = reset_pc; avr->pc = reset_pc;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
// Erased EEPROM, as hardware powers up (simavr zeroes it). // Erased EEPROM, as hardware powers up (simavr zeroes it).
uint8_t blank[1024]; std::uint8_t blank[1024];
memset(blank, 0xff, sizeof(blank)); std::memset(blank, 0xff, sizeof(blank));
avr_eeprom_desc_t seed = {.ee = blank, .offset = 0, .size = 0}; 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)) { if (avr_ioctl(avr, AVR_IOCTL_EEPROM_GET, &seed) == 0 && seed.size <= sizeof(blank)) {
seed.ee = blank; seed.ee = blank;
@@ -449,7 +453,7 @@ int main(int argc, char *argv[])
fix_mega_flash_erase(); fix_mega_flash_erase();
} else { } else {
nvm.page = page; 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.kind = "tiny_nvm";
nvm.io.ioctl = nvm_ioctl; nvm.io.ioctl = nvm_ioctl;
avr_register_io(avr, &nvm.io); avr_register_io(avr, &nvm.io);
@@ -458,37 +462,38 @@ int main(int argc, char *argv[])
if (!link_software) { if (!link_software) {
// POLL_SLEEP paces an idle-polling loader in host real time (a // POLL_SLEEP paces an idle-polling loader in host real time (a
// no-hardware CPU-saving hack); clear it so cycles run free. // 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); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP; flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
uart_pty_init(avr, &uart_pty); uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, uart_digit); 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 { } else {
bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly bit_cycles = (avr->frequency + baud / 2) / baud; // matches uart.hpp's own rounding exactly
if (sw_tx_owner) if (sw_tx_owner)
find_tx_owner(); find_tx_owner();
rx_pin = avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ(sw_rx_port), (unsigned)sw_rx_bit); 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), (unsigned)sw_tx_bit), tx_hook, avr_irq_register_notify(
NULL); 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 avr_raise_irq(rx_pin, 1); // idle line
int slave; int slave;
struct termios raw; struct termios raw;
cfmakeraw(&raw); cfmakeraw(&raw);
if (openpty(&pty_master, &slave, NULL, &raw, NULL) != 0) { if (openpty(&pty_master, &slave, nullptr, &raw, nullptr) != 0) {
fprintf(stderr, "device: openpty failed\n"); std::println(stderr, "device: openpty failed");
return 1; return 1;
} }
fcntl(pty_master, F_SETFL, O_NONBLOCK); 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); std::signal(SIGTERM, finish);
signal(SIGINT, finish); std::signal(SIGINT, finish);
signal(SIGUSR1, request_reset); // an external reset line, for the tests std::signal(SIGUSR1, request_reset); // an external reset line, for the tests
long since_poll = 0; long since_poll = 0;
for (;;) { for (;;) {
@@ -500,7 +505,7 @@ int main(int argc, char *argv[])
avr_reset(avr); avr_reset(avr);
avr->pc = reset_pc; avr->pc = reset_pc;
if (!link_software) { // reset restores the pacing hack; re-clear it 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); avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS(uart_digit), &flags);
flags &= ~AVR_UART_FLAG_POLL_SLEEP; flags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags); avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS(uart_digit), &flags);
@@ -524,5 +529,4 @@ int main(int argc, char *argv[])
} }
} }
finish(0); finish(0);
return 0;
} }