Use embassy-boot bootloader

Experimental

DFU is experimental. Flashing a bootloader repartitions your flash, and on the nRF52840 it overwrites the Adafruit UF2 bootloader. The partition layout can change between RMK releases, so only follow this guide on a board you can recover with a debug probe.

embassy-boot is a bootloader framework from the Embassy project. It splits the flash into two firmware slots (active and DFU download), so updating is safe even if power is lost or the transfer fails — the old firmware remains intact and the bootloader can roll back automatically. You can build your own bootloader, or use a ready-made one like rmk-boot.

RMK supports DFU for RP2040 (via dfu_rp) and nRF52840 / nRF52833 (via dfu_nrf).

rmk-boot is a ready-made embassy-boot bootloader for both platforms. It sits at the beginning of flash and handles the dual-slot firmware switching and rollback on boot. RMK itself provides the DFU USB interface (via embassy-usb-dfu) for subsequent updates, so once rmk-boot and RMK are flashed, you never need to press BOOTSEL again.

Building rmk-boot produces a rmk-memory.x file next to its firmware binary. This file is your project's memory.x — it contains the correct flash layout for the ACTIVE partition. Rename it to memory.x and place it next to your Cargo.toml (replacing any existing memory.x). Your project's build.rs already copies memory.x into the linker's search path.

Example for RP2040 with 2MB flash:

rmk-memory.x → rename to memory.x
/* rmk-memory.x for RP2040 2 MB — generated by rmk-boot
 *
 * Provides both the MEMORY layout (absolute XIP addresses) and
 * flash-relative DFU symbols consumed by partitions_from_linkerscript().
 */

MEMORY {
  FLASH : ORIGIN = 0x10007000, LENGTH = 1015808   /* ACTIVE region */
  RAM   : ORIGIN = 0x20000000, LENGTH = 256K
}

/* embassy-boot partition symbols — offsets relative to flash start */
__bootloader_state_start   = 0x6000;
__bootloader_state_end     = 0x7000;
__bootloader_active_start  = 0x7000;
__bootloader_active_end    = 0xFF000;
__bootloader_dfu_start     = 0xFF000;
__bootloader_dfu_end       = 0x1F8000;
__bootloader_storage_start = 0x1F8000;
__bootloader_storage_end   = 0x200000;

At runtime, RMK reads the DFU partition offsets from the linker symbols embedded in the file.

// All partition offsets come from the DFU symbols in memory.x
use core::cell::RefCell;
use rmk::dfu::{FlashMutex, partitions_from_linkerscript};

let flash_mutex = FlashMutex::new(RefCell::new(Flash::new_blocking(p.FLASH)));
let (storage_partition, mut state_partition, dfu_partition) = partitions_from_linkerscript(&flash_mutex);

The #[rmk_keyboard] macro generates this call automatically when RMK is built with the dfu_rp or dfu_nrf feature. The macro reads the [dfu] section for LED and unlock-key options; the chip defaults for RP2040, nRF52840, nice!nano and Pico W already ship an empty [dfu], so you only add one to override those options.

The storage size defaults to 32 KB (8 sectors × 4K pages). To change it, edit STORAGE_SIZE in rmk-boot's build.rs, rebuild, adjust num_sectors in your [storage] section accordingly and copy the newly generated rmk-memory.x into your project.

Using a custom bootloader

If you built your own embassy-boot bootloader with a different flash layout, add these eight symbols to your existing 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. All symbols must use flash-relative offsets (byte offsets from the start of the flash chip, not absolute XIP addresses). For RP2040, subtract the flash base (0x10000000). For nRF52, flash-relative and absolute offsets are the same (flash starts at 0x00000000).

External Flash DFU

If your firmware is too large for the internal DFU/ACTIVE partition split, you can offload the DFU download slot to an external SPI NOR flash chip. This frees the internal DFU space and roughly doubles the ACTIVE region. See External Flash DFU for setup instructions.

No DFU updates needed?

If your firmware doesn't fit into the ACTIVE partition and you don't need USB DFU updates, use an rmk-boot noswap build — it grows ACTIVE to the full flash. See No-swap bootloader.

Prerequisites

rmk-boot + rmk-memory.x

