fix: --flash never ran the brick guard, because it never read the fuses
check_walk_region() refuses an image writing into the span reset crosses to reach the loader, and a plain --flash never ran it: the fuses were read for --fuses and for --update-loader, so the tool read them to protect the loader and never to protect the reset path. It cost an ssd1306 board - an application grown through 0x7c00 on a 328P with hfuse 0xdc, reset landing mid-function, an ICE the only way back. The check now fetches its own input, so the operation that asks for no fuses cannot skip it and neither can a direct API caller: pbdirty and pbmute call op_flash() as a library and are guarded without a line changing in them. Fuses that cannot be read are a refusal naming --assume-fuses and --force, because unknown is not empty. The rig had to stop lying first. The fuse read is an LPM diverted by BLBSET, which simavr executes straight out of flash with no hook, so a fuse read answered flash bytes and --fuses had been printing them on every chip. The runner models the diversion at the SPMCSR write, -f states the profile - and stores the register itself, since a registered handler replaces simavr's store and would otherwise swallow every SPM command on the cores where nothing else watches it. pureboot.walk reproduces the brick: the unfixed tool writes 249 pages through 0x7c00 in silence, the fixed one refuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,13 @@
|
||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
||||
// 'F' command answers with flash bytes - the tests assert transport only.
|
||||
// instantly. The fuse readout's LPM diversion is missing from every core and
|
||||
// is supplied too, so a fuse read answers fuses (-f) and not flash bytes.
|
||||
//
|
||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
||||
// ground-truth cross-check against what the host read back.
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -177,6 +178,58 @@ void fix_mega_flash_erase()
|
||||
std::println(stderr, "device: no flash module to fix - SPM page erases may misalign");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- fuses ---
|
||||
|
||||
// The fuse and lock bytes answer an LPM, not an SPM: BLBSET|SELFPRGEN in
|
||||
// SPMCSR diverts the next LPM to them, selected by Z (Atmel-8271 section
|
||||
// 26.8.9). simavr executes LPM straight out of avr->flash and offers no hook
|
||||
// on it, so the diversion is modeled where there is one - the SPMCSR write -
|
||||
// by lending the four flash bytes Z can select to the fuses for as long as the
|
||||
// hardware holds SELFPRGEN. Unprogrammed until -f says otherwise, as a part
|
||||
// ships and as the erased flash above is.
|
||||
auto fuses = std::to_array<std::uint8_t>({0xFF, 0xFF, 0xFF, 0xFF}); // Z order: low, lock, extended, high
|
||||
decltype(fuses) lent;
|
||||
|
||||
int parse_fuses(std::string_view spec)
|
||||
{
|
||||
if (spec.size() != 2 * fuses.size()) {
|
||||
return -1;
|
||||
}
|
||||
for (std::size_t at = 0; at < fuses.size(); at++) {
|
||||
const auto digits = spec.substr(2 * at, 2);
|
||||
if (std::from_chars(digits.data(), digits.data() + digits.size(), fuses[at], 16).ec != std::errc{}) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
avr_cycle_count_t end_fuse_read(avr_t *mcu, avr_cycle_count_t, void *)
|
||||
{
|
||||
std::memcpy(mcu->flash, lent.data(), lent.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
void spmcsr_written(avr_t *mcu, avr_io_addr_t at, std::uint8_t value, void *)
|
||||
{
|
||||
// A registered write handler *replaces* the store simavr would have done
|
||||
// (sim_core.c), so performing it is this handler's job - on the tinies
|
||||
// nothing else is watching the register, and swallowing the store would
|
||||
// leave every SPM command unseen by the NVM model above.
|
||||
avr_core_watch_write(mcu, at, value);
|
||||
constexpr std::uint8_t read_fuse_command = 0x09; // BLBSET|SELFPRGEN
|
||||
// The LPM must follow within three cycles of the arming store (Atmel-8271
|
||||
// section 26.8.9), and simavr runs its cycle timers between instructions -
|
||||
// so the loan is returned after the LPM that took it, never during.
|
||||
constexpr avr_cycle_count_t selfprgen_window = 3;
|
||||
if ((value & 0x1F) != read_fuse_command) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(lent.data(), mcu->flash, lent.size());
|
||||
std::memcpy(mcu->flash, fuses.data(), fuses.size());
|
||||
avr_cycle_timer_register(mcu, selfprgen_window, end_fuse_read, nullptr);
|
||||
}
|
||||
|
||||
void request_reset(int)
|
||||
{
|
||||
reset_requested = 1;
|
||||
@@ -184,6 +237,9 @@ void request_reset(int)
|
||||
|
||||
// ------------------------------------------------------------- tiny NVM ---
|
||||
|
||||
// Where SPMCSR sits on the cores that carry no flash module to name it.
|
||||
constexpr avr_io_addr_t tiny_spmcsr = 0x57;
|
||||
|
||||
struct tiny_nvm_t {
|
||||
avr_io_t io;
|
||||
std::array<std::uint8_t, 128> buffer;
|
||||
@@ -200,7 +256,7 @@ int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
|
||||
}
|
||||
auto *n = reinterpret_cast<tiny_nvm_t *>(io);
|
||||
avr_t *mcu = io->avr;
|
||||
std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
||||
std::uint8_t command = mcu->data[tiny_spmcsr] & 0x1f;
|
||||
auto z = static_cast<std::uint16_t>(mcu->data[30] | (mcu->data[31] << 8));
|
||||
std::uint32_t page_base = static_cast<std::uint32_t>(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
||||
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
||||
@@ -222,7 +278,7 @@ int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
|
||||
std::memset(n->buffer.data(), 0xff, n->page);
|
||||
std::memset(n->used.data(), 0, n->page);
|
||||
}
|
||||
mcu->data[0x57] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
|
||||
mcu->data[tiny_spmcsr] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -494,11 +550,18 @@ void poll_pty()
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
bool link_given = false;
|
||||
for (int opt; (opt = getopt(argc, argv, "l:w")) != -1;) {
|
||||
for (int opt; (opt = getopt(argc, argv, "l:wf:")) != -1;) {
|
||||
if (opt == 'w') {
|
||||
window_report = true;
|
||||
continue;
|
||||
}
|
||||
if (opt == 'f') {
|
||||
if (parse_fuses(optarg) != 0) {
|
||||
std::println(stderr, "device: -f takes 8 hex digits: low,lock,extended,high");
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
||||
return 2;
|
||||
@@ -508,10 +571,12 @@ int main(int argc, char *argv[])
|
||||
int args = argc - optind;
|
||||
if (args < 7 || args > 9) {
|
||||
std::print(stderr,
|
||||
"usage: {} [-l link] [-w] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||
" [reset_hex] [resume_flash]\n"
|
||||
"usage: {} [-l link] [-w] [-f fuses] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud>"
|
||||
" <flash_dump> [reset_hex] [resume_flash]\n"
|
||||
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
||||
" them); default: the chip's own\n"
|
||||
" -f fuses: 8 hex digits, low,lock,extended,high - what a fuse read answers;\n"
|
||||
" default: unprogrammed. reset_hex stays the reset target\n"
|
||||
" -w: print PB_WINDOW_TX <cycle> at the first transmit activity and\n"
|
||||
" free-run idle time (window measurement mode)\n"
|
||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
||||
@@ -599,6 +664,9 @@ int main(int argc, char *argv[])
|
||||
nvm.io.ioctl = nvm_ioctl;
|
||||
avr_register_io(avr, &nvm.io);
|
||||
}
|
||||
// Where the fuse read is armed: the mega's flash module names its SPMCSR,
|
||||
// and the tinies keep theirs at the address both those cores share.
|
||||
avr_register_io_write(avr, mega_flash ? mega_flash->r_spm : tiny_spmcsr, spmcsr_written, nullptr);
|
||||
|
||||
if (!link_software) {
|
||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
||||
|
||||
Reference in New Issue
Block a user