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 (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 the MEMORY layout (absolute XIP addresses) and the standard
 * embassy-boot `__bootloader_*` partition symbols (flash-relative offsets)
 * consumed by init_flash_from_linkerscript().
 */

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

/* 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   = 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 offsets come from linker symbols in memory.x
let flash = async_flash_wrapper(
    rmk::dfu::init_flash_from_linkerscript(p.FLASH)
);

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 */

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.

Make sure FLASH : ORIGIN in your MEMORY block starts at your ACTIVE partition. RMK's init_flash_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 nRF52840, flash-relative and absolute offsets are the same (flash starts at 0x00000000).

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

cargo make uf2-2mb   # → rmk-boot-rp2040-2mb.uf2  (or uf2-4mb / uf2-8mb / uf2-16mb)
cargo make uf2-nrf   # → rmk-boot-nrf52840.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-2mbrmk-boot-rp2040-2mb.uf2 – for 2 MB flash
  • uf2-4mbrmk-boot-rp2040-4mb.uf2 – for 4 MB flash
  • uf2-8mbrmk-boot-rp2040-8mb.uf2 – for 8 MB flash
  • uf2-16mbrmk-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: cargo make uf2-nrf builds the 1 MB variant. To flash with a probe instead, run cargo run --release --target thumbv7em-none-eabihf --features nrf52840 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: 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) 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 init_flash_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, 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 – 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 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
probe-rs run --chip RP2040 target/thumbv6m-none-eabi/release/rmk-boot
# nRF52840
probe-rs run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/release/rmk-boot
# or build and run inside the rmk-boot project:
cargo run --release --target thumbv6m-none-eabi --features rp2040-2mb  # RP2040
cargo run --release --target thumbv7em-none-eabihf --features nrf52840 # nRF52840

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

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

All subsequent updates via DFU

Once 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 nRF52840), the DFU functionality of rmk-boot (nRF52840) 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 (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 (nRF52840) 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.

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 (see above) 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). Up to four peripherals are supported.

# 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 call init_flash_from_linkerscript() to partition its flash and mark_booted() so that the bootloader does not revert the update. With #[rmk_peripheral(id = …)] (TOML API) this is generated automatically when RMK is built with the dfu_rp or dfu_nrf feature.

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 linker symbols in memory.x
rmk::dfu::init_flash_from_linkerscript(p.FLASH);

// mark the firmware as booted otherwise the bootloader thinks it didn't and will revert to the old firmware
rmk::dfu::mark_booted();

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

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

Without mark_booted() embassy-boot will revert the update at next reboot, because it thinks the new firmware did not boot. (Visible as three short flashes of the DFU LED when using rmk-boot.)

Troubleshooting

SymptomLikely cause
Peripheral flashes 3 times after updateFirmware crashed or mark_booted() not called in the peripheral. Build with the dfu_rp/dfu_nrf feature (using config) or call rmk::dfu::mark_booted() (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.