Skip to content

Split keyboards

A split keyboard consists of one or more peripheral devices communicating matrix events to one central device, which then creates HID keyboard reports to send to a host device.

Generally, a split keyboard will require compiling multiple binaries, one for each device/part of the split keyboard. For example, you will need one binary for the left half, and another binary for the right half.

Continue reading to see how to implement a “central” and a “peripheral” device using rumcake.

Example

The following documentation will show an example for a split keyboard with a left and right half, no dongle.

The central device code will be placed in left.rs, and the peripheral device code will be placed in right.rs.

For a similar full example of how to implement a split keyboard, check the template repo.

Central setup

The “central” device in a split keyboard setup defines the keyboard layout, communicates with the host device, and receives matrix events from other peripherals. There should only be one central device. If the split keyboard also uses extra features like backlighting or underglow, the central device will also be responsible for sending their related commands to the peripherals.

Typically, the central device could be a dongle (good for saving battery life), or one of the keyboard halves.

Required Cargo features for central device

You must compile a binary with the following rumcake features:

Required code for central device

To set up the central device, you must add split_central(driver_setup_fn = <setup_fn>) to your #[keyboard] macro invocation, and your keyboard must implement the CentralDevice trait. Your CentralDevice implementation should include type Layout = Self;. This will tell rumcake to redirect matrix events (received from other peripherals) to the layout, to be processed as keycodes.

The driver_setup_fn must be an async function that has no parameters, and returns a type that implements the CentralDeviceDriver trait.

left.rs
use rumcake::keyboard;
#[keyboard(
// somewhere in your keyboard macro invocation ...
split_central(
driver_setup_fn = my_central_setup
)
)]
struct MyKeyboardLeftHalf;
// KeyboardLayout should already be implemented
use rumcake::keyboard::KeyboardLayout;
impl KeyboardLayout for MyKeyboardLeftHalf { /* ... */ }
// Split central setup
use rumcake::split::central::{CentralDevice, CentralDeviceDriver};
async fn my_central_setup() -> impl CentralDeviceDriver {
// TODO: We will fill this out soon!
todo!()
}
impl CentralDevice for MyKeyboardLeftHalf {
type Layout = Self;
}

Lastly, you must set up the driver. To do this, you need to complete your driver_setup_fn by constructing the driver. You can check the API reference for your chosen driver for a set up function or macro to make this process easier.

Depending on the driver, you may also need to implement the appropriate trait that corresponds to your chosen driver in the #[keyboard] macro. Check the list of available split drivers for this information.

For example, with the SerialSplitDriver struct, you can construct it like so:

// KeyboardLayout should already be implemented
use rumcake::keyboard::KeyboardLayout;
impl KeyboardLayout for MyKeyboardLeftHalf { /* ... */ }
// Split central setup
use rumcake::split::central::{CentralDevice, CentralDeviceDriver};
use rumcake::drivers::SerialSplitDriver;
use rumcake::hw::platform::setup_buffered_uarte;
async fn my_central_setup() -> impl CentralDeviceDriver {
// TODO: We will fill this out soon!
todo!()
SerialSplitDriver {
serial: setup_buffered_uarte! { // Note: this assumes nRF5x, other MCUs have their own macros with their own arguments.
interrupt: UARTE0_UART0,
uarte: UARTE0,
timer: TIMER1,
ppi_ch0: PPI_CH0,
ppi_ch1: PPI_CH1,
ppi_group: PPI_GROUP0,
rx_pin: P0_29,
tx_pin: P0_31,
},
}
}
impl CentralDevice for MyKeyboardLeftHalf {
type Layout = Self;
}

Peripheral setup

The “peripheral” device in a split keyboard setup has a switch matrix, and sends matrix events to the central device. A split keyboard setup could have more than one peripheral. If the split keyboard also uses extra features, then all the peripherals should receive the related commands from the central device.

Required Cargo features for peripheral device

You must compile a binary with the following rumcake features:

Required code for peripheral device

To set up the peripheral device, you must add split_peripheral(driver_setup_fn = <setup_fn>) to your #[keyboard] macro invocation, and your keyboard must implement the PeripheralDevice trait. Your KeyboardMatrix implementation (which should already be implemented) should include type PeripheralDeviceType = Self. This will tell rumcake to redirect matrix events to the peripheral device driver, to be sent to the central device.

The driver_setup_fn must be an async function that has no parameters, and returns a type that implements the PeripheralDeviceDriver trait.

right.rs
use rumcake::keyboard;
#[keyboard(
// somewhere in your keyboard macro invocation ...
split_peripheral(
driver_setup_fn = my_peripheral_setup
)
)]
struct MyKeyboardRightHalf;
// KeyboardMatrix should already be implemented
use rumcake::keyboard::KeyboardMatrix;
impl KeyboardMatrix for MyKeyboardRightHalf {
type PeripheralDeviceType = Self;
}
// Split peripheral setup
use rumcake::split::peripheral::{PeripheralDevice, PeripheralDeviceDriver};
async fn my_peripheral_setup() -> impl PeripheralDeviceDriver {
// TODO: We will fill this out soon!
todo!()
}
impl PeripheralDevice for MyKeyboardRightHalf {}

Lastly, you must set up the driver. To do this, you need to complete your driver_setup_fn by constructing the driver. You can check the API reference for your chosen driver for a set up function or macro to make this process easier.

