From 50d605e7e86ae4e2aa9e9193614dcf318cc3d162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20J=C3=A4ger?= Date: Mon, 3 Aug 2026 00:43:55 +0200 Subject: [PATCH 1/3] feat: create rmk-boot.x linkerscript RMK can use to automagically partition flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pascal Jäger --- .cargo/config.toml | 5 + .github/workflows/release.yml | 1 + .gitignore | 5 + Cargo.lock | 36 +++---- Makefile.toml | 64 ++++++------ Readme.org | 158 ++++++++++++++++++----------- build.rs | 181 ++++++++++++++++++++++++---------- 7 files changed, 295 insertions(+), 155 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..d217f9d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.thumbv6m-none-eabi] +runner = "probe-rs run --chip RP2040" + +[target.thumbv7em-none-eabihf] +runner = "probe-rs run --chip nRF52840_xxAA" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27f7e1e..25b8f7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,7 @@ jobs: files: | *.uf2 *.hex + *-memory.x rmk-boot-nrf52840.elf env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad67955..2ed0427 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ debug target +# Linker scripts generated by build.rs +# rmk-memory.x and the variant-specific rmk--memory.x +rmk-memory.x +rmk-*-memory.x + # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index 16abc28..34b110c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,24 +83,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "rmk-boot" -version = "0.1.0" -dependencies = [ - "cortex-m", - "cortex-m-rt", - "embassy-boot", - "embassy-boot-rp", - "embassy-embedded-hal", - "embassy-nrf", - "embassy-rp", - "embassy-sync", - "embassy-time", - "embassy-usb", - "embassy-usb-dfu", - "embedded-storage", -] - [[package]] name = "bytemuck" version = "1.25.0" @@ -1022,6 +1004,24 @@ version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +[[package]] +name = "rmk-boot" +version = "0.1.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "embassy-boot", + "embassy-boot-rp", + "embassy-embedded-hal", + "embassy-nrf", + "embassy-rp", + "embassy-sync", + "embassy-time", + "embassy-usb", + "embassy-usb-dfu", + "embedded-storage", +] + [[package]] name = "rp-pac" version = "7.0.0" diff --git a/Makefile.toml b/Makefile.toml index e58e273..9ba1c2c 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -4,75 +4,75 @@ install_crate = { rustup_component_name = "llvm-tools" } [tasks.flip-link] install_crate = { crate_name = "flip-link", binary = "flip-link", test_arg = ["-h"] } -[tasks.build-2mb] +[tasks.build-rp2040-2mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb"] -[tasks.build-4mb] +[tasks.build-rp2040-4mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb"] -[tasks.build-8mb] +[tasks.build-rp2040-8mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb"] -[tasks.build-16mb] +[tasks.build-rp2040-16mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb"] -[tasks.objcopy-2mb] -dependencies = ["install-llvm-tools", "build-2mb"] +[tasks.build-nrf52840] +command = "cargo" +args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840"] + +[tasks.objcopy-rp2040-2mb] +dependencies = ["install-llvm-tools", "build-rp2040-2mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb", "--", "-O", "ihex", "rmk-boot-rp2040-2mb.hex"] -[tasks.objcopy-4mb] -dependencies = ["install-llvm-tools", "build-4mb"] +[tasks.objcopy-rp2040-4mb] +dependencies = ["install-llvm-tools", "build-rp2040-4mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb", "--", "-O", "ihex", "rmk-boot-rp2040-4mb.hex"] -[tasks.objcopy-8mb] -dependencies = ["install-llvm-tools", "build-8mb"] +[tasks.objcopy-rp2040-8mb] +dependencies = ["install-llvm-tools", "build-rp2040-8mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb", "--", "-O", "ihex", "rmk-boot-rp2040-8mb.hex"] -[tasks.objcopy-16mb] -dependencies = ["install-llvm-tools", "build-16mb"] +[tasks.objcopy-rp2040-16mb] +dependencies = ["install-llvm-tools", "build-rp2040-16mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb", "--", "-O", "ihex", "rmk-boot-rp2040-16mb.hex"] -[tasks.uf2-2mb] -dependencies = ["objcopy-2mb"] +[tasks.objcopy-nrf52840] +dependencies = ["install-llvm-tools", "build-nrf52840"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840", "--", "-O", "ihex", "rmk-boot-nrf52840.hex"] + +[tasks.uf2-rp2040-2mb] +dependencies = ["objcopy-rp2040-2mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-2mb.hex", "--output-path", "rmk-boot-rp2040-2mb.uf2", "--family", "rp2040"] -[tasks.uf2-4mb] -dependencies = ["objcopy-4mb"] +[tasks.uf2-rp2040-4mb] +dependencies = ["objcopy-rp2040-4mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-4mb.hex", "--output-path", "rmk-boot-rp2040-4mb.uf2", "--family", "rp2040"] -[tasks.uf2-8mb] -dependencies = ["objcopy-8mb"] +[tasks.uf2-rp2040-8mb] +dependencies = ["objcopy-rp2040-8mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-8mb.hex", "--output-path", "rmk-boot-rp2040-8mb.uf2", "--family", "rp2040"] -[tasks.uf2-16mb] -dependencies = ["objcopy-16mb"] +[tasks.uf2-rp2040-16mb] +dependencies = ["objcopy-rp2040-16mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-16mb.hex", "--output-path", "rmk-boot-rp2040-16mb.uf2", "--family", "rp2040"] -[tasks.build-nrf] -command = "cargo" -args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840"] - -[tasks.objcopy-nrf] -dependencies = ["install-llvm-tools", "build-nrf"] -command = "cargo" -args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840", "--", "-O", "ihex", "rmk-boot-nrf52840.hex"] - -[tasks.uf2-nrf] -dependencies = ["objcopy-nrf"] +[tasks.uf2-nrf52840] +dependencies = ["objcopy-nrf52840"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-nrf52840.hex", "--output-path", "rmk-boot-nrf52840.uf2", "--family", "nrf52840"] [tasks.uf2-all] -dependencies = ["uf2-2mb", "uf2-4mb", "uf2-8mb", "uf2-16mb", "uf2-nrf"] +dependencies = ["uf2-rp2040-2mb", "uf2-rp2040-4mb", "uf2-rp2040-8mb", "uf2-rp2040-16mb", "uf2-nrf52840"] diff --git a/Readme.org b/Readme.org index c4951d5..06c2730 100644 --- a/Readme.org +++ b/Readme.org @@ -10,20 +10,28 @@ Combine it with RMK's =dfu_rp= or =dfu_nrf= feature — after the initial flash you never need to press BOOTSEL again; all subsequent firmware updates happen over USB via =dfu-util=. +rmk-boot ships a linker script called =rmk-memory.x= alongside its firmware +binaries. This file is the single source of truth for the flash partition +layout — RMK reads partition offsets from embedded linker symbols at runtime +via =init_flash_from_linkerscript()=. No manual address calculation, no +matching =memory.x= needed. + * Features -- **Dual-slot firmware** — flash is split into ACTIVE and DFU partitions; +- =Dual-slot firmware= — flash is split into ACTIVE and DFU partitions; embassy-boot copies from DFU to ACTIVE on boot. If the new firmware panics or fails to =mark_booted()= in time, the bootloader reverts automatically. -- **Power-loss safe** — the swap operation is crash-recoverable; a partially +- =Power-loss safe= — the swap operation is crash-recoverable; a partially written ACTIVE slot is detected and rolled back. -- **LED** — a single GPIO LED to signal bootloader states -- **USB DFU via double-tap (nRF52840 only)** — two NRST resets within ~500 ms +- =LED= — a single GPIO LED to signal bootloader states +- =USB DFU via double-tap (nRF52840 only)= — two NRST resets within ~500 ms enter DFU mode (similar to Adafruit bootloader) -- **Flash-size variants** — pre-configured for 2 MB, 4 MB, 8 MB and 16 MB +- =Flash-size variants= — pre-configured for 2 MB, 4 MB, 8 MB and 16 MB (RP2040) and 1 MB (nRF52840) flash chips. -- **UF2 builds** — =cargo make uf2-* = generates ready-to-flash =*.uf2= files. -- **Panic handler** — on bootloader panic, blinks SOS in Morse code on the +- =UF2 builds= — =cargo make uf2-*= generates ready-to-flash =*.uf2= files. +- =rmk-memory.x= — each build also produces a matching linker script that RMK + consumes at link time for zero-config flash layout. +- =Panic handler= — on bootloader panic, blinks SOS in Morse code on the LED so you know something went wrong inside the bootloader itself (as distinct from a firmware panic, which is handled by the app's own handler). @@ -33,7 +41,7 @@ over USB via =dfu-util=. Needs a target toolchain, install with =rustup target add thumbv6m-none-eabi= (RP2040) / =rustup target add thumbv7em-none-eabihf= (NRF52840) #+BEGIN_SRC sh -cargo make build-2mb # or build-4mb / build-8mb / build-16mb / build-nrf +cargo make build-rp2040-2mb # or build-rp2040-4mb / build-rp2040-8mb / build-rp2040-16mb / build-nrf52840 #+END_SRC ** Flash via probe-rs @@ -45,20 +53,28 @@ cargo run --release --target thumbv6m-none-eabi --features rp2040-2mb cargo run --release --target thumbv7em-none-eabihf --features nrf52840 #+END_SRC -** Generate a UF2 (for drag-and-drop flashing) +** Generate a UF2 (for drag-and-drop flashing) and rmk-memory.x + +=build.rs= writes the linker script natively, both under the generic name +=rmk-memory.x= and under the variant-specific name used by RMK on every +=build.rs= run for a supported platform feature (e.g. =rp2040-2mb= also emits +=rmk-rp2040-2mb-memory.x=). No copy step is required. + +Each =cargo make uf2-*= target builds the bootloader and produces a matching +linker script: #+BEGIN_SRC sh -cargo make uf2-2mb # → rmk-boot-rp2040-2mb.uf2 -cargo make uf2-4mb # → rmk-boot-rp2040-4mb.uf2 -cargo make uf2-8mb # → rmk-boot-rp2040-8mb.uf2 -cargo make uf2-16mb # → rmk-boot-rp2040-16mb.uf2 -cargo make uf2-nrf # → rmk-boot-nrf52840.uf2 +cargo make uf2-rp2040-2mb # → rmk-boot-rp2040-2mb.uf2 + rmk-rp2040-2mb-memory.x +cargo make uf2-rp2040-4mb # → rmk-boot-rp2040-4mb.uf2 + rmk-rp2040-4mb-memory.x +cargo make uf2-rp2040-8mb # → rmk-boot-rp2040-8mb.uf2 + rmk-rp2040-8mb-memory.x +cargo make uf2-rp2040-16mb # → rmk-boot-rp2040-16mb.uf2 + rmk-rp2040-16mb-memory.x +cargo make uf2-nrf52840 # → rmk-boot-nrf52840.uf2 + rmk-nrf52840-memory.x #+END_SRC Copy the =*.uf2= to the =RPI-RP2= mass-storage device that appears when you hold BOOTSEL while plugging in USB. For nRF52840 this only works if it has the Adafruit UF2 bootloader installed. -**BE AWARE THAT RMK-BOOT OVERWRITES THAT BOOTLOADER ON THE NRF52840!** +=BE AWARE THAT RMK-BOOT OVERWRITES THAT BOOTLOADER ON THE NRF52840!= * Entering DFU mode (nRF52840) @@ -67,21 +83,22 @@ that does not implement DFU flashing or does not boot, can make flashing impossi (if no debugger is available) Therefore the bootloader provides the possibility to flash firmware via DFU. -The bootloader implements the **double-tap** convention known from the Adafruit +The bootloader implements the =double-tap= convention known from the Adafruit bootloader for the nrf52840. Double tap NRST (reset pin) to GND to enter DFU mode, to LED (P0_15 by default) starts breathing slowly to show that DFU mode is active. * Flash-size variants -Exactly one feature must be enabled at build time: +Exactly one feature must be enabled at build time. Storage defaults to 32 KB +(8 sectors × 4 K). To change it, edit =STORAGE_SIZE= in =build.rs= and rebuild. -| Feature | Flash size | ACTIVE | DFU | ACTIVE start | DFU start | -|---------------+---------------+--------+--------+--------------+------------| -| =rp2040-2mb= | 2 MB | 944 K | 948 K | 0x10007000 | 0x100F3000 | -| =rp2040-4mb= | 4 MB | 1968 K | 1972 K | 0x10007000 | 0x101F3000 | -| =rp2040-8mb= | 8 MB | 4016 K | 4020 K | 0x10007000 | 0x103F3000 | -| =rp2040-16mb= | 16 MB | 8112 K | 8116 K | 0x10007000 | 0x104F3000 | -| =nrf52840= | 1 MB | 432 K | 436 K | 0x00007000 | 0x00073000 | +| Feature | Flash size | ACTIVE | DFU | ACTIVE start | DFU start | +|---------------+---------------+---------+---------+--------------+------------| +| =rp2040-2mb= | 2 MB | 992 K | 996 K | 0x10007000 | 0x10100000 | +| =rp2040-4mb= | 4 MB | 2016 K | 2020 K | 0x10007000 | 0x101FF000 | +| =rp2040-8mb= | 8 MB | 4064 K | 4068 K | 0x10007000 | 0x103FF000 | +| =rp2040-16mb= | 16 MB | 8160 K | 8164 K | 0x10007000 | 0x107FF000 | +| =nrf52840= | 1 MB | 480 K | 484 K | 0x00007000 | 0x0007B000 | The fixed regions for RP2040 (identical for all variants): @@ -102,14 +119,44 @@ Fixed regions for nRF52840: The 2 MB variant works on boards with 4 MB, 8 MB or 16 MB flash too — it simply leaves the extra space unused. You must switch to a larger variant -only if your RMK firmware exceeds 944 KB. +only if your RMK firmware exceeds 992 KB. * Integration with RMK -rmk-boot's partition layout is the default that RMK's =[dfu]= -configuration expects. When you compile your RMK firmware with the =dfu_rp= or -=dfu_nrf= feature, the DFU USB interface is registered automatically and the -flash partitioning matches rmk-boot's =memory.x=. +RMK integrates with rmk-boot through the =rmk-memory.x= linker script. The +file is the single source of truth — it contains both the flash memory layout +(=MEMORY=) for the linker and the standard embassy-boot partition symbols +(=__bootloader_*=) that both RMK and the bootloader read at runtime via +=init_flash_from_linkerscript()= and =from_linkerfile_blocking()=. + +To use rmk-boot with RMK: + +1. Download the matching =rmk-boot-*.uf2= and =rmk-boot-*.x= from the + [[https://github.com/rmk-rs/rmk-boot/releases][GitHub releases]]. +2. Rename the =.x= file to =rmk-memory.x= and place it next to your =Cargo.toml=. +3. Enable =dfu_rp= or =dfu_nrf= in RMK and add a =[dfu]= section to + =keyboard.toml= (only =led= / =unlock_keys= / =page_size= are needed — + partition offsets come from rmk-memory.x). + +RMK's =init_flash_from_linkerscript()= reads these linker symbols from +=rmk-memory.x= at runtime (all values are flash-relative offsets): + +| Symbol | Description | +|-------------------------------+--------------------------| +| =__bootloader_state_start= | Boot state start | +| =__bootloader_state_end= | Boot state end | +| =__bootloader_active_start= | Active slot start | +| =__bootloader_active_end= | Active slot end | +| =__bootloader_dfu_start= | DFU download slot start | +| =__bootloader_dfu_end= | DFU download slot end | +| =__bootloader_storage_start= | Storage partition start | +| =__bootloader_storage_end= | Storage partition end | + +The =__bootloader_active_*=, =__bootloader_state_*= and =__bootloader_dfu_*= +symbols are the same ones embassy-boot's =from_linkerfile_blocking()= reads; +only =__bootloader_storage_*= is an RMK extension. If you build a custom +embassy-boot bootloader, define these same symbols in your own linker script — +RMK will pick them up automatically. After the initial flash (bootloader + RMK firmware), all subsequent updates can be done over USB: @@ -243,28 +290,30 @@ PWM peripheral and releases the GPIO pin so the firmware can reclaim it. * Partition layout detail -The build script (=build.rs=) generates =memory.x= at compile time based on the -selected feature. The formula for each variant: +The build script (=build.rs=) generates both =memory.x= (for the bootloader's +own linking) and =rmk-memory.x= (for the firmware's linking) at compile time +based on the selected feature. Both share the same computed values. -- ACTIVE offset: =0x10007000= (fixed, after embassy-boot) -- RP2040 ACTIVE size: =(flash_size - 28K (BOOT2 + bootloader + state) - - STORAGE_SIZE - 4K (1 page)) / 2= -- nRF52840 ACTIVE size: =(flash_size - 28K (bootloader + state) - STORAGE_SIZE - - 4K (1 page)) / 2= -- DFU offset: =ACTIVE offset + ACTIVE size= -- DFU size: =active_size + 4 K= (one extra page for embassy-boot's swap - algorithm) +The formula for each variant (=STORAGE_SIZE= defaults to 32 K): -The linker symbols that embassy-boot reads: - -| Symbol | Value | -|----------------------------+-------| -| =__bootloader_state_start= | state offset relative to BOOT2 base | -| =__bootloader_state_end= | end of state partition | -| =__bootloader_active_start= | ACTIVE offset relative to BOOT2 base | -| =__bootloader_active_end= | end of ACTIVE partition | -| =__bootloader_dfu_start= | DFU offset relative to BOOT2 base | -| =__bootloader_dfu_end= | end of DFU partition | +- ACTIVE offset: =0x10007000= (RP2040) / =0x00007000= (nRF52840) +- ACTIVE size: =(flash_size - 28K - STORAGE_SIZE - 4K) / 2 +- DFU offset: =ACTIVE offset + ACTIVE size= +- DFU size: =ACTIVE size + 4 K= (one extra page for embassy-boot's swap algorithm) + +The =rmk-memory.x= file exposes these values as linker symbols that RMK's +=init_flash_from_linkerscript()= reads at runtime (all flash-relative offsets): + +| Symbol | Description | +|-------------------------------+--------------------------| +| =__bootloader_state_start= | Boot state start | +| =__bootloader_state_end= | Boot state end | +| =__bootloader_active_start= | Active slot start | +| =__bootloader_active_end= | Active slot end | +| =__bootloader_dfu_start= | DFU download slot start | +| =__bootloader_dfu_end= | DFU download slot end | +| =__bootloader_storage_start= | Storage partition start | +| =__bootloader_storage_end= | Storage partition end | * Testing the DFU→ACTIVE copy manually (for developers) @@ -275,7 +324,7 @@ cargo build arm-none-eabi-objcopy -O binary target/thumbv6m-none-eabi/debug/firmware firmware.bin # 2. Write it to the DFU partition -probe-rs download --chip RP2040 --binary-format bin --base-address 0x10087000 firmware.bin +probe-rs download --chip RP2040 --binary-format bin --base-address 0x100FF000 firmware.bin # 3. Set the SWAP magic in the state partition python3 -c "open('state_swap.bin', 'wb').write(b'\xF0' + b'\xFF'*4095)" @@ -284,10 +333,10 @@ python3 -c "open('state_swap.bin', 'wb').write(b'\xF0' + b'\xFF'*4095)" probe-rs download --chip RP2040 --binary-format bin --base-address 0x10006000 state_swap.bin # 5. Power-cycle the board -# → LED goes solid for 1 s (forward swap in progress) -# → LED fades (copy in progress) -# → 5 quick blinks (copy done) -# → new firmware boots +# -> LED goes solid for 1 s (forward swap in progress) +# -> LED fades (copy in progress) +# -> 5 quick blinks (copy done) +# -> new firmware boots #+END_SRC * Notes @@ -299,4 +348,3 @@ probe-rs download --chip RP2040 --binary-format bin --base-address 0x10006000 st - If you use the =Pico W= board, the on-board CYW43 wireless LED is not connected to a normal GPIO — pick a different pin such as =GPIO 16= (the default) for an external LED. - diff --git a/build.rs b/build.rs index d576b76..9998cf6 100644 --- a/build.rs +++ b/build.rs @@ -1,45 +1,44 @@ use std::env; use std::fs; -use std::path::PathBuf; +use std::path::Path; fn main() { - let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let out_dir = env::var("OUT_DIR").unwrap(); + let out = Path::new(&out_dir); let is_rp2040 = env::var("CARGO_FEATURE_RP2040").is_ok(); let is_nrf52840 = env::var("CARGO_FEATURE_NRF52840").is_ok(); const PAGE_SIZE: usize = 4 * 1024; - - fn fmt_size(bytes: usize) -> String { - if bytes % (1024 * 1024) == 0 { - format!("{}M", bytes / (1024 * 1024)) - } else if bytes % 1024 == 0 { - format!("{}K", bytes / 1024) - } else { - bytes.to_string() - } - } + const STORAGE_SIZE: usize = 32 * 1024; if is_rp2040 { - let flash_size = if env::var("CARGO_FEATURE_RP2040_2MB").is_ok() { - 2 * 1024 * 1024 - } else if env::var("CARGO_FEATURE_RP2040_4MB").is_ok() { - 4 * 1024 * 1024 - } else if env::var("CARGO_FEATURE_RP2040_8MB").is_ok() { - 8 * 1024 * 1024 - } else if env::var("CARGO_FEATURE_RP2040_16MB").is_ok() { - 16 * 1024 * 1024 - } else { - panic!("No RP2040 flash size feature enabled"); - }; + let (variant_label, variant_slug, flash_size) = + if env::var("CARGO_FEATURE_RP2040_2MB").is_ok() { + ("RP2040 2 MB", "rp2040-2mb", 2 * 1024 * 1024) + } else if env::var("CARGO_FEATURE_RP2040_4MB").is_ok() { + ("RP2040 4 MB", "rp2040-4mb", 4 * 1024 * 1024) + } else if env::var("CARGO_FEATURE_RP2040_8MB").is_ok() { + ("RP2040 8 MB", "rp2040-8mb", 8 * 1024 * 1024) + } else if env::var("CARGO_FEATURE_RP2040_16MB").is_ok() { + ("RP2040 16 MB", "rp2040-16mb", 16 * 1024 * 1024) + } else { + panic!("No RP2040 flash size feature enabled"); + }; - let flash_base = 0x1000_0000u32; - const STORAGE_SIZE: usize = 128 * 1024; let remaining = flash_size - 28 * 1024 - STORAGE_SIZE; let active_size = (remaining - PAGE_SIZE) / 2; let dfu_size = active_size + PAGE_SIZE; - let active_offset = flash_base + 0x7000; - let dfu_offset = active_offset + active_size as u32; + // Absolute XIP addresses + let abs_active_offset = 0x1000_7000u32; + let abs_dfu_offset = abs_active_offset + active_size as u32; + let abs_state_offset = 0x1000_6000u32; + // Flash-relative offsets (for DFU symbols) + let flash_base = 0x1000_0000u32; + let rel_state_offset = abs_state_offset - flash_base; + let rel_dfu_offset = abs_dfu_offset - flash_base; + let rel_dfu_size = dfu_size as u32; + let rel_storage_offset = abs_dfu_offset - flash_base + dfu_size as u32; let memory_x = format!( "\ @@ -48,8 +47,8 @@ MEMORY BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 FLASH : ORIGIN = 0x10000100, LENGTH = 24K - 0x100 BOOTLOADER_STATE : ORIGIN = 0x10006000, LENGTH = 4K - ACTIVE : ORIGIN = 0x{:08X}, LENGTH = {} - DFU : ORIGIN = 0x{:08X}, LENGTH = {} + ACTIVE : ORIGIN = 0x{abs_active_offset:08X}, LENGTH = {active_size} + DFU : ORIGIN = 0x{abs_dfu_offset:08X}, LENGTH = {dfu_size} RAM : ORIGIN = 0x20000000, LENGTH = 256K }} @@ -62,12 +61,10 @@ __bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE) - ORIGIN(BOOT2); __bootloader_dfu_start = ORIGIN(DFU) - ORIGIN(BOOT2); __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(BOOT2); -", - active_offset, fmt_size(active_size), - dfu_offset, fmt_size(dfu_size), +" ); - fs::write(out.join("memory.x"), memory_x).unwrap(); + fs::write(out.join("memory.x"), &memory_x).unwrap(); println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rustc-link-arg-bins=-Tlink.x"); println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); @@ -76,27 +73,41 @@ __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(BOOT2); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_RP2040_4MB"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_RP2040_8MB"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_RP2040_16MB"); + + let rmk_boot_x = build_rmk_boot_x( + variant_label, + abs_active_offset, + active_size as u32, + rel_state_offset, + 0x1000, + rel_dfu_offset, + rel_dfu_size, + rel_storage_offset, + STORAGE_SIZE as u32, + 0x1000_0000, // XIP flash base + ); + let project_root = Path::new(env!("CARGO_MANIFEST_DIR")); + write_rmk_boot_x(&project_root, variant_slug, &rmk_boot_x); } else if is_nrf52840 { let flash_size = 1024 * 1024; let bootloader_size = 24 * 1024; let state_size = 4 * 1024; - let storage_size = 128 * 1024; - let remaining = flash_size - bootloader_size - state_size - storage_size; + let remaining = flash_size - bootloader_size - state_size - STORAGE_SIZE; let active_size = (remaining - PAGE_SIZE) / 2; let dfu_size = active_size + PAGE_SIZE; - let state_offset = bootloader_size as u32; - let active_offset = (bootloader_size + state_size) as u32; - let dfu_offset = active_offset + active_size as u32; + let abs_state_offset = bootloader_size as u32; + let abs_active_offset = (bootloader_size + state_size) as u32; + let abs_dfu_offset = abs_active_offset + active_size as u32; let memory_x = format!( "\ MEMORY {{ FLASH : ORIGIN = 0x00000000, LENGTH = {bootloader_size} - BOOTLOADER_STATE : ORIGIN = 0x{state_offset:08X}, LENGTH = {state_size} - ACTIVE : ORIGIN = 0x{active_offset:08X}, LENGTH = {active_size} - DFU : ORIGIN = 0x{dfu_offset:08X}, LENGTH = {dfu_size} + BOOTLOADER_STATE : ORIGIN = 0x{abs_state_offset:08X}, LENGTH = {state_size} + ACTIVE : ORIGIN = 0x{abs_active_offset:08X}, LENGTH = {active_size} + DFU : ORIGIN = 0x{abs_dfu_offset:08X}, LENGTH = {dfu_size} RAM : ORIGIN = 0x20000000, LENGTH = 256K }} @@ -109,22 +120,92 @@ __bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE); __bootloader_dfu_start = ORIGIN(DFU); __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU); -", - bootloader_size = bootloader_size, - state_offset = state_offset, - state_size = state_size, - active_offset = active_offset, - active_size = fmt_size(active_size), - dfu_offset = dfu_offset, - dfu_size = fmt_size(dfu_size), +" ); - fs::write(out.join("memory.x"), memory_x).unwrap(); + fs::write(out.join("memory.x"), &memory_x).unwrap(); println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rustc-link-arg-bins=-Tlink.x"); + + let rmk_boot_x = build_rmk_boot_x( + "nRF52840", + abs_active_offset, + active_size as u32, + abs_state_offset, + state_size as u32, + abs_dfu_offset, + dfu_size as u32, + abs_dfu_offset + dfu_size as u32, + STORAGE_SIZE as u32, // nRF flash_base is 0, so absolute = relative + 0x0000_0000, // flash base + ); + let project_root = Path::new(env!("CARGO_MANIFEST_DIR")); + write_rmk_boot_x(&project_root, "nrf52840", &rmk_boot_x); } else { panic!("No platform feature enabled (rp2040 or nrf52840)"); } println!("cargo:rerun-if-changed=build.rs"); } + +/// Write the RMK linker script to the project root both under the generic +/// name `rmk-memory.x` and under the variant-specific release name. +fn write_rmk_boot_x(project_root: &Path, variant_slug: &str, content: &str) { + fs::write(project_root.join("rmk-memory.x"), content).unwrap(); + fs::write( + project_root.join(format!("rmk-{variant_slug}-memory.x")), + content, + ) + .unwrap(); +} + +fn build_rmk_boot_x( + variant: &str, + active_offset: u32, + active_size: u32, + state_offset: u32, + state_size: u32, + dfu_offset: u32, + dfu_size: u32, + storage_offset: u32, + storage_size: u32, + flash_base: u32, +) -> String { + let rel_active_offset = active_offset - flash_base; + let state_end = state_offset + state_size; + let active_end = rel_active_offset + active_size; + let dfu_end = dfu_offset + dfu_size; + let storage_end = storage_offset + storage_size; + format!( + "\ +/* rmk-memory.x for {variant} — generated by rmk-boot/build.rs + * + * Provides the MEMORY layout (absolute XIP addresses) and the standard + * embassy-boot `__bootloader_*` partition symbols (flash-relative offsets) + * consumed by init_flash_from_linkerscript(). + * + * If your board has a different flash size, replace this file with the + * matching variant from the rmk-boot releases: + * https://github.com/rmk-rs/rmk-boot/releases + */ + +MEMORY {{ + FLASH : ORIGIN = 0x{active_offset:08X}, LENGTH = {active_size} /* ACTIVE region */ + RAM : ORIGIN = 0x20000000, LENGTH = 256K /* SRAM */ +}} + +/* Bootloader partition symbols — offsets relative to flash start. + * The active/state/dfu names match embassy-boot's from_linkerfile_blocking(), + * storage is an RMK extension. + */ +__bootloader_state_start = 0x{state_offset:X}; +__bootloader_state_end = 0x{state_end:X}; +__bootloader_active_start = 0x{rel_active_offset:X}; +__bootloader_active_end = 0x{active_end:X}; +__bootloader_dfu_start = 0x{dfu_offset:X}; +__bootloader_dfu_end = 0x{dfu_end:X}; +__bootloader_storage_start = 0x{storage_offset:X}; +__bootloader_storage_end = 0x{storage_end:X}; +" + ) +} From a07fd86c867b4f3fe14234a7b4b73d701f786071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20J=C3=A4ger?= Date: Sun, 9 Aug 2026 08:40:16 +0200 Subject: [PATCH 2/3] feat(dfu_ext): add external flash dfu partition --- .github/workflows/release.yml | 16 ++- Cargo.lock | 1 + Cargo.toml | 9 +- Makefile.toml | 231 ++++++++++++++++++++++++++++++--- Readme.org | 232 +++++++++++++++++++++------------- build.rs | 145 +++++++++++++++++++-- src/dfu.rs | 71 ++++++++++- src/driver/mod.rs | 1 + src/driver/w25q.rs | 170 +++++++++++++++++++++++++ src/led_pwm.rs | 12 +- src/main.rs | 28 ++-- src/nrf52840.rs | 200 +++++++++++++++++++++-------- src/rp2040.rs | 170 ++++++++++++++++++------- 13 files changed, 1036 insertions(+), 250 deletions(-) create mode 100644 src/driver/mod.rs create mode 100644 src/driver/w25q.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25b8f7f..b6d11c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,8 +20,15 @@ jobs: - name: Build all UF2 variants run: cargo make uf2-all - - name: Copy nRF ELF - run: cp target/thumbv7em-none-eabihf/release/rmk-boot rmk-boot-nrf52840.elf + - name: Copy nRF ELFs + run: | + cp target/thumbv7em-none-eabihf/release/rmk-boot rmk-boot-nrf52840.elf + cargo build --release --target thumbv7em-none-eabihf --features nrf52833 + cp target/thumbv7em-none-eabihf/release/rmk-boot rmk-boot-nrf52833.elf + cargo build --release --target thumbv7em-none-eabihf --features nrf52840,noswap + cp target/thumbv7em-none-eabihf/release/rmk-boot rmk-boot-nrf52840-noswap.elf + cargo build --release --target thumbv7em-none-eabihf --features nrf52833,noswap + cp target/thumbv7em-none-eabihf/release/rmk-boot rmk-boot-nrf52833-noswap.elf - name: Upload release assets uses: softprops/action-gh-release@v2 @@ -29,7 +36,10 @@ jobs: files: | *.uf2 *.hex - *-memory.x + *.x rmk-boot-nrf52840.elf + rmk-boot-nrf52833.elf + rmk-boot-nrf52840-noswap.elf + rmk-boot-nrf52833-noswap.elf env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 34b110c..c0e5f7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,6 +1019,7 @@ dependencies = [ "embassy-time", "embassy-usb", "embassy-usb-dfu", + "embedded-hal 1.0.0", "embedded-storage", ] diff --git a/Cargo.toml b/Cargo.toml index 57758ae..6a46b15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2024" embassy-embedded-hal = "0.6" embassy-sync = "0.8" embassy-time = "0.5.1" +embedded-hal = "1" embedded-storage = "0.3" cortex-m = "0.7.6" cortex-m-rt = "0.7.5" @@ -25,9 +26,8 @@ embassy-usb-dfu = { version = "0.3", default-features = false, features = [ "cortex-m", ], optional = true } -# nRF52840 +# nRF52 embassy-nrf = { version = "0.10", features = [ - "nrf52840", "time-driver-rtc1", "unstable-pac", ], optional = true } @@ -39,7 +39,10 @@ rp2040-2mb = ["rp2040"] rp2040-4mb = ["rp2040"] rp2040-8mb = ["rp2040"] rp2040-16mb = ["rp2040"] -nrf52840 = ["embassy-nrf", "cortex-m/critical-section-single-core", "embassy-usb", "embassy-usb-dfu"] +nrf52840 = ["embassy-nrf", "embassy-nrf/nrf52840", "cortex-m/critical-section-single-core", "embassy-usb", "embassy-usb-dfu"] +nrf52833 = ["embassy-nrf", "embassy-nrf/nrf52833", "cortex-m/critical-section-single-core", "embassy-usb", "embassy-usb-dfu"] +noswap = [] +dfu_ext = [] [profile.release] opt-level = "z" diff --git a/Makefile.toml b/Makefile.toml index 9ba1c2c..b35855f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -4,19 +4,19 @@ install_crate = { rustup_component_name = "llvm-tools" } [tasks.flip-link] install_crate = { crate_name = "flip-link", binary = "flip-link", test_arg = ["-h"] } -[tasks.build-rp2040-2mb] +[tasks.build-2mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb"] -[tasks.build-rp2040-4mb] +[tasks.build-4mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb"] -[tasks.build-rp2040-8mb] +[tasks.build-8mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb"] -[tasks.build-rp2040-16mb] +[tasks.build-16mb] command = "cargo" args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb"] @@ -24,23 +24,23 @@ args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "r command = "cargo" args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840"] -[tasks.objcopy-rp2040-2mb] -dependencies = ["install-llvm-tools", "build-rp2040-2mb"] +[tasks.objcopy-2mb] +dependencies = ["install-llvm-tools", "build-2mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb", "--", "-O", "ihex", "rmk-boot-rp2040-2mb.hex"] -[tasks.objcopy-rp2040-4mb] -dependencies = ["install-llvm-tools", "build-rp2040-4mb"] +[tasks.objcopy-4mb] +dependencies = ["install-llvm-tools", "build-4mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb", "--", "-O", "ihex", "rmk-boot-rp2040-4mb.hex"] -[tasks.objcopy-rp2040-8mb] -dependencies = ["install-llvm-tools", "build-rp2040-8mb"] +[tasks.objcopy-8mb] +dependencies = ["install-llvm-tools", "build-8mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb", "--", "-O", "ihex", "rmk-boot-rp2040-8mb.hex"] -[tasks.objcopy-rp2040-16mb] -dependencies = ["install-llvm-tools", "build-rp2040-16mb"] +[tasks.objcopy-16mb] +dependencies = ["install-llvm-tools", "build-16mb"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb", "--", "-O", "ihex", "rmk-boot-rp2040-16mb.hex"] @@ -49,30 +49,219 @@ dependencies = ["install-llvm-tools", "build-nrf52840"] command = "cargo" args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840", "--", "-O", "ihex", "rmk-boot-nrf52840.hex"] -[tasks.uf2-rp2040-2mb] -dependencies = ["objcopy-rp2040-2mb"] +[tasks.uf2-2mb] +dependencies = ["objcopy-2mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-2mb.hex", "--output-path", "rmk-boot-rp2040-2mb.uf2", "--family", "rp2040"] -[tasks.uf2-rp2040-4mb] -dependencies = ["objcopy-rp2040-4mb"] +[tasks.uf2-4mb] +dependencies = ["objcopy-4mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-4mb.hex", "--output-path", "rmk-boot-rp2040-4mb.uf2", "--family", "rp2040"] -[tasks.uf2-rp2040-8mb] -dependencies = ["objcopy-rp2040-8mb"] +[tasks.uf2-8mb] +dependencies = ["objcopy-8mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-8mb.hex", "--output-path", "rmk-boot-rp2040-8mb.uf2", "--family", "rp2040"] -[tasks.uf2-rp2040-16mb] -dependencies = ["objcopy-rp2040-16mb"] +[tasks.uf2-16mb] +dependencies = ["objcopy-16mb"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-16mb.hex", "--output-path", "rmk-boot-rp2040-16mb.uf2", "--family", "rp2040"] +[tasks.build-nrf52833] +command = "cargo" +args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52833"] + +[tasks.objcopy-nrf52833] +dependencies = ["install-llvm-tools", "build-nrf52833"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52833", "--", "-O", "ihex", "rmk-boot-nrf52833.hex"] + +[tasks.uf2-nrf52833] +dependencies = ["objcopy-nrf52833"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-nrf52833.hex", "--output-path", "rmk-boot-nrf52833.uf2", "--family", "0x621E937A"] + [tasks.uf2-nrf52840] dependencies = ["objcopy-nrf52840"] command = "cargo" args = ["hex-to-uf2", "--input-path", "rmk-boot-nrf52840.hex", "--output-path", "rmk-boot-nrf52840.uf2", "--family", "nrf52840"] [tasks.uf2-all] -dependencies = ["uf2-rp2040-2mb", "uf2-rp2040-4mb", "uf2-rp2040-8mb", "uf2-rp2040-16mb", "uf2-nrf52840"] +dependencies = ["uf2-2mb", "uf2-4mb", "uf2-8mb", "uf2-16mb", "uf2-nrf52840", "uf2-nrf52833", "uf2-2mb-noswap", "uf2-4mb-noswap", "uf2-8mb-noswap", "uf2-16mb-noswap", "uf2-nrf52840-noswap", "uf2-nrf52833-noswap", "uf2-dfu_ext"] + +# ── noswap variants ── + +[tasks.build-2mb-noswap] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb,noswap"] + +[tasks.objcopy-2mb-noswap] +dependencies = ["install-llvm-tools", "build-2mb-noswap"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb,noswap", "--", "-O", "ihex", "rmk-boot-rp2040-2mb-noswap.hex"] + +[tasks.uf2-2mb-noswap] +dependencies = ["objcopy-2mb-noswap"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-2mb-noswap.hex", "--output-path", "rmk-boot-rp2040-2mb-noswap.uf2", "--family", "rp2040"] + +[tasks.build-4mb-noswap] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb,noswap"] + +[tasks.objcopy-4mb-noswap] +dependencies = ["install-llvm-tools", "build-4mb-noswap"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb,noswap", "--", "-O", "ihex", "rmk-boot-rp2040-4mb-noswap.hex"] + +[tasks.uf2-4mb-noswap] +dependencies = ["objcopy-4mb-noswap"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-4mb-noswap.hex", "--output-path", "rmk-boot-rp2040-4mb-noswap.uf2", "--family", "rp2040"] + +[tasks.build-8mb-noswap] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb,noswap"] + +[tasks.objcopy-8mb-noswap] +dependencies = ["install-llvm-tools", "build-8mb-noswap"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb,noswap", "--", "-O", "ihex", "rmk-boot-rp2040-8mb-noswap.hex"] + +[tasks.uf2-8mb-noswap] +dependencies = ["objcopy-8mb-noswap"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-8mb-noswap.hex", "--output-path", "rmk-boot-rp2040-8mb-noswap.uf2", "--family", "rp2040"] + +[tasks.build-16mb-noswap] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb,noswap"] + +[tasks.objcopy-16mb-noswap] +dependencies = ["install-llvm-tools", "build-16mb-noswap"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb,noswap", "--", "-O", "ihex", "rmk-boot-rp2040-16mb-noswap.hex"] + +[tasks.uf2-16mb-noswap] +dependencies = ["objcopy-16mb-noswap"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-16mb-noswap.hex", "--output-path", "rmk-boot-rp2040-16mb-noswap.uf2", "--family", "rp2040"] + +[tasks.build-nrf52840-noswap] +command = "cargo" +args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840,noswap"] + +[tasks.objcopy-nrf52840-noswap] +dependencies = ["install-llvm-tools", "build-nrf52840-noswap"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840,noswap", "--", "-O", "ihex", "rmk-boot-nrf52840-noswap.hex"] + +[tasks.uf2-nrf52840-noswap] +dependencies = ["objcopy-nrf52840-noswap"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-nrf52840-noswap.hex", "--output-path", "rmk-boot-nrf52840-noswap.uf2", "--family", "nrf52840"] + +[tasks.build-nrf52833-noswap] +command = "cargo" +args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52833,noswap"] + +[tasks.objcopy-nrf52833-noswap] +dependencies = ["install-llvm-tools", "build-nrf52833-noswap"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52833,noswap", "--", "-O", "ihex", "rmk-boot-nrf52833-noswap.hex"] + +[tasks.uf2-nrf52833-noswap] +dependencies = ["objcopy-nrf52833-noswap"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-nrf52833-noswap.hex", "--output-path", "rmk-boot-nrf52833-noswap.uf2", "--family", "0x621E937A"] + +# ── dfu_ext variants ── + +[tasks.build-2mb-dfu_ext] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb,dfu_ext"] + +[tasks.objcopy-2mb-dfu_ext] +dependencies = ["install-llvm-tools", "build-2mb-dfu_ext"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-2mb,dfu_ext", "--", "-O", "ihex", "rmk-boot-rp2040-2mb-dfu_ext.hex"] + +[tasks.uf2-2mb-dfu_ext] +dependencies = ["objcopy-2mb-dfu_ext"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-2mb-dfu_ext.hex", "--output-path", "rmk-boot-rp2040-2mb-dfu_ext.uf2", "--family", "rp2040"] + +[tasks.build-4mb-dfu_ext] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb,dfu_ext"] + +[tasks.objcopy-4mb-dfu_ext] +dependencies = ["install-llvm-tools", "build-4mb-dfu_ext"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-4mb,dfu_ext", "--", "-O", "ihex", "rmk-boot-rp2040-4mb-dfu_ext.hex"] + +[tasks.uf2-4mb-dfu_ext] +dependencies = ["objcopy-4mb-dfu_ext"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-4mb-dfu_ext.hex", "--output-path", "rmk-boot-rp2040-4mb-dfu_ext.uf2", "--family", "rp2040"] + +[tasks.build-8mb-dfu_ext] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb,dfu_ext"] + +[tasks.objcopy-8mb-dfu_ext] +dependencies = ["install-llvm-tools", "build-8mb-dfu_ext"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-8mb,dfu_ext", "--", "-O", "ihex", "rmk-boot-rp2040-8mb-dfu_ext.hex"] + +[tasks.uf2-8mb-dfu_ext] +dependencies = ["objcopy-8mb-dfu_ext"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-8mb-dfu_ext.hex", "--output-path", "rmk-boot-rp2040-8mb-dfu_ext.uf2", "--family", "rp2040"] + +[tasks.build-16mb-dfu_ext] +command = "cargo" +args = ["build", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb,dfu_ext"] + +[tasks.objcopy-16mb-dfu_ext] +dependencies = ["install-llvm-tools", "build-16mb-dfu_ext"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv6m-none-eabi", "--features", "rp2040-16mb,dfu_ext", "--", "-O", "ihex", "rmk-boot-rp2040-16mb-dfu_ext.hex"] + +[tasks.uf2-16mb-dfu_ext] +dependencies = ["objcopy-16mb-dfu_ext"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-rp2040-16mb-dfu_ext.hex", "--output-path", "rmk-boot-rp2040-16mb-dfu_ext.uf2", "--family", "rp2040"] + +[tasks.build-nrf52840-dfu_ext] +command = "cargo" +args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840,dfu_ext"] + +[tasks.objcopy-nrf52840-dfu_ext] +dependencies = ["install-llvm-tools", "build-nrf52840-dfu_ext"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52840,dfu_ext", "--", "-O", "ihex", "rmk-boot-nrf52840-dfu_ext.hex"] + +[tasks.uf2-nrf52840-dfu_ext] +dependencies = ["objcopy-nrf52840-dfu_ext"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-nrf52840-dfu_ext.hex", "--output-path", "rmk-boot-nrf52840-dfu_ext.uf2", "--family", "nrf52840"] + +[tasks.build-nrf52833-dfu_ext] +command = "cargo" +args = ["build", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52833,dfu_ext"] + +[tasks.objcopy-nrf52833-dfu_ext] +dependencies = ["install-llvm-tools", "build-nrf52833-dfu_ext"] +command = "cargo" +args = ["objcopy", "--release", "--target", "thumbv7em-none-eabihf", "--features", "nrf52833,dfu_ext", "--", "-O", "ihex", "rmk-boot-nrf52833-dfu_ext.hex"] + +[tasks.uf2-nrf52833-dfu_ext] +dependencies = ["objcopy-nrf52833-dfu_ext"] +command = "cargo" +args = ["hex-to-uf2", "--input-path", "rmk-boot-nrf52833-dfu_ext.hex", "--output-path", "rmk-boot-nrf52833-dfu_ext.uf2", "--family", "0x621E937A"] + +[tasks.uf2-dfu_ext] +dependencies = ["uf2-2mb-dfu_ext", "uf2-4mb-dfu_ext", "uf2-8mb-dfu_ext", "uf2-16mb-dfu_ext", "uf2-nrf52840-dfu_ext", "uf2-nrf52833-dfu_ext"] diff --git a/Readme.org b/Readme.org index 06c2730..36d340b 100644 --- a/Readme.org +++ b/Readme.org @@ -1,8 +1,8 @@ #+TITLE: rmk-boot -#+SUBTITLE: A pre-built embassy-boot bootloader for RP2040 and nRF52840 +#+SUBTITLE: A pre-built embassy-boot bootloader for RP2040 and nRF52840 / nRF52833 A ready-to-use [[https://github.com/embassy-rs/embassy/tree/main/embassy-boot][embassy-boot]] bootloader for the Raspberry Pi RP2040 and -NRF52840. It sits at the beginning of flash, handles dual-slot firmware +nRF52840 / nRF52833. It sits at the beginning of flash, handles dual-slot firmware switching (active / DFU) with automatic rollback on failure, and signals its state via a PWM-driven LED. @@ -10,28 +10,38 @@ Combine it with RMK's =dfu_rp= or =dfu_nrf= feature — after the initial flash you never need to press BOOTSEL again; all subsequent firmware updates happen over USB via =dfu-util=. -rmk-boot ships a linker script called =rmk-memory.x= alongside its firmware +rmk-boot ships a linker script called **=rmk-memory.x=** alongside its firmware binaries. This file is the single source of truth for the flash partition layout — RMK reads partition offsets from embedded linker symbols at runtime -via =init_flash_from_linkerscript()=. No manual address calculation, no +via =dfu_flash_layout()=. No manual address calculation, no matching =memory.x= needed. * Features -- =Dual-slot firmware= — flash is split into ACTIVE and DFU partitions; +- **Dual-slot firmware** — flash is split into ACTIVE and DFU partitions; embassy-boot copies from DFU to ACTIVE on boot. If the new firmware panics or fails to =mark_booted()= in time, the bootloader reverts automatically. -- =Power-loss safe= — the swap operation is crash-recoverable; a partially +- **Power-loss safe** — the swap operation is crash-recoverable; a partially written ACTIVE slot is detected and rolled back. -- =LED= — a single GPIO LED to signal bootloader states -- =USB DFU via double-tap (nRF52840 only)= — two NRST resets within ~500 ms +- **LED** — a single GPIO LED to signal bootloader states +- **USB DFU via double-tap (nRF52 only)** — two NRST resets within ~500 ms enter DFU mode (similar to Adafruit bootloader) -- =Flash-size variants= — pre-configured for 2 MB, 4 MB, 8 MB and 16 MB - (RP2040) and 1 MB (nRF52840) flash chips. -- =UF2 builds= — =cargo make uf2-*= generates ready-to-flash =*.uf2= files. -- =rmk-memory.x= — each build also produces a matching linker script that RMK +- **Flash-size variants** — pre-configured for 2 MB, 4 MB, 8 MB and 16 MB + (RP2040), 1 MB (nRF52840), and 512 KB (nRF52833) flash chips. +- **noswap mode** — build with =noswap= feature to disable the swap mechanism + entirely. The bootloader always boots directly into ACTIVE (no state checks, + no DFU→ACTIVE copy). Double-tap DFU still works and writes directly to + ACTIVE instead of a separate DFU slot. Ideal for smaller flash chips where + RMK is too large for a dual-slot layout. +- **External flash DFU (=dfu_ext=)** — move the DFU download slot to an + external SPI NOR flash chip (e.g. W25Q64JV). The internal DFU partition is + removed entirely and the ACTIVE region expands to roughly double the space + for firmware. Requires a 25-series SPI flash chip connected via SPI. + Mutually exclusive with =noswap=. +- **UF2 builds** — =cargo make uf2-* = generates ready-to-flash =*.uf2= files. +- **rmk-memory.x** — each build also produces a matching linker script that RMK consumes at link time for zero-config flash layout. -- =Panic handler= — on bootloader panic, blinks SOS in Morse code on the +- **Panic handler** — on bootloader panic, blinks SOS in Morse code on the LED so you know something went wrong inside the bootloader itself (as distinct from a firmware panic, which is handled by the app's own handler). @@ -39,9 +49,10 @@ matching =memory.x= needed. ** Build -Needs a target toolchain, install with =rustup target add thumbv6m-none-eabi= (RP2040) / =rustup target add thumbv7em-none-eabihf= (NRF52840) +Needs a target toolchain, install with =rustup target add thumbv6m-none-eabi= (RP2040) / =rustup target add thumbv7em-none-eabihf= (nRF52840 / nRF52833) #+BEGIN_SRC sh -cargo make build-rp2040-2mb # or build-rp2040-4mb / build-rp2040-8mb / build-rp2040-16mb / build-nrf52840 +cargo make build-2mb # or build-4mb / build-8mb / build-16mb / build-nrf52840 / build-nrf52833 +cargo make build-2mb-dfu_ext # build with external SPI flash DFU (dfu_ext variants exist for all flash sizes) #+END_SRC ** Flash via probe-rs @@ -49,43 +60,54 @@ cargo make build-rp2040-2mb # or build-rp2040-4mb / build-rp2040-8mb / build-r #+BEGIN_SRC sh # RP2040 cargo run --release --target thumbv6m-none-eabi --features rp2040-2mb -# NRF52840 +# nRF52840 cargo run --release --target thumbv7em-none-eabihf --features nrf52840 +# nRF52833 +cargo run --release --target thumbv7em-none-eabihf --features nrf52833 #+END_SRC ** Generate a UF2 (for drag-and-drop flashing) and rmk-memory.x -=build.rs= writes the linker script natively, both under the generic name -=rmk-memory.x= and under the variant-specific name used by RMK on every -=build.rs= run for a supported platform feature (e.g. =rp2040-2mb= also emits -=rmk-rp2040-2mb-memory.x=). No copy step is required. +Each =cargo make uf2-*= target also produces a matching =rmk-memory.x= linker script: -Each =cargo make uf2-*= target builds the bootloader and produces a matching -linker script: +#+BEGIN_SRC sh +cargo make uf2-2mb # → rmk-boot-rp2040-2mb.uf2 + rmk-rp2040-2mb-memory.x +cargo make uf2-4mb # → rmk-boot-rp2040-4mb.uf2 + rmk-rp2040-4mb-memory.x +cargo make uf2-8mb # → rmk-boot-rp2040-8mb.uf2 + rmk-rp2040-8mb-memory.x +cargo make uf2-16mb # → rmk-boot-rp2040-16mb.uf2 + rmk-rp2040-16mb-memory.x +cargo make uf2-nrf52840 # → rmk-boot-nrf52840.uf2 + rmk-nrf52840-memory.x +cargo make uf2-nrf52833 # → rmk-boot-nrf52833.uf2 + rmk-nrf52833-memory.x +#+END_SRC +All variants also have =*-noswap= counterparts (e.g. =uf2-nrf52833-noswap=) that +skip the DFU→ACTIVE swap and boot directly into ACTIVE. +All variants also have =*-dfu_ext= counterparts (e.g. =uf2-2mb-dfu_ext=) for +external SPI flash as DFU slot. +Build all at once: #+BEGIN_SRC sh -cargo make uf2-rp2040-2mb # → rmk-boot-rp2040-2mb.uf2 + rmk-rp2040-2mb-memory.x -cargo make uf2-rp2040-4mb # → rmk-boot-rp2040-4mb.uf2 + rmk-rp2040-4mb-memory.x -cargo make uf2-rp2040-8mb # → rmk-boot-rp2040-8mb.uf2 + rmk-rp2040-8mb-memory.x -cargo make uf2-rp2040-16mb # → rmk-boot-rp2040-16mb.uf2 + rmk-rp2040-16mb-memory.x -cargo make uf2-nrf52840 # → rmk-boot-nrf52840.uf2 + rmk-nrf52840-memory.x +cargo make uf2-all #+END_SRC Copy the =*.uf2= to the =RPI-RP2= mass-storage device that appears when you hold BOOTSEL while plugging in USB. -For nRF52840 this only works if it has the Adafruit UF2 bootloader installed. -=BE AWARE THAT RMK-BOOT OVERWRITES THAT BOOTLOADER ON THE NRF52840!= +For nRF52 this only works if the chip has the Adafruit UF2 bootloader installed. +**BE AWARE THAT RMK-BOOT OVERWRITES THAT BOOTLOADER ON THE NRF52!** -* Entering DFU mode (nRF52840) +* Entering DFU mode (nRF52) -Because the nRF52840 does not have a built in bootloader in ROM, flashing a firmware +Because the nRF52840 / nRF52833 does not have a built in bootloader in ROM, flashing a firmware that does not implement DFU flashing or does not boot, can make flashing impossible. (if no debugger is available) Therefore the bootloader provides the possibility to flash firmware via DFU. -The bootloader implements the =double-tap= convention known from the Adafruit -bootloader for the nrf52840. Double tap NRST (reset pin) to GND to enter DFU mode, -to LED (P0_15 by default) starts breathing slowly to show that DFU mode is active. +The bootloader implements the **double-tap** convention known from the Adafruit +bootloader for the nRF52. Double tap NRST (reset pin) to GND to enter DFU mode, +the LED (P0_15 by default) starts breathing slowly to show that DFU mode is active. + +With the =noswap= feature, DFU mode writes new firmware directly to the ACTIVE +partition instead of a separate DFU slot. +With the =dfu_ext= feature, DFU mode writes to the external SPI flash instead +of the internal DFU partition. * Flash-size variants @@ -99,8 +121,10 @@ Exactly one feature must be enabled at build time. Storage defaults to 32 KB | =rp2040-8mb= | 8 MB | 4064 K | 4068 K | 0x10007000 | 0x103FF000 | | =rp2040-16mb= | 16 MB | 8160 K | 8164 K | 0x10007000 | 0x107FF000 | | =nrf52840= | 1 MB | 480 K | 484 K | 0x00007000 | 0x0007B000 | +| =nrf52833= | 512 KB | 224 K | 228 K | 0x00007000 | 0x0003F000 | -The fixed regions for RP2040 (identical for all variants): +With =dfu_ext=, the internal DFU partition is removed (size 0) and ACTIVE +expands to fill the freed space. | Region | Start | Size | |--------------------------+-------------+------| @@ -108,7 +132,7 @@ The fixed regions for RP2040 (identical for all variants): | Bootloader code | 0x10000100 | ~24 K | | Boot state | 0x10006000 | 4 K | -Fixed regions for nRF52840: +Fixed regions for nRF52840 / nRF52833: | Region | Start | Size | |--------------------------+------------+------| @@ -123,40 +147,34 @@ only if your RMK firmware exceeds 992 KB. * Integration with RMK -RMK integrates with rmk-boot through the =rmk-memory.x= linker script. The +RMK integrates with rmk-boot through the **=rmk-memory.x=** linker script. The file is the single source of truth — it contains both the flash memory layout -(=MEMORY=) for the linker and the standard embassy-boot partition symbols -(=__bootloader_*=) that both RMK and the bootloader read at runtime via -=init_flash_from_linkerscript()= and =from_linkerfile_blocking()=. +(=MEMORY=) for the linker and DFU partition symbols (=__bootloader_*=) that RMK +reads at runtime via =dfu_flash_layout()=. To use rmk-boot with RMK: -1. Download the matching =rmk-boot-*.uf2= and =rmk-boot-*.x= from the +1. Download the matching =rmk-boot-*.uf2= and =rmk-*-memory.x= from the [[https://github.com/rmk-rs/rmk-boot/releases][GitHub releases]]. -2. Rename the =.x= file to =rmk-memory.x= and place it next to your =Cargo.toml=. +2. Rename the =.x= file to =memory.x= and place it next to your =Cargo.toml=. 3. Enable =dfu_rp= or =dfu_nrf= in RMK and add a =[dfu]= section to =keyboard.toml= (only =led= / =unlock_keys= / =page_size= are needed — partition offsets come from rmk-memory.x). -RMK's =init_flash_from_linkerscript()= reads these linker symbols from +RMK's =dfu_flash_layout()= reads these linker symbols from =rmk-memory.x= at runtime (all values are flash-relative offsets): -| Symbol | Description | -|-------------------------------+--------------------------| -| =__bootloader_state_start= | Boot state start | -| =__bootloader_state_end= | Boot state end | -| =__bootloader_active_start= | Active slot start | -| =__bootloader_active_end= | Active slot end | -| =__bootloader_dfu_start= | DFU download slot start | -| =__bootloader_dfu_end= | DFU download slot end | -| =__bootloader_storage_start= | Storage partition start | -| =__bootloader_storage_end= | Storage partition end | - -The =__bootloader_active_*=, =__bootloader_state_*= and =__bootloader_dfu_*= -symbols are the same ones embassy-boot's =from_linkerfile_blocking()= reads; -only =__bootloader_storage_*= is an RMK extension. If you build a custom -embassy-boot bootloader, define these same symbols in your own linker script — -RMK will pick them up automatically. +| Symbol | Description | +|-----------------------------+--------------------------| +| __bootloader_state_start= | Boot state start | +| __bootloader_state_end= | Boot state end | +| __bootloader_dfu_start= | DFU download slot start | +| __bootloader_dfu_end= | DFU download slot end | +| __bootloader_storage_start= | Storage partition start | +| __bootloader_storage_end= | Storage partition end | + +If you build a custom embassy-boot bootloader, define these same symbols in +your own linker script — RMK will pick them up automatically. After the initial flash (bootloader + RMK firmware), all subsequent updates can be done over USB: @@ -176,18 +194,18 @@ No BOOTSEL button needed. The bootloader drives a single LED via hardware PWM: - RP2040: GPIO 25 (=PWM_SLICE0=, channel B), defined in =src/rp2040.rs= -- nRF52840: P0.15 (=PWM0=, channel 0), defined in =src/nrf52840.rs= +- nRF52840 / nRF52833: P0.15 (=PWM0=, channel 0), defined in =src/nrf52840.rs= See [[Changing the LED pin]] below for how to adapt them. | Pattern | Meaning | |----------------------------------------+---------| -| 2 short blinks (≈2 Hz) | Normal boot — bootloader ran and jumped to ACTIVE | -| 1 s solid on | Bootloader detected a pending DFU→ACTIVE swap and is about to copy | -| Fast breathing (300 ms period) | DFU→ACTIVE copy in progress | -| 3 short blinks (50 ms) | Previous forward swap completed but the new app did not call =mark_booted()= — reverting to the old ACTIVE | -| 5 short blinks (50 ms) | Successful DFU→ACTIVE copy; about to jump | -| Slow breathing (3 s period) | USB DFU mode active (nRF52840 only) — waiting for =dfu-util= | +| 2 short blinks (≈2 Hz) | Normal boot — bootloader ran and jumped to ACTIVE. With =noswap= this is the only LED pattern on boot. | +| 1 s solid on | Bootloader detected a pending DFU→ACTIVE swap and is about to copy (not present with =noswap=) | +| Fast breathing (300 ms period) | DFU→ACTIVE copy in progress (not present with =noswap=) | +| 3 short blinks (50 ms) | Previous forward swap completed but the new app did not call =mark_booted()= — reverting to the old ACTIVE (not present with =noswap=) | +| 5 short blinks (50 ms) | Successful DFU→ACTIVE copy; about to jump (not present with =noswap=) | +| Slow breathing (3 s period) | USB DFU mode active (nRF52 only) — waiting for =dfu-util= | | SOS (... --- ...), repeating | Bootloader itself panicked (e.g. flash read error, invalid state partition) | If the LED stays dark the bootloader either isn't running (no bootloader @@ -218,10 +236,10 @@ wrong channel — swap =a= / =_b= or pick a different slice. For PWM slice / channel to PIN mapping see: https://rp2040.implrust.com/pwm/pwm-in-rp2040.html#mapping-of-pwm-channels-to-gpio-pins -*** nRF52840 +*** nRF52 In =src/nrf52840.rs=, change the pin in =SimplePwm::new_1ch()=. Unlike the -RP2040, the nRF52840 has /no fixed PWM pin mapping/: any GPIO can be routed +RP2040, the nRF52 has /no fixed PWM pin mapping/: any GPIO can be routed to any PWM output channel via the PSEL register (=embassy-nrf= handles this internally). The pin doesn't need to match any specific PWM instance. @@ -239,6 +257,43 @@ let pwm = SimplePwm::new_1ch(p.PWM0, p.P1_09, &pwm_cfg); You can also use a different PWM instance (=PWM1=, =PWM2=) if =PWM0= is already in use — just pass =p.PWM1= instead of =p.PWM0=. +** Changing the SPI pins for external flash + +When building with =dfu_ext=, the bootloader uses hard-coded SPI pins to talk +to the external flash chip. Defaults: + +| Platform | SCK | MOSI | MISO | CS | +|----------+--------+--------+--------+--------| +| RP2040 | PIN_18 | PIN_19 | PIN_16 | PIN_17 | +| nRF52840 | P0_25 | P0_23 | P0_24 | P0_22 | + +If your board is wired differently, edit the platform source file. + +*** RP2040 + +In =src/rp2040.rs=, change the pins in the =Spi::new_blocking= call inside +the =#[cfg(feature = "dfu_ext")]= block: + +#+BEGIN_SRC rust +let spi_bus = Spi::new_blocking(p.SPI0, p.PIN_18, p.PIN_19, p.PIN_16, spi_cfg); +let cs = Output::new(p.PIN_17, Level::High); +#+END_SRC + +*** nRF52 + +In =src/nrf52840.rs=, change the pins in both =#[cfg(feature = "dfu_ext")]= +blocks: + +#+BEGIN_SRC rust +let spi = Spim::new(p.TWISPI0, ExtFlashIrqs, p.P0_25, p.P0_24, p.P0_23, spi_cfg); +let cs = Output::new(p.P0_22, Level::High, OutputDrive::Standard); +#+END_SRC + +The =TWISPI0= instance (SPIM0) is the default; change it if you use a +different SPI peripheral. + +The =EXT_FLASH_SIZE= constant (default 8 MB) lives in =src/main.rs=. + * Flashing *** First time (probe-rs) @@ -246,27 +301,29 @@ already in use — just pass =p.PWM1= instead of =p.PWM0=. #+BEGIN_SRC sh # RP2040 cargo run --release --target thumbv6m-none-eabi --features rp2040-2mb -# NRF52840 +# nRF52840 cargo run --release --target thumbv7em-none-eabihf --features nrf52840 +# nRF52833 +cargo run --release --target thumbv7em-none-eabihf --features nrf52833 #+END_SRC *** First time (UF2) -WHEN USING AN NRF52840 WITH THE ADAFRUIT UF2 BOOTLOADER, THIS WILL OVERWRITE +WHEN USING AN NRF52840 / NRF52833 WITH THE ADAFRUIT UF2 BOOTLOADER, THIS WILL OVERWRITE THE UF2 BOOTLOADER! On RP2040 the UF2 bootloader is in ROM, so nothing can happen to it. -1. Hold BOOTSEL, plug in USB, release. (For nRF52840 connect RESET to GND +1. Hold BOOTSEL, plug in USB, release. (For nRF52 connect RESET to GND twice within 500 ms — but note that rmk-boot replaces the Adafruit bootloader, so after the first flash you need probe-rs or dfu-util.) 2. A mass-storage device =RPI-RP2= / =NICENANO= appears. -3. Copy =rmk-boot-rp2040-SIZE.uf2= / =rmk-boot-nrf52840.uf2= onto it. +3. Copy =rmk-boot-rp2040-SIZE.uf2= / =rmk-boot-nrf52840.uf2= / =rmk-boot-nrf52833.uf2= onto it. 4. The board reboots and the bootloader is active. *** Updating the bootloader itself Repeat one of the methods above. A new rmk-boot build replaces the old -one at flash address =0x100= (RP2040) / =0x0= (nRF52840). The existing ACTIVE +one at flash address =0x100= (RP2040) / =0x0= (nRF52). The existing ACTIVE and DFU partitions are preserved as long as the flash-size variant stays the same. @@ -278,12 +335,13 @@ The bootloader is split into five source files: |-------------------+---------| | =src/main.rs= | Feature checks, shared constants, entry point, SysTick handler, panic handler | | =src/rp2040.rs= | RP2040 bootloader — =#[cfg(feature = "rp2040")]=, compiled only for RP2040 | -| =src/nrf52840.rs= | nRF52840 bootloader — =#[cfg(feature = "nrf52840")]=, compiled only for nRF52840 | -| =src/dfu.rs= | nRF52840 USB DFU — =#[cfg(feature = "nrf52840")]=, =block_on()= runtime, USB DFU stack | +| =src/nrf52840.rs= | nRF52 bootloader — =#[cfg(any(feature = "nrf52840", feature = "nrf52833"))]= | +| =src/dfu.rs= | nRF52 USB DFU — =#[cfg(any(feature = "nrf52840", feature = "nrf52833"))]=, =block_on()= runtime, USB DFU stack | +| =src/driver/w25q.rs= | W25Q-compatible SPI NOR flash driver (=NorFlash= impl via JEDEC commands), compiled with =dfu_ext= | | =src/led_pwm.rs= | Cross-platform PWM LED singleton — breathing =tick()= (SysTick), hard on/off =set_raw()=, =deinit()= cleanup | =led_pwm.rs= is platform-agnostic; each platform module constructs the correct -PWM peripheral (=Pwm<'static>= for RP2040, =SimplePwm<'static>= for nRF52840) +PWM peripheral (=Pwm<'static>= for RP2040, =SimplePwm<'static>= for nRF52) and passes it into =led_pwm::init()=. Before jumping to firmware, both call =led_pwm::deinit()= which drops the PWM device — this disables the hardware PWM peripheral and releases the GPIO pin so the firmware can reclaim it. @@ -302,18 +360,16 @@ The formula for each variant (=STORAGE_SIZE= defaults to 32 K): - DFU size: =ACTIVE size + 4 K= (one extra page for embassy-boot's swap algorithm) The =rmk-memory.x= file exposes these values as linker symbols that RMK's -=init_flash_from_linkerscript()= reads at runtime (all flash-relative offsets): - -| Symbol | Description | -|-------------------------------+--------------------------| -| =__bootloader_state_start= | Boot state start | -| =__bootloader_state_end= | Boot state end | -| =__bootloader_active_start= | Active slot start | -| =__bootloader_active_end= | Active slot end | -| =__bootloader_dfu_start= | DFU download slot start | -| =__bootloader_dfu_end= | DFU download slot end | -| =__bootloader_storage_start= | Storage partition start | -| =__bootloader_storage_end= | Storage partition end | +=dfu_flash_layout()= reads at runtime (all flash-relative offsets): + +| Symbol | Description | +|----------------------------+--------------------------| +| __bootloader_state_start= | Boot state start | +| __bootloader_state_end= | Boot state end | +| __bootloader_dfu_start= | DFU download slot start | +| __bootloader_dfu_end= | DFU download slot end | +| __bootloader_storage_start= | Storage partition start | +| __bootloader_storage_end= | Storage partition end | * Testing the DFU→ACTIVE copy manually (for developers) diff --git a/build.rs b/build.rs index 9998cf6..2909728 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,13 @@ fn main() { let is_rp2040 = env::var("CARGO_FEATURE_RP2040").is_ok(); let is_nrf52840 = env::var("CARGO_FEATURE_NRF52840").is_ok(); + let is_nrf52833 = env::var("CARGO_FEATURE_NRF52833").is_ok(); + let is_noswap = env::var("CARGO_FEATURE_NOSWAP").is_ok(); + let is_dfu_ext = env::var("CARGO_FEATURE_DFU_EXT").is_ok(); + + if is_nrf52840 && is_nrf52833 { + panic!("nrf52840 and nrf52833 are mutually exclusive"); + } const PAGE_SIZE: usize = 4 * 1024; const STORAGE_SIZE: usize = 32 * 1024; @@ -25,10 +32,20 @@ fn main() { } else { panic!("No RP2040 flash size feature enabled"); }; + let variant_slug = if is_noswap { + format!("{variant_slug}-noswap") + } else if is_dfu_ext { + format!("{variant_slug}-dfu_ext") + } else { + variant_slug.to_string() + }; let remaining = flash_size - 28 * 1024 - STORAGE_SIZE; - let active_size = (remaining - PAGE_SIZE) / 2; - let dfu_size = active_size + PAGE_SIZE; + let (active_size, dfu_size) = if is_noswap || is_dfu_ext { + (remaining, 0) + } else { + ((remaining - PAGE_SIZE) / 2, (remaining - PAGE_SIZE) / 2 + PAGE_SIZE) + }; // Absolute XIP addresses let abs_active_offset = 0x1000_7000u32; let abs_dfu_offset = abs_active_offset + active_size as u32; @@ -73,6 +90,8 @@ __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(BOOT2); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_RP2040_4MB"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_RP2040_8MB"); println!("cargo:rerun-if-env-changed=CARGO_FEATURE_RP2040_16MB"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_NOSWAP"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_DFU_EXT"); let rmk_boot_x = build_rmk_boot_x( variant_label, @@ -80,8 +99,8 @@ __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(BOOT2); active_size as u32, rel_state_offset, 0x1000, - rel_dfu_offset, - rel_dfu_size, + if is_noswap || is_dfu_ext { 0 } else { rel_dfu_offset }, + if is_noswap || is_dfu_ext { 0 } else { rel_dfu_size }, rel_storage_offset, STORAGE_SIZE as u32, 0x1000_0000, // XIP flash base @@ -93,13 +112,28 @@ __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(BOOT2); let bootloader_size = 24 * 1024; let state_size = 4 * 1024; let remaining = flash_size - bootloader_size - state_size - STORAGE_SIZE; - let active_size = (remaining - PAGE_SIZE) / 2; - let dfu_size = active_size + PAGE_SIZE; + let (active_size, dfu_size) = if is_noswap || is_dfu_ext { + (remaining, 0) + } else { + ((remaining - PAGE_SIZE) / 2, (remaining - PAGE_SIZE) / 2 + PAGE_SIZE) + }; let abs_state_offset = bootloader_size as u32; let abs_active_offset = (bootloader_size + state_size) as u32; let abs_dfu_offset = abs_active_offset + active_size as u32; + let (dfu_start_sym, dfu_end_sym) = if is_noswap || is_dfu_ext { + ( + format!("__bootloader_dfu_start = ORIGIN(ACTIVE);"), + format!("__bootloader_dfu_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE);"), + ) + } else { + ( + format!("__bootloader_dfu_start = ORIGIN(DFU);"), + format!("__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU);"), + ) + }; + let memory_x = format!( "\ MEMORY @@ -118,14 +152,16 @@ __bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE) __bootloader_active_start = ORIGIN(ACTIVE); __bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE); -__bootloader_dfu_start = ORIGIN(DFU); -__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU); +{dfu_start_sym} +{dfu_end_sym} " ); fs::write(out.join("memory.x"), &memory_x).unwrap(); println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_NOSWAP"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_DFU_EXT"); let rmk_boot_x = build_rmk_boot_x( "nRF52840", @@ -133,16 +169,85 @@ __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU); active_size as u32, abs_state_offset, state_size as u32, - abs_dfu_offset, - dfu_size as u32, + if is_noswap || is_dfu_ext { 0 } else { abs_dfu_offset }, + if is_noswap || is_dfu_ext { 0 } else { dfu_size as u32 }, abs_dfu_offset + dfu_size as u32, - STORAGE_SIZE as u32, // nRF flash_base is 0, so absolute = relative - 0x0000_0000, // flash base + STORAGE_SIZE as u32, + 0x0000_0000, // flash base ); let project_root = Path::new(env!("CARGO_MANIFEST_DIR")); - write_rmk_boot_x(&project_root, "nrf52840", &rmk_boot_x); + write_rmk_boot_x(&project_root, &nrf_variant_slug("nrf52840", is_noswap, is_dfu_ext), &rmk_boot_x); + } else if is_nrf52833 { let flash_size = 512 * 1024; + let bootloader_size = 24 * 1024; + let state_size = 4 * 1024; + let remaining = flash_size - bootloader_size - state_size - STORAGE_SIZE; + let (active_size, dfu_size) = if is_noswap || is_dfu_ext { + (remaining, 0) + } else { + ((remaining - PAGE_SIZE) / 2, (remaining - PAGE_SIZE) / 2 + PAGE_SIZE) + }; + + let abs_state_offset = bootloader_size as u32; + let abs_active_offset = (bootloader_size + state_size) as u32; + let abs_dfu_offset = abs_active_offset + active_size as u32; + + let (dfu_start_sym, dfu_end_sym) = if is_noswap || is_dfu_ext { + ( + format!("__bootloader_dfu_start = ORIGIN(ACTIVE);"), + format!("__bootloader_dfu_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE);"), + ) + } else { + ( + format!("__bootloader_dfu_start = ORIGIN(DFU);"), + format!("__bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU);"), + ) + }; + + let memory_x = format!( + "\ +MEMORY +{{ + FLASH : ORIGIN = 0x00000000, LENGTH = {bootloader_size} + BOOTLOADER_STATE : ORIGIN = 0x{abs_state_offset:08X}, LENGTH = {state_size} + ACTIVE : ORIGIN = 0x{abs_active_offset:08X}, LENGTH = {active_size} + DFU : ORIGIN = 0x{abs_dfu_offset:08X}, LENGTH = {dfu_size} + + RAM : ORIGIN = 0x20000000, LENGTH = 256K +}} + +__bootloader_state_start = ORIGIN(BOOTLOADER_STATE); +__bootloader_state_end = ORIGIN(BOOTLOADER_STATE) + LENGTH(BOOTLOADER_STATE); + +__bootloader_active_start = ORIGIN(ACTIVE); +__bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE); + +{dfu_start_sym} +{dfu_end_sym} +" + ); + + fs::write(out.join("memory.x"), &memory_x).unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_NOSWAP"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_DFU_EXT"); + + let rmk_boot_x = build_rmk_boot_x( + "nRF52833", + abs_active_offset, + active_size as u32, + abs_state_offset, + state_size as u32, + if is_noswap || is_dfu_ext { 0 } else { abs_dfu_offset }, + if is_noswap || is_dfu_ext { 0 } else { dfu_size as u32 }, + abs_dfu_offset + dfu_size as u32, + STORAGE_SIZE as u32, + 0x0000_0000, // flash base + ); + let project_root = Path::new(env!("CARGO_MANIFEST_DIR")); + write_rmk_boot_x(&project_root, &nrf_variant_slug("nrf52833", is_noswap, is_dfu_ext), &rmk_boot_x); } else { - panic!("No platform feature enabled (rp2040 or nrf52840)"); + panic!("No platform feature enabled (rp2040 or nrf52840 or nrf52833)"); } println!("cargo:rerun-if-changed=build.rs"); @@ -159,6 +264,18 @@ fn write_rmk_boot_x(project_root: &Path, variant_slug: &str, content: &str) { .unwrap(); } +/// Variant slug for the nRF platform, appending the layout-modifying +/// `noswap`/`dfu_ext` feature when enabled. +fn nrf_variant_slug(platform: &str, is_noswap: bool, is_dfu_ext: bool) -> String { + if is_noswap { + format!("{platform}-noswap") + } else if is_dfu_ext { + format!("{platform}-dfu_ext") + } else { + platform.to_string() + } +} + fn build_rmk_boot_x( variant: &str, active_offset: u32, diff --git a/src/dfu.rs b/src/dfu.rs index 87e61d7..5542c84 100644 --- a/src/dfu.rs +++ b/src/dfu.rs @@ -6,11 +6,13 @@ use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::blocking_mutex::Mutex; use embassy_nrf::nvmc::Nvmc; +use embassy_embedded_hal::flash::partition::BlockingPartition; +use embedded_storage::nor_flash::NorFlash; // --------------------------------------------------------------------------- -// Interrupt binding – nRF52840 +// Interrupt binding – nRF52 // --------------------------------------------------------------------------- -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] embassy_nrf::bind_interrupts! { pub(crate) struct DfuIrqs { USBD => embassy_nrf::usb::InterruptHandler; @@ -75,6 +77,7 @@ pub fn run_dfu_nrf>( /// Build and run the full USB DFU stack – never returns. /// /// Called from main. +#[cfg(not(feature = "dfu_ext"))] pub fn run_dfu_usb( flash_mutex: &'static Mutex>>, ) -> ! { @@ -135,3 +138,67 @@ pub fn run_dfu_usb( let mut usb_dev = builder.build(); run_dfu_nrf(&mut usb_dev, crate::DFU_BREATHE_MS) } + +/// Same as [`run_dfu_usb`], but writes the DFU image to an external +/// SPI flash instead of the internal DFU partition. +#[cfg(feature = "dfu_ext")] +pub fn run_dfu_usb_ext( + flash_mutex: &'static Mutex>>, + ext_mutex: &'static Mutex>, + ext_flash_size: u32, +) -> ! { + use embassy_boot::{BlockingFirmwareUpdater, FirmwareUpdaterConfig}; + use embassy_usb::class::dfu::consts::DfuAttributes; + use embassy_usb_dfu::{self as dfu, ResetImmediate}; + + let p = unsafe { embassy_nrf::Peripherals::steal() }; + + let vbus_obj = embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(true, true); + let vbus: &'static _ = unsafe { &*(&vbus_obj as *const _) }; + let driver = embassy_nrf::usb::Driver::new(p.USBD, crate::dfu::DfuIrqs, vbus); + + let mut usb_config = embassy_usb::Config::new(0x1209, 0x0001); + usb_config.manufacturer = Some("rmk-boot"); + usb_config.product = Some("nRF52840 DFU"); + usb_config.serial_number = Some("123456"); + usb_config.max_power = 100; + usb_config.composite_with_iads = false; + usb_config.device_class = 0xFE; + usb_config.device_sub_class = 0x01; + usb_config.device_protocol = 0x01; + + static mut CFG: [u8; 256] = [0; 256]; + static mut BOS: [u8; 128] = [0; 128]; + static mut MSOS: [u8; 128] = [0; 128]; + static mut CTL: [u8; 2048] = [0; 2048]; + static mut AL: [u8; 4] = [0; 4]; + + let mut builder = embassy_usb::Builder::new( + driver, + usb_config, + unsafe { &mut *core::ptr::addr_of_mut!(CFG) }, + unsafe { &mut *core::ptr::addr_of_mut!(BOS) }, + unsafe { &mut *core::ptr::addr_of_mut!(MSOS) }, + unsafe { &mut *core::ptr::addr_of_mut!(CTL) }, + ); + + let dfu_part = BlockingPartition::new(ext_mutex, 0, ext_flash_size); + let state_part = BlockingPartition::new(flash_mutex, 0x6000, 0x1000); + let uc = FirmwareUpdaterConfig { + dfu: dfu_part, + state: state_part, + }; + let upd = BlockingFirmwareUpdater::new(uc, unsafe { &mut *core::ptr::addr_of_mut!(AL) }); + + const BS: usize = 2048; + let mut s = dfu::new_state::<_, _, ResetImmediate, BS>( + upd, + DfuAttributes::CAN_DOWNLOAD | DfuAttributes::WILL_DETACH, + ResetImmediate, + ); + let s: &'static mut _ = unsafe { &mut *(&mut s as *mut _) }; + dfu::usb_dfu::<_, _, _, _, BS>(&mut builder, s, |_| {}); + + let mut usb_dev = builder.build(); + run_dfu_nrf(&mut usb_dev, crate::DFU_BREATHE_MS) +} diff --git a/src/driver/mod.rs b/src/driver/mod.rs new file mode 100644 index 0000000..7a77d1c --- /dev/null +++ b/src/driver/mod.rs @@ -0,0 +1 @@ +pub mod w25q; diff --git a/src/driver/w25q.rs b/src/driver/w25q.rs new file mode 100644 index 0000000..fcf3c81 --- /dev/null +++ b/src/driver/w25q.rs @@ -0,0 +1,170 @@ +use embedded_hal::digital::OutputPin; +use embedded_hal::spi::SpiBus; +use embedded_storage::nor_flash::{ErrorType, MultiwriteNorFlash, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash}; + +const CMD_READ: u8 = 0x03; +const CMD_PAGE_PROGRAM: u8 = 0x02; +const CMD_WRITE_ENABLE: u8 = 0x06; +const CMD_READ_STATUS: u8 = 0x05; +const CMD_SECTOR_ERASE: u8 = 0x20; +const CMD_BLOCK_ERASE_64K: u8 = 0xD8; + +const PAGE_SIZE: u32 = 256; +const SECTOR_SIZE: u32 = 4096; +const BLOCK64_SIZE: u32 = 65536; + +/// [`NorFlash`] implementation for W25Q and compatible 25-series SPI NOR flash +/// chips. Uses standard JEDEC commands valid across Winbond, Macronix, ISSI, +/// and similar families. +pub struct W25qNorFlash { + bus: BUS, + cs: CS, + flash_size: u32, +} + +impl W25qNorFlash { + pub fn new(bus: BUS, mut cs: CS, flash_size: u32) -> Self { + cs.set_high().ok(); + Self { bus, cs, flash_size } + } + + fn wait_wip(&mut self) -> Result<(), W25qError> { + loop { + self.cs.set_low().ok(); + let res = self.bus.write(&[CMD_READ_STATUS]); + if res.is_err() { + self.cs.set_high().ok(); + return Err(W25qError::Spi(res.unwrap_err())); + } + let mut status = [0u8; 1]; + let res = self.bus.read(&mut status); + self.cs.set_high().ok(); + res.map_err(W25qError::Spi)?; + if status[0] & 0x01 == 0 { + return Ok(()); + } + } + } + + fn write_enable(&mut self) -> Result<(), W25qError> { + self.cs.set_low().ok(); + let res = self.bus.write(&[CMD_WRITE_ENABLE]).map_err(W25qError::Spi); + self.cs.set_high().ok(); + res + } + + fn read_data(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), W25qError> { + self.wait_wip()?; + let cmd = [CMD_READ, (addr >> 16) as u8, (addr >> 8) as u8, addr as u8]; + self.cs.set_low().ok(); + let res = self.bus.write(&cmd); + if res.is_err() { + self.cs.set_high().ok(); + return Err(W25qError::Spi(res.unwrap_err())); + } + let res = self.bus.read(buf).map_err(W25qError::Spi); + self.cs.set_high().ok(); + res + } + + fn page_program(&mut self, addr: u32, data: &[u8]) -> Result<(), W25qError> { + self.write_enable()?; + let cmd = [CMD_PAGE_PROGRAM, (addr >> 16) as u8, (addr >> 8) as u8, addr as u8]; + self.cs.set_low().ok(); + let res = self.bus.write(&cmd); + if res.is_err() { + self.cs.set_high().ok(); + return Err(W25qError::Spi(res.unwrap_err())); + } + let res = self.bus.write(data).map_err(W25qError::Spi); + self.cs.set_high().ok(); + res + } + + fn sector_erase(&mut self, addr: u32) -> Result<(), W25qError> { + self.write_enable()?; + let cmd = [CMD_SECTOR_ERASE, (addr >> 16) as u8, (addr >> 8) as u8, addr as u8]; + self.cs.set_low().ok(); + let res = self.bus.write(&cmd).map_err(W25qError::Spi); + self.cs.set_high().ok(); + res + } + + fn block_erase_64k(&mut self, addr: u32) -> Result<(), W25qError> { + self.write_enable()?; + let cmd = [CMD_BLOCK_ERASE_64K, (addr >> 16) as u8, (addr >> 8) as u8, addr as u8]; + self.cs.set_low().ok(); + let res = self.bus.write(&cmd).map_err(W25qError::Spi); + self.cs.set_high().ok(); + res + } +} + +#[derive(Debug)] +pub enum W25qError { + Spi(SPI), +} + +impl core::fmt::Display for W25qError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + W25qError::Spi(e) => write!(f, "SPI error: {:?}", e), + } + } +} + +impl NorFlashError for W25qError { + fn kind(&self) -> NorFlashErrorKind { + NorFlashErrorKind::Other + } +} + +impl ErrorType for W25qNorFlash { + type Error = W25qError; +} + +impl ReadNorFlash for W25qNorFlash { + const READ_SIZE: usize = 1; + + fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { + self.read_data(offset, bytes) + } + + fn capacity(&self) -> usize { + self.flash_size as usize + } +} + +impl NorFlash for W25qNorFlash { + const WRITE_SIZE: usize = 1; + const ERASE_SIZE: usize = SECTOR_SIZE as usize; + + fn erase(&mut self, mut from: u32, to: u32) -> Result<(), Self::Error> { + while from < to { + self.wait_wip()?; + let remaining = to - from; + if remaining >= BLOCK64_SIZE && from % BLOCK64_SIZE == 0 { + self.block_erase_64k(from)?; + from += BLOCK64_SIZE; + } else { + self.sector_erase(from)?; + from += SECTOR_SIZE; + } + } + Ok(()) + } + + fn write(&mut self, mut offset: u32, mut bytes: &[u8]) -> Result<(), Self::Error> { + while !bytes.is_empty() { + self.wait_wip()?; + let page_offset = offset & (PAGE_SIZE - 1); + let chunk = bytes.len().min((PAGE_SIZE - page_offset) as usize); + self.page_program(offset, &bytes[..chunk])?; + offset += chunk as u32; + bytes = &bytes[chunk..]; + } + Ok(()) + } +} + +impl MultiwriteNorFlash for W25qNorFlash {} diff --git a/src/led_pwm.rs b/src/led_pwm.rs index dfd0cef..f6a3d01 100644 --- a/src/led_pwm.rs +++ b/src/led_pwm.rs @@ -6,16 +6,16 @@ use embassy_sync::blocking_mutex::Mutex; static MS: Mutex> = Mutex::new(Cell::new(0)); static PERIOD: Mutex> = Mutex::new(Cell::new(0)); -// ── nRF52840 ── -#[cfg(feature = "nrf52840")] +// ── nRF52 ── +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] use embassy_nrf::pwm::{DutyCycle, SimplePwm}; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] type PwmDev = SimplePwm<'static>; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] static PWM: Mutex>> = Mutex::new(RefCell::new(None)); -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] fn set_hw_duty(duty: u16) { PWM.lock(|c| { if let Some(ref mut pwm) = *c.borrow_mut() { @@ -83,7 +83,7 @@ pub fn set_raw(on: bool) { /// Set PWM duty cycle directly (0-255). /// Use this from a main loop instead of relying on SysTick. -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] pub fn set_duty(duty: u8) { set_hw_duty(duty as u16); } diff --git a/src/main.rs b/src/main.rs index 9260030..136d3d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,14 @@ // --------------------------------------------------------------------------- // Feature-exclusion checks // --------------------------------------------------------------------------- -#[cfg(all(feature = "rp2040", feature = "nrf52840"))] -compile_error!("rp2040 and nrf52840 features are mutually exclusive"); +#[cfg(all(feature = "noswap", feature = "dfu_ext"))] +compile_error!("noswap and dfu_ext are mutually exclusive"); + +#[cfg(all(feature = "rp2040", any(feature = "nrf52840", feature = "nrf52833")))] +compile_error!("rp2040 and nRF52 features are mutually exclusive"); + +#[cfg(all(feature = "nrf52840", feature = "nrf52833"))] +compile_error!("nrf52840 and nrf52833 features are mutually exclusive"); #[cfg(feature = "rp2040")] const _: () = { @@ -37,12 +43,14 @@ const _: () = { // --------------------------------------------------------------------------- #[cfg(feature = "rp2040")] mod rp2040; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] mod nrf52840; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] mod dfu; mod led_pwm; +#[cfg(feature = "dfu_ext")] +mod driver; // --------------------------------------------------------------------------- // Shared imports @@ -64,9 +72,9 @@ const PAGE_SIZE: usize = 4096; #[cfg(feature = "rp2040")] const WRITE_SIZE: usize = 1; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] const PAGE_SIZE: usize = 4096; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] const WRITE_SIZE: usize = 4; const STATE_ERASE_VALUE: u8 = 0xFF; @@ -86,10 +94,12 @@ const LETTER_GAP_MS: u64 = 450; const WORD_GAP_MS: u64 = 1050; const SWAP_BREATHE_MS: u32 = 300; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] const DFU_BREATHE_MS: u32 = 3000; -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] const DTAP_SIGNAL_MS: u64 = 500; +#[cfg(feature = "dfu_ext")] +const EXT_FLASH_SIZE: u32 = 8 * 1024 * 1024; // --------------------------------------------------------------------------- // Entry point @@ -99,7 +109,7 @@ fn platform_run() -> ! { rp2040::run() } -#[cfg(feature = "nrf52840")] +#[cfg(any(feature = "nrf52840", feature = "nrf52833"))] fn platform_run() -> ! { nrf52840::run() } diff --git a/src/nrf52840.rs b/src/nrf52840.rs index 5e68a34..b80086c 100644 --- a/src/nrf52840.rs +++ b/src/nrf52840.rs @@ -1,9 +1,18 @@ -use embassy_boot::{BootLoader, BootLoaderConfig, State}; +use embassy_boot::BootLoaderConfig; use embassy_nrf::nvmc::Nvmc; use embassy_nrf::pwm::{Prescaler, SimpleConfig, SimplePwm}; +#[cfg(feature = "dfu_ext")] +use embassy_nrf::spim::Spim; use super::*; +#[cfg(feature = "dfu_ext")] +embassy_nrf::bind_interrupts! { + pub(crate) struct ExtFlashIrqs { + TWISPI0 => embassy_nrf::spim::InterruptHandler; + } +} + pub fn run() -> ! { let mut cfg = embassy_nrf::config::Config::default(); cfg.debug = embassy_nrf::config::Debug::NotConfigured; @@ -43,8 +52,30 @@ pub fn run() -> ! { if reset_pin && magic == DBL_MAGIC { unsafe { core::ptr::write_volatile(DBL_MEM, 0) }; - let fm: &'static _ = unsafe { &*(&flash_mutex as *const _) }; - crate::dfu::run_dfu_usb(fm); + + #[cfg(feature = "dfu_ext")] + { + use embassy_nrf::gpio::{Level, Output}; + + let mut spi_cfg = embassy_nrf::spim::Config::default(); + spi_cfg.frequency = embassy_nrf::spim::Frequency::M8; + + let spi = Spim::new(p.TWISPI0, ExtFlashIrqs, p.P0_25, p.P0_24, p.P0_23, spi_cfg); + let cs = Output::new(p.P0_22, Level::High, embassy_nrf::gpio::OutputDrive::Standard); + + let ext_flash = crate::driver::w25q::W25qNorFlash::new(spi, cs, EXT_FLASH_SIZE); + let ext_mutex_data = Mutex::new(RefCell::new(ext_flash)); + let ext_mutex: &'static _ = unsafe { &*core::ptr::addr_of!(ext_mutex_data) }; + let fm: &'static _ = unsafe { &*core::ptr::addr_of!(flash_mutex) }; + + crate::dfu::run_dfu_usb_ext(fm, ext_mutex, EXT_FLASH_SIZE); + } + + #[cfg(not(feature = "dfu_ext"))] + { + let fm: &'static _ = unsafe { &*(&flash_mutex as *const _) }; + crate::dfu::run_dfu_usb(fm); + } } else if reset_pin { unsafe { core::ptr::write_volatile(DBL_MEM, DBL_MAGIC) }; led_pwm::set_raw(false); @@ -55,32 +86,45 @@ pub fn run() -> ! { } // Phase 2 — normal boot flow - let mut config = - BootLoaderConfig::from_linkerfile_blocking(&flash_mutex, &flash_mutex, &flash_mutex); - let active_offset = config.active.offset(); - - let mut state_word = [0u8; WRITE_SIZE]; - config.state.read(0, &mut state_word).unwrap(); - let current_state = State::from(&state_word[..]); - - if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; - let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; - - if !is_swapped { - led_pwm::set_raw(true); - block_for(Duration::from_millis(PRE_SWAP_MS)); - led_pwm::set_raw(false); - } else { - for _ in 0..PRE_REVERT_COUNT { - led_pwm::set_raw(true); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); - led_pwm::set_raw(false); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); - } - } - } else { + #[cfg(not(feature = "dfu_ext"))] + let (active_offset, mut config) = { + let config = BootLoaderConfig::from_linkerfile_blocking( + &flash_mutex, &flash_mutex, &flash_mutex, + ); + (config.active.offset(), config) + }; + + #[cfg(feature = "dfu_ext")] + let (active_offset, mut config) = { + use embassy_nrf::gpio::{Level, Output}; + + let mut spi_cfg = embassy_nrf::spim::Config::default(); + spi_cfg.frequency = embassy_nrf::spim::Frequency::M8; + + // Default SPI pins — change if your board is wired differently + let spi = Spim::new(p.TWISPI0, ExtFlashIrqs, p.P0_25, p.P0_24, p.P0_23, spi_cfg); + let cs = Output::new(p.P0_22, Level::High, embassy_nrf::gpio::OutputDrive::Standard); + + let ext_flash = crate::driver::w25q::W25qNorFlash::new(spi, cs, EXT_FLASH_SIZE); + let ext_mutex_data = Mutex::new(RefCell::new(ext_flash)); + let ext_mutex: &'static Mutex> = + unsafe { &*core::ptr::addr_of!(ext_mutex_data) }; + + let internal_cfg = BootLoaderConfig::from_linkerfile_blocking( + &flash_mutex, &flash_mutex, &flash_mutex, + ); + let active_offset = internal_cfg.active.offset(); + + let config = BootLoaderConfig { + active: internal_cfg.active, + dfu: BlockingPartition::new(ext_mutex, 0, EXT_FLASH_SIZE), + state: internal_cfg.state, + }; + (active_offset, config) + }; + + #[cfg(feature = "noswap")] + { block_for(Duration::from_millis(HB_HALF_MS)); for _ in 0..HB_CYCLES { led_pwm::set_raw(true); @@ -88,40 +132,86 @@ pub fn run() -> ! { led_pwm::set_raw(false); block_for(Duration::from_millis(HB_HALF_MS)); } - } - if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; - let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; - if !is_swapped { - led_pwm::start(SWAP_BREATHE_MS); + let mut syst = unsafe { cortex_m::Peripherals::steal() }.SYST; + syst.disable_interrupt(); + syst.disable_counter(); + led_pwm::deinit(); + + unsafe { + let vector_table = active_offset as *const u32; + cortex_m::asm::bootload(vector_table) } } - let mut page = [0u8; PAGE_SIZE]; - let mut bl = BootLoader::new(config); - let state = bl.prepare_boot(&mut page).unwrap_or(State::Boot); + #[cfg(not(feature = "noswap"))] + { + use embassy_boot::{BootLoader, State}; - led_pwm::stop(); + let mut state_word = [0u8; WRITE_SIZE]; + config.state.read(0, &mut state_word).unwrap(); + let current_state = State::from(&state_word[..]); - if state == State::Swap { - for _ in 0..POST_SWAP_COUNT { - led_pwm::set_raw(true); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); - led_pwm::set_raw(false); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + if current_state == State::Swap { + let page_count = config.active.capacity() / PAGE_SIZE; + let progress = current_progress(&mut config.state); + let is_swapped = progress >= page_count * 2; + + if !is_swapped { + led_pwm::set_raw(true); + block_for(Duration::from_millis(PRE_SWAP_MS)); + led_pwm::set_raw(false); + } else { + for _ in 0..PRE_REVERT_COUNT { + led_pwm::set_raw(true); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + led_pwm::set_raw(false); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + } + } + } else { + block_for(Duration::from_millis(HB_HALF_MS)); + for _ in 0..HB_CYCLES { + led_pwm::set_raw(true); + block_for(Duration::from_millis(HB_HALF_MS)); + led_pwm::set_raw(false); + block_for(Duration::from_millis(HB_HALF_MS)); + } } - } - // Disable SysTick and PWM before handing over to firmware - let mut syst = unsafe { cortex_m::Peripherals::steal() }.SYST; - syst.disable_interrupt(); - syst.disable_counter(); - led_pwm::deinit(); + if current_state == State::Swap { + let page_count = config.active.capacity() / PAGE_SIZE; + let progress = current_progress(&mut config.state); + let is_swapped = progress >= page_count * 2; + if !is_swapped { + led_pwm::start(SWAP_BREATHE_MS); + } + } + + let mut page = [0u8; PAGE_SIZE]; + let mut bl = BootLoader::new(config); + let state = bl.prepare_boot(&mut page).unwrap_or(State::Boot); + + led_pwm::stop(); + + if state == State::Swap { + for _ in 0..POST_SWAP_COUNT { + led_pwm::set_raw(true); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + led_pwm::set_raw(false); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + } + } + + // Disable SysTick and PWM before handing over to firmware + let mut syst = unsafe { cortex_m::Peripherals::steal() }.SYST; + syst.disable_interrupt(); + syst.disable_counter(); + led_pwm::deinit(); - unsafe { - let vector_table = active_offset as *const u32; - cortex_m::asm::bootload(vector_table) + unsafe { + let vector_table = active_offset as *const u32; + cortex_m::asm::bootload(vector_table) + } } } diff --git a/src/rp2040.rs b/src/rp2040.rs index e1d3025..14d959c 100644 --- a/src/rp2040.rs +++ b/src/rp2040.rs @@ -1,4 +1,3 @@ -use embassy_boot_rp::{BootLoader, BootLoaderConfig, State}; use embassy_rp::flash::{Blocking, Flash}; use embassy_rp::pwm::{Config as PwmConfig, Pwm}; @@ -37,32 +36,44 @@ pub fn run() -> ! { let flash = Flash::<_, Blocking, FLASH_SIZE>::new_blocking(p.FLASH); let flash_mutex = Mutex::new(RefCell::new(flash)); - let mut config = - BootLoaderConfig::from_linkerfile_blocking(&flash_mutex, &flash_mutex, &flash_mutex); + #[cfg(not(feature = "dfu_ext"))] + let config = embassy_boot_rp::BootLoaderConfig::from_linkerfile_blocking( + &flash_mutex, &flash_mutex, &flash_mutex, + ); + #[cfg(not(feature = "dfu_ext"))] let active_offset = config.active.offset(); - let mut state_word = [0u8; WRITE_SIZE]; - config.state.read(0, &mut state_word).unwrap(); - let current_state = State::from(&state_word[..]); - - if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; - let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; - - if !is_swapped { - led_pwm::set_raw(true); - block_for(Duration::from_millis(PRE_SWAP_MS)); - led_pwm::set_raw(false); - } else { - for _ in 0..PRE_REVERT_COUNT { - led_pwm::set_raw(true); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); - led_pwm::set_raw(false); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); - } - } - } else { + #[cfg(feature = "dfu_ext")] + let (active_offset, config) = { + use embassy_rp::gpio::{Level, Output}; + use embassy_rp::spi::{Config as SpiConfig, Spi}; + + // Default SPI pins for external flash — change if your board is wired + // differently. + let spi_cfg = SpiConfig::default(); + let spi_bus = Spi::new_blocking(p.SPI0, p.PIN_18, p.PIN_19, p.PIN_16, spi_cfg); + let cs = Output::new(p.PIN_17, Level::High); + let ext_flash = crate::driver::w25q::W25qNorFlash::new(spi_bus, cs, EXT_FLASH_SIZE); + let ext_mutex_data = Mutex::new(RefCell::new(ext_flash)); + let ext_mutex: &'static Mutex> = + unsafe { &*core::ptr::addr_of!(ext_mutex_data) }; + + // Get active + state partitions from linker symbols + let internal_cfg = embassy_boot_rp::BootLoaderConfig::from_linkerfile_blocking( + &flash_mutex, &flash_mutex, &flash_mutex, + ); + let active_offset = internal_cfg.active.offset(); + + let config = embassy_boot::BootLoaderConfig { + active: internal_cfg.active, + dfu: BlockingPartition::new(ext_mutex, 0, EXT_FLASH_SIZE), + state: internal_cfg.state, + }; + (active_offset, config) + }; + + #[cfg(feature = "noswap")] + { block_for(Duration::from_millis(HB_HALF_MS)); for _ in 0..HB_CYCLES { led_pwm::set_raw(true); @@ -70,38 +81,99 @@ pub fn run() -> ! { led_pwm::set_raw(false); block_for(Duration::from_millis(HB_HALF_MS)); } - } - if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; - let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; - if !is_swapped { - led_pwm::start(SWAP_BREATHE_MS); + let mut syst = unsafe { cortex_m::Peripherals::steal() }.SYST; + syst.disable_interrupt(); + syst.disable_counter(); + led_pwm::deinit(); + + unsafe { + let vector_table = + (embassy_rp::flash::FLASH_BASE as u32 + active_offset) as *const u32; + cortex_m::asm::bootload(vector_table) } } - let bl: BootLoader = BootLoader::prepare(config); + #[cfg(not(feature = "noswap"))] + { + #[cfg(not(feature = "dfu_ext"))] + use embassy_boot_rp::{BootLoader, State}; + #[cfg(feature = "dfu_ext")] + use embassy_boot::{BootLoader, State}; + let mut config = config; - led_pwm::stop(); + let mut state_word = [0u8; WRITE_SIZE]; + config.state.read(0, &mut state_word).unwrap(); + let current_state = State::from(&state_word[..]); - if bl.state == State::Swap { - for _ in 0..POST_SWAP_COUNT { - led_pwm::set_raw(true); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); - led_pwm::set_raw(false); - block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + if current_state == State::Swap { + let page_count = config.active.capacity() / PAGE_SIZE; + let progress = current_progress(&mut config.state); + let is_swapped = progress >= page_count * 2; + + if !is_swapped { + led_pwm::set_raw(true); + block_for(Duration::from_millis(PRE_SWAP_MS)); + led_pwm::set_raw(false); + } else { + for _ in 0..PRE_REVERT_COUNT { + led_pwm::set_raw(true); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + led_pwm::set_raw(false); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + } + } + } else { + block_for(Duration::from_millis(HB_HALF_MS)); + for _ in 0..HB_CYCLES { + led_pwm::set_raw(true); + block_for(Duration::from_millis(HB_HALF_MS)); + led_pwm::set_raw(false); + block_for(Duration::from_millis(HB_HALF_MS)); + } } - } - // Disable SysTick and PWM before handing over to firmware - let mut syst = unsafe { cortex_m::Peripherals::steal() }.SYST; - syst.disable_interrupt(); - syst.disable_counter(); - led_pwm::deinit(); + if current_state == State::Swap { + let page_count = config.active.capacity() / PAGE_SIZE; + let progress = current_progress(&mut config.state); + let is_swapped = progress >= page_count * 2; + if !is_swapped { + led_pwm::start(SWAP_BREATHE_MS); + } + } - unsafe { - let vector_table = (embassy_rp::flash::FLASH_BASE as u32 + active_offset) as *const u32; - cortex_m::asm::bootload(vector_table) + #[cfg(not(feature = "dfu_ext"))] + let state = { + let bl: BootLoader = BootLoader::prepare(config); + bl.state + }; + #[cfg(feature = "dfu_ext")] + let state = { + let mut page = [0u8; PAGE_SIZE]; + let mut bl = BootLoader::new(config); + bl.prepare_boot(&mut page).unwrap_or(State::Boot) + }; + + led_pwm::stop(); + + if state == State::Swap { + for _ in 0..POST_SWAP_COUNT { + led_pwm::set_raw(true); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + led_pwm::set_raw(false); + block_for(Duration::from_millis(PRE_REVERT_BLINK_MS)); + } + } + + let mut syst = unsafe { cortex_m::Peripherals::steal() }.SYST; + syst.disable_interrupt(); + syst.disable_counter(); + led_pwm::deinit(); + + unsafe { + let vector_table = + (embassy_rp::flash::FLASH_BASE as u32 + active_offset) as *const u32; + cortex_m::asm::bootload(vector_table) + } } } From 4e8a784b9e09a25817b223bd9249fefbcc5f62ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20J=C3=A4ger?= Date: Sun, 9 Aug 2026 09:19:13 +0200 Subject: [PATCH 3/3] feat: add optional defmt logging over RTT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pascal Jäger --- .cargo/config.toml | 3 ++ Cargo.lock | 131 +++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 33 +++++++++--- build.rs | 37 ++++++++++--- src/dfu.rs | 11 +++- src/driver/w25q.rs | 32 ++++++++--- src/main.rs | 25 +++++++-- src/nrf52840.rs | 29 +++++++--- src/rp2040.rs | 56 ++++++++++++------- 9 files changed, 294 insertions(+), 63 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index d217f9d..42a984c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,6 @@ runner = "probe-rs run --chip RP2040" [target.thumbv7em-none-eabihf] runner = "probe-rs run --chip nRF52840_xxAA" + +[env] +DEFMT_LOG = "trace" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c0e5f7a..6484438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.0" @@ -101,6 +107,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "defmt", + "num-traits", + "pure-rust-locales", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -141,7 +158,7 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -190,6 +207,48 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05144944c117db54127a8ac17a3c9a28d55e7047bfddb950bcd20b4a94c79b10" +dependencies = [ + "critical-section", + "defmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -221,6 +280,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73bfc955c9238df2ce9fecc6f18a64b13ecaa41a3cd42d940a09f304765d10db" dependencies = [ + "defmt", "digest", "document-features", "embassy-embedded-hal", @@ -239,6 +299,7 @@ dependencies = [ "cfg-if", "cortex-m", "cortex-m-rt", + "defmt", "embassy-boot", "embassy-rp", "embassy-sync", @@ -253,6 +314,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0641612053b2f34fc250bb63f6630ae75de46e02ade7f457268447081d709ce" dependencies = [ + "defmt", "embassy-futures", "embassy-hal-internal 0.4.0", "embassy-sync", @@ -294,6 +356,7 @@ checksum = "568659fc53866d3d85c60fa33723fb751aa69e71507634fc2c19e7649432fb75" dependencies = [ "cortex-m", "critical-section", + "defmt", "num-traits", ] @@ -320,11 +383,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b715380574504335c40ec3016bc44598fac3c385f82959b26c41b25c30b7648d" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cfg-if", "cortex-m", "cortex-m-rt", "critical-section", + "defmt", "document-features", "embassy-embedded-hal", "embassy-futures", @@ -353,9 +417,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d98f472894c1ecffac5596c657af5305c57282d29e8746d7fec033951931bc8" dependencies = [ "cfg-if", + "chrono", "cortex-m", "cortex-m-rt", "critical-section", + "defmt", "document-features", "embassy-embedded-hal", "embassy-futures", @@ -392,6 +458,7 @@ checksum = "7bbd85cf5a5ae56bdf26f618364af642d1d0a4e245cdd75cd9aabda382f65a81" dependencies = [ "cfg-if", "critical-section", + "defmt", "embedded-io-async 0.7.0", "futures-core", "futures-sink", @@ -406,6 +473,7 @@ checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a" dependencies = [ "cfg-if", "critical-section", + "defmt", "document-features", "embassy-time-driver", "embedded-hal 0.2.7", @@ -439,7 +507,8 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a25746d8b152b72fbf2a217f489a083dbbe243f281f09184a1f2cfbe9bbb245f" dependencies = [ - "bitflags", + "bitflags 2.13.0", + "defmt", "embassy-futures", "embassy-net-driver-channel", "embassy-sync", @@ -455,8 +524,9 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36d3678d16e1e71b052d00f7518620b5839a878f2bdab2019b81d984fb2a74f" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cortex-m", + "defmt", "embassy-boot", "embassy-futures", "embassy-sync", @@ -471,6 +541,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa675c5f4349b6aa0fcffc4bf9b241f18cd11b97c1f8323273fb9a5449937fbd" dependencies = [ + "defmt", "embedded-io-async 0.6.1", "embedded-io-async 0.7.0", ] @@ -797,7 +868,7 @@ checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -899,7 +970,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -927,7 +998,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -939,6 +1010,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pure-rust-locales" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869675ad2d7541aea90c6d88c81f46a7f4ea9af8cd0395d38f11a95126998a0d" +dependencies = [ + "defmt", +] + [[package]] name = "quote" version = "1.0.45" @@ -966,7 +1046,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] @@ -1010,6 +1090,8 @@ version = "0.1.0" dependencies = [ "cortex-m", "cortex-m-rt", + "defmt", + "defmt-rtt", "embassy-boot", "embassy-boot-rp", "embassy-embedded-hal", @@ -1168,6 +1250,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "term" version = "1.2.1" @@ -1186,6 +1279,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "typenum" version = "1.20.1" @@ -1288,5 +1401,5 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] diff --git a/Cargo.toml b/Cargo.toml index 6a46b15..aa3a22f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,9 @@ edition = "2024" [dependencies] # Shared -embassy-embedded-hal = "0.6" -embassy-sync = "0.8" -embassy-time = "0.5.1" +embassy-embedded-hal = { version = "0.6", default-features = false } +embassy-sync = { version = "0.8", default-features = false } +embassy-time = { version = "0.5", default-features = false } embedded-hal = "1" embedded-storage = "0.3" cortex-m = "0.7.6" @@ -31,9 +31,26 @@ embassy-nrf = { version = "0.10", features = [ "time-driver-rtc1", "unstable-pac", ], optional = true } -embassy-boot = "0.7" +embassy-boot = { version = "0.7" } + +# Logging (optional, opt-in via feature) +defmt = { version = "1", default-features = false, optional = true } +defmt-rtt = { version = "1", optional = true } [features] +defmt = [ + "dep:defmt", + "dep:defmt-rtt", + "embassy-embedded-hal/defmt", + "embassy-sync/defmt", + "embassy-time/defmt", + "embassy-rp?/defmt", + "embassy-boot-rp?/defmt", + "embassy-usb?/defmt", + "embassy-usb-dfu?/defmt", + "embassy-nrf?/defmt", + "embassy-boot/defmt", +] rp2040 = ["embassy-rp", "embassy-boot-rp", "embassy-usb", "embassy-usb-dfu"] rp2040-2mb = ["rp2040"] rp2040-4mb = ["rp2040"] @@ -45,9 +62,9 @@ noswap = [] dfu_ext = [] [profile.release] -opt-level = "z" -lto = true codegen-units = 1 -debug = false +lto = "fat" +opt-level = "z" +# debug = false +debug = 2 panic = "abort" -overflow-checks = false diff --git a/build.rs b/build.rs index 2909728..2f3a8c9 100644 --- a/build.rs +++ b/build.rs @@ -11,6 +11,11 @@ fn main() { let is_nrf52833 = env::var("CARGO_FEATURE_NRF52833").is_ok(); let is_noswap = env::var("CARGO_FEATURE_NOSWAP").is_ok(); let is_dfu_ext = env::var("CARGO_FEATURE_DFU_EXT").is_ok(); + let is_defmt = env::var("CARGO_FEATURE_DEFMT").is_ok(); + + if is_defmt { + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); + } if is_nrf52840 && is_nrf52833 { panic!("nrf52840 and nrf52833 are mutually exclusive"); @@ -41,7 +46,12 @@ fn main() { }; let remaining = flash_size - 28 * 1024 - STORAGE_SIZE; - let (active_size, dfu_size) = if is_noswap || is_dfu_ext { + const SWAP_PAGE_SIZE: usize = 64 * 1024; + let (active_size, dfu_size) = if is_dfu_ext { + // ACTIVE capacity must be a multiple of the swap page size + // (embassy computes PAGE_SIZE = max(ERASE_SIZE) of both flashes) + ((remaining / SWAP_PAGE_SIZE) * SWAP_PAGE_SIZE, 0) + } else if is_noswap { (remaining, 0) } else { ((remaining - PAGE_SIZE) / 2, (remaining - PAGE_SIZE) / 2 + PAGE_SIZE) @@ -106,13 +116,19 @@ __bootloader_dfu_end = ORIGIN(DFU) + LENGTH(DFU) - ORIGIN(BOOT2); 0x1000_0000, // XIP flash base ); let project_root = Path::new(env!("CARGO_MANIFEST_DIR")); - write_rmk_boot_x(&project_root, variant_slug, &rmk_boot_x); + write_rmk_boot_x(&project_root, &variant_slug, &rmk_boot_x); } else if is_nrf52840 { let flash_size = 1024 * 1024; - let bootloader_size = 24 * 1024; + // 28K bootloader + 4K state = 32K: ACTIVE capacity then is an exact + // 64K multiple. With defmt the binary is bigger — 32K boot, and the + // dfu_ext align-down below absorbs the resulting remainder. + let bootloader_size = if is_dfu_ext && !is_defmt { 28 * 1024 } else { 32 * 1024 }; let state_size = 4 * 1024; let remaining = flash_size - bootloader_size - state_size - STORAGE_SIZE; - let (active_size, dfu_size) = if is_noswap || is_dfu_ext { + const SWAP_PAGE_SIZE: usize = 64 * 1024; + let (active_size, dfu_size) = if is_dfu_ext { + ((remaining / SWAP_PAGE_SIZE) * SWAP_PAGE_SIZE, 0) + } else if is_noswap { (remaining, 0) } else { ((remaining - PAGE_SIZE) / 2, (remaining - PAGE_SIZE) / 2 + PAGE_SIZE) @@ -177,11 +193,18 @@ __bootloader_active_end = ORIGIN(ACTIVE) + LENGTH(ACTIVE); ); let project_root = Path::new(env!("CARGO_MANIFEST_DIR")); write_rmk_boot_x(&project_root, &nrf_variant_slug("nrf52840", is_noswap, is_dfu_ext), &rmk_boot_x); - } else if is_nrf52833 { let flash_size = 512 * 1024; - let bootloader_size = 24 * 1024; + } else if is_nrf52833 { + let flash_size = 512 * 1024; + // 28K bootloader + 4K state = 32K: ACTIVE capacity then is an exact + // 64K multiple. With defmt the binary is bigger — 32K boot, and the + // dfu_ext align-down below absorbs the resulting remainder. + let bootloader_size = if is_dfu_ext && !is_defmt { 28 * 1024 } else { 32 * 1024 }; let state_size = 4 * 1024; let remaining = flash_size - bootloader_size - state_size - STORAGE_SIZE; - let (active_size, dfu_size) = if is_noswap || is_dfu_ext { + const SWAP_PAGE_SIZE: usize = 64 * 1024; + let (active_size, dfu_size) = if is_dfu_ext { + ((remaining / SWAP_PAGE_SIZE) * SWAP_PAGE_SIZE, 0) + } else if is_noswap { (remaining, 0) } else { ((remaining - PAGE_SIZE) / 2, (remaining - PAGE_SIZE) / 2 + PAGE_SIZE) diff --git a/src/dfu.rs b/src/dfu.rs index 5542c84..a11415f 100644 --- a/src/dfu.rs +++ b/src/dfu.rs @@ -6,7 +6,9 @@ use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::blocking_mutex::Mutex; use embassy_nrf::nvmc::Nvmc; +#[cfg(feature = "dfu_ext")] use embassy_embedded_hal::flash::partition::BlockingPartition; +#[cfg(feature = "dfu_ext")] use embedded_storage::nor_flash::NorFlash; // --------------------------------------------------------------------------- @@ -85,14 +87,16 @@ pub fn run_dfu_usb( use embassy_usb::class::dfu::consts::DfuAttributes; use embassy_usb_dfu::{self as dfu, ResetImmediate}; - let p = unsafe { embassy_nrf::Peripherals::steal() }; +let p = unsafe { embassy_nrf::Peripherals::steal() }; + + #[cfg(feature = "defmt")] + defmt::info!("USB DFU active (internal DFU partition)"); // ── USB driver (nRF USBD peripheral + VBUS detection) ── let vbus_obj = embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(true, true); let vbus: &'static _ = unsafe { &*(&vbus_obj as *const _) }; let driver = embassy_nrf::usb::Driver::new(p.USBD, DfuIrqs, vbus); - // ── USB device descriptor ── let mut usb_config = embassy_usb::Config::new(0x1209, 0x0001); usb_config.manufacturer = Some("rmk-boot"); usb_config.product = Some("nRF52840 DFU"); @@ -153,6 +157,9 @@ pub fn run_dfu_usb_ext( let p = unsafe { embassy_nrf::Peripherals::steal() }; + #[cfg(feature = "defmt")] + defmt::info!("USB DFU active (external SPI flash, size 0x{:x})", ext_flash_size); + let vbus_obj = embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(true, true); let vbus: &'static _ = unsafe { &*(&vbus_obj as *const _) }; let driver = embassy_nrf::usb::Driver::new(p.USBD, crate::dfu::DfuIrqs, vbus); diff --git a/src/driver/w25q.rs b/src/driver/w25q.rs index fcf3c81..083805d 100644 --- a/src/driver/w25q.rs +++ b/src/driver/w25q.rs @@ -13,16 +13,25 @@ const PAGE_SIZE: u32 = 256; const SECTOR_SIZE: u32 = 4096; const BLOCK64_SIZE: u32 = 65536; +/// Swap-page granularity exposed to the boot flow: the erase unit is the 64K +/// block, so embassy-boot's swap page size becomes 64K (matching the internal +/// ACTIVE partition, whose capacity is 64K-aligned in the dfu_ext layout). +pub const SWAP_PAGE_SIZE: usize = BLOCK64_SIZE as usize; + /// [`NorFlash`] implementation for W25Q and compatible 25-series SPI NOR flash /// chips. Uses standard JEDEC commands valid across Winbond, Macronix, ISSI, /// and similar families. -pub struct W25qNorFlash { +/// +/// `ERASE_UNIT` is the erase granularity exposed to the boot flow: 64K block +/// erase, matching the 64K-multiple ACTIVE partition sizing on both RP2040 +/// and nRF52 with `dfu_ext` (4K when not using `dfu_ext`). +pub struct W25qNorFlash { bus: BUS, cs: CS, flash_size: u32, } -impl W25qNorFlash { +impl W25qNorFlash { pub fn new(bus: BUS, mut cs: CS, flash_size: u32) -> Self { cs.set_high().ok(); Self { bus, cs, flash_size } @@ -119,11 +128,15 @@ impl NorFlashError for W25qError { } } -impl ErrorType for W25qNorFlash { +impl ErrorType + for W25qNorFlash +{ type Error = W25qError; } -impl ReadNorFlash for W25qNorFlash { +impl ReadNorFlash + for W25qNorFlash +{ const READ_SIZE: usize = 1; fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { @@ -135,9 +148,11 @@ impl ReadNorFlash for W25qNorFlash { } } -impl NorFlash for W25qNorFlash { +impl NorFlash + for W25qNorFlash +{ const WRITE_SIZE: usize = 1; - const ERASE_SIZE: usize = SECTOR_SIZE as usize; + const ERASE_SIZE: usize = ERASE_UNIT; fn erase(&mut self, mut from: u32, to: u32) -> Result<(), Self::Error> { while from < to { @@ -167,4 +182,7 @@ impl NorFlash for W25qNorFlash { } } -impl MultiwriteNorFlash for W25qNorFlash {} +impl MultiwriteNorFlash + for W25qNorFlash +{ +} diff --git a/src/main.rs b/src/main.rs index 136d3d5..d0068c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,10 @@ #![no_std] #![no_main] +// Logging over RTT (opt-in via the `defmt` feature) +#[cfg(feature = "defmt")] +use defmt_rtt as _; + // --------------------------------------------------------------------------- // Feature-exclusion checks // --------------------------------------------------------------------------- @@ -60,7 +64,7 @@ use core::cell::RefCell; use cortex_m_rt::entry; use embassy_embedded_hal::flash::partition::BlockingPartition; use embassy_sync::blocking_mutex::Mutex; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_time::{block_for, Duration}; use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; @@ -101,6 +105,14 @@ const DTAP_SIGNAL_MS: u64 = 500; #[cfg(feature = "dfu_ext")] const EXT_FLASH_SIZE: u32 = 8 * 1024 * 1024; +/// Swap page size: with an external DFU flash the erase unit is the 64K W25Q +/// block (and ACTIVE is sized to a multiple of it); otherwise 4K pages. +#[cfg(feature = "dfu_ext")] +const SWAP_PAGE_SIZE: usize = 64 * 1024; + +#[cfg(not(feature = "dfu_ext"))] +const SWAP_PAGE_SIZE: usize = PAGE_SIZE; + // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- @@ -116,6 +128,8 @@ fn platform_run() -> ! { #[entry] fn main() -> ! { + #[cfg(feature = "defmt")] + defmt::info!("rmk-boot {} starting", env!("CARGO_PKG_VERSION")); platform_run() } @@ -130,8 +144,8 @@ fn SysTick() { // --------------------------------------------------------------------------- // Shared helper: progress reader // --------------------------------------------------------------------------- -fn current_progress( - state: &mut BlockingPartition<'_, NoopRawMutex, STATE>, +fn current_progress( + state: &mut BlockingPartition<'_, M, STATE>, ) -> usize { let mut validity = [0u8; WRITE_SIZE]; state.read(WRITE_SIZE as u32, &mut validity).unwrap(); @@ -153,8 +167,11 @@ fn current_progress( // --------------------------------------------------------------------------- // Panic handler – SOS via PWM // --------------------------------------------------------------------------- +#[cfg_attr(not(feature = "defmt"), allow(unused_variables))] #[panic_handler] -fn panic(_info: &core::panic::PanicInfo) -> ! { +fn panic(info: &core::panic::PanicInfo) -> ! { + #[cfg(feature = "defmt")] + defmt::error!("PANIC: {}", defmt::Display2Format(info)); loop { for _ in 0..3 { led_pwm::set_raw(true); diff --git a/src/nrf52840.rs b/src/nrf52840.rs index b80086c..12e26a3 100644 --- a/src/nrf52840.rs +++ b/src/nrf52840.rs @@ -3,6 +3,8 @@ use embassy_nrf::nvmc::Nvmc; use embassy_nrf::pwm::{Prescaler, SimpleConfig, SimplePwm}; #[cfg(feature = "dfu_ext")] use embassy_nrf::spim::Spim; +#[cfg(feature = "dfu_ext")] +use embassy_sync::blocking_mutex::raw::NoopRawMutex; use super::*; @@ -51,6 +53,8 @@ pub fn run() -> ! { let magic = unsafe { core::ptr::read_volatile(DBL_MEM) }; if reset_pin && magic == DBL_MAGIC { + #[cfg(feature = "defmt")] + defmt::info!("double tap detected: entering USB DFU mode"); unsafe { core::ptr::write_volatile(DBL_MEM, 0) }; #[cfg(feature = "dfu_ext")] @@ -58,12 +62,14 @@ pub fn run() -> ! { use embassy_nrf::gpio::{Level, Output}; let mut spi_cfg = embassy_nrf::spim::Config::default(); - spi_cfg.frequency = embassy_nrf::spim::Frequency::M8; + spi_cfg.frequency = embassy_nrf::spim::Frequency::M32; let spi = Spim::new(p.TWISPI0, ExtFlashIrqs, p.P0_25, p.P0_24, p.P0_23, spi_cfg); let cs = Output::new(p.P0_22, Level::High, embassy_nrf::gpio::OutputDrive::Standard); - let ext_flash = crate::driver::w25q::W25qNorFlash::new(spi, cs, EXT_FLASH_SIZE); + let ext_flash = crate::driver::w25q::W25qNorFlash::<_, _, { 64 * 1024 }>::new( + spi, cs, EXT_FLASH_SIZE, + ); let ext_mutex_data = Mutex::new(RefCell::new(ext_flash)); let ext_mutex: &'static _ = unsafe { &*core::ptr::addr_of!(ext_mutex_data) }; let fm: &'static _ = unsafe { &*core::ptr::addr_of!(flash_mutex) }; @@ -99,13 +105,15 @@ pub fn run() -> ! { use embassy_nrf::gpio::{Level, Output}; let mut spi_cfg = embassy_nrf::spim::Config::default(); - spi_cfg.frequency = embassy_nrf::spim::Frequency::M8; + spi_cfg.frequency = embassy_nrf::spim::Frequency::M32; // Default SPI pins — change if your board is wired differently let spi = Spim::new(p.TWISPI0, ExtFlashIrqs, p.P0_25, p.P0_24, p.P0_23, spi_cfg); let cs = Output::new(p.P0_22, Level::High, embassy_nrf::gpio::OutputDrive::Standard); - let ext_flash = crate::driver::w25q::W25qNorFlash::new(spi, cs, EXT_FLASH_SIZE); + let ext_flash = crate::driver::w25q::W25qNorFlash::<_, _, { 64 * 1024 }>::new( + spi, cs, EXT_FLASH_SIZE, + ); let ext_mutex_data = Mutex::new(RefCell::new(ext_flash)); let ext_mutex: &'static Mutex> = unsafe { &*core::ptr::addr_of!(ext_mutex_data) }; @@ -123,8 +131,13 @@ pub fn run() -> ! { (active_offset, config) }; + #[cfg(feature = "defmt")] + defmt::info!("nrf52: active=0x{:08x}, dfu mode = {}", active_offset, cfg!(feature = "dfu_ext")); + #[cfg(feature = "noswap")] { + #[cfg(feature = "defmt")] + defmt::info!("noswap: booting ACTIVE directly"); block_for(Duration::from_millis(HB_HALF_MS)); for _ in 0..HB_CYCLES { led_pwm::set_raw(true); @@ -153,9 +166,10 @@ pub fn run() -> ! { let current_state = State::from(&state_word[..]); if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; + #[cfg(feature = "defmt")] + defmt::info!("swap in progress"); let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; + let is_swapped = progress >= (config.active.capacity() / SWAP_PAGE_SIZE) * 2; if !is_swapped { led_pwm::set_raw(true); @@ -180,9 +194,8 @@ pub fn run() -> ! { } if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; + let is_swapped = progress >= (config.active.capacity() / SWAP_PAGE_SIZE) * 2; if !is_swapped { led_pwm::start(SWAP_BREATHE_MS); } diff --git a/src/rp2040.rs b/src/rp2040.rs index 14d959c..df87e97 100644 --- a/src/rp2040.rs +++ b/src/rp2040.rs @@ -2,6 +2,8 @@ use embassy_rp::flash::{Blocking, Flash}; use embassy_rp::pwm::{Config as PwmConfig, Pwm}; use super::*; +#[cfg(feature = "dfu_ext")] +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; #[cfg(feature = "rp2040-2mb")] const FLASH_SIZE: usize = 2 * 1024 * 1024; @@ -23,7 +25,7 @@ pub fn run() -> ! { // For different LED pin change p.PIN_25 and PWM_SLICE0 below // PIN must match the PWMSLICE and new_output_x, for PWM slice to PIN mapping see: // https://rp2040.implrust.com/pwm/pwm-in-rp2040.html#mapping-of-pwm-channels-to-gpio-pins - // e.g. led_pwm::init(Pwm::new_output_a(p.PWM_SLICE0, p.PIN_16, cfg)); + // led_pwm::init(Pwm::new_output_a(p.PWM_SLICE0, p.PIN_16, cfg)); led_pwm::init(Pwm::new_output_b(p.PWM_SLICE4, p.PIN_25, cfg)); // ── SysTick ── @@ -44,36 +46,54 @@ pub fn run() -> ! { let active_offset = config.active.offset(); #[cfg(feature = "dfu_ext")] - let (active_offset, config) = { + let ext_mutex: Mutex> = { use embassy_rp::gpio::{Level, Output}; use embassy_rp::spi::{Config as SpiConfig, Spi}; // Default SPI pins for external flash — change if your board is wired // differently. - let spi_cfg = SpiConfig::default(); - let spi_bus = Spi::new_blocking(p.SPI0, p.PIN_18, p.PIN_19, p.PIN_16, spi_cfg); +let mut spi_cfg = SpiConfig::default(); + spi_cfg.frequency = 32_000_000; + let spi_bus = Spi::new_blocking(p.SPI0, p.PIN_18, p.PIN_19, p.PIN_20, spi_cfg); let cs = Output::new(p.PIN_17, Level::High); - let ext_flash = crate::driver::w25q::W25qNorFlash::new(spi_bus, cs, EXT_FLASH_SIZE); - let ext_mutex_data = Mutex::new(RefCell::new(ext_flash)); - let ext_mutex: &'static Mutex> = - unsafe { &*core::ptr::addr_of!(ext_mutex_data) }; - + let ext_flash = crate::driver::w25q::W25qNorFlash::<_, _, { 64 * 1024 }>::new( + spi_bus, cs, EXT_FLASH_SIZE, + ); + Mutex::new(RefCell::new(ext_flash)) + }; + #[cfg(feature = "dfu_ext")] + let config = { // Get active + state partitions from linker symbols let internal_cfg = embassy_boot_rp::BootLoaderConfig::from_linkerfile_blocking( &flash_mutex, &flash_mutex, &flash_mutex, ); - let active_offset = internal_cfg.active.offset(); - let config = embassy_boot::BootLoaderConfig { + embassy_boot::BootLoaderConfig { active: internal_cfg.active, - dfu: BlockingPartition::new(ext_mutex, 0, EXT_FLASH_SIZE), + dfu: BlockingPartition::new(&ext_mutex, 0, EXT_FLASH_SIZE), state: internal_cfg.state, - }; - (active_offset, config) + } }; + #[cfg(feature = "dfu_ext")] + let active_offset = config.active.offset(); + #[cfg(all(feature = "dfu_ext", feature = "defmt"))] + defmt::info!( + "dfu_ext: active=0x{:08x}, external flags=0x{:x}", + active_offset, + EXT_FLASH_SIZE + ); + + #[cfg(feature = "defmt")] + defmt::info!( + "rp2040: active=0x{:08x}, state=0x{:08x}", + active_offset, + embassy_rp::flash::FLASH_BASE as u32 + config.state.offset(), + ); #[cfg(feature = "noswap")] { + #[cfg(feature = "defmt")] + defmt::info!("noswap: booting ACTIVE directly"); block_for(Duration::from_millis(HB_HALF_MS)); for _ in 0..HB_CYCLES { led_pwm::set_raw(true); @@ -107,9 +127,10 @@ pub fn run() -> ! { let current_state = State::from(&state_word[..]); if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; + #[cfg(feature = "defmt")] + defmt::info!("swap in progress"); let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; + let is_swapped = progress >= (config.active.capacity() / SWAP_PAGE_SIZE) * 2; if !is_swapped { led_pwm::set_raw(true); @@ -134,9 +155,8 @@ pub fn run() -> ! { } if current_state == State::Swap { - let page_count = config.active.capacity() / PAGE_SIZE; let progress = current_progress(&mut config.state); - let is_swapped = progress >= page_count * 2; + let is_swapped = progress >= (config.active.capacity() / SWAP_PAGE_SIZE) * 2; if !is_swapped { led_pwm::start(SWAP_BREATHE_MS); }