Use embassy-boot bootloader
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:
At runtime, RMK reads the DFU partition offsets from the linker symbols embedded in the file.
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):
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:
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-2mb→rmk-boot-rp2040-2mb.uf2– for 2 MB flashuf2-4mb→rmk-boot-rp2040-4mb.uf2– for 4 MB flashuf2-8mb→rmk-boot-rp2040-8mb.uf2– for 8 MB flashuf2-16mb→rmk-boot-rp2040-16mb.uf2– for 16 MB flash
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.
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 withnrf52840_blefor simultaneous BLE support
If configuring manually, ensure:
Cargo.tomluses thedfu_rp(RP2040) ordfu_nrf(nRF52840) featurememory.xfrom rmk-boot is present next to yourCargo.toml- a
[dfu]section is present, either inkeyboard.tomlor 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:
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.
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.
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.
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:
- Put your RP2040 into bootloader mode (hold BOOTSEL button, plug in USB, release)
- A USB drive named
RPI-RP2should appear - Copy the matching
rmk-boot-rp2040-<size>mb.uf2to the drive - The RP2040 reboots – the bootloader is now active
nRF52840 – via UF2:
- Double-tap the reset pin (connect to GND twice within 500 ms) — the LED pulses and a
NICENANOdrive appears - Copy
rmk-boot-nrf52840.uf2to the drive - The board reboots — the bootloader is now active ⚠️ This overwrites the Adafruit UF2 bootloader on the nRF52840.
Via debug probe (probe-rs):
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
.uf2file to the appearingRPI-RP2drive. - 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):
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):
Option 2 – using cargo make bin:
(Requires a Makefile.toml with a bin task — the generated template from rmkit and the examples already include one.)
Flash via dfu-util
Or with a device ID if multiple DFU devices are connected:
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) orsudo 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:
- Run
dfu-util -D your-firmware.bin— the download will be rejected, but this signals the keyboard to open the unlock window - The DFU LED turns solid on — you have 10 seconds to press the
unlock_keyscombination configured inkeyboard.toml(e.g.[[0, 0], [1, 1]]) - Once the keys are pressed, the LED blinks (toggles every 200 ms) — the DFU lock is released for another 10 seconds
- Re-run
dfu-util -D your-firmware.bin— this time the download proceeds, the LED flickers, and the firmware is updated - 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"):
Additionally the rmk-boot bootloader has blinking codes using PIN_25 (RP2040) / P0_15 (nRF52840) as well:
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)
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
dfu_split requires dfu (implied by dfu_rp / dfu_nrf) and split.
Architecture
Two complementary paths are provided:
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.
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.
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.
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.
Do not use -R (reset) when flashing a peripheral. The peripheral
resets itself after the update. -R would reset the central.
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:
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.)