pureboot: autobaud variant, two versions for review
Measure the host's bit timing at runtime from a 0xC0 calibration pulse, so one clock-agnostic image per chip runs at any F_CPU — the RC-oscillator deployments no longer need a per-clock build. Two source files, differing only in the write-guard/purity tradeoff: pureboot_autobaud_pure.cpp (the measured unit in the GPIOR I/O scratch registers, running-slot write guard dropped, 508 B on the 1284) stays strictly pure; pureboot_autobaud_reg.cpp (unit in one global register variable, guard kept, 512 B) keeps every feature at the cost of that single GRV. Both fit 512/510 on all 37 chips and share two licensed simplifications: a slimmed info block (version + signature; the host derives geometry from the chip database) and a single-byte activation knock. pureboot/autobaud.md records the decision, the hand-assembly floor (506 B) that set the target, and the compiler-knob path to it. Size-tested on every chip via pureboot_add_autobaud(); the fixed-baud loader is untouched. Sim validation, the host calibration handshake, and real-hardware acceptance remain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -245,6 +245,19 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||
endfunction()
|
||||
|
||||
# The autobaud variants under review (pureboot/autobaud.md): one clock-agnostic
|
||||
# image per chip, no clock × baud axis. Both are size-tested against the same
|
||||
# per-chip budget on every chip — the decider is whether pure C++ fits 512,
|
||||
# and the 1284 is the tight one (pure 508, register 512).
|
||||
function(pureboot_autobaud_variant name source)
|
||||
pureboot_add_autobaud(${name} ${source})
|
||||
add_test(NAME ${name}.size
|
||||
COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$<TARGET_FILE:${name}>
|
||||
-DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake)
|
||||
endfunction()
|
||||
pureboot_autobaud_variant(pureboot_autobaud_pure pureboot_autobaud_pure.cpp)
|
||||
pureboot_autobaud_variant(pureboot_autobaud_reg pureboot_autobaud_reg.cpp)
|
||||
|
||||
# One point of the exhaustive matrix, named from its resolved parameters
|
||||
# so the enumeration cannot collide with itself. Unreachable rates drop
|
||||
# out here rather than aborting the configure.
|
||||
|
||||
@@ -310,3 +310,38 @@ function(pureboot_add_loader name)
|
||||
set_target_properties(${name} PROPERTIES PUREBOOT_HZ ${PB_CLOCK} PUREBOOT_BAUD ${PB_BAUD}
|
||||
PUREBOOT_LINK ${_link})
|
||||
endfunction()
|
||||
|
||||
# pureboot_add_autobaud(<name> <source> [RX <pin>] [TX <pin>])
|
||||
#
|
||||
# An autobaud software-serial loader from <source> (pureboot_autobaud_*.cpp).
|
||||
# Autobaud measures the host's bit timing at runtime, so the image carries no
|
||||
# clock and no baud — one binary per chip runs at any F_CPU. Same per-chip
|
||||
# geometry, link and codegen flags as pureboot_add_loader(); only the clock and
|
||||
# baud axes fall away. Two source files are under review (autobaud.md):
|
||||
# pureboot_autobaud_pure.cpp and pureboot_autobaud_reg.cpp.
|
||||
function(pureboot_add_autobaud name source)
|
||||
cmake_parse_arguments(PB "" "RX;TX" "" ${ARGN})
|
||||
if(NOT PB_RX)
|
||||
set(PB_RX pb0)
|
||||
endif()
|
||||
if(NOT PB_TX)
|
||||
set(PB_TX pb1)
|
||||
endif()
|
||||
foreach(_pin ${PB_RX} ${PB_TX})
|
||||
if(NOT _pin MATCHES "^p[a-h][0-7]$")
|
||||
message(FATAL_ERROR "pureboot_add_autobaud(${name}): pin '${_pin}' is not of the form pb1")
|
||||
endif()
|
||||
endforeach()
|
||||
get_property(_base_hex GLOBAL PROPERTY PUREBOOT_BASE_HEX)
|
||||
get_property(_app GLOBAL PROPERTY PUREBOOT_APP)
|
||||
get_property(_wrap GLOBAL PROPERTY PUREBOOT_WRAP)
|
||||
|
||||
add_executable(${name} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/${source})
|
||||
target_link_libraries(${name} PRIVATE libavr)
|
||||
target_compile_definitions(${name} PRIVATE PUREBOOT_RX=${PB_RX} PUREBOOT_TX=${PB_TX})
|
||||
target_compile_options(${name} PRIVATE
|
||||
-fno-ivopts -fira-algorithm=priority -fno-move-loop-invariants -fno-tree-ter -fno-split-wide-types)
|
||||
target_link_options(${name} PRIVATE -nostartfiles -Wl,--section-start=.text=${_base_hex}
|
||||
-Wl,--defsym=pureboot_app=${_app} ${_wrap})
|
||||
add_custom_command(TARGET ${name} POST_BUILD COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${name}>)
|
||||
endfunction()
|
||||
|
||||
193
pureboot/autobaud.md
Normal file
193
pureboot/autobaud.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# pureboot autobaud — findings and the version decision
|
||||
|
||||
The autobaud variant measures the host's bit timing **at runtime** from a
|
||||
calibration pulse, so the image carries no clock: one clock-agnostic binary per
|
||||
chip runs at any F_CPU and locks onto whatever baud the host sends. It exists
|
||||
for the software-serial deployments — the RC-oscillator parts (the tinies,
|
||||
internal-oscillator megas) whose exact clock is uncertain and drifts, so today
|
||||
each needs a per-clock build. Autobaud erases that axis. That is the win —
|
||||
deployment, not bytes.
|
||||
|
||||
This document records how the fit was established, why there are **two source
|
||||
files under review**, and what remains before either can ship.
|
||||
|
||||
## The decision to make
|
||||
|
||||
Two complete autobaud loaders sit side by side for review. They implement the
|
||||
identical wire protocol and the identical command set; they differ in exactly
|
||||
two things, and the choice between them is a single tradeoff — **the running-slot
|
||||
write guard vs. strict purity.**
|
||||
|
||||
| | `pureboot_autobaud_pure.cpp` | `pureboot_autobaud_reg.cpp` |
|
||||
|---|---|---|
|
||||
| measured unit lives in | GPIOR I/O scratch regs (RAM where absent) | one global register variable (`r4`) |
|
||||
| running-slot write guard | **dropped** | **kept** |
|
||||
| purity (no asm, no GRV) | **yes** | one GRV — the single break |
|
||||
| ATmega1284P size | **508 B** (4 B spare) | **512 B** (exact) |
|
||||
| all 37 chips | 440–508 B | 452–512 B |
|
||||
|
||||
Both also take two shared, licensed simplifications (below): a slimmed info
|
||||
block and a single-byte activation knock.
|
||||
|
||||
**The pure version** keeps pureboot's defining constraint — *"one C++ source, no
|
||||
inline assembly, no global register variables"* (README.md) — intact, and pays
|
||||
for it by dropping the write guard, which the host compensates for. **The
|
||||
register version** keeps the guard by pinning the measured unit to a call-saved
|
||||
register, which is only expressible as a global register variable — the one
|
||||
thing the purity rule forbids.
|
||||
|
||||
Recommendation deferred to the owner. The pure version aligns with the stated
|
||||
constraints (purity mandated; relaxing host-guaranteed safety licensed); the
|
||||
register version keeps every feature at the cost of the purity claim and leaves
|
||||
the 1284 with zero margin.
|
||||
|
||||
## The mechanism
|
||||
|
||||
The host sends calibration byte **0xC0** — a start bit plus six zero data bits
|
||||
form a single low pulse of seven bit-times. The loader counts that pulse in a
|
||||
poll loop the compiler emits at **seven cycles an iteration**, so the count is
|
||||
the pulse length in cycles ÷ 7 × 7 = one bit period in cycles, and `count >> 2`
|
||||
is that period in `_delay_loop_2`'s four-cycle iterations — the delay unit fed
|
||||
to every rx/tx bit. Numerically sound: the per-bit delay tracks the true period
|
||||
to **< 0.2 %** from 1–16 MHz at 9600 (worst seen −2.8 % at 128 kHz/1200, still
|
||||
inside UART tolerance).
|
||||
|
||||
**The catch — codegen coupling.** `count >> 2` is exact only because the
|
||||
calibration pulse's bit-count (7) equals the poll loop's cycles/iter (7). That
|
||||
couples the on-wire calibration byte to what the compiler emits; a toolchain bump
|
||||
that reshapes the loop breaks it silently. This must be pinned by a sim timing
|
||||
test that fails on drift (see *What remains*).
|
||||
|
||||
The activation window is a **fixed poll budget** (`PUREBOOT_AUTOBAUD_POLLS`,
|
||||
default 4,000,000), not `timeout × clock` — with no clock, whole seconds cannot
|
||||
be timed. A `__uint24` holds it; a `std::uint32_t`'s fourth byte would cost two
|
||||
words at each countdown step.
|
||||
|
||||
## How the target was found — the hand-assembly floor
|
||||
|
||||
Pure C++ first came out **560 B** on the 1284 (full info + guard). To learn
|
||||
whether that was a hard floor or C++ overhead, a hand-optimal assembly loader was
|
||||
written with the **full v4 protocol** (all commands, the full 12-byte info block,
|
||||
the write guard, position independence) plus autobaud — `local/scratch/autobaud/`
|
||||
in the libavr checkout, off-tree, a size probe.
|
||||
|
||||
**The hand-asm floor is 506 B** — it fits 512 with the *full* info block *and*
|
||||
the guard, no concessions. That proved a fit was possible and gave a target. The
|
||||
hand-asm achieves it two ways pure C++ cannot express directly:
|
||||
|
||||
1. **The measured unit in a call-saved register (Y).** rx/tx/spin read it
|
||||
directly — no argument, no `movw`. In C++ an *outlined* rx/tx can only receive
|
||||
the unit as a parameter (a `movw` at each of ~19 call sites, +38 B) or read it
|
||||
from RAM (an `lds`), unless it is a global register variable. That register is
|
||||
worth ~32 B and is exactly the impurity at issue.
|
||||
2. **A cheap PC-relative slot anchor** (`rcall .+0; pop; pop`, ~10 B) where the
|
||||
C++ `__builtin_return_address(0)` costs ~18–26 B (GCC re-derives the stack
|
||||
slot and byteswaps).
|
||||
|
||||
Measured on identical C++ (the same loader, unit in a register vs. RAM):
|
||||
|
||||
| unit home | 1284 size |
|
||||
|---|---|
|
||||
| global register variable | 528 |
|
||||
| RAM static | 560 |
|
||||
| hand-asm (register + cheap anchor) | 506 |
|
||||
|
||||
So even *with* a GRV, C++ carries ~22 B over hand-asm; with RAM, ~54 B. The
|
||||
compiler flag axis was spent (the tuned `-f` set is optimal; `-fipa-ra`,
|
||||
`-fipa-icf`, `-ftree-tail-merge`, `-flto`, `-fwhole-program` all gave nothing —
|
||||
GCC's AVR ABI passes arguments in fixed registers regardless).
|
||||
|
||||
## The pure lever — GPIOR
|
||||
|
||||
The register cost is the RAM home's `lds`/`sts` (two words each) plus `.bss` and
|
||||
its `__do_clear_bss`. The general-purpose I/O scratch registers (**GPIOR1:GPIOR2**,
|
||||
adjacent) are reached by `in`/`out` — one word — through libavr's named register
|
||||
surface: **no inline asm, no global register variable, fully pure.** Storing the
|
||||
unit there sidesteps `.bss` and halves each access. Where a chip has no GPIOR
|
||||
(the t13, m8, m16/32) the pure version falls back to a plain static; those chips
|
||||
have ample headroom.
|
||||
|
||||
GPIOR alone did not close the gap (the write guard's `slot_high` machinery is the
|
||||
bulk). The pure version fits by combining GPIOR storage with the licensed
|
||||
simplifications; the register version fits by pinning the unit and so affording
|
||||
the guard.
|
||||
|
||||
### The ablation (1284, all pure except the GRV row)
|
||||
|
||||
| config | GPIOR unit | GRV unit |
|
||||
|---|---|---|
|
||||
| full info + guard + 2-knock | 550 | 528 |
|
||||
| slim info + guard + 2-knock | 540 | 518 |
|
||||
| slim info + guard + 1-knock | — | **512** ✓ |
|
||||
| slim info + no guard + 2-knock | 514 | — |
|
||||
| slim info + no guard + 1-knock | **508** ✓ | — |
|
||||
|
||||
The pivotal cost is the write guard's `slot_high` (`__builtin_return_address`,
|
||||
~26 B), removable only when the info block is slim (so nothing else needs
|
||||
`slot_high`) *and* the guard is gone. That is why the pure version drops the
|
||||
guard and the register version keeps it.
|
||||
|
||||
## Sizes — every chip, both versions
|
||||
|
||||
Both build and size-test green on all 37 (patched-vector budget 510, else 512):
|
||||
|
||||
| chip class | pure | register | budget |
|
||||
|---|---|---|---|
|
||||
| ATtiny13/13A | 446 | 452 | 510 |
|
||||
| ATtiny25/45/85 | 440–444 | 456–460 | 510 |
|
||||
| ATmega8/8A | 472 | 476 | 512 |
|
||||
| ATmega16/32 | 476 | 478 | 512 |
|
||||
| ATmega48 family | 440 | 456 | 510 |
|
||||
| ATmega88/168/328 | 462–466 | 476–478 | 512 |
|
||||
| ATmega164/324/644 | 466 | 478 | 512 |
|
||||
| **ATmega1284/P** | **508** | **512** | 512 |
|
||||
|
||||
The 1284 is the tight one — the far-flash machinery (ELPM/RAMPZ, word-addressed
|
||||
wire) it alone carries. The `pureboot_autobaud_*.size` tests run per chip in the
|
||||
port's build.
|
||||
|
||||
## The shared simplifications, and why each is licensed
|
||||
|
||||
- **Slimmed info block.** `'b'` returns the version and the three signature bytes
|
||||
— the loader's identity and the chip's — as immediates, instead of the full
|
||||
12-byte block read from flash. The host derives page size, loader base, EEPROM
|
||||
size and the addressing flags from the signature via its own chip database.
|
||||
This drops the flash-resident table and, with the guard also gone in the pure
|
||||
version, the entire `slot_high` anchor. Owner-approved: "keep version + chip
|
||||
type, simplify the rest; the host derives geometry."
|
||||
- **Single-byte knock.** One `'p'` activates; the calibration pulse has already
|
||||
proven a host is present. (Fixed-baud pureboot needs `p`+`b` because it has no
|
||||
prior proof.)
|
||||
- **No running-slot write guard** *(pure version only)*. The guard stops a
|
||||
*buggy* host from programming the loader's own slot; the host is guaranteed
|
||||
never to do so (it knows the loader's location). Licensed by "the host
|
||||
guarantees safety" (README.md). Self-update still works — it is the safety net
|
||||
against host bugs that is lost, not a functional path.
|
||||
|
||||
## What remains (either version)
|
||||
|
||||
The decider — does pure C++ fit 512 on every chip — is answered (yes). Not yet
|
||||
done, and required before shipping:
|
||||
|
||||
1. **Sim-validate the lock.** Run the chosen variant over the GPIO⇄pty bridge at
|
||||
≥3 exact F_CPU (1/8/16 MHz, the tiny13's 9.6 MHz): confirm it measures the
|
||||
right unit and completes a flash + verify. This test is also what pins the
|
||||
codegen-coupled calibration constant (0xC0 vs the 7-cycle loop) against a
|
||||
toolchain bump.
|
||||
2. **Host tool + protocol.** `pureboot.py` learns the calibration handshake
|
||||
(send the 0xC0 lead, then knock at the locked rate), the slim info format
|
||||
(derive geometry from the signature), and — for the pure version — that the
|
||||
loader carries no write guard. README protocol section updated.
|
||||
3. **Real-hardware acceptance.** Autobaud exists for what a cycle-exact simulator
|
||||
cannot produce: a real RC oscillator at ±10 % with drift and jitter. Drive an
|
||||
internal-oscillator ATtiny from the host at a fixed baud; confirm lock plus a
|
||||
full flash + verify (Windows environment, hardware present — confirm first).
|
||||
|
||||
## Files
|
||||
|
||||
- `pureboot_autobaud_pure.cpp` — the pure version (GPIOR/RAM unit, no guard).
|
||||
- `pureboot_autobaud_reg.cpp` — the register version (GRV unit, guard kept).
|
||||
- `CMakeLists.txt` — `pureboot_add_autobaud()` builds either; the port's build
|
||||
size-tests both on every chip.
|
||||
- `local/scratch/autobaud/floor_1284.S` (libavr checkout) — the hand-asm floor
|
||||
probe, off-tree and gitignored; not sim-verified, a size reference only.
|
||||
368
pureboot/pureboot_autobaud_pure.cpp
Normal file
368
pureboot/pureboot_autobaud_pure.cpp
Normal file
@@ -0,0 +1,368 @@
|
||||
// pureboot autobaud — the software-serial variant that measures the host's bit
|
||||
// timing at runtime, so one binary runs at any F_CPU: the image carries no
|
||||
// clock. THE PURE VERSION — no inline assembly, no global register variables,
|
||||
// exactly the constraints the fixed-baud loader keeps.
|
||||
//
|
||||
// Two source files exist for review (autobaud.md):
|
||||
// this one, pure, and pureboot_autobaud_reg.cpp, which keeps the running-slot
|
||||
// write guard at the cost of one global register variable. They differ only in
|
||||
// where the measured unit lives and whether the guard is present.
|
||||
//
|
||||
// What this version trades to fit 512 B in pure C++ (the 1284 at 508), each
|
||||
// licensed by "the host guarantees safety" (README.md) and the owner's approval
|
||||
// to simplify the info block:
|
||||
// - the measured per-bit unit lives in the two general-purpose I/O scratch
|
||||
// registers (GPIOR) where the chip has them, in a static otherwise —
|
||||
// reached through libavr's named register surface, so no asm and no global
|
||||
// register variable; the loader stays pure;
|
||||
// - the info block is slimmed to the version and the signature — the chip's
|
||||
// identity — from which the host derives page size, loader base, EEPROM
|
||||
// size and the addressing flags via its own chip database;
|
||||
// - no running-slot write guard: the host never programs the loader's own
|
||||
// slot, and a broken host bricking the target is the host's bug;
|
||||
// - a single-byte activation knock: the calibration pulse already proves a
|
||||
// host is present.
|
||||
//
|
||||
// Position independence is kept and is in fact total here: control flow is
|
||||
// PC-relative, the wire carries addresses, and with the slimmed info block and
|
||||
// no write guard nothing anchors on the runtime address at all.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include <util/delay_basic.h>
|
||||
|
||||
using namespace avr::literals;
|
||||
namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
namespace hw = avr::hw;
|
||||
|
||||
namespace pureboot {
|
||||
namespace {
|
||||
|
||||
// Purely polled: every interrupt guard folds to nothing.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
constexpr std::uint8_t ack = '+';
|
||||
|
||||
// Autobaud carries no clock, so PUREBOOT_CLOCK_HZ / PUREBOOT_BAUD are not
|
||||
// consulted; only the software-UART pins are a deployment parameter.
|
||||
#if !defined(PUREBOOT_RX)
|
||||
#define PUREBOOT_RX pb0
|
||||
#endif
|
||||
#if !defined(PUREBOOT_TX)
|
||||
#define PUREBOOT_TX pb1
|
||||
#endif
|
||||
|
||||
// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR.
|
||||
consteval std::int16_t wdrf_field()
|
||||
{
|
||||
auto reg = std::string_view{hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
||||
return hw::db.field_index(reg, "WDRF");
|
||||
}
|
||||
|
||||
// The loader owns the top 512 bytes; a staging copy goes in the slot below.
|
||||
constexpr std::uint16_t slot_bytes = 512;
|
||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
constexpr bool boot_section = hw::curated::has_boot_section();
|
||||
|
||||
// Past 64 KiB a byte address no longer fits the wire's 16 bits, so flash
|
||||
// addresses there are word addresses ('J' always was one).
|
||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||
|
||||
// The loader's one identity number (README.md); the slimmed info block carries
|
||||
// it and the signature, and the host maps a version to its protocol.
|
||||
constexpr std::uint8_t version = 4;
|
||||
|
||||
// The activation window as a fixed poll budget: with no clock, whole seconds
|
||||
// cannot be timed. A __uint24 (AVR's three-byte type) holds it — a fourth byte
|
||||
// would cost two words at each countdown step for range never used.
|
||||
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||
#endif
|
||||
constexpr __uint24 autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||
|
||||
// The measured per-bit delay (in _delay_loop_2 four-cycle iterations). It lives
|
||||
// in the two adjacent general-purpose I/O scratch registers (GPIOR1:GPIOR2)
|
||||
// where the chip has them — in/out reach them in one word where a static's
|
||||
// lds/sts take two, and there is no .bss to clear — and in a plain static
|
||||
// otherwise (the t13, m8 and m16/32 have no GPIOR). Both are pure: the named
|
||||
// register surface, no inline asm, no global register variable.
|
||||
constexpr bool have_gpior = hw::db.reg_index("GPIOR1") >= 0 && hw::db.reg_index("GPIOR2") >= 0;
|
||||
|
||||
std::uint16_t unit_backing;
|
||||
|
||||
template <bool Gpior = have_gpior>
|
||||
[[gnu::always_inline]] inline std::uint16_t get_unit()
|
||||
{
|
||||
if constexpr (Gpior)
|
||||
return static_cast<std::uint16_t>(hw::reg_impl<hw::db.reg_index("GPIOR1")>::read() |
|
||||
(hw::reg_impl<hw::db.reg_index("GPIOR2")>::read() << 8));
|
||||
else
|
||||
return unit_backing;
|
||||
}
|
||||
|
||||
template <bool Gpior = have_gpior>
|
||||
[[gnu::always_inline]] inline void put_unit(std::uint16_t u)
|
||||
{
|
||||
if constexpr (Gpior) {
|
||||
hw::reg_impl<hw::db.reg_index("GPIOR1")>::write(static_cast<std::uint8_t>(u));
|
||||
hw::reg_impl<hw::db.reg_index("GPIOR2")>::write(static_cast<std::uint8_t>(u >> 8));
|
||||
} else
|
||||
unit_backing = u;
|
||||
}
|
||||
|
||||
// The autobaud software link: bit-banged with cycle-counted delays like the
|
||||
// fixed-baud software backend, but the per-bit delay is the measured unit, not
|
||||
// a consteval constant. rx/tx load the unit once into a local, so each bit
|
||||
// spins from a register with no per-bit reload.
|
||||
struct link {
|
||||
using in_t = avr::io::input<avr::PUREBOOT_RX, avr::io::pull::up>;
|
||||
using out_t = avr::io::output<avr::PUREBOOT_TX>;
|
||||
|
||||
static void init()
|
||||
{
|
||||
avr::init<in_t, out_t>();
|
||||
out_t::set(); // idle high
|
||||
}
|
||||
|
||||
// Time the calibration pulse into the unit. The host sends 0xC0 — a start
|
||||
// bit plus six zero data bits are one low pulse of seven bit-times — and the
|
||||
// counted poll loop is seven cycles an iteration, so the count is the pulse
|
||||
// length in cycles ÷ 7 × 7 = one bit period in cycles, and count >> 2 is that
|
||||
// period in _delay_loop_2's four-cycle iterations. Waits for the start edge
|
||||
// under the poll budget; 0 (returned, and stored) means the budget expired.
|
||||
static std::uint16_t measure(__uint24 budget)
|
||||
{
|
||||
while (in_t::read())
|
||||
if (!--budget)
|
||||
return 0;
|
||||
std::uint16_t count = 0;
|
||||
while (!in_t::read())
|
||||
++count;
|
||||
const std::uint16_t u = static_cast<std::uint16_t>(count >> 2);
|
||||
put_unit(u);
|
||||
return u;
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
{
|
||||
const std::uint16_t unit = get_unit();
|
||||
while (in_t::read()) // await the start edge
|
||||
;
|
||||
_delay_loop_2(static_cast<std::uint16_t>(unit + (unit >> 1))); // 1.5 bits to the LSB centre
|
||||
std::uint8_t value = 0;
|
||||
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||
value >>= 1;
|
||||
if (in_t::read())
|
||||
value |= 0x80;
|
||||
_delay_loop_2(unit);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static void tx(std::uint8_t value)
|
||||
{
|
||||
const std::uint16_t unit = get_unit();
|
||||
out_t::clear(); // start bit
|
||||
_delay_loop_2(unit);
|
||||
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||
out_t::write(value & 1);
|
||||
value >>= 1;
|
||||
_delay_loop_2(unit);
|
||||
}
|
||||
out_t::set(); // stop bit
|
||||
_delay_loop_2(unit);
|
||||
}
|
||||
|
||||
static void drain()
|
||||
{
|
||||
// The software transmitter returns only after the stop bit.
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" [[noreturn]] void pureboot_app();
|
||||
|
||||
[[gnu::noipa, noreturn]] void jump(void (*target)())
|
||||
{
|
||||
target();
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
[[gnu::noinline, noreturn]] void run_app()
|
||||
{
|
||||
jump(pureboot_app);
|
||||
}
|
||||
|
||||
// Inlined: read across a call, the first byte strands in a call-saved register
|
||||
// the caller has to push and pop.
|
||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||
{
|
||||
std::uint16_t low = link::rx();
|
||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||||
{
|
||||
return std::bit_cast<std::uint16_t>(pair);
|
||||
}
|
||||
|
||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_far(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
||||
do {
|
||||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
||||
if (++z == 0)
|
||||
++rampz;
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
if constexpr (word_flash)
|
||||
send_flash_far(address, count);
|
||||
else
|
||||
send_flash_near(address, count);
|
||||
}
|
||||
|
||||
[[gnu::noinline]] void tx_ack()
|
||||
{
|
||||
link::tx(ack);
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(ee::read(address++));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do {
|
||||
ee::write<off>(address++, link::rx());
|
||||
tx_ack();
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
// One page into the SPM buffer, then erase and program. No running-slot write
|
||||
// guard: the host guarantees it never targets the loader's own slot (the pure
|
||||
// version's one dropped safety net, licensed — README.md).
|
||||
void program_flash(std::uint16_t wire_address)
|
||||
{
|
||||
spm::flash_address_t address;
|
||||
if constexpr (word_flash) {
|
||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1) & ~static_cast<std::uint16_t>(page - 1);
|
||||
std::uint16_t z = z0;
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
||||
z += 2;
|
||||
} while (static_cast<std::uint8_t>(z));
|
||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||
} else {
|
||||
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>(address, word_of({low, high}));
|
||||
address += 2;
|
||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||
}
|
||||
spm::erase_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
spm::write_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
void send_fuses()
|
||||
{
|
||||
std::uint8_t which = 0;
|
||||
do
|
||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||
while (++which != 4);
|
||||
}
|
||||
|
||||
[[noreturn]] void run()
|
||||
{
|
||||
if (hw::field_impl<wdrf_field()>::test())
|
||||
run_app();
|
||||
|
||||
link::init();
|
||||
|
||||
// Measure the calibration pulse into the unit, then take one 'p' knock. An
|
||||
// expired budget (no host) boots the application; a pulse that decodes to
|
||||
// anything but 'p' re-measures.
|
||||
for (;;) {
|
||||
if (link::measure(autobaud_budget) == 0)
|
||||
run_app();
|
||||
if (link::rx() == 'p')
|
||||
break;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
ee::wait();
|
||||
tx_ack();
|
||||
const std::uint8_t command = link::rx();
|
||||
switch (command) {
|
||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||
tx_ack();
|
||||
link::drain();
|
||||
jump(target);
|
||||
}
|
||||
case 'b': // chip identity: version then the three signature bytes
|
||||
link::tx(version);
|
||||
link::tx(hw::db.signature[0]);
|
||||
link::tx(hw::db.signature[1]);
|
||||
link::tx(hw::db.signature[2]);
|
||||
break;
|
||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
||||
case 'r': // read EEPROM: addr16, n8
|
||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||
// One address-and-count path for the three, so the flash streamer
|
||||
// keeps a single call site and inlines into this never-returning
|
||||
// loop — its cursor then lives in the loop's own call-saved
|
||||
// registers instead of being saved and restored around a call
|
||||
// (the call-site-count lesson, autobaud.md).
|
||||
const std::uint16_t address = rx16();
|
||||
const std::uint8_t count = link::rx();
|
||||
if (command == 'r')
|
||||
send_eeprom(address, count);
|
||||
else if (command == 'w')
|
||||
store_eeprom(address, count);
|
||||
else
|
||||
send_flash(address, count);
|
||||
break;
|
||||
}
|
||||
case 'W': // program one flash page: addr16, page bytes
|
||||
program_flash(rx16());
|
||||
break;
|
||||
case 'F': // fuse and lock bytes
|
||||
send_fuses();
|
||||
break;
|
||||
default: // unknown bytes are ignored; the loop re-acks
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace pureboot
|
||||
|
||||
template struct avr::startup::entry<pureboot::run>;
|
||||
343
pureboot/pureboot_autobaud_reg.cpp
Normal file
343
pureboot/pureboot_autobaud_reg.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
// pureboot autobaud — the software-serial variant that measures the host's bit
|
||||
// timing at runtime, so one binary runs at any F_CPU: the image carries no
|
||||
// clock. THE REGISTER VERSION — keeps the running-slot write guard, at the cost
|
||||
// of one global register variable (r4) holding the measured unit. That variable
|
||||
// is pureboot's single, deliberate break from its no-global-register-variable
|
||||
// rule, present only in this variant; everything else stays pure C++.
|
||||
//
|
||||
// Two source files exist for review (autobaud.md):
|
||||
// pureboot_autobaud_pure.cpp, fully pure but dropping the write guard, and this
|
||||
// one. They differ only in where the measured unit lives (a call-saved register
|
||||
// here, GPIOR/RAM there) and whether the guard is present.
|
||||
//
|
||||
// The register buys ~32 B over a RAM home — an outlined rx/tx reads it with one
|
||||
// move where a static costs an lds — and that is what lets the write guard stay
|
||||
// while the image still fits 512 B (the 1284 at 512, exactly). The unit is
|
||||
// written once through a noinline setter so the store lands immediately before a
|
||||
// ret: GCC otherwise deletes a global-register store whose only readers are
|
||||
// callees (autobaud.md, upstream bug 6).
|
||||
//
|
||||
// Simplifications shared with the pure version, each licensed: a slimmed info
|
||||
// block (version + signature; the host derives geometry from its chip database)
|
||||
// and a single-byte activation knock (the calibration pulse already proves a
|
||||
// host). Position independence is kept: control flow is PC-relative and the
|
||||
// write guard anchors on the runtime return address, as the fixed-baud loader.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
#include <util/delay_basic.h>
|
||||
|
||||
using namespace avr::literals;
|
||||
namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
namespace hw = avr::hw;
|
||||
|
||||
// The measured per-bit delay (in _delay_loop_2 four-cycle iterations) in a
|
||||
// call-saved register that the serial callees read directly, so no wire path
|
||||
// threads it and rx/tx reach it with a move, not a load. Written only through
|
||||
// set_unit() below.
|
||||
register std::uint16_t g_unit asm("r4");
|
||||
|
||||
namespace pureboot {
|
||||
namespace {
|
||||
|
||||
// Purely polled: every interrupt guard folds to nothing.
|
||||
constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
constexpr std::uint8_t ack = '+';
|
||||
|
||||
// Autobaud carries no clock; only the software-UART pins are a parameter.
|
||||
#if !defined(PUREBOOT_RX)
|
||||
#define PUREBOOT_RX pb0
|
||||
#endif
|
||||
#if !defined(PUREBOOT_TX)
|
||||
#define PUREBOOT_TX pb1
|
||||
#endif
|
||||
|
||||
consteval std::int16_t wdrf_field()
|
||||
{
|
||||
auto reg = std::string_view{hw::db.regs[static_cast<std::size_t>(avr::power::detail::reset_reg())].name};
|
||||
return hw::db.field_index(reg, "WDRF");
|
||||
}
|
||||
|
||||
constexpr std::uint16_t slot_bytes = 512;
|
||||
constexpr std::uint32_t base = spm::flash_bytes - slot_bytes;
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
constexpr bool boot_section = hw::curated::has_boot_section();
|
||||
constexpr bool word_flash = spm::flash_bytes > 65536;
|
||||
|
||||
constexpr std::uint8_t version = 4;
|
||||
|
||||
#if !defined(PUREBOOT_AUTOBAUD_POLLS)
|
||||
#define PUREBOOT_AUTOBAUD_POLLS 4000000
|
||||
#endif
|
||||
constexpr __uint24 autobaud_budget = PUREBOOT_AUTOBAUD_POLLS;
|
||||
|
||||
// The one store into g_unit, isolated so it lands right before the ret: a
|
||||
// global-register store whose only later readers are callees is dropped
|
||||
// otherwise (autobaud.md, upstream bug 6).
|
||||
[[gnu::noinline]] void set_unit(std::uint16_t v)
|
||||
{
|
||||
g_unit = v;
|
||||
}
|
||||
|
||||
// The autobaud software link: bit-banged with cycle-counted delays, but the
|
||||
// per-bit delay is g_unit, measured from the host's calibration pulse.
|
||||
struct link {
|
||||
using in_t = avr::io::input<avr::PUREBOOT_RX, avr::io::pull::up>;
|
||||
using out_t = avr::io::output<avr::PUREBOOT_TX>;
|
||||
|
||||
static void init()
|
||||
{
|
||||
avr::init<in_t, out_t>();
|
||||
out_t::set(); // idle high
|
||||
}
|
||||
|
||||
// Time the calibration pulse into g_unit. The host sends 0xC0 — a start bit
|
||||
// plus six zero data bits are one low pulse of seven bit-times — and the
|
||||
// counted poll loop is seven cycles an iteration, so count >> 2 is the bit
|
||||
// period in _delay_loop_2's four-cycle iterations. 0 means the budget
|
||||
// expired.
|
||||
static std::uint16_t measure(__uint24 budget)
|
||||
{
|
||||
while (in_t::read())
|
||||
if (!--budget)
|
||||
return 0;
|
||||
std::uint16_t count = 0;
|
||||
while (!in_t::read())
|
||||
++count;
|
||||
const std::uint16_t u = static_cast<std::uint16_t>(count >> 2);
|
||||
set_unit(u);
|
||||
return u;
|
||||
}
|
||||
|
||||
static std::uint8_t rx()
|
||||
{
|
||||
while (in_t::read()) // await the start edge
|
||||
;
|
||||
_delay_loop_2(static_cast<std::uint16_t>(g_unit + (g_unit >> 1))); // 1.5 bits to the LSB centre
|
||||
std::uint8_t value = 0;
|
||||
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||
value >>= 1;
|
||||
if (in_t::read())
|
||||
value |= 0x80;
|
||||
_delay_loop_2(g_unit);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static void tx(std::uint8_t value)
|
||||
{
|
||||
out_t::clear(); // start bit
|
||||
_delay_loop_2(g_unit);
|
||||
for (std::uint8_t bit = 0; bit < 8; ++bit) {
|
||||
out_t::write(value & 1);
|
||||
value >>= 1;
|
||||
_delay_loop_2(g_unit);
|
||||
}
|
||||
out_t::set(); // stop bit
|
||||
_delay_loop_2(g_unit);
|
||||
}
|
||||
|
||||
static void drain()
|
||||
{
|
||||
// The software transmitter returns only after the stop bit.
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" [[noreturn]] void pureboot_app();
|
||||
|
||||
[[gnu::noipa, noreturn]] void jump(void (*target)())
|
||||
{
|
||||
target();
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
[[gnu::noinline, noreturn]] void run_app()
|
||||
{
|
||||
jump(pureboot_app);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] inline std::uint16_t rx16()
|
||||
{
|
||||
std::uint16_t low = link::rx();
|
||||
return static_cast<std::uint16_t>(low | (link::rx() << 8));
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] inline std::uint16_t word_of(std::array<std::uint8_t, 2> pair)
|
||||
{
|
||||
return std::bit_cast<std::uint16_t>(pair);
|
||||
}
|
||||
|
||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_near(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(avr::flash_load(reinterpret_cast<const std::uint8_t *>(address++)));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
[[maybe_unused, gnu::always_inline]] inline void send_flash_far(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
std::uint8_t rampz = static_cast<std::uint8_t>(address >> 15);
|
||||
std::uint16_t z = static_cast<std::uint16_t>(address << 1);
|
||||
do {
|
||||
link::tx(avr::flash_load_far<std::uint8_t>((static_cast<std::uint32_t>(rampz) << 16) | z));
|
||||
if (++z == 0)
|
||||
++rampz;
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] inline void send_flash(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
if constexpr (word_flash)
|
||||
send_flash_far(address, count);
|
||||
else
|
||||
send_flash_near(address, count);
|
||||
}
|
||||
|
||||
[[gnu::noinline]] void tx_ack()
|
||||
{
|
||||
link::tx(ack);
|
||||
}
|
||||
|
||||
void send_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do
|
||||
link::tx(ee::read(address++));
|
||||
while (--count);
|
||||
}
|
||||
|
||||
void store_eeprom(std::uint16_t address, std::uint8_t count)
|
||||
{
|
||||
do {
|
||||
ee::write<off>(address++, link::rx());
|
||||
tx_ack();
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
// One page into the SPM buffer, then erase and program — except the slot this
|
||||
// code is running in (`slot_high`, from run()), which is drained and left
|
||||
// alone. A broken host therefore cannot brick the running loader, and a copy one
|
||||
// slot lower may rewrite the resident one.
|
||||
void program_flash(std::uint16_t wire_address, std::uint8_t slot_high)
|
||||
{
|
||||
spm::flash_address_t address;
|
||||
std::uint8_t page_high;
|
||||
if constexpr (word_flash) {
|
||||
const std::uint8_t rampz = static_cast<std::uint8_t>(wire_address >> 15);
|
||||
const std::uint16_t z0 = static_cast<std::uint16_t>(wire_address << 1) & ~static_cast<std::uint16_t>(page - 1);
|
||||
std::uint16_t z = z0;
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>((static_cast<spm::flash_address_t>(rampz) << 16) | z, word_of({low, high}));
|
||||
z += 2;
|
||||
} while (static_cast<std::uint8_t>(z));
|
||||
address = (static_cast<spm::flash_address_t>(rampz) << 16) | z0;
|
||||
page_high = static_cast<std::uint8_t>(wire_address >> 8);
|
||||
} else {
|
||||
address = static_cast<spm::flash_address_t>(wire_address & ~static_cast<std::uint16_t>(page - 1));
|
||||
do {
|
||||
std::uint8_t low = link::rx();
|
||||
std::uint8_t high = link::rx();
|
||||
spm::fill<off>(address, word_of({low, high}));
|
||||
address += 2;
|
||||
} while (static_cast<std::uint8_t>(address) & (page - 1));
|
||||
address -= 2; // back inside the page — erase and write ignore the word bits
|
||||
page_high = static_cast<std::uint8_t>(address >> 8) & 0xfe;
|
||||
}
|
||||
if (page_high != slot_high) {
|
||||
spm::erase_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
spm::write_page<off>(address);
|
||||
if constexpr (boot_section)
|
||||
spm::wait();
|
||||
}
|
||||
if constexpr (boot_section)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
void send_fuses()
|
||||
{
|
||||
std::uint8_t which = 0;
|
||||
do
|
||||
link::tx(spm::read_fuse<off>(static_cast<spm::fuse>(which)));
|
||||
while (++which != 4);
|
||||
}
|
||||
|
||||
[[noreturn]] void run()
|
||||
{
|
||||
if (hw::field_impl<wdrf_field()>::test())
|
||||
run_app();
|
||||
|
||||
link::init();
|
||||
|
||||
// The high byte of the slot this copy runs at, which the write guard
|
||||
// follows: the return address is a word address, so its high byte is the
|
||||
// 256-word slot index, doubled back into byte terms on a byte-addressed
|
||||
// chip. Taken as byteswap's low byte — the builtin already swaps the two
|
||||
// stacked bytes, and the double swap folds away.
|
||||
const std::uint16_t ra_words = reinterpret_cast<std::uint16_t>(__builtin_return_address(0));
|
||||
const std::uint8_t ra_high = static_cast<std::uint8_t>(std::byteswap(ra_words));
|
||||
const std::uint8_t slot_high = word_flash ? ra_high : static_cast<std::uint8_t>(ra_high << 1);
|
||||
|
||||
// Measure the calibration pulse into g_unit, then take one 'p' knock. An
|
||||
// expired budget (no host) boots the application; a pulse that decodes to
|
||||
// anything but 'p' re-measures.
|
||||
for (;;) {
|
||||
if (link::measure(autobaud_budget) == 0)
|
||||
run_app();
|
||||
if (link::rx() == 'p')
|
||||
break;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
ee::wait();
|
||||
tx_ack();
|
||||
const std::uint8_t command = link::rx();
|
||||
switch (command) {
|
||||
case 'J': { // jump to a wire word address: hand-over and staging transfer
|
||||
auto target = reinterpret_cast<void (*)()>(rx16());
|
||||
tx_ack();
|
||||
link::drain();
|
||||
jump(target);
|
||||
}
|
||||
case 'b': // chip identity: version then the three signature bytes
|
||||
link::tx(version);
|
||||
link::tx(hw::db.signature[0]);
|
||||
link::tx(hw::db.signature[1]);
|
||||
link::tx(hw::db.signature[2]);
|
||||
break;
|
||||
case 'R': // read flash: addr16, n8 (0 = 256)
|
||||
case 'r': // read EEPROM: addr16, n8
|
||||
case 'w': { // write EEPROM: addr16, n8, then n bytes each acked
|
||||
// One address-and-count path for the three, so the flash streamer
|
||||
// keeps a single call site and inlines into this never-returning
|
||||
// loop (autobaud.md).
|
||||
const std::uint16_t address = rx16();
|
||||
const std::uint8_t count = link::rx();
|
||||
if (command == 'r')
|
||||
send_eeprom(address, count);
|
||||
else if (command == 'w')
|
||||
store_eeprom(address, count);
|
||||
else
|
||||
send_flash(address, count);
|
||||
break;
|
||||
}
|
||||
case 'W': // program one flash page: addr16, page bytes
|
||||
program_flash(rx16(), slot_high);
|
||||
break;
|
||||
case 'F': // fuse and lock bytes
|
||||
send_fuses();
|
||||
break;
|
||||
default: // unknown bytes are ignored; the loop re-acks
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace pureboot
|
||||
|
||||
template struct avr::startup::entry<pureboot::run>;
|
||||
Reference in New Issue
Block a user