The pin crosses libavr's phase-6 close and the guideline sweep behind it. All 37 chips green, 43 tests each, the README size table matching every built image, and all 13602 flash images byte-identical to the previous pin. The bump broke one gate and exposed another as ornamental. `check_unit.cmake` matched the autobaud loader's measured unit by the symbol `unit_E`; libavr's rule-46 sweep renamed the member to `m_unit`, which the mangling spells `6m_unitE`. On the RAM-home chips the check went red and said so. On the GPIOR chips it went green - the branch that asserts the unit is *not* in RAM passes on an empty match, and an empty match is what a stale regex returns for every image. Both branches mean something again. `tools/check.sh` ran the 37-chip loop under `set -e`, so the first red chip ended the gate and the 36 behind it were never built - a stale size canary on attiny13 would have been an alibi for every loader after it. It accumulates now and fails at the end naming every red preset, which is the shape libavr's own check.sh carries and the reason it carries it. The port's own sweep, verified by byte identity: the four TSB tiers' 16-byte info block is `std::to_array` rather than an extent written beside the sixteen elements the compiler can count, the three-member serial and loader configs break one member per line, the turn-around loops are braced, and the test fixture's config pair is a deduced `std::array` (rules 36, 40, 34). Two comments stop narrating how the code came to be and one stops citing a repro at a path it left two phases ago (rules 12, 13). pureboot's identity stamp stays the raw array rule 36 bans, and now says why: its reads must fold to immediates because the bytes are in program memory and a formed address is dereferenced as data space. As a `std::array` the read loop stopped unrolling and emitted exactly that - measured at +8 B and a wrong answer on the wire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
338 lines
9.9 KiB
C++
338 lines
9.9 KiB
C++
// TinySafeBoot on libavr - tier 1: pure, idiomatic C++.
|
|
//
|
|
// A serial flash bootloader for the ATmega328P boot section, reimplementing the
|
|
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
|
|
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
|
|
// a config-page activation timeout, the password gate, emergency erase, and
|
|
// config/flash/EEPROM read-write. This variant is written for clarity -
|
|
// well-factored functions, no compiler-specific size hacks, no inline assembly.
|
|
// The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
|
|
// are libavr's to handle; the only attribute is the naked reset entry that
|
|
// stands in for the absent C runtime.
|
|
|
|
#include <libavr/libavr.hpp>
|
|
|
|
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry
|
|
|
|
using namespace avr::literals;
|
|
namespace spm = avr::spm;
|
|
namespace ee = avr::eeprom;
|
|
|
|
using dev = avr::device<{.clock = 16_MHz}>;
|
|
// One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
|
|
// 115200 at 16 MHz lands +2.1 % off, past the receiver-tolerance table the
|
|
// solver holds rates to - the oracle's own deployment has run there for a
|
|
// decade, so the override states that it is meant.
|
|
using serial_t = dev::uart0<{
|
|
.baud = 115200_Bd,
|
|
.allow_baud_error = true,
|
|
.half_duplex = true,
|
|
}>;
|
|
inline constexpr serial_t serial{};
|
|
|
|
namespace tsb {
|
|
namespace {
|
|
|
|
// The loader is purely polled - it never enables interrupts - so every SPM and
|
|
// EEPROM lock folds to nothing under this posture.
|
|
constexpr auto off = avr::irq::guard_policy::unused;
|
|
|
|
// Strict request/response: every SPM operation is waited out before the next
|
|
// byte moves, so no flash operation is ever in flight at an EEPROM access -
|
|
// the write procedure's step 2 has nothing to guard, the omission the
|
|
// datasheet grants (DS40002061B section 8.6.3).
|
|
constexpr auto no_spm = ee::spm_interlock::omitted;
|
|
|
|
// The handshake bytes, identical across every TSB host.
|
|
constexpr std::uint8_t confirm = '!';
|
|
constexpr std::uint8_t request = '?';
|
|
constexpr std::uint8_t knock = '@';
|
|
|
|
// Boot geometry for the 1 KB boot section (BOOTSZ=10). The page size and the
|
|
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
|
// page (the LASTPAGE holding the app-jump vector, activation timeout and
|
|
// password), one page below the boot section.
|
|
constexpr std::uint16_t page = spm::page_bytes;
|
|
constexpr std::uint16_t boot_bytes = 1024;
|
|
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
|
|
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
|
|
|
|
// Firmware version stamp: YY*512 + MM*32 + DD, the encoding the host decodes.
|
|
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 20;
|
|
|
|
// The 16-byte device-info block the host reads on activation. A flash_table
|
|
// keeps it in progmem with no .data image (there is no crt to copy one).
|
|
// clang-format off
|
|
inline constexpr std::array<std::uint8_t, 16> info_data = {
|
|
'T', 'S', 'B',
|
|
build_date & 0xFF, build_date >> 8,
|
|
0xF3, // status byte (native-UART fixed-baud lineage)
|
|
0x1E, 0x95, 0x0F, // ATmega328P signature
|
|
page / 2, // page size in words
|
|
(app_end / 2) & 0xFF, (app_end / 2) >> 8, // app-flash boundary, words
|
|
eeprom_end & 0xFF, eeprom_end >> 8,
|
|
0xAA, 0xAA, // ATmega processor-type marker (bytes 14 == 15)
|
|
};
|
|
// clang-format on
|
|
using info = avr::flash_table<info_data>;
|
|
|
|
// Blocking byte read/write over the one-wire line: read() releases the line to
|
|
// the receiver, write() takes it and holds it until the frame is out.
|
|
std::uint8_t rx()
|
|
{
|
|
return serial.read_blocking();
|
|
}
|
|
|
|
void tx(std::uint8_t byte)
|
|
{
|
|
serial.write(byte);
|
|
}
|
|
|
|
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
|
{
|
|
return reinterpret_cast<const std::uint8_t *>(addr);
|
|
}
|
|
|
|
// Stream `count` bytes to the host, from flash (LPM) or from EEPROM.
|
|
void send_flash(std::uint16_t addr, std::uint8_t count)
|
|
{
|
|
while (count--) {
|
|
tx(avr::flash_load(flash_ptr(addr++)));
|
|
}
|
|
}
|
|
|
|
void send_eeprom(std::uint16_t addr, std::uint8_t count)
|
|
{
|
|
while (count--) {
|
|
tx(ee::read<no_spm>(addr++));
|
|
}
|
|
}
|
|
|
|
// Prompt the host with '?' and report whether it answered '!'.
|
|
bool request_confirm()
|
|
{
|
|
tx(request);
|
|
return rx() == confirm;
|
|
}
|
|
|
|
// Stream one page from the host straight into the already-erased flash page at
|
|
// `addr`, filling the SPM word buffer low byte then high - no SRAM staging, so
|
|
// receiving and programming are the same loop.
|
|
void store_flash_page(std::uint16_t addr)
|
|
{
|
|
const auto open = spm::page::begin<spm::from::boot_section, off>(addr);
|
|
for (std::uint16_t i = 0; i < page; i += 2) {
|
|
std::uint8_t lo = rx();
|
|
std::uint8_t hi = rx();
|
|
spm::fill<off>(open, addr + i, static_cast<std::uint16_t>(lo | (hi << 8)));
|
|
}
|
|
spm::write_page<spm::from::boot_section, off>(addr); // blocking: waits the write out
|
|
}
|
|
|
|
// Stream one page from the host straight into EEPROM, byte by byte.
|
|
void store_eeprom_page(std::uint16_t addr)
|
|
{
|
|
for (std::uint16_t i = 0; i < page; ++i) {
|
|
ee::write<off, no_spm>(addr + i, rx());
|
|
}
|
|
}
|
|
|
|
// Erase one flash page, waited out by the blocking spelling - the erase step
|
|
// shared by the whole-app erase, the config-page rewrite and the emergency
|
|
// wipe.
|
|
void erase_page(std::uint16_t addr)
|
|
{
|
|
spm::erase_page<spm::from::boot_section, off>(addr);
|
|
}
|
|
|
|
// Erase the whole application, one page at a time, top-down as the reference
|
|
// loader does (unwritten pages stay erased and the host cannot observe the
|
|
// order; the loop bound becomes a compare with zero).
|
|
void erase_application()
|
|
{
|
|
for (std::uint16_t a = app_end; a != 0;) {
|
|
a -= page;
|
|
erase_page(a);
|
|
}
|
|
spm::rww_enable<off>();
|
|
}
|
|
|
|
// The application's reset vector; the linker pins it to 0x0000 (--defsym).
|
|
extern "C" [[noreturn]] void tsb_app();
|
|
|
|
// Run the application. Any non-command byte, a wrong password, or an idle
|
|
// programmer port lands here.
|
|
[[noreturn]] void appjump()
|
|
{
|
|
spm::wait(); // make sure any pending SPM finished before handing over
|
|
tsb_app();
|
|
}
|
|
|
|
// 'f': stream the application flash back, one page per host '!'. Self-terminates
|
|
// at the application boundary; the host normally stops earlier with a non-'!'.
|
|
void read_flash()
|
|
{
|
|
for (std::uint16_t a = 0; a < app_end; a += page) {
|
|
if (rx() != confirm) {
|
|
return;
|
|
}
|
|
send_flash(a, page);
|
|
}
|
|
}
|
|
|
|
// 'e': stream EEPROM back, one page per host '!', until the host stops.
|
|
void read_eeprom()
|
|
{
|
|
for (std::uint16_t a = 0;; a += page) {
|
|
if (rx() != confirm) {
|
|
return;
|
|
}
|
|
send_eeprom(a, page);
|
|
}
|
|
}
|
|
|
|
// 'F': erase the whole application first, then take pages the host offers
|
|
// behind '?'.
|
|
void write_flash()
|
|
{
|
|
erase_application();
|
|
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
|
store_flash_page(a);
|
|
}
|
|
}
|
|
|
|
// 'E': take pages the host offers behind '?' into EEPROM.
|
|
void write_eeprom()
|
|
{
|
|
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
|
store_eeprom_page(a);
|
|
}
|
|
}
|
|
|
|
// 'C': replace the config page, then echo it back for the host to verify.
|
|
void write_config()
|
|
{
|
|
if (!request_confirm()) {
|
|
return;
|
|
}
|
|
erase_page(app_end);
|
|
store_flash_page(app_end);
|
|
spm::rww_enable<off>();
|
|
send_flash(app_end, page);
|
|
}
|
|
|
|
// Emergency erase: wipe the application flash, the EEPROM and the config page.
|
|
// Reachable only from the password gate (a wrong byte can never reach it), so a
|
|
// blank config still leaves the loader recoverable.
|
|
void emergency_erase()
|
|
{
|
|
erase_application();
|
|
for (std::uint16_t a = 0; a <= eeprom_end; ++a) {
|
|
ee::write<off, no_spm>(a, 0xff);
|
|
}
|
|
erase_page(app_end);
|
|
spm::rww_enable<off>();
|
|
}
|
|
|
|
// The password gate. The config page holds the password at app_end+3,
|
|
// terminated by 0xff (a blank page means no password). A byte of 0 requests
|
|
// emergency erase; a wrong byte hangs the loader, still draining the line, so a
|
|
// wrong password can never fall through to the erase.
|
|
enum class gate : std::uint8_t { pass, emergency };
|
|
|
|
gate password_gate()
|
|
{
|
|
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
|
|
std::uint8_t expected = avr::flash_load(pw);
|
|
if (expected == 0xff) {
|
|
return gate::pass;
|
|
}
|
|
std::uint8_t got = rx();
|
|
if (got == 0) {
|
|
return gate::emergency;
|
|
}
|
|
if (got != expected) {
|
|
for (;;) {
|
|
rx();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[[noreturn]] void run()
|
|
{
|
|
// A watchdog reset hands straight back to the application, as the reference
|
|
// loader does, rather than re-entering the bootloader.
|
|
if (avr::hw::mcusr::wdrf.test()) {
|
|
appjump();
|
|
}
|
|
|
|
avr::init<serial_t>();
|
|
|
|
// Activation: the host knocks three '@' inside a window whose length is the
|
|
// config page's timeout byte (floored so a corrupt page can never lock the
|
|
// loader out). An idle port times out and boots the application.
|
|
__uint24 idle = static_cast<__uint24>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
|
|
std::uint8_t knocks = 0;
|
|
while (knocks < 3) {
|
|
if (auto byte = serial.read()) {
|
|
knocks = *byte == knock ? knocks + 1 : 0;
|
|
} else if (--idle == 0) {
|
|
appjump();
|
|
}
|
|
}
|
|
|
|
switch (password_gate()) {
|
|
case gate::pass:
|
|
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
|
|
break;
|
|
case gate::emergency:
|
|
if (!request_confirm() || !request_confirm()) {
|
|
appjump();
|
|
}
|
|
emergency_erase();
|
|
break;
|
|
}
|
|
|
|
for (;;) {
|
|
tx(confirm); // Mainloop ready
|
|
switch (rx()) {
|
|
case 'f':
|
|
read_flash();
|
|
break;
|
|
case 'F':
|
|
write_flash();
|
|
break;
|
|
case 'e':
|
|
read_eeprom();
|
|
break;
|
|
case 'E':
|
|
write_eeprom();
|
|
break;
|
|
case 'c':
|
|
send_flash(app_end, page);
|
|
break;
|
|
case 'C':
|
|
write_config();
|
|
break;
|
|
default:
|
|
appjump(); // 'q' or any other byte runs the application
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace tsb
|
|
|
|
// Reset lands here: BOOTRST vectors to the boot section base and .vectors is
|
|
// laid first, so this is the first instruction executed. No crt ran, so set the
|
|
// stack pointer before anything is called.
|
|
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
|
{
|
|
SP = RAMEND;
|
|
// The one line of crt this loader needs: compiled code assumes
|
|
// __zero_reg__ (r1) is 0, and power-on registers are undefined.
|
|
asm volatile("clr __zero_reg__");
|
|
tsb::run();
|
|
}
|