From v0.8 to v0.9
RMK v0.9 is a major release with lots of updates and improvements. The key changes in v0.9 include:
-
keyboard.tomlrestructuring: layers move out of[layout]into a new[keymap]section, and the config is validated more strictly at build time. -
New Rust API (Rust API users only): input devices, input processors, and controllers are merged into one event/processor system, and
run_rmkis replaced by explicit transports plusrun_all!. -
New features: display support, a BLE dongle, and a hardware watchdog that is enabled by default. Rynk (a native alternative to Vial) and DFU firmware update ship as experimental — Vial remains the default and needs no changes.
-
Dependency upgrades: newer embassy releases,
bt-hciv0.10, and updated BLE stacks.
If you configure your keyboard with keyboard.toml, only the Cargo.toml
and keyboard.toml sections apply to you. The
Rust API section is for keyboards written against the Rust API.
Cargo.toml Update
Update RMK and embassy-* dependencies
Update the rmk version to v0.9 and adjust its feature flags: the controller and col2row
features are removed, and vial_lock is renamed to host_lock. Update embassy-* dependencies and
HALs as well (embassy-time is unchanged):
watchdog is a new default feature. If your rmk dependency sets default-features = false, add
"watchdog" to the feature list to get the hardware watchdog (see Watchdog).
For BLE keyboards, the rand, rand_core, and rand_chacha dependencies can be removed — the BLE
transport builds the stack itself (see the Rust API update).
nRF specific update
For nRF, replace the nrf-sdc git pin with the crates.io release and enable nrf-sdc's central
feature — the BLE transport needs it even on a non-split keyboard:
Rust API users: the new nrf-sdc's support_* builder methods no longer return Result, so drop
the ? on those calls.
Pico W specific update
For Pico W, update cyw43 and cyw43-pio, and replace your whole [patch.crates-io] block with the
one from the
pi_pico_w_ble example
— it now pins the complete embassy graph to one git revision, not just three crates:
cyw43 also needs a fourth firmware blob, nvram_rp2040.bin, next to the existing three in
cyw43-firmware/. If your build.rs downloads the blobs, add it to the download list; otherwise
copy the file from the example. Rust API users also port the updated cyw43 task wiring from the
example.
ESP32 specific update
For ESP32, update all esp-* dependencies, drop rand_core and static_cell, and copy the
[patch.crates-io] block from the
esp32c3_ble example —
it pins the whole esp-hal graph to a git revision, because the released esp-radio still speaks
bt-hci 0.8 instead of the bt-hci-transport trait RMK needs:
keyboard.toml Update
[layout] and the new [keymap]
The layer definitions move out of [layout] into the new [keymap] section, and matrix_map is
renamed to map:
The keymap is also checked more strictly than in v0.8:
- Every layer's
keysmust list exactly one action per position in[layout].map. v0.8 silently padded a short layer withNo; v0.9 fails the build. Use_for transparent keys. - Layer numbers in key actions (
MO(2),LT(2, ...),TG(2), ...) must be smaller than[keymap].layers. A config withlayers = 2that referenced layer 2 built in v0.8 but fails now — raiselayers. //comments are no longer allowed inside themapandkeysstrings. Move them to#comment lines outside the string.
Other layout changes:
- If you used the 3D-array form (
keymap = [[[...]]]withoutmatrix_map), you must now author a[layout].maplisting the(row,col)position of every key in visual order, then move each layer of the array into a[[keymap.layer]]keysstring. [layout].encoder_mapis replaced byencodersinside each[[keymap.layer]].
See Layout for the full syntax.
[rmk] channel options move to [event]
The global channel knobs are replaced by per-event settings:
See Event Channels for the available event names and defaults.
[ble] update
ble_use_2m_phy is renamed to use_2m_phy:
[behavior] update
Morse/tap-hold hold_timeout and gap_timeout values above 8191 ms now fail the build, both in
[behavior.morse] and in every named profile under [behavior.morse.profiles] — lower them to
8191 ms or less.
Stricter validation
keyboard.toml is now checked up front at build time, with a clear error instead of a silently
ignored key or an obscure compile error further down. Expect a build failure for:
- Unknown keys in any section — leftovers from the old schema are flagged for you.
- A
row_pins/col_pins/direct_pinscount that doesn't matchrows/cols— in[matrix],[split.central], and each[[split.peripheral]]. - A split board whose
rows/colsplusrow_offset/col_offsetexceed[layout], or that overlaps another board. ble_addron aconnection = "serial"split, orserialon aconnection = "ble"split.- More than 4
[host].unlock_keys, or an unlock key outside[layout].
Rust API Update
Every example's main() changed in v0.9 — the fastest migration is to port your customizations into
the v0.9 example for your chip under
examples/use_rust. The core change:
run_rmk, run_devices!, and EVENT_CHANNEL are gone, every component is a runnable passed to one
run_all!, and the USB/BLE plumbing becomes an explicit transport:
DeviceConfig::default() now fills in a serial number that embeds the RMK version, so the
hard-coded serial_number can go. HAL-level changes that come with the embassy upgrade (embassy-rp
0.10's Flash::new takes an Irqs binding, embassy-executor 0.10 spawns with
spawner.spawn(task).unwrap()) are not RMK changes — take them from the example.
For rotary encoders, use KeymapData::new_with_encoder(keymap, encoder_map) — the separate
initialize_encoder_keymap_and_storage is removed.
For BLE, the transport owns the BLE stack: hand it the controller and address instead of building a
stack yourself. HostResources and the ChaCha RNG disappear from your main. BleTransport::new
consumes rmk_config, so construct UsbTransport first:
Split keyboards
On the peripheral, run_rmk_split_peripheral takes the controller and address too, and no longer
takes storage — pass the peripheral's storage to run_all! instead. The storage constructor is
renamed:
On a BLE central, the BLE transport is the split central: BleTransport::new takes a fourth
argument describing each peripheral's matrix, and run_peripheral_manager, scan_peripherals,
read_peripheral_addresses, and OffsetMatrixWrapper are gone:
On a serial (wired) central, run_peripheral_manager loses its const generics and takes the same
config struct:
Custom devices, processors, and controllers
The Controller / EventController / InputProcessor traits, run_processor_chain!, and the
central Event enum are replaced by the unified event/processor model: define events with
#[event] (or #[derive(Event)]), implement input devices with #[input_device(publish = ...)],
and handle events in #[processor(subscribe = [...])] processors passed to run_all!. See
Input devices and Processors for the new model.
Tap-hold profiles
KeyAction::TapHold now stores a u8 index into behavior.morse.profiles instead of an inline
MorseProfile. The default-profile macros (th!/mt!/lt!/tt!) are unchanged. If you use the
custom-profile macros (thp!/mtp!/ltp!/ttp!), push the profile into
behavior_config.morse.profiles and pass its 0-based index; an index with no entry falls back to
the default profile:
MorseProfile itself is now packed into a u64 instead of a u32 (to make room for
quick_tap_timeout); code that relied on its raw u32 representation must be updated.
Renamed and moved APIs
Also: NrfAdc::new gains an event_device_ids array argument, and BatteryProcessor::new drops
its &keymap argument.
BLE hosts must re-pair
The HID report ids are renumbered in v0.9 (Keyboard=1, Mouse=2, Media=3, System=4), which
changes the BLE report map. USB hosts re-read the descriptor on every enumeration, so nothing
changes for them, but a BLE host bonded to a pre-v0.9 firmware caches the old report map — forget
the keyboard on the host and pair again after flashing v0.9.
Watchdog
v0.9 enables the hardware watchdog by default on RP2040, nRF52, and ESP32 (a no-op on other chips).
keyboard.toml users get it automatically as long as the watchdog feature is on (it is a default
feature; see the Cargo.toml update if you use
default-features = false). Rust API users construct a runner and pass it to run_all!:
ESP32 has no default_runner: configure and enable a timer-group watchdog yourself, wrap it in
Esp32Watchdog::new, and pass it to WatchdogRunner::new with the feed interval.
To disable the watchdog, set default-features = false and list your features without watchdog.
See Watchdog for details.
Experimental features
Two v0.9 features are experimental: they are opt-in, off by default, and nothing in this guide depends on them. Their configuration and APIs can change in any later release without a migration guide, so skip this section unless you want to try them.
- Rynk — RMK's native host protocol, an alternative to Vial (they are mutually
exclusive, so enable exactly one). To try it, set
[host] rynk_enabled = true/vial_enabled = falseinkeyboard.toml, and inCargo.tomlsetdefault-features = falseand list your features withrynkinstead ofvial(vialis a default feature, so it can't be swapped out otherwise). Vial stays the default and needs no changes. - DFU firmware update — update firmware over USB with the
rmk-bootbootloader, via thedfu_rp/dfu_nrffeatures. It repartitions your flash, so read the bootloader docs before enabling it on a board you rely on.