You need the rmk-boot bootloader built for the correct flash size for your board, plus the rmk-memory.x that build generates. There are no pre-built downloads yet, so clone rmk-boot and build the variant you need:

rustup target add thumbv6m-none-eabi     # RP2040
rustup target add thumbv7em-none-eabihf  # nRF52840 / nRF52833

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

Each build also writes rmk-memory.x into the rmk-boot project root. Rename it to memory.x and place it next to your Cargo.toml.

RP2040 variants:

  • uf2-rp2040-2mb → rmk-boot-rp2040-2mb.uf2 – for 2 MB flash
  • uf2-rp2040-4mb → rmk-boot-rp2040-4mb.uf2 – for 4 MB flash
  • uf2-rp2040-8mb → rmk-boot-rp2040-8mb.uf2 – for 8 MB flash
  • uf2-rp2040-16mb → rmk-boot-rp2040-16mb.uf2 – for 16 MB flash
Tip

The 2 MB version works with larger flash chips as well (e.g. 4 MB or 8 MB), using only the first 2 MB. On 2 MB you get ~992K of ACTIVE space, which is ample for most RMK firmwares. If you need more room (e.g. with large keymaps, displays, RGB, or many features), pick the matching flash size variant.

nRF52840 / nRF52833: cargo make uf2-nrf52840 builds the 1 MB variant, cargo make uf2-nrf52833 the 512 KB variant. To flash with a probe instead, run cargo run --release --target thumbv7em-none-eabihf --features nrf52840 (or --features nrf52833) from the rmk-boot directory.

Warning

When using the nRF52840 with the Adafruit UF2 bootloader, flashing rmk-boot overwrites the Adafruit bootloader (even when flashing via UF2). Subsequent UF2 uploads won't work.

Compile RMK with DFU support

RMK must be compiled with the dfu feature and the platform-specific feature:

  • RP2040: dfu_rp
  • nRF52840 / nRF52833: dfu_nrf — optionally with nrf52840_ble for simultaneous BLE support

If configuring manually, ensure:

  • Cargo.toml uses the dfu_rp (RP2040) or dfu_nrf (nRF52840 / nRF52833) feature
  • memory.x from rmk-boot is present next to your Cargo.toml
  • a [dfu] section is present, either in keyboard.toml or in the chip default — RP2040, nRF52840, nice!nano and Pico W ship an empty one, so on those chips you only add [dfu] to override its options

The [dfu] section needs only behavioural options — partition offsets come from the linker symbols in memory.x:

[dfu]
led = "PIN_25"          # optional DFU activity LED
unlock_keys = [[0, 0], [1, 1]]  # optional DFU lock key combo

All partition offsets are read at link time via partitions_from_linkerscript().

Important: The flash size variant of your memory.x must match the rmk-boot binary flashed to your board.

Tip

You can still flash firmware via UF2 (when using RP2040, on nRF52840 the UF2 bootloader was overwritten by rmk-boot) or probe-rs — as long as the firmware is built with the correct memory.x (from rmk-boot), the linker places it at the ACTIVE partition offset and leaves rmk-boot untouched.

If you flash RMK firmware built with a normal memory.x (FLASH ORIGIN = 0x0), it will overwrite rmk-boot.

Tip

On RP2040 the UF2 bootloader lives in ROM and can't be overwritten. On nRF52840 with the Adafruit UF2 bootloader, flashing rmk-boot via UF2 overwrites it — use DFU or probe-rs for subsequent uploads.

Tip

On nRF52840 / nRF52833, rmk-boot implements both entry paths known from the Adafruit UF2 bootloader:

  • Double-tap NRST (reset pin to GND twice within ~500 ms) enters rmk-boot's USB DFU mode
  • The adafruit_bl Cargo feature also works with rmk-boot: the Bootloader keycode writes the same GPREGRET magic (0x57) that the Adafruit bootloader expects, and rmk-boot detects it on boot and enters its DFU mode :::

::: tip On nRF52840 / nRF52833, DFU and BLE can be used simultaneously. Enable both dfu_nrf and nrf52840_ble features in RMK, set [ble] enabled = true in keyboard.toml, and your keyboard works over USB + BLE with DFU updates (over USB).

Step-by-step guide

First-time flashing

