Skip to content

Commit f667f20

Browse files
committed
feat(dfu_split): add dfu_split, possibility to update peripherals via central
Signed-off-by: Pascal Jäger <pascal.jaeger@leimstift.de>
1 parent 8dbbf1e commit f667f20

45 files changed

Lines changed: 8698 additions & 85 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/docs/main/docs/development/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ There are a bunch of things to do with RMK in the near future. I plan to ship 1.
6464
- [ ] 🔴🔵 GUI keymap configurator which supports windows/macos/linux/web
6565
- [ ] Default bootloader
6666
- [x] USB DFU
67-
- [ ] Flashing peripherals from the central via uart
67+
- [x] Flashing peripherals from the central via uart
6868
- [ ] Flashing peripherals from the central via ble
6969
- [ ] OTA updates
7070

docs/docs/main/docs/user_guide/flash_firmware/use_embassy_boot.mdx

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,13 @@ dfu-util -D your-firmware.bin -R
166166

167167
Or with a device ID if multiple DFU devices are connected:
168168
```shell
169-
dfu-util -d 4c4b:4643 -D your-firmware.bin
169+
dfu-util -d 4c4b:4643 -D your-firmware.bin -R
170170
```
171171

172172
Find your device's VID:PID via `lsusb` on Linux or Device Manager (under "Universal Serial Bus devices") on Windows.
173173

