The tricks and asm tiers spelled 16_MHz three times each and now name it once; the policy tier's lean bring-up solves the baud from the link config the serial unit is declared with instead of restating 115200 8N1. The asm tier's immediates - RXEN0, RXC0's bit, SPMEN, PGWRT|SPMEN and the three UART addresses - are operands read from the chip database rather than numbers with comments beside them, so avr/io.h is left for SPMCSR's I/O address alone; pure and tricks drop an include nothing used. Headers that described the code as it was are corrected (pure's "only attribute", the policy tier's history), the rule references leave the comments, and locals are const auto. All four images are byte-identical to before, in both modes for tsb_asm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
351 lines
11 KiB
C++
351 lines
11 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, every SPM/EEPROM lock and
|
|
// the crt-free reset entry are libavr's to handle.
|
|
|
|
#include <libavr/libavr.hpp>
|
|
|
|
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 auto info_data = std::to_array<std::uint8_t>({
|
|
'T', 'S', 'B',
|
|
build_date & 0xFF, build_date >> 8,
|
|
0xF3, // status byte (native-UART fixed-baud lineage)
|
|
avr::hw::db.signature[0], avr::hw::db.signature[1], avr::hw::db.signature[2],
|
|
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>;
|
|
|
|
// The lockout-proof floor for the receive window: the oracle's F_CPU/1MHz, so
|
|
// it follows the clock rather than restating it.
|
|
constexpr auto act_min = static_cast<std::uint8_t>(dev::clock.hz / 1'000'000);
|
|
|
|
// The receive window, pre-floored where it is set. In .noinit: there is no crt
|
|
// to clear a .bss image, and run() stores it before the first receive.
|
|
[[gnu::section(".noinit")]] std::uint8_t window;
|
|
|
|
// Bounded byte read over the one-wire line - read() releases the line to the
|
|
// receiver - answering 0 on silence. That 0 falls through every compare below:
|
|
// not a knock, not a confirm, not a command, so a silent host unwinds the
|
|
// loader to the application from anywhere and a mid-session cable pull cannot
|
|
// wedge it. The oracle lists that timeout among its own fixes, and a blocking
|
|
// read is how a tier loses it.
|
|
std::uint8_t rx()
|
|
{
|
|
std::uint16_t outer = static_cast<std::uint16_t>(window) << 8;
|
|
do {
|
|
std::uint8_t fine = 0;
|
|
do {
|
|
if (auto byte = serial.read()) {
|
|
return *byte;
|
|
}
|
|
} while (--fine);
|
|
} while (--outer);
|
|
return 0;
|
|
}
|
|
|
|
// write() takes the line and holds it until the frame is out.
|
|
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) {
|
|
const auto lo = rx();
|
|
const auto 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) {
|
|
const auto expected = avr::flash_load(pw);
|
|
if (expected == 0xff) {
|
|
return gate::pass;
|
|
}
|
|
const auto 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; the same
|
|
// window then bounds every receive of the session.
|
|
window = avr::flash_load(flash_ptr(app_end + 2)) | act_min;
|
|
__uint24 idle = static_cast<__uint24>(window) << 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 at the boot section base (BOOTRST): the entry stub in .vectors
|
|
// is laid first and does the one line of crt a crt-less image needs.
|
|
template struct avr::startup::entry<tsb::run, avr::startup::stack::hardware>;
|