The first time, you need to flash the bootloader first, then RMK.

1. Flash rmk-boot

RP2040 – via UF2:

  1. Put your RP2040 into bootloader mode (hold BOOTSEL button, plug in USB, release)
  2. A USB drive named RPI-RP2 should appear
  3. Copy the matching rmk-boot-rp2040-<size>mb.uf2 to the drive
  4. The RP2040 reboots – the bootloader is now active

nRF52840 / nRF52833 – via UF2:

  1. Double-tap the reset pin (connect to GND twice within 500 ms) — the LED pulses and a NICENANO drive appears
  2. Copy rmk-boot-nrf52840.uf2 (or rmk-boot-nrf52833.uf2) to the drive
  3. The board reboots — the bootloader is now active ⚠️ This overwrites the Adafruit UF2 bootloader on the nRF52840.

Via debug probe (probe-rs):

# in the rmk-boot directory
# RP2040
cargo run --release --target thumbv6m-none-eabi --features rp2040-2mb  # RP2040
# nRF52840
cargo run --release --target thumbv7em-none-eabihf --features nrf52840 # nRF52840
# nRF52833
cargo run --release --target thumbv7em-none-eabihf --features nrf52833 # nRF52833

2. Flash RMK firmware

Now flash the actual RMK firmware. The bootloader stays in place.

Method A – via UF2:

  • RP2040: Put the board into bootloader mode (hold BOOTSEL button, plug in USB, release), then copy the RMK firmware .uf2 file to the appearing RPI-RP2 drive.
  • nRF52840: Since the Adafruit bootloader was overwritten when flashing the rmk-boot bootloader, this method does not work for nRF52840 anymore.

Method B – via debug probe (probe-rs):

# in your RMK firmware directory
# RP2040
probe-rs run --chip RP2040 target/thumbv6m-none-eabi/release/your-firmware
# nRF52840
probe-rs run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/release/your-firmware
# nRF52833
probe-rs run --chip nRF52833_xxAA target/thumbv7em-none-eabihf/release/your-firmware

# or (for both RP2040 and nRF52840)
cargo run --release

All subsequent updates via DFU

After rmk-boot and the RMK firmware have been flashed once, all future updates can be done easily over USB via DFU. As stated above, as long as you build with memory.x present, you can still use probe-rs (RP2040 and nRF52), the DFU functionality of rmk-boot (nRF52) or the UF2 bootloader. (RP2040)

Generate the .bin file

DFU flashing requires a .bin file (not .elf or .uf2). You have two options:

Option 1 – using rust-objcopy (or arm-none-eabi-objcopy):

# RP2040 (thumbv6m-none-eabi)
rust-objcopy -O binary target/thumbv6m-none-eabi/release/your-firmware your-firmware.bin
# nRF52840 / nRF52833 (thumbv7em-none-eabihf)
rust-objcopy -O binary target/thumbv7em-none-eabihf/release/your-firmware your-firmware.bin

Option 2 – using cargo make bin:

cargo make bin --release

(Requires a Makefile.toml with a bin task — the generated template from rmkit and the examples already include one.)

Flash via dfu-util

dfu-util -D your-firmware.bin -R

Or with a device ID if multiple DFU devices are connected:

dfu-util -d 4c4b:4643 -D your-firmware.bin -R

Find your device's VID:PID via lsusb on Linux or Device Manager (under "Universal Serial Bus devices") on Windows.

This will download the firmware binary onto the DFU partition of the flash and set a flag to tell embassy-boot at next boot that the firmware should be updated. Embassy-boot then proceeds to copy the data in the DFU partition into the ACTIVE partition by swapping page-wise. This process takes about 20 seconds for a ~120kB firmware. rmk-boot pulses the LED during that.

Installing dfu-util:

  • Linux: sudo apt install dfu-util (Debian/Ubuntu) or sudo pacman -S dfu-util (Arch)
  • macOS: brew install dfu-util
  • Windows: Download from dfu-util.sourceforge.net

dfu-util will automatically detect the board — the RMK firmware exposes a DFU USB interface at runtime. No need to press BOOTSEL or trigger a special mode. (With the optional dfu_lock feature, DFU downloads require a physical key press to unlock — see below.)

Unlocking DFU (optional, dfu_lock)