174+
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.
175+
174176
**Installing `dfu-util`:**
175177
- Linux: `sudo apt install dfu-util` (Debian/Ubuntu) or `sudo pacman -S dfu-util` (Arch)
176178
- macOS: `brew install dfu-util`
@@ -211,3 +213,182 @@ Additionally the bootymcbootface bootloader has blinking codes using PIN_25 (RP2
211213
| Successful DFU→ACTIVE copy; about to jump | 5 short blinks (50 ms) |
212214
| Bootloader itself panicked (e.g. flash read error, invalid state partition) | Morse SOS (... --- ...), repeating |
213215

216+
---
217+
218+
## Split peripheral updates (dfu_split)
219+
220+
When using a split keyboard with separate microcontrollers for each half,
221+
`dfu_split` lets you update the peripheral's firmware without a debug
222+
probe or direct USB connection — the central acts as a relay.
223+
224+
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)
225+
226+
::: note
227+
228+
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.
229+
230+
:::
231+
232+
### Feature flags
233+
234+
```toml title="Cargo.toml"
235+
[dependencies]
236+
rmk = { features = [ "dfu_rp", "split", "dfu_split"] } # or dfu_nrf instead of dfu_rp
237+
```
238+
239+
`dfu_split` requires `dfu` (implied by `dfu_rp` / `dfu_nrf`) and `split`.
240+
241+
### Architecture
242+
243+
Two complementary paths are provided:
244+
245+
| Path | Mechanism | When to use |
246+
|---|---|---|
247+
| **Embedded** | The 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" |
248+
| **Passthrough** | `dfu-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 |
249+
250+
Both paths split the firmware into 256-byte chunks and send them over the
251+
split link as `FirmwareChunk` messages. The peripheral writes them to its
252+
own DFU flash partition, verifies the CRC, and resets into the new image.
253+
254+
### Embedded firmware path
255+
256+
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.
257+
Note that this must be a .bin-file. You can create one using `cargo make bin`, see also the Makefile.toml in the examples.
258+
259+
::: tip
260+
261+
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.
262+
263+
:::
264+
265+
import { Tab, Tabs, Rust, Toml } from '@theme'
266+
267+
<Tabs>
268+
<Tab label={<Toml />}>
269+
270+
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.
271+
272+
```toml title="keyboard.toml"
273+
[[split.peripheral]]
274+
rows = 2
275+
cols = 1
276+
firmware = "./peripheral.bin"
277+
update_policy = "MatchHash" # optional
278+
# update_policy = "force" to force a peripheral update every start of central
279+
```
280+
281+
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.
282+
283+
The `#[rmk_central]` macro then generates the `set_firmware_update_data`
284+
call automatically. When the field is absent the central relies on
285+
passthrough only. (see below)
286+
287+
</Tab>
288+
<Tab label={<Rust />}>
289+
290+
Add this inside the main function of the central:
291+
292+
```rust title="central.rs"
293+
// NOTE: CARGO_MANIFEST_DIR is where your Cargo.toml is
294+
const PERIPHERAL_FW: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/peripheral.bin"));
295+
rmk::dfu::set_firmware_update_data(0, PERIPHERAL_FW, rmk::crc32::crc32(PERIPHERAL_FW));
296+
297+
//...
298+
299+
// in the call to run_peripheral_manaager, set an update policy
300+
// use UpdatePolicy::Force to force the peripheral update at every start of central
301+
run_peripheral_manager::<2, 1, 2, 2, _>(0, uart_receiver, rmk::split::central::UpdatePolicy::MatchHash
302+
),
303+
```
304+
305+
Where the first argument of `set_firmware_update_data` is the index of the peripheral. (The same as the first argument of `run_peripheral_manager`.)
306+
307+
When the `set_firmware_update_data()` is not called, the central relies on passthrough only.
308+
309+
</Tab>
310+
</Tabs>
311+
312+
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.
313+
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.
314+
315+
### Passthrough path
316+
317+
The passthrough pass works identically to the normal DFU flash (see above) — just select a
318+
different alternate setting (`-a`). Up do four peripherals are supported.
319+
320+
```shell
321+
# Flash peripheral 0
322+
dfu-util -a 1 -D peripheral.bin
323+
324+
# Flash peripheral 1 (if you have a second peripheral)
325+
dfu-util -a 2 -D peripheral2.bin
326+
327+
# Flash central
328+
dfu-util -a 0 -D central.bin -R
329+
```
330+
331+
::: warning
332+
Do **not** use `-R` (reset) when flashing a peripheral. The peripheral
333+
resets itself after the update. `-R` would reset the central.
334+
:::
335+
336+
::: note
337+
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`.
338+
:::
339+
340+
### Peripheral firmware requirements
341+
342+
The peripheral must call `init_flash()` to partition its flash and `mark_booted()` so that the
343+
bootloader does not revert the update. With `#[rmk_peripheral(id = …)]`
344+
(TOML API) this is generated automatically when a `[dfu]` section is
345+
present in `keyboard.toml`.
346+
347+
If you are using the Rust API on the peripheral, ensure you have this in your main function of the peripheral:
348+
349+
```rust
350+
// for the different addresses for different chips see above
351+
const FLASH_SIZE: u32 = 2 * 1024 * 1024;
352+
const PAGE_SIZE: u32 = 4 * 1024;
353+
const STORAGE_SIZE: u32 = 128 * 1024;
354+
const STATE_OFFSET: u32 = 0x6000;
355+
const STATE_SIZE: u32 = 0x1000;
356+
const ACTIVE_OFFSET: u32 = 0x7000;
357+
let remaining: u32 = FLASH_SIZE - 28 * 1024 - STORAGE_SIZE;
358+
let active_size: u32 = (remaining - PAGE_SIZE) / 2;
359+
let dfu_size: u32 = active_size + PAGE_SIZE;
360+
let dfu_offset: u32 = ACTIVE_OFFSET + active_size;
361+
let storage_offset: u32 = dfu_offset + dfu_size;
362+
363+
rmk::dfu::init_flash(
364+
p.FLASH,
365+
storage_offset,
366+
STORAGE_SIZE,
367+
STATE_OFFSET,
368+
STATE_SIZE,
369+
dfu_offset,
370+
dfu_size,
371+
);
372+
373+
// mark the firmware as booted otherwise the bootloader thinks it didn't and will revert to the old firmware
374+
rmk::dfu::mark_booted();
375+
376+
// you can also set a DFU LED for the perpheral, it flashes during updates
377+
let mut dfu_led_processor = DfuLedProcessor::new(Output::new(p.PIN_25, Level::Low), false);
378+
// if used, run the procesor in run_all:
379+
join(
380+
run_all!(matrix, dfu_led_processor, watchdog_runner),
381+
run_rmk_split_peripheral(uart_instance),
382+
)
383+
.await;
384+
```
385+
386+
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).
387+
388+
### Troubleshooting
389+
390+
| Symptom | Likely cause |
391+
|---|---|
392+
| Peripheral flashes 3 times after update | Firmware crashed or `mark_booted()` not called in the peripheral. Add `[dfu]` to `keyboard.toml` (using config) or call `rmk::dfu::mark_booted()` (using Rust). |
393+
| `dfu-util -a 1` shows `dfuERROR` or hangs at 0% | Peripheral not connected or its firmware is built without the `dfu_split` feature. |
394+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
2+
runner = "probe-rs run --chip RP2040"
3+
linker = "flip-link"
4+
5+
[build]
6+
target = "thumbv6m-none-eabi"
7+
8+
[env]
9+
DEFMT_LOG = "info"
10+
KEYBOARD_TOML_PATH = { value = "keyboard.toml", relative = true }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
rmk-rp2040-dfu-split-*.bin
2+
rmk-rp2040-dfu-split-*.hex
3+
rmk-rp2040-dfu-split-*.uf2
4+
peripheral.bin
5+
central.bin
6+
central.hex
7+
peripheral.hex
8+
central.uf2
9+
peripheral.uf2

0 commit comments

Comments
 (0)