diff --git a/CMakeLists.txt b/CMakeLists.txt index 1390bba..126ed7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -283,12 +283,14 @@ if(PROJECT_IS_TOP_LEVEL) add_test(NAME pureboot_autobaud.size COMMAND ${CMAKE_COMMAND} -DSIZE_TOOL=${CMAKE_SIZE} -DELF=$ -DLIMIT=${PUREBOOT_LIMIT} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_size.cmake) - # The measured unit is the loader's only RAM object and sits at the very - # start of SRAM — where the host reads the bit period from (--info's - # measured clock), so the address is wire contract, not layout accident. + # The measured unit's home is wire contract, not layout accident: the + # host reads the bit period from it (--info's measured clock). In the + # GPIOR home the image must carry no RAM copy at all; in the RAM home it + # is the loader's only RAM object, at the very start of SRAM. add_test(NAME pureboot_autobaud.unit COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${CMAKE_OBJDUMP} -DELF=$ - -DRAM_START=${PUREBOOT_RAM_START} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.cmake) + -DRAM_START=${PUREBOOT_RAM_START} -DGPIOR=${PUREBOOT_UNIT_GPIOR} + -P ${CMAKE_CURRENT_SOURCE_DIR}/test/check_unit.cmake) # One point of the exhaustive matrix, named from its resolved parameters # so the enumeration cannot collide with itself. `pins` is empty for the diff --git a/ide/README.md b/ide/README.md index c55d583..2162ffc 100644 --- a/ide/README.md +++ b/ide/README.md @@ -2,7 +2,7 @@ `master` carries `bootloader.atsln`, so this branch does too: `ide/bootloader.atsln` builds the loaders from the same sources Ninja does, to a **byte-identical -`.text`** — 400 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in +`.text`** — 390 B for the 328P pureboot loader, 510 B for the `tsb_asm` tier in its 512-byte section. CMake remains the build system; the solution is here so the port opens in Studio as its predecessor did. diff --git a/libavr b/libavr index 911a875..082309a 160000 --- a/libavr +++ b/libavr @@ -1 +1 @@ -Subproject commit 911a87538f2dc99a22ac0b2faa9434ad6e438362 +Subproject commit 082309a4cb8013cedfbefc70afe6ccd947a47e0e diff --git a/pureboot/CMakeLists.txt b/pureboot/CMakeLists.txt index b5c793f..07a2e60 100644 --- a/pureboot/CMakeLists.txt +++ b/pureboot/CMakeLists.txt @@ -138,15 +138,24 @@ endif() # Where SRAM begins: the classic megas keep it right after the plain I/O # registers, the x8/x4 generations push it past their extended I/O file, and -# the tinies match the classics. An autobaud loader's measured unit lives at -# exactly this address (the host reads it there — pureboot.py), and the -# unit-position test holds the layout to it. +# the tinies match the classics. An autobaud loader keeps its measured unit +# in GPIOR2:GPIOR1 wherever the chip has the pair (data 0x32 on the +# t25/45/85, 0x4A from the x8 generation on) and as the first RAM object at +# SRAM start where it does not (the t13s and classic megas). The host reads +# whichever home applies (pureboot.py's geometry), and the unit-position +# test holds the image to the same split. if(LIBAVR_MCU MATCHES "^atmega(8|16|32)a?$") set(_pb_ram 0x60) + set(_pb_unit_gpior "") elseif(LIBAVR_MCU MATCHES "^atmega") set(_pb_ram 0x100) + set(_pb_unit_gpior 0x4A) +elseif(LIBAVR_MCU MATCHES "^attiny13") + set(_pb_ram 0x60) + set(_pb_unit_gpior "") else() set(_pb_ram 0x60) + set(_pb_unit_gpior 0x32) endif() # The function runs in its caller's scope, so everything it needs crosses @@ -169,6 +178,7 @@ set(PUREBOOT_LIMIT ${_pb_limit} PARENT_SCOPE) set(PUREBOOT_EEPROM ${_pb_eeprom} PARENT_SCOPE) set(PUREBOOT_DEFAULT_HZ ${_pb_hz} PARENT_SCOPE) set(PUREBOOT_RAM_START ${_pb_ram} PARENT_SCOPE) +set(PUREBOOT_UNIT_GPIOR "${_pb_unit_gpior}" PARENT_SCOPE) set(PUREBOOT_HAS_USART ${_pb_has_usart} PARENT_SCOPE) set(PUREBOOT_HAS_USART1 ${_pb_has_usart1} PARENT_SCOPE) set(PUREBOOT_SIM_MCU ${_pb_sim_mcu} PARENT_SCOPE) diff --git a/pureboot/README.md b/pureboot/README.md index 3d0a411..d4a6875 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -28,31 +28,31 @@ it carries the calibration machinery and no clock at all. | Chip | Flash | Loader at | Link | Stock | Autobaud | |---|---|---|---|---|---| -| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 390 B | 460 B | -| ATtiny25 † | 2 KiB | 0x0600 | software | 394 B | 464 B | -| ATtiny45 † | 4 KiB | 0x0e00 | software | 398 B | 468 B | -| ATtiny85 † | 8 KiB | 0x1e00 | software | 398 B | 468 B | -| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 360 B | 474 B | -| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 362 B | 480 B | -| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 362 B | 480 B | -| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 388 B | 464 B | -| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 398 B | 474 B | -| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B | -| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B | -| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 400 B | 480 B | -| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 400 B | 480 B | -| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 394 B | 474 B | -| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 420 B | 500 B | +| ATtiny13, ATtiny13A † | 1 KiB | 0x0200 | software | 384 B | 452 B | +| ATtiny25 † | 2 KiB | 0x0600 | software | 388 B | 442 B | +| ATtiny45 † | 4 KiB | 0x0e00 | software | 388 B | 442 B | +| ATtiny85 † | 8 KiB | 0x1e00 | software | 388 B | 442 B | +| ATmega8, 8A | 8 KiB | 0x1e00 | USART0 | 358 B | 470 B | +| ATmega16, 16A | 16 KiB | 0x3e00 | USART0 | 360 B | 474 B | +| ATmega32, 32A | 32 KiB | 0x7e00 | USART0 | 360 B | 474 B | +| ATmega48, 48A, 48P, 48PA † | 4 KiB | 0x0e00 | USART0 | 378 B | 438 B | +| ATmega88, 88A, 88P, 88PA | 8 KiB | 0x1e00 | USART0 | 388 B | 448 B | +| ATmega168, 168A, 168P, 168PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B | +| ATmega328, 328P | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B | +| ATmega164A, 164P, 164PA | 16 KiB | 0x3e00 | USART0 | 390 B | 454 B | +| ATmega324A, 324P, 324PA | 32 KiB | 0x7e00 | USART0 | 390 B | 454 B | +| ATmega644, 644A, 644P, 644PA | 64 KiB | 0xfe00 | USART0 | 384 B | 448 B | +| ATmega1284, 1284P | 128 KiB | 0x1fe00 | USART0 | 410 B | 474 B | † No hardware boot section: the host patches the reset vector, and the budget is 510 bytes, since the slot's last word is the trampoline. The tightest fit in the whole space is the 1284s' autobaud build deployed on a -USART's own pins with the `OSCCAL` trim baked, 510 of its 512 — they alone +USART's own pins with the `OSCCAL` trim baked, 484 of its 512 — they alone carry the far-flash machinery (ELPM reads, RAMPZ page commands), autobaud alone carries the calibration loop, a bit-banged link on a USART's pins alone has to release it (below), and the trim adds its one register write. Without -the trim that build is 504; on the default pins, 500. The flash bank riding +the trim that build is 478; on the default pins, 474. The flash bank riding in a transfer's selector byte keeps even those chips' addressing the same 16-bit form every other chip uses, which is why they are no longer the outlier they were. @@ -187,8 +187,10 @@ reply, repeat. Addresses are **byte addresses within a 64 KiB bank**, and the bank rides in the command's selector byte, so no command has to speak word addresses. `J` is -the exception: it takes a word address, because that is what the hardware's own -jump takes. EEPROM and data-space addresses and all counts are bytes. +the exception: its address is a word address, because that is what the +hardware's own jump takes — it still carries a selector byte (reserved, +ignored) so its decode is the same three reads as every other command's. +EEPROM and data-space addresses and all counts are bytes. The loader trusts the host to keep addresses in range: it does not bound them against the chip. **Gotcha:** a write (or read) that runs past `E2END` wraps — @@ -203,7 +205,7 @@ better spent on features than on re-checking a bound the host already holds. | `G` | sel8, addr16, n8 | n bytes from the selected space (n = 0 means 256) | | `g` | sel8, addr16, n8, then n data bytes | `+` per byte, sent once its write has begun | | `W` | sel8, addr16, then one page of data | — (completion = next prompt) | -| `J` | word address (16-bit) | `+`, then execution continues there | +| `J` | sel8 (reserved), word address (16-bit) | `+`, then execution continues there | | other | — | ignored; the loop re-prompts (send a junk byte, await `+`, to resync) | `G` and `g` are one letter in two cases, which is the whole command set for @@ -291,7 +293,12 @@ from `b`, and a command per memory (`R`/`W` flash, `r`/`w` EEPROM, `F` fuses). above; the shipped tool speaks both, choosing on the version it reads, so a deployed pureboot 4 stays drivable and self-updatable to 5. **6** changes nothing on the wire: it marks the builds that may carry a baked `OSCCAL` trim -(Configuration), so a tool driving an update knows such images exist. +(Configuration), so a tool driving an update knows such images exist. **7** +moves `J` onto the unified decode — it gains the selector byte the table +shows, which older loaders do not read, so the tool sends each form to the +version that speaks it — and re-homes the autobaud unit into the GPIOR pair +on the chips that have one (Session: what must not be written), which is +where `--info`'s measured clock now reads it on those parts. Every closed generation is tagged in this repo at its era's last commit — the commit just before the next version bump, so a tag holds everything its @@ -476,11 +483,14 @@ Reads are safe anywhere; **two small regions cannot be written without ending th session,** because they are what the loader is standing on: - the **top of SRAM**, where its stack lives — a handful of bytes below RAMEND; -- on an **autobaud** build, the **two bytes at RAMSTART**: the measured bit - period, in `.noinit`, which is the whole of that loader's static RAM. Overwrite - it and its next reply is timed against garbage. On an ATtiny13A that is - `0x60..0x61`, and the symptom is a mangled prompt byte rather than any error — - the loader is fine, it simply is no longer speaking the agreed rate. +- on an **autobaud** build, the **measured bit period**: two bytes in + GPIOR2:GPIOR1 where the chip has the pair (data `0x32..0x33` on the + t25/45/85, `0x4A..0x4B` from the x8 generation on — such a loader has *no* + static RAM at all), and the two bytes at RAMSTART on the chips without one + (the t13s and classic megas), where they are the whole of the loader's + static RAM. Overwrite either home and the next reply is timed against + garbage — the symptom is a mangled prompt byte rather than any error; the + loader is fine, it simply is no longer speaking the agreed rate. Both are self-inflicted rather than defects, and a reset clears them. Note also that `--poke` can write OSCCAL, which does take effect — but a session can only @@ -518,9 +528,10 @@ Per chip preset, `ctest` runs: below sweeps. `pureboot*osccal*.size` add the `OSCCAL` trim on the stock shape and on the tightest image in the space (autobaud on a USART's own pins), holding both of the trim write's addressing encodings to the budget; -- `pureboot_autobaud.unit` — the measured bit period is the loader's only RAM - object and sits exactly at ram_start, where `--info` reads it: wire - contract, not layout accident; +- `pureboot_autobaud.unit` — the measured bit period sits where `--info` + reads it (wire contract, not layout accident): in the GPIOR pair, with no + RAM object at all, on the chips that have one; as the loader's only RAM + object at exactly ram_start elsewhere; - `pbm_*.size` — with `PUREBOOT_FULL_MATRIX=1`, the exhaustive cross product replacing that compact matrix, on **every** chip: every plausible oscillator (the internal ones, the CKDIV8 floor, the plain and the UART crystals) × diff --git a/pureboot/pureboot.cpp b/pureboot/pureboot.cpp index 4665b19..b93302c 100644 --- a/pureboot/pureboot.cpp +++ b/pureboot/pureboot.cpp @@ -75,7 +75,7 @@ static_assert(PUREBOOT_OSCCAL >= 0 && PUREBOOT_OSCCAL <= 0xff, "PUREBOOT_OSCCAL // The loader's one identity number. The protocol carries none of its own — // a version implies it, and the host tool holds that map (README.md). -constexpr std::uint8_t version = 6; +constexpr std::uint8_t version = 7; // The image's identity stamp, for the host tool rather than for the wire: an // update image is a bare 512-byte slot, and without this nothing in it says @@ -241,7 +241,10 @@ struct software_link { // every rate. Activation differs in kind from the other two — there is no // clock to time a window against — so this backend brings its own, below. struct autobaud_link { - using uart = avr::uart::software_autobaud; + // The unit in GPIOR2:GPIOR1 where the chip has them: the loader owns the + // whole chip while it runs, and the pair is four instructions cheaper per + // session than a RAM word. + using uart = avr::uart::software_autobaud; static void init() { @@ -288,7 +291,7 @@ extern "C" [[noreturn]] void pureboot_app(); __builtin_unreachable(); } -[[gnu::noinline, noreturn]] void run_app() +[[noreturn]] void run_app() { jump(pureboot_app); } @@ -488,12 +491,6 @@ void fill_page(std::uint8_t bank, std::uint16_t at) 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(rx16()); - tx_ack(); - link::drain(); - jump(target); - } case 'b': // identity: the version, then the three signature bytes // Straight out of the stamp, so the wire and the image can never // disagree about what this loader is. The indices are constant and @@ -502,18 +499,26 @@ void fill_page(std::uint8_t bank, std::uint16_t at) for (std::uint8_t at = stamp_identity; at != sizeof identity_stamp; ++at) link::tx(identity_stamp[at]); break; + case 'J': // jump: sel8 (reserved), addr16 as a wire word address case 'W': // fill one flash page buffer: sel8, addr16, then page bytes case 'G': // read: sel8, addr16, n8 (0 = 256) case 'g': { // write: sel8, addr16, n8, then n bytes, each acked - // One decode, one cursor and one loop for every space and both - // directions: a command per memory would carry a copy of all three - // each. 'W' joins the same decode rather than keeping an address - // form of its own, so flash addressing is uniform across every - // command that names it. + // One decode, one cursor and one loop for every space, both + // directions and the jump: a command per memory would carry a copy + // of all three each. 'J' — the hand-over and staging transfer — + // carries a selector it ignores so its address rides the same two + // reads as everything else; 'W' joins the same decode rather than + // keeping an address form of its own, so flash addressing is + // uniform across every command that names it. const std::uint8_t selector = link::rx(); const std::uint8_t space = space_of(selector); const std::uint8_t bank = bank_of(selector); std::uint16_t at = rx16(); + if (command == 'J') { + tx_ack(); + link::drain(); + jump(reinterpret_cast(at)); + } if (command == 'W') { fill_page(bank, at); break; @@ -540,4 +545,7 @@ void fill_page(std::uint8_t bank, std::uint16_t at) } // namespace } // namespace pureboot -template struct avr::startup::entry; +// stack::hardware: activation is reset-only, so the reset logic's own +// SP = RAMEND stands wherever the datasheet guarantees it (the classic +// megas still get the write); a 'J' entry runs on the caller's live stack. +template struct avr::startup::entry; diff --git a/pureboot/pureboot.py b/pureboot/pureboot.py index a906382..645f72e 100644 --- a/pureboot/pureboot.py +++ b/pureboot/pureboot.py @@ -26,7 +26,7 @@ else: import termios PROMPT = b"+" -VERSION = 6 # this tool's own version — free to drift from a loader's +VERSION = 7 # this tool's own version — free to drift from a loader's # The loader versions this tool can drive. A pureboot version implies its wire # protocol, which carries no number of its own, so this window is where that # map lives: the tool keeps a decoder for every generation in it (1–4 speak @@ -34,7 +34,7 @@ VERSION = 6 # this tool's own version — free to drift from a loader's # builds and changes nothing on the wire), and a version it has no decoder # for moves the floor. OLDEST_LOADER = 1 -NEWEST_LOADER = 6 +NEWEST_LOADER = 7 SLOT = 512 # the loader slot, on every chip RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops @@ -45,11 +45,13 @@ RETRIES = 3 # rewrites of a page that reads back wrong, before the run stops UNIFIED_LOADER = 5 SP_FLASH, SP_EEPROM, SP_RAM, SP_FUSE, SP_SPM = 0, 1, 2, 3, 4 -# A v5+ autobaud loader keeps its measured bit period at ram_start, encoded -# as delay-loop counts: (bit cycles − UNIT_DISCOUNT) / UNIT_LOOP_CYCLES, +# An autobaud loader keeps its measured bit period readable, encoded as +# delay-loop counts: (bit cycles − UNIT_DISCOUNT) / UNIT_LOOP_CYCLES, # floored — the spin granule and per-bit overhead of libavr's software UART. -# --info undoes the encoding to report the true clock, which therefore sits -# within one granule below it. +# v5/v6 keep it at ram_start; v7 moves it into GPIOR2:GPIOR1 on the chips +# that have the pair (their data addresses are in the geometry) and keeps +# ram_start only where they do not exist. --info undoes the encoding to +# report the true clock, which therefore sits within one granule below it. UNIT_LOOP_CYCLES, UNIT_DISCOUNT = 4, 8 # A selector's high nibble is the flash bank — the address bits above the 16-bit @@ -77,36 +79,40 @@ CALIBRATE = 0xC0 # from its chip database at build time). Die revisions that share a signature # share this row, as they share the silicon. CHIP_GEOMETRY = { - # signature : (flash, page, eeprom, patch_vector, ram_start) + # signature : (flash, page, eeprom, patch_vector, ram_start, gpior1) # ram_start is where SRAM begins in data space: the classic megas and the # tinies keep it right after the plain I/O registers (0x60), the x8/x4 - # generations past their extended I/O file (0x100). An autobaud loader's - # measured bit period lives at exactly ram_start (its only RAM object; - # the loader's own build pins the layout), which is what --info reads. - (0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60), # ATtiny13/13A - (0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60), # ATtiny25 - (0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60), # ATtiny45 - (0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60), # ATtiny85 - (0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100), # ATmega48/48A - (0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100), # ATmega48P/48PA - (0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60), # ATmega8/8A - (0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100), # ATmega88/88A - (0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100), # ATmega88P/88PA - (0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60), # ATmega16/16A - (0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100), # ATmega168/168A - (0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100), # ATmega168P/168PA - (0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100), # ATmega164P/164PA - (0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100), # ATmega164A - (0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60), # ATmega32/32A - (0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100), # ATmega328P - (0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100), # ATmega328 - (0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100), # ATmega324P - (0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100), # ATmega324PA - (0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100), # ATmega324A - (0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100), # ATmega644/644A - (0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100), # ATmega644P/644PA - (0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100),# ATmega1284P - (0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100),# ATmega1284 + # generations past their extended I/O file (0x100). gpior1 is GPIOR1's + # data address — 0x32 on the t25/45/85, 0x4A from the x8 generation on, + # None where the chip has no pair (t13, classic megas). A v7 autobaud + # loader's measured bit period lives in GPIOR2:GPIOR1 where they exist + # and at exactly ram_start elsewhere (its only RAM object; the loader's + # own build pins the layout); v5/v6 always used ram_start. --info reads + # whichever home the answering version implies. + (0x1E, 0x90, 0x07): (1024, 32, 64, True, 0x60, None), # ATtiny13/13A + (0x1E, 0x91, 0x08): (2048, 32, 128, True, 0x60, 0x32), # ATtiny25 + (0x1E, 0x92, 0x06): (4096, 64, 256, True, 0x60, 0x32), # ATtiny45 + (0x1E, 0x93, 0x0B): (8192, 64, 512, True, 0x60, 0x32), # ATtiny85 + (0x1E, 0x92, 0x05): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48/48A + (0x1E, 0x92, 0x0A): (4096, 64, 256, True, 0x100, 0x4A), # ATmega48P/48PA + (0x1E, 0x93, 0x07): (8192, 64, 512, False, 0x60, None), # ATmega8/8A + (0x1E, 0x93, 0x0A): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88/88A + (0x1E, 0x93, 0x0F): (8192, 64, 512, False, 0x100, 0x4A), # ATmega88P/88PA + (0x1E, 0x94, 0x03): (16384, 128, 512, False, 0x60, None), # ATmega16/16A + (0x1E, 0x94, 0x06): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168/168A + (0x1E, 0x94, 0x0B): (16384, 128, 512, False, 0x100, 0x4A), # ATmega168P/168PA + (0x1E, 0x94, 0x0A): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164P/164PA + (0x1E, 0x94, 0x0F): (16384, 128, 512, False, 0x100, 0x4A), # ATmega164A + (0x1E, 0x95, 0x02): (32768, 128, 1024, False, 0x60, None), # ATmega32/32A + (0x1E, 0x95, 0x0F): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328P + (0x1E, 0x95, 0x14): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega328 + (0x1E, 0x95, 0x08): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324P + (0x1E, 0x95, 0x11): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324PA + (0x1E, 0x95, 0x15): (32768, 128, 1024, False, 0x100, 0x4A), # ATmega324A + (0x1E, 0x96, 0x09): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644/644A + (0x1E, 0x96, 0x0A): (65536, 256, 2048, False, 0x100, 0x4A), # ATmega644P/644PA + (0x1E, 0x97, 0x05): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284P + (0x1E, 0x97, 0x06): (131072, 256, 4096, False, 0x100, 0x4A),# ATmega1284 } VERBOSE = False @@ -450,7 +456,7 @@ class Info: if geometry is None: sig = " ".join(f"{b:02x}" for b in signature) raise Error(f"unknown signature {sig} — this tool has no geometry for it") - flash, page, eeprom, patch, _ = geometry + flash, page, eeprom, patch, _, _ = geometry base = flash - SLOT word_flash = flash > 0x10000 wire_base = base // 2 if word_flash else base @@ -491,6 +497,11 @@ class Info: # permits where from_identity refuses. geometry = CHIP_GEOMETRY.get(tuple(self.signature)) self.ram = geometry[4] if geometry else None + # Where this loader keeps the measured bit period (None when a fixed + # signature row is missing): the GPIOR pair from v7 where the chip + # has one, ram_start before that and everywhere without the pair. + gpior1 = geometry[5] if geometry else None + self.unit_home = gpior1 if self.version >= 7 and gpior1 is not None else self.ram def describe(self): sig = " ".join(f"{b:02x}" for b in self.signature) @@ -745,8 +756,13 @@ class Loader: return self._command(b"F", 4, 2.0) def jump(self, word_address): - """The device acks, then execution continues at the word address.""" - self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8))) + """The device acks, then execution continues at the word address. + From v7 'J' rides the unified decode, so it carries a selector byte + the loader ignores; older loaders take the bare address.""" + if self.info.version >= 7: + self.port.write(bytes((ord("J"), 0, word_address & 0xFF, word_address >> 8))) + else: + self.port.write(bytes((ord("J"), word_address & 0xFF, word_address >> 8))) self._expect_prompt() def enter_copy(self, byte_address, wait, link=None): @@ -1541,14 +1557,14 @@ def main(): print("device:") for line in info.lines(): print(f" {line}") - if args.autobaud and info.ram is not None: - # The whole of the loader's RAM is the measured bit period at - # ram_start; decoded and times the rate this session drives, + if args.autobaud and info.unit_home is not None: + # The measured bit period, from wherever this version keeps it + # (unit_home); decoded and times the rate this session drives, # that is the true clock — the number to hold an OSCCAL bake # or a fixed-baud build against (README.md: deployment). The - # autobaud identity path refuses unknown signatures, so ram is - # always known here; the guard states that dependency. - unit = int.from_bytes(loader.read_ram(info.ram, 2), "little") + # autobaud identity path refuses unknown signatures, so the + # home is always known here; the guard states that dependency. + unit = int.from_bytes(loader.read_ram(info.unit_home, 2), "little") cycles = unit * UNIT_LOOP_CYCLES + UNIT_DISCOUNT clock = cycles * args.baud offset = f", {(clock / args.clock - 1) * 100:+.1f} % of {args.clock}" if args.clock else "" diff --git a/test/check_unit.cmake b/test/check_unit.cmake index 4833dc3..4d31006 100644 --- a/test/check_unit.cmake +++ b/test/check_unit.cmake @@ -1,7 +1,10 @@ -# Asserts the autobaud loader's measured unit is the first RAM object: the -# host tool reads the bit period from ram_start (--info's measured clock), so -# the unit's address is wire contract. Run as -# cmake -DOBJDUMP=... -DELF=... -DRAM_START= -P check_unit.cmake +# Asserts the autobaud loader's measured unit sits where the host will read +# it (--info's measured clock — the address is wire contract). Two homes: on +# a chip with the GPIOR pair the unit lives there and the image must carry no +# RAM word for it at all; elsewhere it is the first RAM object at SRAM start. +# Run as +# cmake -DOBJDUMP=... -DELF=... -DRAM_START= [-DGPIOR=] +# -P check_unit.cmake execute_process(COMMAND ${OBJDUMP} -t ${ELF} OUTPUT_VARIABLE _syms RESULT_VARIABLE _res) if(NOT _res EQUAL 0) @@ -9,7 +12,17 @@ if(NOT _res EQUAL 0) endif() # The symbol line: "00800100 l O .noinit 00000002 unit_E". -string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_[^ \t\n]*\n" _line "${_syms}") +string(REGEX MATCH "\n0*([0-9a-f]+)[^\n]+[ \t][^ \t\n]*unit_E\n" _line "${_syms}") + +if(GPIOR) + if(_line) + message(FATAL_ERROR "unit_ RAM symbol present although the unit's home is GPIOR ${GPIOR} — " + "the host peeks the pair, and a RAM copy would be dead weight") + endif() + message(STATUS "no unit_ RAM object — the unit lives in the GPIOR pair at ${GPIOR}") + return() +endif() + if(NOT _line) message(FATAL_ERROR "no unit_ symbol in ${ELF} — is this the autobaud loader?") endif() diff --git a/test/pbautobaud.py b/test/pbautobaud.py index 5e7831b..2ab52a0 100644 --- a/test/pbautobaud.py +++ b/test/pbautobaud.py @@ -64,8 +64,9 @@ def main(): for needed in ("version", "signature", "fuses", "verify:", "stays"): if needed not in out: fail(f"{label}: session output lacks {needed!r}\n{out}") - # The measured clock, decoded from the unit at ram_start. The - # runner's clock is exact, so the figure must land inside the + # The measured clock, decoded from the unit at whichever home this + # version keeps it in. The runner's clock is exact, so the figure + # must land inside the # encoding's own envelope: the loader floors the bit period to # 4-cycle spin granules after an 8-cycle discount, and the edge # poll can shave a few cycles more — one granule of slack below