Firmware Update over RDM
Every KLSTR.nano device can be flashed over SWD/SWIM. But in the field the device is often tucked inside a fixture with only DMX/RDM as a communication option - that's why KLSTR.nano also supports Firmware Updates over RDM.
This page explains the protocol, MCU-agnostically. The reference implementation is the KLSTR.nano (STM32F103); a second runs on the ESP32-S3 (KLSTR.fixture).
The idea in one picture
The controller streams the firmware as a sequence of small chunks, each carried
in one RDM SET. The device writes each chunk to its inactive flash slot,
accumulates a checksum, and - once the image lands and verifies - flips a boot flag
and reboots into it.
Loading diagram…
Two properties make this practical on a daisy chain: cut-through forwarding
(every device re-emits RDM bytes port-to-port, so a broadcast chunk reaches all 32
devices deep) and manufacturer broadcast (one SET to a manufacturer's
broadcast UID flashes every device of that manufacturer at once, leaving others
untouched).
The protocol: a manufacturer-specific PID set
The transfer is driven by six manufacturer-specific RDM parameters (PIDs) in the
0xc8xx range - not standard E1.20/E1.37-2 PIDs, since RDM has none for firmware.
Both ends must speak the same PID numbers.
| PID | Name | Class | Payload | Role |
|---|---|---|---|---|
0xc8d7 | file_setup | SET | {u32 flags} → {u32 status} | Arm the receiver before streaming. flags bit 0 = listen to broadcast chunks. |
0xc8cf | file_chunk | SET | {u32 chunk_nb}{≤128 B data} → {u32 status} | One block of the image. chunk 0 is a self-describing header. |
0xc8cd | file_report | GET | {} → {u32 status, u32 crc} | End-of-transfer check. status == 0 ⇒ device believes the image is valid; crc = accumulated CRC32. |
0xc8ce | partition_info | GET | {u32 part_nb} → {u32 addr, u32 crc_fw, u32 crc_ctrl} | A/B slot introspection. addr == 0 ⇒ slot invalid. |
0xc8c8 | control | SET | {u16 index, u32 value} (stackable) → {u32 status} | Device control. index 0x0019 = reset_device → reboot into the new image. |
0xc8f9 | firmware_state | GET | {} → {string} | Human-readable state (silicon vendor / flash size / partition / linker). |
How a transfer runs
Loading diagram…
- Arm -
file_setup(unicast). The controller names each target once, withflagsbit 0 set so it will accept the broadcast chunks that follow. - Stream -
file_chunk. The image is sliced into fixed 128-byte chunks, numbered from zero. Chunk 0 is a header, not data - it carries where in flash the image belongs, where its control block lives, and (if encrypted) the decryption seed; the device uses it to pick the inactive partition and reset its stream state. Real data starts at chunk 1, and the flash offset is computed asstart + 128 × chunk_nb- sequence-addressed, not offset-addressed. - Confirm -
file_report(must be unicast - it's aGET). Returnsstatus == 0if the on-device CRC32 matched, plus thecrcitself so the controller can cross-check against its own. A mismatch means a chunk was dropped. - Reboot -
control{reset_device}. The device replies first, then reboots, so the ACK is never lost. On the way up, the trampoline independently re-validates the image before jumping into it (see ).
Delivery: broadcast fast-path vs. unicast
Chunks broadcast to the manufacturer's broadcast UID (mmmm:FFFFFFFF), fixed
inter-chunk delay, no per-chunk ACK - flashes the whole rig in one pass.
- Spec-legal: ANSI E1.20 §5.3 permits
SETto a manufacturer-specific broadcast; responders must not reply. - Fire-and-forget: pacing is time-based. The nano uses 55 ms between chunks - enough for the slowest device to finish its blocking flash write.
- The catch: no ACK, no retransmit. A dropped chunk fails the final CRC; catch
it per-device via unicast
file_report, then fall back to unicast for stragglers.
Each chunk addressed to one UID, status checked. Reliable but one device at a
time, and requires the controller to relax its RDM reply timeout (the device
answers late while erasing flash).
On the nano this path is kept alive but no longer the primary flow - reserved for single-fixture targeting and retrying a device that failed its CRC after a broadcast pass.
SET can be broadcast; GET cannot - a broadcast may not be answered. So
file_setup and the chunk stream may be broadcast, but file_report must always be
unicast.Each device family advertises its own RDM manufacturer ID, giving it a naturally isolated broadcast group - one family's stream can never touch another's devices, and each family keeps its own broadcast fast-path.
The load-bearing constraint: flash writes vs. the RDM deadline
An RDM responder must answer within a few milliseconds, but a flash erase takes tens of milliseconds. You cannot both write flash and answer RDM on time.
Fail-safe A/B boot
The transfer is brick-safe by construction:
- Write the other slot. The running firmware writes into the inactive partition only, so a failure mid-transfer leaves the running image intact.
- Bump the boot priority of the new slot to one above the running slot, emulating an in-place overwrite without the risk.
- Trampoline validates, then boots. On reset, a small trampoline picks the highest-priority valid partition - independently re-checking the image and control-block CRCs before jumping. A half-written slot fails validation and is skipped; the device falls back to the previous good image.
Integrity & security
RDM is unauthenticated and unencrypted - any controller on the cable can SET, and
a firmware push is the highest-privilege operation there is. The reference
implementation mitigates this two ways:
- Integrity (CRC32) - accumulated on the fly, returned in
file_report, and re-checked independently by the trampoline before boot. Catches corruption, not tampering. - Confidentiality (encrypted images) - release images ship obfuscated/encrypted and are decrypted on-device in the chunk handler; the seed lives in the chunk-0 header.
File format
A release file (.nano on the reference platform) is a ZIP of the two slot
images - one linked for partition A, one for B - each encrypted. The controller
picks the image matching the slot the device will write, reads the embedded control
block (size, CRCs, SHA-1) for its own pre-flight check, and streams it.
Throughput: is it practical?
| Per 128 B chunk | Chunks | Wall-clock | |
|---|---|---|---|
| ~130 KB image, vendorcast @ 55 ms | 55 ms | ~1,000 | ~1 minute (whole rig at once) |
| ~1.5 MB image, vendorcast | ~10–55 ms | ~12,000 | ~2–11 minutes (whole rig at once) |
| ~1.5 MB image, unicast (retry) | ~20–55 ms | ~12,000 | ~4–11 minutes per device |
Expect minutes, not seconds - RDM flashing is a deliberate recovery / no-network fallback, not a replacement for the faster network path. Vendorcast doesn't scale with fixture count (the whole rig flashes in one pass); unicast does. Present it to the operator as the fallback it is.