Bootloader Configuration (embassy-boot)

embassy-boot is a library of the embassy framework that is used to build bootloaders. RMK supports DFU firmware updates via embassy-boot for RP2040 and nRF52840 / nRF52833. An embassy-boot based bootloader splits flash into ACTIVE and DFU slots, providing safe updates with automatic rollback on failure. This is an optional feature of RMK, the default bootloaders of the devices can still be used as usual without runtime updates via USB DFU.

Experimental

DFU is experimental. Enabling it repartitions your flash, and the partition layout, the [dfu] options on this page, and the Rust API can change in any release. A firmware built for one partition layout cannot be flashed over a bootloader built for another, so treat a board with DFU enabled as one you may have to re-flash with a probe.

An embassy-boot based bootloader for both platforms lives in rmk-boot. RMK integrates with it through a memory.x file that the bootloader generates: build rmk-boot for your chip and it writes its matching rmk-memory.x into the rmk-boot project directory. Rename that file to memory.x and place it next to your project's Cargo.toml, so the firmware and the bootloader agree on the partition layout.

At runtime RMK reads partition offsets from linker symbols embedded in memory.x — you never compute or hardcode partition addresses yourself.

See the flashing guide for step-by-step instructions on getting the bootloader and RMK flashed.

RP2040

Add a [dfu] section to your keyboard.toml or use the Rust API directly.

Toml
Rust
keyboard.toml
[dfu]
# (Optional) DFU activity LED pin, default "PIN_25".
led = "PIN_25"
# led = "none" to omit DFU LED

# (Optional) Unlock keys for dfu_lock (physical matrix positions).
# Only works with dfu_lock feature enabled in Cargo.toml.
unlock_keys = [[0, 0], [1, 1]]

nRF52840 / nRF52833

Add a [dfu] section to your keyboard.toml or use the Rust API directly.

Toml
Rust
keyboard.toml
[dfu]
# (Optional) Flash page size in bytes (4096 for nRF52840 / nRF52833).
page_size = 4096

# (Optional) DFU activity LED pin, default "P0_15".
led = "P0_15"
# led = "none" to omit DFU LED

# (Optional) Unlock keys for dfu_lock (physical matrix positions).
# Only works with dfu_lock feature enabled in Cargo.toml.
unlock_keys = [[0, 0], [1, 1]]
mark_booted is automatic

When using FlashDfuHandler (in run_all!) or run_rmk_split_peripheral(), mark_booted() is called automatically — no manual call needed. You only need rmk::dfu::mark_booted(&mut state_partition) when using a embassy-boot based bootloader to flash the firmware that still provides safe firmware updates and rollback. E.g. rmk-boot's DFU flash feature without it's noswap feature and you don't use DFU flashing/ FlashDfuHandler in RMK itself.

Partition layout

The bootloader divides flash into regions. All offsets and sizes come from linker symbols in the memory.x file from rmk-boot. The default layout with rmk-boot (2MB RP2040, 32K storage) is:

RegionOffsetSize
Bootloader(s)0x000000024 KB
Boot state0x60004 KB
Active firmware0x7000(flash_size - 28K (Bootloader) - 32K (Storage) - 4K (1 Page)) / 2
DFU downloadfollows activeactive_size + 4K (1 Page)
Storagefollows DFU32 KB (8 sectors × 4K)

The DFU partition size follows embassy-boot guidelines, the additional page is used for status information during flashing.

The [dfu] section is optional and configures only DFU behaviour (LED, unlock keys). Partition offsets are read at link time from memory.x — you do not set state_offset, dfu_offset, or flash_size in keyboard.toml.

Custom bootloader

If you built your own embassy-boot bootloader, add these eight symbols with matching values to your memory.x (all values are flash-relative offsets):