If your RMK was compiled with the dfu_lock feature, DFU starts in a locked state. To unlock:

  1. Run dfu-util -D your-firmware.bin — the download will be rejected, but this signals the keyboard to open the unlock window
  2. The DFU LED turns solid on — you have 10 seconds to press the unlock_keys combination configured in keyboard.toml (e.g. [[0, 0], [1, 1]])
  3. Once the keys are pressed, the LED blinks (toggles every 200 ms) — the DFU lock is released for another 10 seconds
  4. Re-run dfu-util -D your-firmware.bin — this time the download proceeds, the LED flickers, and the firmware is updated
  5. After the update completes, the LED turns off and the board reboots

If you don't press the unlock keys within the first 10 seconds, the window closes and the LED turns off. If no DFU download starts within the unlocked 10 seconds, the lock re-engages automatically. In both cases just repeat from step 1.

LED behavior during DFU transfer (optional)

If an LED pin is configured in keyboard.toml (default PIN_25 on RP2040 and P0_15 on nRF52; disable with [dfu] led = "none"):

StateLED behavior
Unlock window open (waiting for keys)Solid on
DFU unlocked (ready for dfu-util)Blinks (toggles every 200 ms)
DFU download startedOn
Writing data blocksToggles on each block (flickers)
DFU finished / system resetOff

Additionally the rmk-boot bootloader has blinking codes using PIN_25 (RP2040) / P0_15 (nRF52) as well:

StateLED behavior
Normal boot — bootloader ran and jumped to ACTIVE2 short blinks (≈2 Hz)
Bootloader detected a pending DFU→ACTIVE swap and is about to copy1 s solid on
Previous forward swap completed but the new app did not call mark_booted() — reverting to the old ACTIVE3 short blinks (50 ms)
Successful DFU→ACTIVE copy; about to jump5 short blinks (50 ms)
Bootloader itself panicked (e.g. flash read error, invalid state partition)Morse SOS (... --- ...), repeating

Split peripheral updates (dfu_split)

When using a split keyboard with separate microcontrollers for each half, dfu_split lets you update the peripheral's firmware without a debug probe or direct USB connection — the central acts as a relay.

In order for this to work, both, central and peripheral need to be flashed with an embassy-boot bootloader (e.g. rmk-boot) and with a firmware that has dfu_split enabled. (See above on how to do that)

Note

This feature currently only works for split keyboards that use UART (serial) as the interface between them. Split BLE and updates over the air wont work.

Feature flags

Cargo.toml
[dependencies]
rmk = { features = [ "dfu_rp", "split", "dfu_split"] }

dfu_split requires _dfu (implied by dfu_rp / dfu_nrf) and split.

Peripherals count

Each connected peripheral needs one subscriber slot on DfuCmdEvent. The number of subscribers is automatically calculated using the SPLIT_PERIPHERALS_NUM constant. The constant is automatically set to 1 when the split feature is enabled. However, if you have more than one peripheral, increase the constant accordingly:

keyboard.toml
[rmk]
split_peripherals_num = 2 # central + 2 peripherals

See RMK Internal Configuration for details on tuning RMK's general constants.

Architecture

Two complementary paths are provided:

PathMechanismWhen to use
EmbeddedThe central includes the peripheral binary at compile time via include_bytes!() and flashes it automatically when the peripheral connects.CI/CD, production, "flash-and-forget"
Passthroughdfu-util -a 1 -D peripheral.bin sends the firmware through the central's USB DFU interface, which forwards it to the peripheral in real time.Development, ad-hoc updates without connecting the peripheral to the host to flash

Both paths split the firmware into 256-byte chunks and send them over the split link as FirmwareChunk messages. The peripheral writes them to its own DFU flash partition, verifies the CRC, and resets into the new image.

Embedded firmware path

In order to embed the peripheral's firmware into the central's firmware, we need to tell the central during compilation where the binary of the peripheral is. Note that this must be a .bin-file. You can create one using cargo make bin, see also the Makefile.toml in the examples.

Tip

Use dfu_lock to lock the central against malicious flashing. dfu_lock will lock using the central as relay to flash a peripheral as well.

Toml
Rust

