diff --git a/CMakeLists.txt b/CMakeLists.txt index 2dd7ce0..d4f13b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,10 @@ endif() # compile-time constant; a different PUREBOOT_TIMEOUT builds the re-timed # binary a self-update then installs. set(PUREBOOT_TIMEOUT 8 CACHE STRING "pureboot activation window, seconds") +# Every mega runs the loader from its hardware boot section and boots the +# application at word 0; the tinies get the trampoline surgery. All megas +# assume a 16 MHz crystal at 115200 Bd; the tinies their internal RC at +# 57600 Bd over the software UART. if(LIBAVR_MCU STREQUAL "attiny13a") set(_pb_flash 1024) set(_pb_wrap "") @@ -158,6 +162,48 @@ elseif(LIBAVR_MCU STREQUAL "attiny85") set(_pb_baud 57600) set(_pb_eeprom 512) set(_pb_limit 510) +elseif(LIBAVR_MCU MATCHES "^atmega8a?$") + set(_pb_flash 8192) + set(_pb_wrap -Wl,--pmem-wrap-around=8k) + set(_pb_page 64) + set(_pb_hz 16000000) + set(_pb_baud 115200) + set(_pb_eeprom 512) + set(_pb_limit 512) +elseif(LIBAVR_MCU STREQUAL "atmega16") + set(_pb_flash 16384) + set(_pb_wrap -Wl,--pmem-wrap-around=16k) + set(_pb_page 128) + set(_pb_hz 16000000) + set(_pb_baud 115200) + set(_pb_eeprom 512) + set(_pb_limit 512) +elseif(LIBAVR_MCU MATCHES "^atmega32a?$") + set(_pb_flash 32768) + set(_pb_wrap -Wl,--pmem-wrap-around=32k) + set(_pb_page 128) + set(_pb_hz 16000000) + set(_pb_baud 115200) + set(_pb_eeprom 1024) + set(_pb_limit 512) +elseif(LIBAVR_MCU STREQUAL "atmega168a") + set(_pb_flash 16384) + set(_pb_wrap -Wl,--pmem-wrap-around=16k) + set(_pb_page 128) + set(_pb_hz 16000000) + set(_pb_baud 115200) + set(_pb_eeprom 512) + set(_pb_limit 512) +elseif(LIBAVR_MCU STREQUAL "atmega1284p") + # 128 KiB: wire flash addresses are word addresses, reads go through + # ELPM, and the PC's modulo wrap exceeds what --pmem-wrap-around models. + set(_pb_flash 131072) + set(_pb_wrap "") + set(_pb_page 256) + set(_pb_hz 16000000) + set(_pb_baud 115200) + set(_pb_eeprom 4096) + set(_pb_limit 512) else() set(_pb_flash 32768) set(_pb_wrap -Wl,--pmem-wrap-around=32k) @@ -169,12 +215,22 @@ else() endif() math(EXPR _pb_base "${_pb_flash} - 512") math(EXPR _pb_base_hex "${_pb_base}" OUTPUT_FORMAT HEXADECIMAL) -if(LIBAVR_MCU STREQUAL "atmega328p") +if(LIBAVR_MCU MATCHES "^atmega") set(_pb_app 0) else() math(EXPR _pb_app "${_pb_base} - 2") endif() +# simavr names its cores after the base dies; the A revisions run on them. +set(_pb_sim_mcu ${LIBAVR_MCU}) +if(LIBAVR_MCU STREQUAL "atmega8a") + set(_pb_sim_mcu atmega8) +elseif(LIBAVR_MCU STREQUAL "atmega32a") + set(_pb_sim_mcu atmega32) +elseif(LIBAVR_MCU STREQUAL "atmega168a") + set(_pb_sim_mcu atmega168) +endif() + add_executable(pureboot pureboot/pureboot.cpp) target_link_libraries(pureboot PRIVATE libavr) target_compile_definitions(pureboot PRIVATE PUREBOOT_TIMEOUT=${PUREBOOT_TIMEOUT}) @@ -205,7 +261,7 @@ if(PROJECT_IS_TOP_LEVEL) COMMAND ${CMAKE_OBJCOPY} -O binary $ $.bin) add_test(NAME pureboot.protocol COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbtest.py - ${PB_DEVICE} $ ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex} + ${PB_DEVICE} $ ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} ${_pb_eeprom} $.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbtest-work) @@ -215,7 +271,7 @@ if(PROJECT_IS_TOP_LEVEL) # installed one slot lower, must serve the full command set. add_test(NAME pureboot.reloc COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbreloc.py - ${PB_DEVICE} $ ${LIBAVR_MCU} ${_pb_hz} ${_pb_base_hex} + ${PB_DEVICE} $ ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbreloc-work) set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180 @@ -233,7 +289,7 @@ if(PROJECT_IS_TOP_LEVEL) add_image_outputs(pureboot9) add_test(NAME pureboot.update COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbupdate.py - ${PB_DEVICE} $ $ ${LIBAVR_MCU} + ${PB_DEVICE} $ $ ${_pb_sim_mcu} ${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $.bin ${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py ${CMAKE_BINARY_DIR}/pbupdate-work) diff --git a/CMakePresets.json b/CMakePresets.json index 49bc2e3..2125036 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -16,71 +16,502 @@ { "name": "atmega328p-generated", "inherits": "base", - "cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "OFF" } + "cacheVariables": { + "LIBAVR_MCU": "atmega328p", + "LIBAVR_REFLECT": "OFF" + } }, { "name": "atmega328p-reflect", "inherits": "base", - "cacheVariables": { "LIBAVR_MCU": "atmega328p", "LIBAVR_REFLECT": "ON" } + "cacheVariables": { + "LIBAVR_MCU": "atmega328p", + "LIBAVR_REFLECT": "ON" + } }, { "name": "attiny85-generated", "inherits": "base", - "cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "OFF" } + "cacheVariables": { + "LIBAVR_MCU": "attiny85", + "LIBAVR_REFLECT": "OFF" + } }, { "name": "attiny85-reflect", "inherits": "base", - "cacheVariables": { "LIBAVR_MCU": "attiny85", "LIBAVR_REFLECT": "ON" } + "cacheVariables": { + "LIBAVR_MCU": "attiny85", + "LIBAVR_REFLECT": "ON" + } }, { "name": "attiny13a-generated", "inherits": "base", - "cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "OFF" } + "cacheVariables": { + "LIBAVR_MCU": "attiny13a", + "LIBAVR_REFLECT": "OFF" + } }, { "name": "attiny13a-reflect", "inherits": "base", - "cacheVariables": { "LIBAVR_MCU": "attiny13a", "LIBAVR_REFLECT": "ON" } + "cacheVariables": { + "LIBAVR_MCU": "attiny13a", + "LIBAVR_REFLECT": "ON" + } + }, + { + "name": "atmega8-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega8", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega8-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega8", + "LIBAVR_REFLECT": "ON" + } + }, + { + "name": "atmega8a-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega8a", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega8a-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega8a", + "LIBAVR_REFLECT": "ON" + } + }, + { + "name": "atmega16-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega16", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega16-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega16", + "LIBAVR_REFLECT": "ON" + } + }, + { + "name": "atmega32-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega32", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega32-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega32", + "LIBAVR_REFLECT": "ON" + } + }, + { + "name": "atmega32a-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega32a", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega32a-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega32a", + "LIBAVR_REFLECT": "ON" + } + }, + { + "name": "atmega168a-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega168a", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega168a-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega168a", + "LIBAVR_REFLECT": "ON" + } + }, + { + "name": "atmega1284p-generated", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega1284p", + "LIBAVR_REFLECT": "OFF" + } + }, + { + "name": "atmega1284p-reflect", + "inherits": "base", + "cacheVariables": { + "LIBAVR_MCU": "atmega1284p", + "LIBAVR_REFLECT": "ON" + } } ], "buildPresets": [ - { "name": "atmega328p-generated", "configurePreset": "atmega328p-generated" }, - { "name": "atmega328p-reflect", "configurePreset": "atmega328p-reflect" }, - { "name": "attiny85-generated", "configurePreset": "attiny85-generated" }, - { "name": "attiny85-reflect", "configurePreset": "attiny85-reflect" }, - { "name": "attiny13a-generated", "configurePreset": "attiny13a-generated" }, - { "name": "attiny13a-reflect", "configurePreset": "attiny13a-reflect" } + { + "name": "atmega328p-generated", + "configurePreset": "atmega328p-generated" + }, + { + "name": "atmega328p-reflect", + "configurePreset": "atmega328p-reflect" + }, + { + "name": "attiny85-generated", + "configurePreset": "attiny85-generated" + }, + { + "name": "attiny85-reflect", + "configurePreset": "attiny85-reflect" + }, + { + "name": "attiny13a-generated", + "configurePreset": "attiny13a-generated" + }, + { + "name": "attiny13a-reflect", + "configurePreset": "attiny13a-reflect" + }, + { + "name": "atmega8-generated", + "configurePreset": "atmega8-generated" + }, + { + "name": "atmega8-reflect", + "configurePreset": "atmega8-reflect" + }, + { + "name": "atmega8a-generated", + "configurePreset": "atmega8a-generated" + }, + { + "name": "atmega8a-reflect", + "configurePreset": "atmega8a-reflect" + }, + { + "name": "atmega16-generated", + "configurePreset": "atmega16-generated" + }, + { + "name": "atmega16-reflect", + "configurePreset": "atmega16-reflect" + }, + { + "name": "atmega32-generated", + "configurePreset": "atmega32-generated" + }, + { + "name": "atmega32-reflect", + "configurePreset": "atmega32-reflect" + }, + { + "name": "atmega32a-generated", + "configurePreset": "atmega32a-generated" + }, + { + "name": "atmega32a-reflect", + "configurePreset": "atmega32a-reflect" + }, + { + "name": "atmega168a-generated", + "configurePreset": "atmega168a-generated" + }, + { + "name": "atmega168a-reflect", + "configurePreset": "atmega168a-reflect" + }, + { + "name": "atmega1284p-generated", + "configurePreset": "atmega1284p-generated" + }, + { + "name": "atmega1284p-reflect", + "configurePreset": "atmega1284p-reflect" + } ], "workflowPresets": [ { "name": "atmega328p-generated", "steps": [ - { "type": "configure", "name": "atmega328p-generated" }, - { "type": "build", "name": "atmega328p-generated" }, - { "type": "test", "name": "atmega328p-generated" } + { + "type": "configure", + "name": "atmega328p-generated" + }, + { + "type": "build", + "name": "atmega328p-generated" + }, + { + "type": "test", + "name": "atmega328p-generated" + } ] }, { "name": "attiny85-generated", "steps": [ - { "type": "configure", "name": "attiny85-generated" }, - { "type": "build", "name": "attiny85-generated" }, - { "type": "test", "name": "attiny85-generated" } + { + "type": "configure", + "name": "attiny85-generated" + }, + { + "type": "build", + "name": "attiny85-generated" + }, + { + "type": "test", + "name": "attiny85-generated" + } ] }, { "name": "attiny13a-generated", "steps": [ - { "type": "configure", "name": "attiny13a-generated" }, - { "type": "build", "name": "attiny13a-generated" }, - { "type": "test", "name": "attiny13a-generated" } + { + "type": "configure", + "name": "attiny13a-generated" + }, + { + "type": "build", + "name": "attiny13a-generated" + }, + { + "type": "test", + "name": "attiny13a-generated" + } + ] + }, + { + "name": "atmega8-generated", + "steps": [ + { + "type": "configure", + "name": "atmega8-generated" + }, + { + "type": "build", + "name": "atmega8-generated" + } + ] + }, + { + "name": "atmega8-reflect", + "steps": [ + { + "type": "configure", + "name": "atmega8-reflect" + }, + { + "type": "build", + "name": "atmega8-reflect" + } + ] + }, + { + "name": "atmega8a-generated", + "steps": [ + { + "type": "configure", + "name": "atmega8a-generated" + }, + { + "type": "build", + "name": "atmega8a-generated" + } + ] + }, + { + "name": "atmega8a-reflect", + "steps": [ + { + "type": "configure", + "name": "atmega8a-reflect" + }, + { + "type": "build", + "name": "atmega8a-reflect" + } + ] + }, + { + "name": "atmega16-generated", + "steps": [ + { + "type": "configure", + "name": "atmega16-generated" + }, + { + "type": "build", + "name": "atmega16-generated" + } + ] + }, + { + "name": "atmega16-reflect", + "steps": [ + { + "type": "configure", + "name": "atmega16-reflect" + }, + { + "type": "build", + "name": "atmega16-reflect" + } + ] + }, + { + "name": "atmega32-generated", + "steps": [ + { + "type": "configure", + "name": "atmega32-generated" + }, + { + "type": "build", + "name": "atmega32-generated" + } + ] + }, + { + "name": "atmega32-reflect", + "steps": [ + { + "type": "configure", + "name": "atmega32-reflect" + }, + { + "type": "build", + "name": "atmega32-reflect" + } + ] + }, + { + "name": "atmega32a-generated", + "steps": [ + { + "type": "configure", + "name": "atmega32a-generated" + }, + { + "type": "build", + "name": "atmega32a-generated" + } + ] + }, + { + "name": "atmega32a-reflect", + "steps": [ + { + "type": "configure", + "name": "atmega32a-reflect" + }, + { + "type": "build", + "name": "atmega32a-reflect" + } + ] + }, + { + "name": "atmega168a-generated", + "steps": [ + { + "type": "configure", + "name": "atmega168a-generated" + }, + { + "type": "build", + "name": "atmega168a-generated" + } + ] + }, + { + "name": "atmega168a-reflect", + "steps": [ + { + "type": "configure", + "name": "atmega168a-reflect" + }, + { + "type": "build", + "name": "atmega168a-reflect" + } + ] + }, + { + "name": "atmega1284p-generated", + "steps": [ + { + "type": "configure", + "name": "atmega1284p-generated" + }, + { + "type": "build", + "name": "atmega1284p-generated" + } + ] + }, + { + "name": "atmega1284p-reflect", + "steps": [ + { + "type": "configure", + "name": "atmega1284p-reflect" + }, + { + "type": "build", + "name": "atmega1284p-reflect" + } ] } ], "testPresets": [ - { "name": "atmega328p-generated", "configurePreset": "atmega328p-generated", "output": { "outputOnFailure": true } }, - { "name": "attiny85-generated", "configurePreset": "attiny85-generated", "output": { "outputOnFailure": true } }, - { "name": "attiny13a-generated", "configurePreset": "attiny13a-generated", "output": { "outputOnFailure": true } } + { + "name": "atmega328p-generated", + "configurePreset": "atmega328p-generated", + "output": { + "outputOnFailure": true + } + }, + { + "name": "attiny85-generated", + "configurePreset": "attiny85-generated", + "output": { + "outputOnFailure": true + } + }, + { + "name": "attiny13a-generated", + "configurePreset": "attiny13a-generated", + "output": { + "outputOnFailure": true + } + } ] } diff --git a/pureboot/README.md b/pureboot/README.md index ff5ee7d..f153d04 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -23,7 +23,7 @@ trampoline (below). | Chip | Serial | Baud | Clock assumed | |---|---|---|---| -| ATmega328P | USART0, RXD/TXD = PD0/PD1 | 115200 8N1 | 16 MHz crystal | +| every ATmega (8/8A, 16, 32/32A, 168A, 328P, 1284P) | the hardware USART (USART0), RXD/TXD per pinout | 115200 8N1 | 16 MHz crystal | | ATtiny85 | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 8 MHz internal RC | | ATtiny13A | software UART, RX = PB0, TX = PB1 | 57600 8N1 | 9.6 MHz internal RC | @@ -55,6 +55,10 @@ write to finish and sends the prompt `+` (0x2b) — the prompt is therefore also the completion ack of the previous command. A session is: await `+`, send a command, read its reply, repeat. +On chips whose flash exceeds 64 KiB (the 1284P — info-block flag bit 1) the +`R`/`W` flash addresses are **word** addresses; everywhere else they are byte +addresses. EEPROM addresses are always bytes, counts always bytes. + | Cmd | Arguments | Reply | |---|---|---| | `b` | — | the 12-byte info block | @@ -88,18 +92,29 @@ The info block (`b`): |---|---| | 0–2 | `'P'`, `'B'`, protocol version (1) | | 3–5 | device signature | -| 6 | SPM page size in bytes | -| 7–8 | loader base — application flash ends here | +| 6 | SPM page size in bytes (0 means 256) | +| 7–8 | loader base — application flash ends here (a word address when bit 1 is set) | | 9–10 | EEPROM size | -| 11 | bit 0 set: host must patch the reset vector (no hardware boot section) | +| 11 | bit 0: host must patch the reset vector (no hardware boot section); bit 1: flash wire addresses are word addresses | Composites are the host's job: verify = read back and compare, erase = write `0xff` (per page for flash, per byte for EEPROM). ## Deployment -**ATmega328P**: program the loader at 0x7e00 with an external programmer. -Two fuse profiles, same binary: +**Megas**: program the loader at `flash − 512` with an external programmer. +Every mega's smallest-but-one BOOTSZ puts the boot-section start exactly at +the loader base (512 B — the m8/16/168A reach it at their second-smallest +step, the m32/328P at their smallest), so the ATmega328P profiles below +apply to all of them with their own addresses; the per-chip BOOTSZ ladders +live in the host tool (`BOOT_FUSE`). The **ATmega1284P** is the exception: +its smallest boot section is 1 KB, so the standalone profile does not exist +— BOOTSZ = 512 words always, and with BOOTRST programmed reset lands at +0x1f800, one erased slot below the loader (the loader-first walk behavior +below, built in). Its staging slot sits inside that same 1 KB section, so +self-update needs no fuse change. + +ATmega328P profiles (addresses for its 32 KiB): | BOOTSZ | BOOTRST | Behavior | |---|---|---| diff --git a/pureboot/pureboot.cpp b/pureboot/pureboot.cpp index 93be716..93423bd 100644 --- a/pureboot/pureboot.cpp +++ b/pureboot/pureboot.cpp @@ -53,14 +53,35 @@ consteval avr::hertz_t clock() using dev = avr::device<{.clock = clock()}>; +// The watchdog reset flag's home: MCUSR, or the classic megas' MCUCSR. +consteval std::int16_t wdrf_field() +{ + auto reg = std::string_view{avr::hw::db.regs[static_cast(avr::power::detail::reset_reg())].name}; + return avr::hw::db.field_index(reg, "WDRF"); +} + // Geometry: the resident loader owns the top 512 bytes of flash; the word // below it is the trampoline (the application's relocated reset vector) on // chips without a hardware boot section. The RWWSRE bit marks a separate -// boot section — on classic AVR the two capabilities coincide. +// boot section — on classic AVR the two capabilities coincide (the m8/m32 +// packs spell its register SPMCR). constexpr std::uint16_t boot_bytes = 512; -constexpr std::uint16_t base = static_cast(spm::flash_bytes - boot_bytes); +constexpr std::uint32_t base = spm::flash_bytes - boot_bytes; constexpr std::uint16_t page = spm::page_bytes; -constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0; +constexpr bool boot_section = [] { + for (auto reg : {"SPMCSR", "SPMCR"}) + if (avr::hw::db.field_index(reg, "RWWSRE") >= 0) + return true; + return false; +}(); + +// Past 64 KiB a byte address no longer fits the wire's 16 bits, so on the +// large chips every flash address on the wire — and all slot arithmetic — +// is a word address instead ('J' always was one). Slots stay 512 bytes = +// 256 words, so a slot is one high byte in either unit. +constexpr bool word_flash = spm::flash_bytes > 65536; +constexpr std::uint16_t wire_base = word_flash ? static_cast(base / 2) : static_cast(base); +constexpr std::uint16_t wire_page_mask = word_flash ? (page / 2 - 1) : (page - 1); // The activation window, in seconds, is a compile-time constant (the build // may override it): the whole EEPROM belongs to the application, and @@ -71,8 +92,10 @@ constexpr bool boot_section = avr::hw::db.field_index("SPMCSR", "RWWSRE") >= 0; constexpr std::uint8_t timeout_seconds = PUREBOOT_TIMEOUT; // The 12-byte info block the host reads with the 'b' command; flash-resident -// (there is no crt to copy a .data image). -inline constexpr std::array info_data = { +// (there is no crt to copy a .data image), word-aligned so its wire (word) +// address is exact on the large chips. The page byte is the wire count +// convention: 0 means 256. +[[gnu::progmem]] alignas(2) inline constexpr std::array info_data = { 'P', 'B', 1, // magic, protocol version @@ -80,13 +103,14 @@ inline constexpr std::array info_data = { avr::hw::db.signature[1], avr::hw::db.signature[2], static_cast(page), - base & 0xff, - base >> 8, // app flash ends here; resident loader base + wire_base & 0xff, + wire_base >> 8, // app flash ends here; resident loader base (a word address on large chips) avr::hw::db.mem.eeprom_size & 0xff, avr::hw::db.mem.eeprom_size >> 8, - boot_section ? 0 : 1, // bit 0: host must patch the reset vector (no hardware boot section) + // bit 0: host must patch the reset vector (no hardware boot section); + // bit 1: flash wire addresses are word addresses + static_cast((boot_section ? 0 : 1) | (word_flash ? 2 : 0)), }; -using info = avr::flash_table; // The serial link: the hardware USART where the chip has one, the polled // software UART (no vector — the table belongs to the application) on PB0/PB1 @@ -98,19 +122,19 @@ using info = avr::flash_table; template consteval std::int16_t rxc_field() { - return avr::hw::db.field_index("UCSR0A", "RXC0"); + return avr::uart::detail::ufield<'0', "UCSR#A", "RXC#">(); } template consteval std::int16_t txc_field() { - return avr::hw::db.field_index("UCSR0A", "TXC0"); + return avr::uart::detail::ufield<'0', "UCSR#A", "TXC#">(); } template consteval std::int16_t status_reg() { - return avr::hw::db.reg_index("UCSR0A"); + return avr::uart::detail::ureg<'0', "UCSR#A">(); } template @@ -190,7 +214,8 @@ struct software_link { } }; -using link = std::conditional_t, software_link>; +using link = std::conditional_t, software_link>; // The application's entry, an absolute address the linker pins (--defsym in // CMakeLists.txt): 0x0000 on the mega (word 0 stays the application's own @@ -245,19 +270,22 @@ std::uint16_t rx16() return static_cast(low | (link::rx() << 8)); } -const std::uint8_t *flash_ptr(std::uint16_t address) -{ - return reinterpret_cast(address); -} - // The streamers take the count in the wire's 8-bit form: 0 means 256. // send_flash stays out of line: its two callers ('b' and 'R') otherwise each -// inline a private copy of the loop. +// inline a private copy of the loop. On the large chips the address is a +// word address and the read goes through ELPM (flash_load_far). [[gnu::noinline]] void send_flash(std::uint16_t address, std::uint8_t count) { - do - link::tx(avr::flash_load(flash_ptr(address++))); - while (--count); + if constexpr (word_flash) { + auto byte_address = static_cast(address) << 1; + do + link::tx(avr::flash_load_far(byte_address++)); + while (--count); + } else { + do + link::tx(avr::flash_load(reinterpret_cast(address++))); + while (--count); + } } void send_eeprom(std::uint16_t address, std::uint8_t count) @@ -287,7 +315,7 @@ void store_eeprom(std::uint16_t address, std::uint8_t count) // copy flashed one slot lower may rewrite the slot above it — how pureboot // updates itself. On the mega the RWW section is re-enabled so reads work // immediately. -void program_flash(std::uint16_t address, std::uint8_t slot_high) +void program_flash(std::uint16_t wire_address, std::uint8_t slot_high) { // A buffer word cannot be loaded twice without an erase (§26.2.1), so a // refused page's drained data must not linger for the next write: @@ -297,19 +325,40 @@ void program_flash(std::uint16_t address, std::uint8_t slot_high) spm::rww_enable(); else spm::clear_buffer(); - // The address is the loop's only state: pages are aligned, so the walk - // ends when the offset bits wrap back to zero. - do { - std::uint8_t low = link::rx(); - std::uint8_t high = link::rx(); - spm::fill(address, static_cast(low | (high << 8))); - address += 2; - } while (static_cast(address) & (page - 1)); - address -= 2; // back inside the page — erase and write ignore the word bits - const std::uint8_t page_high = static_cast(address >> 8) & 0xfe; + // One induction either way. On the byte-addressed chips the wire address + // itself walks the page (aligned, so the offset bits wrap to zero); on + // the word-addressed large chips the wire word address becomes a 32-bit + // byte cursor once, and their 256-byte page makes its low byte the whole + // in-page offset. The slot index is one high byte of the wire address — + // two values on byte-addressed chips (the & ~1), bits 16:9 re-packed on + // the large ones. + spm::flash_address_t address; + std::uint8_t page_high; + if constexpr (word_flash) { + address = static_cast(static_cast(wire_address) << 1); + const auto start = address; + do { + std::uint8_t low = link::rx(); + std::uint8_t high = link::rx(); + spm::fill(address, static_cast(low | (high << 8))); + address += 2; + } while (static_cast(address)); + address = start; + page_high = static_cast(static_cast(address >> 8) >> 1); + } else { + address = static_cast(wire_address); + do { + std::uint8_t low = link::rx(); + std::uint8_t high = link::rx(); + spm::fill(address, static_cast(low | (high << 8))); + address += 2; + } while (static_cast(address) & (page - 1)); + address -= 2; // back inside the page — erase and write ignore the word bits + page_high = static_cast(address >> 8) & 0xfe; + } if (page_high != slot_high) { // The tinies halt the CPU through the erase and the write, so only - // the mega — running on while its RWW section programs — waits. + // the megas — running on while their RWW section programs — wait. spm::erase_page(address); if constexpr (boot_section) spm::wait(); @@ -336,18 +385,20 @@ void send_fuses() { // A watchdog reset belongs to the application (whose watchdog stays // forced on until it clears WDRF) — no activation window in its way. - if (avr::hw::mcusr::wdrf.test()) + // The flag register is MCUSR, or the classic megas' MCUCSR. + if (avr::hw::field_impl::test()) run_app(); link::init(); - // The high byte of the 512-byte-aligned base this copy runs at: the word - // return address's high byte is the byte address >> 9 (the slot index), - // doubled back into address terms. program_flash refuses this one slot - // and the info block is addressed from it, so both follow wherever the - // code was flashed. + // The high byte of the 512-byte-aligned base this copy runs at: the + // return address is a word address, whose high byte is the 256-word slot + // index — on byte-addressed chips doubled back into byte terms. + // program_flash refuses this one slot and the info block is addressed + // from it, so both follow wherever the code was flashed. + const std::uint16_t ra_words = reinterpret_cast(__builtin_return_address(0)); const std::uint8_t slot_high = - static_cast((reinterpret_cast(__builtin_return_address(0)) >> 8) << 1); + word_flash ? static_cast(ra_words >> 8) : static_cast((ra_words >> 8) << 1); // The knock: 'p' then 'b', each under a fresh window; any other byte is // line noise and waits again. Falling out of a window runs the app. @@ -364,11 +415,15 @@ void send_fuses() case 'b': { // info block, read relative to the running slot // The block sits in the image's first 256 bytes (the build lint // asserts it), and slots are 512-aligned — so the low byte of its - // link address is its offset in any slot, and the high byte of - // its runtime address is the running slot's. Built as a byte - // pair so no absolute 16-bit address is ever materialized. - const std::uint8_t low = static_cast(reinterpret_cast(info::storage.data())); - send_flash(std::bit_cast(std::array{low, slot_high}), info::size()); + // link address (in wire units: bytes, or words on the large + // chips) is its offset in any slot, and the high byte of its + // runtime address is the running slot's. Built as a byte pair so + // no absolute address is ever materialized. + const auto link_low = reinterpret_cast(info_data.data()); + const std::uint8_t low = + word_flash ? static_cast(link_low >> 1) : static_cast(link_low); + send_flash(std::bit_cast(std::array{low, slot_high}), + static_cast(info_data.size())); break; } case 'J': { // jump to a wire word address: hand-over and staging transfer diff --git a/pureboot/pureboot.py b/pureboot/pureboot.py index c76e886..bffd9e4 100644 --- a/pureboot/pureboot.py +++ b/pureboot/pureboot.py @@ -270,10 +270,14 @@ class Info: raise Error(f"protocol version {raw[2]}, tool speaks {PROTOCOL_VERSION}") self.raw = bytes(raw) self.signature = raw[3:6] - self.page = raw[6] - self.base = raw[7] | (raw[8] << 8) - self.eeprom_size = raw[9] | (raw[10] << 8) + self.page = raw[6] or 256 # the wire count convention: 0 means 256 self.patch_vector = bool(raw[11] & 1) + # Large chips speak word addresses for flash (bit 1); the host keeps + # every address in bytes and converts at the wire. + self.word_flash = bool(raw[11] & 2) + scale = 2 if self.word_flash else 1 + self.base = (raw[7] | (raw[8] << 8)) * scale + self.eeprom_size = raw[9] | (raw[10] << 8) self.flash_size = self.base + SLOT self.stage = self.base - SLOT # where a staging copy of the loader goes # The hand-over target, as the word address 'J' takes: the trampoline @@ -331,25 +335,33 @@ class Loader: self._expect_prompt(timeout) return reply - def _stream_read(self, command, address, count): + def _stream_read(self, command, address, count, address_scale=1): data = b"" while count: chunk = min(count, 256) - head = bytes((ord(command), address & 0xFF, address >> 8, chunk & 0xFF)) + wire = address // address_scale + head = bytes((ord(command), wire & 0xFF, wire >> 8, chunk & 0xFF)) data += self._command(head, chunk, 5.0) address += chunk count -= chunk return data def read_flash(self, address, count): - return self._stream_read("R", address, count) + if not self.info.word_flash: + return self._stream_read("R", address, count) + # Word-addressed wire: widen to even bounds, read, trim. + start = address & ~1 + span = (address + count + 1 & ~1) - start + data = self._stream_read("R", start, span, address_scale=2) + return data[address - start : address - start + count] def read_eeprom(self, address, count): return self._stream_read("r", address, count) def write_page(self, address, data): assert len(data) == self.info.page and address % self.info.page == 0 - head = bytes((ord("W"), address & 0xFF, address >> 8)) + wire = address // (2 if self.info.word_flash else 1) + head = bytes((ord("W"), wire & 0xFF, wire >> 8)) self._command(head + data, 0, 2.0) def write_eeprom(self, address, data): @@ -495,14 +507,32 @@ def covered(pages, info, skip_blank): # ----------------------------------------------------------------- fuses --- -def mega_boot(high_fuse): - """Decode the ATmega328P high fuse's boot configuration (DS40002061B - §27.3, Table 27-13/27-16): BOOTSZ1:0 in bits 2:1 select the boot-section - words, BOOTRST in bit 0 (programmed = 0) re-vectors reset to its start. - Returns (bootrst_programmed, boot_section_start_byte).""" - bootsz = (high_fuse >> 1) & 0x03 - words = {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}[bootsz] - return (high_fuse & 1) == 0, 0x8000 - words * 2 +# Per-chip boot fuse geometry, keyed by the signature's family/part bytes: +# which byte of the 'F' reply (low, lock, extended, high) carries BOOTSZ/ +# BOOTRST, and the BOOTSZ->words ladder. Sources: Atmel-2486/2466/2503 +# (HIGH fuse), Atmel-8271 (m168A: EXTENDED; m328P: HIGH), Atmel-42719. +BOOT_FUSE = { + bytes((0x93, 0x07)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m8/8A + bytes((0x94, 0x03)): (3, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m16 + bytes((0x95, 0x02)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m32/32A + bytes((0x94, 0x06)): (2, {0b11: 128, 0b10: 256, 0b01: 512, 0b00: 1024}), # m168A + bytes((0x95, 0x0F)): (3, {0b11: 256, 0b10: 512, 0b01: 1024, 0b00: 2048}), # m328P + bytes((0x97, 0x05)): (3, {0b11: 512, 0b10: 1024, 0b01: 2048, 0b00: 4096}), # 1284P +} + + +def mega_boot(info, fuse_bytes): + """Decode a mega's boot configuration from its fuses (the byte and the + BOOTSZ ladder are per chip): BOOTSZ1:0 in bits 2:1 select the + boot-section words, BOOTRST in bit 0 (programmed = 0) re-vectors reset + to its start. Returns (bootrst_programmed, boot_section_start_byte).""" + entry = BOOT_FUSE.get(bytes(info.signature[1:3])) + if entry is None: + raise Error(f"unknown mega signature {info.signature.hex()} — no boot fuse map") + which, ladder = entry + fuse = fuse_bytes[which] + words = ladder[(fuse >> 1) & 0x03] + return (fuse & 1) == 0, info.flash_size - words * 2 # ---------------------------------------------------------- loader update --- @@ -557,12 +587,11 @@ def update_preflight(image, info, fuse_bytes): if not info.patch_vector: if fuse_bytes is None: raise Error("a loader update on this chip needs its fuses — unreadable? pass --assume-fuses") - high = fuse_bytes[3] - bootrst, bls_start = mega_boot(high) + bootrst, bls_start = mega_boot(info, fuse_bytes) if info.stage < bls_start: raise Error( f"cannot self-update: the staging slot {info.stage:#06x} lies below the " - f"boot section ({bls_start:#06x}, high fuse {high:#04x}) where SPM is disabled " + f"boot section ({bls_start:#06x}) where SPM is disabled " f"— a boot section of at least 1 KB (BOOTSZ) is required, and only an " f"external programmer can change fuses" ) @@ -710,7 +739,7 @@ def check_walk_region(pages, info, fuse_bytes, force): Only checkable when the fuses are known (--fuses or --assume-fuses).""" if info.patch_vector or fuse_bytes is None: return - bootrst, bls_start = mega_boot(fuse_bytes[3]) + bootrst, bls_start = mega_boot(info, fuse_bytes) if not bootrst or bls_start >= info.base: return overlap = [a for a in sorted(pages) if a >= bls_start and pages[a].count(0xFF) != len(pages[a])] diff --git a/test/check_pi.py b/test/check_pi.py index a320d27..40a44c0 100644 --- a/test/check_pi.py +++ b/test/check_pi.py @@ -37,7 +37,7 @@ def main(): sys.exit(1) symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout - info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line] + info = [line for line in symbols.splitlines() if "info_data" in line] if len(info) != 1: print(f"FAIL: expected one info-block storage symbol, found {len(info)}") sys.exit(1) diff --git a/test/pbapp.cpp b/test/pbapp.cpp index 9115fa6..dab9c11 100644 --- a/test/pbapp.cpp +++ b/test/pbapp.cpp @@ -26,7 +26,8 @@ consteval avr::hertz_t clock() using dev = avr::device<{.clock = clock()}>; -template +template struct link { using tx_t = avr::uart::usart0; static void tx(char c) diff --git a/test/pbsim.py b/test/pbsim.py index 979d29a..c7f0227 100644 --- a/test/pbsim.py +++ b/test/pbsim.py @@ -11,7 +11,7 @@ class Device: def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None): cmd = [binary, elf, mcu, hz, base_hex, str(page), str(baud), dump] if reset_hex is not None or resume is not None: - cmd.append(reset_hex if reset_hex is not None else ("0" if mcu != "atmega328p" else base_hex)) + cmd.append(reset_hex if reset_hex is not None else ("0" if not mcu.startswith("atmega") else base_hex)) if resume is not None: cmd.append(resume) self.log = open(dump + ".log", "a") diff --git a/test/pbtest.py b/test/pbtest.py index 2e7b831..a2f45a5 100644 --- a/test/pbtest.py +++ b/test/pbtest.py @@ -90,11 +90,17 @@ def main(): read_flash = os.path.join(workdir, "readback_flash.bin") read_eeprom = os.path.join(workdir, "readback_eeprom.bin") - # The geometry the host will discover, for computing the expected image. + # The geometry the host will discover, for computing the expected image: + # megas carry a boot section (no vector surgery), the large ones speak + # word addresses, and the page byte is the wire's 0-means-256. + mega = mcu.startswith("atmega") + word_flash = base + 512 > 0x10000 + wire_base = base // 2 if word_flash else base + flags = (0 if mega else 1) | (2 if word_flash else 0) info = pb.Info( - bytes([ord("P"), ord("B"), 1, 0, 0, 0, page]) - + bytes([base & 0xFF, base >> 8, eeprom_size & 0xFF, eeprom_size >> 8]) - + bytes([0 if mcu == "atmega328p" else 1]) + bytes([ord("P"), ord("B"), 1, 0, 0, 0, page & 0xFF]) + + bytes([wire_base & 0xFF, wire_base >> 8, eeprom_size & 0xFF, eeprom_size >> 8]) + + bytes([flags]) ) device = Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump) @@ -150,8 +156,9 @@ def main(): fail("loader region looks erased in the ground-truth dump") # The surgery, decoded independently: the patched vector must land on the - # loader, the trampoline on the application's own entry. - if mcu != "atmega328p": + # loader, the trampoline on the application's own entry (tinies only — + # the megas' word 0 stays the application's). + if not mega: flash_words = (base + 512) // 2 app = open(app_bin, "rb").read() word0 = flash_true[0] | (flash_true[1] << 8) diff --git a/test/pbupdate.py b/test/pbupdate.py index cd46b25..fef4e68 100644 --- a/test/pbupdate.py +++ b/test/pbupdate.py @@ -41,7 +41,17 @@ class PowerFail(Exception): pass -MEGA_FUSES = "ffffffdd" # high 0xdd: BOOTSZ = 1 KB, BOOTRST unprogrammed +def assumed_fuses(pb, image): + """Synthetic 'F' bytes for --assume-fuses: the smallest boot section of + at least 1 KB (what a self-update needs), BOOTRST unprogrammed — the + per-chip BOOTSZ ladder and fuse byte come from the tool's own table, + keyed by the update image's embedded signature.""" + info = pb.image_info(image) + which, ladder = pb.BOOT_FUSE[bytes(info.signature[1:3])] + bits = min((b for b in ladder if ladder[b] * 2 >= 1024), key=lambda b: ladder[b]) + fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF)) + fuses[which] = 0xF8 | (bits << 1) | 1 + return bytes(fuses) def make_fault_loader(pb, base, kill_region, kill_hits, device): @@ -77,7 +87,7 @@ def make_fault_loader(pb, base, kill_region, kill_hits, device): def main(): (device_bin, elf, update_elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:] base, page, baud = int(base_hex, 0), int(page), int(baud) - mega = mcu == "atmega328p" + mega = mcu.startswith("atmega") reset_hex = "0" if mega else None # the mega runs BOOTRST-unprogrammed here sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -95,7 +105,7 @@ def main(): fail("the update image is byte-identical to the resident build") dump = os.path.join(workdir, "dump.bin") state = os.path.join(workdir, "update.pbstate") - fuses = bytes.fromhex(MEGA_FUSES) if mega else None + fuses = assumed_fuses(pb, images["v0"]) if mega else None def connect(device): port = pb.Port(device.pty, baud) @@ -136,7 +146,7 @@ def main(): # A clean CLI update, resident -> v9. args = ["--update-loader", os.path.join(workdir, "v9.bin"), "--state", state, "--stay"] if mega: - args += ["--assume-fuses", MEGA_FUSES] + args += ["--assume-fuses", fuses.hex()] out = pbsim.run_tool(tool, device.pty, baud, *args) if "loader updated" not in out: fail("update did not report success") diff --git a/test/pureboot_device.c b/test/pureboot_device.c index 596c0e1..7496f38 100644 --- a/test/pureboot_device.c +++ b/test/pureboot_device.c @@ -3,7 +3,7 @@ // the patched vector are not what is under test), and exposes the loader's // serial link as a pty for the real host tool: // -// - ATmega328P: the hardware USART0 through simavr's uart_pty. +// - Megas: the hardware USART through simavr's uart_pty. // - Tinies: an 8N1 bridge between a pty and the GPIO software UART // (drives PB0, the loader's RX; decodes PB1, its TX), timed against the // simulated cycle counter. @@ -279,7 +279,7 @@ int main(int argc, char *argv[]) unsigned page = (unsigned)atoi(argv[5]); unsigned baud = (unsigned)atoi(argv[6]); dump_path = argv[7]; - use_uart_pty = strcmp(mcu_name, "atmega328p") == 0; + use_uart_pty = strncmp(mcu_name, "atmega", 6) == 0; // every mega links over its hardware USART avr = avr_make_mcu_by_name(mcu_name); if (!avr) { diff --git a/test/test_planner.py b/test/test_planner.py index bd5fdd8..2817bc4 100644 --- a/test/test_planner.py +++ b/test/test_planner.py @@ -27,9 +27,12 @@ def expect_error(what, fn, *needles): fail(f"{what}: no error raised") -def info_of(pb, base, page, patch, flash): - raw = bytes((0x50, 0x42, 1, 0x1E, 0x93, 0x0B, page, base & 0xFF, base >> 8, - 0, 2, 1 if patch else 0)) +def info_of(pb, base, page, patch, flash, signature=(0x1E, 0x93, 0x0B), word_flash=False): + scale = 2 if word_flash else 1 + wire_base = base // scale + flags = (1 if patch else 0) | (2 if word_flash else 0) + raw = bytes((0x50, 0x42, 1, *signature, page & 0xFF, wire_base & 0xFF, wire_base >> 8, + 0, 2, flags)) info = pb.Info(raw) assert info.flash_size == flash return info @@ -50,16 +53,38 @@ def main(): import pureboot as pb tiny = info_of(pb, 0x1E00, 64, True, 0x2000) - mega = info_of(pb, 0x7E00, 128, False, 0x8000) + mega = info_of(pb, 0x7E00, 128, False, 0x8000, signature=(0x1E, 0x95, 0x0F)) - # mega_boot: BOOTSZ words and the BOOTRST sense, DS40002061B §27. - for bits, start in ((0b11, 0x7E00), (0b10, 0x7C00), (0b01, 0x7800), (0b00, 0x7000)): - prog, at = pb.mega_boot((0xF8 | (bits << 1)) & ~1) - if not prog or at != start: - fail(f"mega_boot BOOTSZ={bits:02b} programmed: {prog} {at:#06x}") - prog, at = pb.mega_boot(0xF8 | (bits << 1) | 1) - if prog or at != start: - fail(f"mega_boot BOOTSZ={bits:02b} unprogrammed: {prog} {at:#06x}") + # mega_boot: BOOTSZ words and the BOOTRST sense per chip — the fuse byte + # index (HIGH everywhere but the m168A's EXTENDED) and the per-family + # ladders (Atmel-2486/2466/2503/8271/42719). Synthetic 'F' replies: only + # the boot byte carries meaning. + cases = ( + ((0x1E, 0x93, 0x07), 0x2000, 3, {0b11: 0x1F00, 0b10: 0x1E00, 0b01: 0x1C00, 0b00: 0x1800}), # m8 + ((0x1E, 0x94, 0x03), 0x4000, 3, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m16 + ((0x1E, 0x95, 0x02), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m32 + ((0x1E, 0x94, 0x06), 0x4000, 2, {0b11: 0x3F00, 0b10: 0x3E00, 0b01: 0x3C00, 0b00: 0x3800}), # m168A + ((0x1E, 0x95, 0x0F), 0x8000, 3, {0b11: 0x7E00, 0b10: 0x7C00, 0b01: 0x7800, 0b00: 0x7000}), # m328P + ((0x1E, 0x97, 0x05), 0x20000, 3, {0b11: 0x1FC00, 0b10: 0x1F800, 0b01: 0x1F000, 0b00: 0x1E000}), # 1284P + ) + for signature, flash, which, ladder in cases: + chip = info_of(pb, flash - 512, 128 if flash < 0x20000 else 0, False, flash, + signature=signature, word_flash=flash > 0x10000) + for bits, start in ladder.items(): + fuses = bytearray((0xFF, 0xFF, 0xFF, 0xFF)) + fuses[which] = (0xF8 | (bits << 1)) & ~1 + prog, at = pb.mega_boot(chip, bytes(fuses)) + if not prog or at != start: + fail(f"mega_boot {signature[1]:02x}{signature[2]:02x} BOOTSZ={bits:02b} programmed: {prog} {at:#07x}") + fuses[which] |= 1 + prog, at = pb.mega_boot(chip, bytes(fuses)) + if prog or at != start: + fail(f"mega_boot {signature[1]:02x}{signature[2]:02b} unprogrammed: {prog} {at:#07x}") + + # Word-addressed info decode: the 1284P's base/page ride the wire scaled. + big = info_of(pb, 0x1FE00, 0, False, 0x20000, signature=(0x1E, 0x97, 0x05), word_flash=True) + if big.page != 256 or big.base != 0x1FE00 or big.stage != 0x1FC00: + fail(f"word-addressed info decode: page {big.page}, base {big.base:#x}, stage {big.stage:#x}") # Surgery: word 0 lands on the loader, the trampoline on the original # entry — checked with an independent decoder.