__bootloader_state_start   = 0x6000;   /* boot state start */
__bootloader_state_end     = 0x7000;   /* boot state end */
__bootloader_active_start  = 0x7000;   /* active (booted) slot start */
__bootloader_active_end    = 0xF3000;  /* active (booted) slot end */
__bootloader_dfu_start     = 0xF3000;  /* DFU download slot start */
__bootloader_dfu_end       = 0x1E0000; /* DFU download slot end */
__bootloader_storage_start = 0x1E0000; /* storage partition start */
__bootloader_storage_end   = 0x200000; /* storage partition end */

Make sure FLASH : ORIGIN in your MEMORY block starts at your ACTIVE partition. RMK's partitions_from_linkerscript() picks up the symbols at runtime.

DFU LED (optional)

A GPIO pin for the DFU LED.

PlatformDefault LED pinExample configuration
RP2040PIN_25led = "PIN_25"
nRF52840 / nRF52833P0_15led = "P0_15"

See the LED behavior table for the full state machine.

DFU lock (optional, feature dfu_lock)

Physical key positions that unlock DFU firmware downloads. Requires the dfu_lock Cargo feature. Keys are identified by matrix position (row, col), not by keycode.

See the DFU lock section for the unlock workflow.

Tip

Choose keys that are easy to press simultaneously but not commonly pressed together accidentally.

External Flash DFU (optional, feature dfu_ext)

When your firmware grows too large for the internal DFU/ACTIVE partition split, you can move the DFU download slot to an external SPI NOR flash chip. With dfu_ext active, the internal DFU partition is removed entirely — the ACTIVE region expands to fill the freed space, giving you roughly double the space for your firmware. The DFU image is written to and read from the external chip.

Requirements:

  • A 25-series SPI NOR flash chip with JEDEC‑standard commands (Winbond W25Q64JV, Macronix MX25R, ISSI IS25LP, etc.)
  • A matching dfu_ext build of rmk-boot for your chip
  • The dfu_ext Cargo feature in your RMK firmware

Partition layout with dfu_ext

All internal flash regions stay at fixed offsets. The DFU download slot moves entirely to the external chip.

RegionInternal flash offsetExternal flash offset
Bootloader0x0000000—
Boot state0x6000—
Active firmware0x7000—
DFU download—0x000000
Storagefollows active—

The active size is flash_size - bootloader - state - storage (no DFU partition is carved out of the internal flash).

Custom bootloader: 64K alignment

If you build your own bootloader, the ACTIVE partition and the external DFU partition must each be a multiple of 64 KB when using the built-in W25Q driver. The erase size is set to 64 KB to make the swap faster, so partitions that are not 64K-aligned will fail to swap correctly.

The default rmk-boot build already aligns ACTIVE this way — its build.rs rounds the ACTIVE size down to a 64 KB multiple (960 KB on a 1 MB nRF52840, 448 KB on a 512 KB nRF52833).

Default SPI pins

The bootloader hard‑codes default SPI pins. Match them in your keyboard.toml (or change the bootloader source for a custom board):

PlatformSPI instanceSCKMOSIMISOCS
RP2040SPI0PIN_18PIN_19PIN_16PIN_17
nRF52840 / nRF52833TWISPI0P0_17P0_22P0_20P0_24
Warning

The SPI pins in keyboard.toml and in the bootloader source must agree. If you change the wiring, update both places.

Configuration

Toml
Rust
keyboard.toml
[dfu.external_flash]
# Flash chip driver: "w25q" (built-in) or "custom"
driver = "w25q"
# Total flash size in bytes (8 MB = 8388608)
flash_size = 8388608

[dfu.external_flash.spi]
instance = "SPI0"
sck = "PIN_18"
mosi = "PIN_19"
miso = "PIN_16"
cs = "PIN_17"
tx_dma = "DMA_CH3"   # required for async SPI on RP2040
rx_dma = "DMA_CH4"   # required for async SPI on RP2040

Enable the feature in Cargo.toml:

Cargo.toml
[dependencies.rmk]
features = ["rp2040", "dfu_rp", "dfu_ext"]
Split keyboards: per-side DFU config

