Use embassy-boot bootloader

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 pre-built one like bootymcbootface.

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

bootymcbootface is a pre-built 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 bootymcbootface and RMK are flashed, you never need to press BOOTSEL again.

RMK and bootymcbootface use an identical partition formula so the layout always fits perfectly:

bootloader+state = 28K (fixed)
storage = 128K (fixed — 32 sectors × 4K for persistent keymap storage)
remaining = flash_size - 28K - 128K
ACTIVE    = (remaining - 4K) / 2
DFU       = ACTIVE + 4K  (1 page delta required by embassy-boot swap)

The 4K page delta is an invariant of embassy-boot's swap algorithm: the DFU slot must always be exactly one erase page larger than the ACTIVE slot. Storage is reserved at the end of flash and cannot be reconfigured when using bootymcbootface — it is always 128K (32 sectors × 4K).

Region2MB (RP2040)4MB (RP2040)8MB (RP2040)16MB (RP2040)1MB (nRF52840)
Boot+State28K (0x7000)28K (0x7000)28K (0x7000)28K (0x7000)28K (0x7000)
ACTIVE944K (0xEC000)1968K (0x1EC000)4016K (0x3EC000)8112K (0x7EC000)432K (0x73000)
DFU948K (0xED000)1972K (0x1ED000)4020K (0x3ED000)8116K (0x7ED000)436K (0x7B000)
Storage128K (0x20000)128K (0x20000)128K (0x20000)128K (0x20000)128K (0x20000)

The size of the ACTIVE Region is what you need to plug in as the flash size in memory.x. (see also the examples memory.x in rmk/examples/use_rust/rp2040_embassy_boot/ for RP2040 or rmk/examples/use_rust/nrf52840_embassy_boot/ for nRF)

If you override flash_size in your keyboard.toml's [dfu] section, RMK's auto-calc recomputes the layout on the fly. You never need to manually set the addresses like dfu_offset or dfu_size when using bootymcbootface.

Prerequisites

bootymcbootface

You need the bootymcbootface bootloader in the correct flash size for your board. The available versions are named by flash size, e.g.:

RP2040:

  • bootymcbootface-rp2040-2mb.uf2 – for 2 MB flash
  • bootymcbootface-rp204o-4mb.uf2 – for 4 MB flash
  • bootymcbootface-rp204o-8mb.uf2 – for 8 MB flash
  • bootymcbootface-rp204o-16mb.uf2 – for 16 MB flash
Tip

The 2 MB version works with larger flash chips too (e.g. 4 MB or 8 MB), using only the first 2 MB. On 2 MB you get 944K 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 — see the table above for exact slot sizes.

nRF52840:

  • bootymcbootface-nrf52840.uf2 – for 1 MB flash
  • bootymcbootface-nrf52840.elf – for 1 MB flash, for direct flashing via probe-rs
Warning

When using the nRF52840 with the Adafruit UF2 bootloader, flashing bootymcbootface 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
  • keyboard.toml has [dfu] section
  • The flash partitioning in memory.x matches your bootloader and flash size (see rmk/examples/use_rust/rp2040_embassy_boot/ for RP2040 or rmk/examples/use_rust/nrf52840_embassy_boot/ for nRF)

Important: For RP2040 the bootymcbootface version (2MB, 4MB, ...) and the RMK settings must use the same flash size. Example: bootymcbootface 2 MB → RMK with 2 MB flash configuration. So that means, if you have a RP2040 with bigger flash, you can always use a bootymcbootface and a memory.x in your firmware for a smaller flash size, but the both must be for the same flash size.

Tip

You can still flash firmware via UF2 (when using RP2040, on nRF52840 the UF2 bootloader was overwritten by bootymcbootface) or probe-rs — as long as it was built with the correct memory.x for the embassy-boot partition layout (i.e. with dfu_rp or dfu_nrf feature), it will land in the active firmware slot and leave bootymcbootface untouched.

If you flash a RMK firmware built with a normal memory.x (without embassy-boot), it will start at flash address 0x0 and overwrite bootymcbootface.

Tip

On RP2040 the UF2 bootloader lives in ROM and can't be overwritten. On nRF52840 with the Adafruit UF2 bootloader, flashing bootymcbootface 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 bootymcbootface

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 bootymcbootface-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 bootymcbootface-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 bootymcbootface directory
cargo build --relese
# RP2040
probe-rs run --chip RP2040 target/thumbv6m-none-eabi/release/bootymcbootface
# nRF52840
probe-rs run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/release/bootymcbootface
# or build and run inside the bootymcbootface 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 bootymcbootface 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 bootymcbootface and the RMK firmware have been flashed once, all future updates can be done easily over USB via DFU. (but as stated above, as long as the memory.x of your firmware fits, you can still use probe-rs (RP2040 and 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 or see embassy-boot examples.)

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 way the data in ACTIVE is swapped and can be rolled back). This process takes about 20 seconds for a ~120kB firmware, during which there is not LED activity.

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 "D F U" in Morse code (-.. ..-. ..-) — 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, if not [dfu] led = "none"):

StateLED behavior
Unlock window open (waiting for keys)Solid on
DFU unlocked (ready for dfu-util)Blinks Morse "D F U"
DFU download startedOn
Writing data blocksToggles on each block (flickers)
DFU finished / system resetOff

Additionally the bootymcbootface 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. bootymcbootface) 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"] } # or dfu_nrf instead of dfu_rp

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 peripherals firmware into the centrals 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 peripherals 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 peripherals DFU LED (see above) are the same like during a normal DFU update. Additionally the centrals 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 it’s 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 do 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() 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 a [dfu] section is present in keyboard.toml.

If you are using the Rust API on the peripheral, ensure you have this in your main function of the peripheral:

// for the different addresses for different chips see above
const FLASH_SIZE: u32 = 2 * 1024 * 1024;
const PAGE_SIZE: u32 = 4 * 1024;
const STORAGE_SIZE: u32 = 128 * 1024;
const STATE_OFFSET: u32 = 0x6000;
const STATE_SIZE: u32 = 0x1000;
const ACTIVE_OFFSET: u32 = 0x7000;
let remaining: u32 = FLASH_SIZE - 28 * 1024 - STORAGE_SIZE;
let active_size: u32 = (remaining - PAGE_SIZE) / 2;
let dfu_size: u32 = active_size + PAGE_SIZE;
let dfu_offset: u32 = ACTIVE_OFFSET + active_size;
let storage_offset: u32 = dfu_offset + dfu_size;

rmk::dfu::init_flash(
    p.FLASH,
    storage_offset,
    STORAGE_SIZE,
    STATE_OFFSET,
    STATE_SIZE,
    dfu_offset,
    dfu_size,
);

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

// you can also set a DFU LED for the perpheral, it flashes during updates
let mut dfu_led_processor = DfuLedProcessor::new(Output::new(p.PIN_25, Level::Low), false);
// if used, run the procesor 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 bootymcbootface).

Troubleshooting

SymptomLikely cause
Peripheral flashes 3 times after updateFirmware crashed or mark_booted() not called in the peripheral. Add [dfu] to keyboard.toml (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.