Depending on the driver, you may also need to implement the appropriate trait that corresponds to your chosen driver in the #[keyboard] macro. Check the list of available split drivers for this information.

For example, with the SerialSplitDriver struct, you can construct it like so:

// KeyboardLayout should already be implemented
use rumcake::keyboard::KeyboardLayout;
impl KeyboardLayout for MyKeyboardLeftHalf { /* ... */ }
// Split central setup
use rumcake::drivers::SerialSplitDriver;
use rumcake::hw::platform::setup_buffered_uarte;
use rumcake::split::peripheral::{PeripheralDevice, PeripheralDeviceDriver};
async fn my_peripheral_setup() -> impl PeripheralDeviceDriver {
// TODO: We will fill this out soon!
todo!()
SerialSplitDriver {
serial: setup_buffered_uarte! { // Note: this assumes nRF5x, other MCUs have their own macros with their own arguments.
interrupt: UARTE0_UART0,
uarte: UARTE0,
timer: TIMER1,
ppi_ch0: PPI_CH0,
ppi_ch1: PPI_CH1,
ppi_group: PPI_GROUP0,
rx_pin: P0_31,
tx_pin: P0_29,
},
}
}
impl PeripheralDevice for MyKeyboardRightHalf {}

Central Device Without a Matrix (Dongle)

An example of a central device without a matrix is a dongle. If you would like to implement such a device, you can add no_matrix to your #[keyboard] macro invocation.

Doing so will remove the need to implement KeyboardMatrix, so you will only have to implement KeyboardLayout.

dongle.rs
use rumcake::keyboard;
#[keyboard(
// somewhere in your keyboard macro invocation ...
no_matrix,
split_central(
driver = "ble" // TODO: change this to your desired split driver, and implement the appropriate trait
)
)]
struct MyKeyboardDongle;
// rest of your config ...

nRF-BLE Driver

If you are using an nRF5x MCU, and want to use BLE for split keyboard communication, there are additional changes you need to make it work.

For both central and peripheral devices, the BluetoothDevice trait must be implemented:

BLUETOOTH_ADDRESS can be whatever you want, as long as it is a valid “Random Static” bluetooth address. See “Random Static Address” here: https://novelbits.io/bluetooth-address-privacy-ble/

// central file
use rumcake::hw::platform::BluetoothDevice;
impl BluetoothDevice for MyKeyboardLeftHalf {
const BLUETOOTH_ADDRESS: [u8; 6] = [0x41, 0x5A, 0xE3, 0x1E, 0x83, 0xE7]; // TODO: Change this to something else
}
// peripheral file
use rumcake::hw::platform::BluetoothDevice;
impl BluetoothDevice for MyKeyboardRightHalf {
const BLUETOOTH_ADDRESS: [u8; 6] = [0x92, 0x32, 0x98, 0xC7, 0xF6, 0xF8]; // TODO: Change this to something else
}

You will also need to change the #[keyboard] macro invocation to add driver_type = "nrf-ble". This will change the requirements for the signature of driver_setup_fn.

// central file
#[keyboard(
// somewhere in your keyboard macro invocation ...
split_central(
driver_setup_fn = my_central_setup,
driver_type = "nrf-ble"
)
)]
struct MyKeyboardLeftHalf;
// peripheral file
#[keyboard(
// somewhere in your keyboard macro invocation ...
split_peripheral(
driver_setup_fn = my_peripheral_setup,
driver_type = "nrf-ble"
)
)]
struct MyKeyboardRightHalf;

Now, your driver_setup_fn will need to change it’s signature.

For central devices, it will need to return:

  • CentralDeviceDriver implementor
  • A slice containing peripheral addresses to connect to

For peripheral devices, it will need to return:

  • PeripheralDeviceDriver implementor
  • Address of the central device to connect to

The setup_nrf_ble_split_central! and setup_nrf_ble_split_peripheral! driver can be used to implement your driver_setup_fn.

// central file
use rumcake::drivers::nrf_ble::central::setup_nrf_ble_split_central;
async fn my_central_setup() -> impl CentralDeviceDriver {
async fn my_central_setup() -> (impl CentralDeviceDriver, &'static [[u8; 6]]) {
setup_nrf_ble_split_central! {
peripheral_addresses: [
[0x92, 0x32, 0x98, 0xC7, 0xF6, 0xF8] // address of peripheral we specified in the peripheral device's file
]
}
}
// peripheral file
use rumcake::drivers::nrf_ble::peripheral::setup_nrf_ble_split_peripheral;
async fn my_peripheral_setup() -> impl PeripheralDeviceDriver {
async fn my_peripheral_setup() -> (impl PeripheralDeviceDriver, [u8; 6]) {
setup_nrf_ble_split_peripheral! {
central_address: [0x41, 0x5A, 0xE3, 0x1E, 0x83, 0xE7] // address of central device we specified in the central device's file
}
}

To-do List

  • Method of syncing backlight and underglow commands from central to peripherals on split keyboard setups
  • Single device that can act as both a peripheral and central device
  • Serial (half duplex) driver
  • I2C driver

Available Drivers

NameFeature FlagRequired Traits
Serial1N/A (available by default)N/A
nRF Bluetooth LEnrf-bleBluetoothDevice

Footnotes

  1. Compatible with any type that implements both embedded_io_async::Read and embedded_io_async::Write. This includes embassy_nrf::buffered_uarte::BufferedUarte (nRF UARTE) and embassy_stm32::usart::BufferedUart (STM32 UART).