On a split keyboard, each side can have its own [dfu] section — [split.central.dfu] / [split.peripheral.dfu] — which completely replaces the global [dfu] for that side. This lets the central use an external flash DFU partition while the peripheral keeps an internal one (and vice versa), or use different SPI pins per side.

This requires two different bootloader builds and two different memory.x files — one per side: flash the central with a dfu_ext bootloader build and the peripheral with a regular one, and give each binary its matching linker script. The build.rs of the rp2040_dfu_split_dfu_ext examples (use_config and use_rust) shows how to select the right memory.x per binary (memory-central.x / memory-peripheral.x). See Split keyboard DFU configuration.

driver = "custom"

If you use a different flash chip or want to provide your own driver, set driver = "custom" and point init_fn to a function in your crate:

keyboard.toml
[dfu.external_flash]
driver = "custom"
init_fn = "crate::my_flash::init"
flash_size = 8388608

[dfu.external_flash.spi]
instance = "SPI0"
sck = "PIN_18"
mosi = "PIN_19"
miso = "PIN_16"
cs = "PIN_17"
tx_dma = "DMA_CH3"   # required for async SPI on RP2040
rx_dma = "DMA_CH4"   # required for async SPI on RP2040

The function must have the signature:

fn init(spi: impl SpiBus, cs: impl OutputPin, flash_size: u32) -> impl NorFlash

RMK only requires NorFlash from your driver — the DFU download partition and its erase/size handling are built on top by RMK, so you don't need to slice the chip yourself.

Building the bootloader

Build rmk-boot with the dfu_ext feature and your board's flash size:

# RP2040 4 MB
cargo build --release --target thumbv6m-none-eabi --features rp2040-4mb,dfu_ext

# nRF52840
cargo build --release --target thumbv7em-none-eabihf --features nrf52840,dfu_ext

# nRF52833
cargo build --release --target thumbv7em-none-eabihf --features nrf52833,dfu_ext

If you need different SPI pins, edit src/rp2040.rs or src/nrf52840.rs of rmk-boot and change the pin assignments before building. The EXT_FLASH_SIZE constant (default 8 MB) lives in src/main.rs.

No-swap bootloader (rmk-boot noswap)

If your firmware no longer fits into the ACTIVE partition but you don't need USB DFU updates, build rmk-boot with the noswap feature. It removes the DFU download partition entirely and grows ACTIVE to fill the whole remaining flash:

BoardACTIVE with swapACTIVE with noswap
RP2040 2 MB~992 KB~1.9 MB
nRF52840~476 KB~956 KB
nRF52833~220 KB~445 KB

The bootloader then becomes a minimal "jump to ACTIVE" loader: no swap, no automatic rollback, and no USB DFU interface in RMK. (But for nRF528xx rmk-boot itself has a USB DFU mode in the bootloader)

Trade-offs:

  • Firmware updates via DFU are not possible in RMK's runtime. (but the board can still be flashed via RP2040's built in UF2 bootloader or via USB DFU mode of rmk-boot on nRF528xx)
  • No rollback on a failed boot

Build:

cargo make uf2-rp2040-2mb-noswap    # → rmk-boot-rp2040-2mb-noswap.uf2  (or uf2-rp2040-4mb-noswap / uf2-rp2040-8mb-noswap / uf2-rp2040-16mb-noswap)
cargo make uf2-nrf52840-noswap      # → rmk-boot-nrf52840-noswap.uf2
cargo make uf2-nrf52833-noswap      # → rmk-boot-nrf52833-noswap.uf2

Rename the generated rmk-memory.x to memory.x as usual and build your RMK firmware without the DFU features (dfu_rp / dfu_nrf) — no [dfu] section in keyboard.toml is needed. Also activate the set-vtor feature of cortex-m-rt in your projects Cargo.toml.

cortex-m-rt = { version = "0.7.5", features = ["set-vtor"] }
Note

noswap is mutually exclusive with dfu_ext.