Add a firmware field with the path to the peripheral's firmware binary in the peripheral's section. This is a relative path starting from where the Cargo.toml is.

keyboard.toml
[[split.peripheral]]
rows     = 2
cols     = 1
firmware = "./peripheral.bin"
update_policy = "MatchHash" # optional
# update_policy = "force" to force a peripheral update every start of central

By default (update_policy = "MatchHash", or when omitted) the central only updates the peripheral, when the firmware on the peripheral has a different checksum than the included firmware given by firmware.

The #[rmk_central] macro then generates the set_firmware_update_data call automatically. When the field is absent the central relies on passthrough only. (see below)

After flashing the central, it will ask the peripheral for its firmware checksum, the peripheral answers and if the checksum does not match with the checksum of the embedded peripheral firmware in the central, it will start flashing the embedded peripheral firmware onto the peripheral. The blink codes of the peripheral's DFU LED (see above) are the same like during a normal DFU update. Additionally the central's DFU LED will start blinking when sending the firmware to the peripheral. When UpdatePolicy::Force is given, the central skips this check and sends the firmware regardless of the checksum. Likewise the peripheral will tell the central its firmware checksum proactively at every boot. So the firmware is still updated if the peripheral booted after the central. To avoid infinite loops of flashing and rebooting even when UpdatePolicy::Force is set, the central will not flash the firmware when the CRC is matching.

Passthrough path

The passthrough pass works identically to the normal DFU flash (see above) — just select a different alternate setting (-a).

# Flash peripheral 0
dfu-util -a 1 -D peripheral.bin

# Flash peripheral 1 (if you have a second peripheral)
dfu-util -a 2 -D peripheral2.bin

# Flash central
dfu-util -a 0 -D central.bin -R
Warning

Do not use -R (reset) when flashing a peripheral. The peripheral resets itself after the update. -R would reset the central.

Note

The index of the alternate setting (-a) is off by one due to technical reasons. So to flash the peripheral with index 0, you have to pass -a 1.

Peripheral firmware requirements

The peripheral must partition its flash from the linker layout (partitions_from_linkerscript()), pass the partitions to run_rmk_split_peripheral() so forwarded firmware can be written to flash — mark_booted() is called automatically. With #[rmk_peripheral(id = …)] (TOML API) this is generated automatically when a [dfu] section is present in keyboard.toml. On a split keyboard the peripheral can use its own [split.peripheral.dfu] section instead — it fully replaces the global [dfu] for that side, so the peripheral's DFU partition can differ from the central's (e.g. internal DFU on the peripheral, external flash on the central). See Split keyboard DFU configuration.

If you are using the Rust API on the peripheral, place the memory.x from rmk-boot next to Cargo.toml (same as the central) and call:

// All partition offsets come from the DFU symbols in memory.x
use core::cell::RefCell;
use rmk::dfu::{partitions_from_linkerscript, FlashDfuHandler, FlashMutex};

// The peripheral has no storage partition, so it is discarded.
let flash_mutex = FlashMutex::new(RefCell::new(Flash::new_blocking(p.FLASH)));
let (_, state_partition, dfu_partition) = partitions_from_linkerscript(&flash_mutex);

let mut dfu_handler = FlashDfuHandler::new(dfu_partition, state_partition);
// optional DFU LED for the peripheral
let mut dfu_led_processor = DfuLedProcessor::new(Output::new(p.PIN_25, Level::Low), false);

// run the FlashDfuHandler in run_all
// if used, run the processor of the DFU LED in run_all:
join(
    run_all!(matrix, dfu_led_processor, watchdog_runner, dfu_handler),
    run_rmk_split_peripheral(uart_instance),
)
.await;

mark_booted() is called automatically inside run_rmk_split_peripheral(). The bootloader reverts the update if the firmware crashes before this happens.

Tip

This setup also enables DFU updates via USB for the peripheral, if a USB connection is set up for it.

Troubleshooting

SymptomLikely cause
Peripheral DFU LED flashes 3 times after updateFirmware crashed before mark_booted() was called automatically. Build with the dfu_rp/dfu_nrf feature (using config) or ensure FlashDfuHandler is used (using Rust).
dfu-util -a 1 shows dfuERROR or hangs at 0%Peripheral not connected or its firmware is built without the dfu_split feature.