Firmware over RDM

Firmware Update over RDM

How KLSTR flashes a device's firmware over the DMX cable using the RDM bus - the protocol, the delivery model, and the safety mechanics, abstracted for any MCU.

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).

Building this on a new MCU? Read this page for the mental model, then follow the step-by-step Implementation Guide.

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.

PIDNameClassPayloadRole
0xc8d7file_setupSET{u32 flags}{u32 status}Arm the receiver before streaming. flags bit 0 = listen to broadcast chunks.
0xc8cffile_chunkSET{u32 chunk_nb}{≤128 B data}{u32 status}One block of the image. chunk 0 is a self-describing header.
0xc8cdfile_reportGET{}{u32 status, u32 crc}End-of-transfer check. status == 0 ⇒ device believes the image is valid; crc = accumulated CRC32.
0xc8cepartition_infoGET{u32 part_nb}{u32 addr, u32 crc_fw, u32 crc_ctrl}A/B slot introspection. addr == 0 ⇒ slot invalid.
0xc8c8controlSET{u16 index, u32 value} (stackable) → {u32 status}Device control. index 0x0019 = reset_device → reboot into the new image.
0xc8f9firmware_stateGET{}{string}Human-readable state (silicon vendor / flash size / partition / linker).
Manufacturer PIDs are namespaced by manufacturer ID, so reusing these exact numbers under a different manufacturer is spec-clean with zero collision. This is what lets one controller driver flash multiple device families.

How a transfer runs

Loading diagram…

  1. Arm - file_setup (unicast). The controller names each target once, with flags bit 0 set so it will accept the broadcast chunks that follow.
  2. 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 as start + 128 × chunk_nb - sequence-addressed, not offset-addressed.
  3. Confirm - file_report (must be unicast - it's a GET). Returns status == 0 if the on-device CRC32 matched, plus the crc itself so the controller can cross-check against its own. A mismatch means a chunk was dropped.
  4. 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 ).
Why 128 bytes? The largest power of two that comfortably fits an RDM parameter payload (232-byte cap). Smaller works but is slower; untested below.

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 SET to 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.
Only 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.

On the STM32F103 the CPU can't execute code while flash erases/writes, so the nano simply blocks. It doesn't try to answer on time - the controller accommodates it instead: the 55 ms vendorcast gap outlasts the write, and the unicast timeout is relaxed for the late ACK. Simplest correct model - build this first.

Fail-safe A/B boot

The transfer is brick-safe by construction:

  1. Write the other slot. The running firmware writes into the inactive partition only, so a failure mid-transfer leaves the running image intact.
  2. Bump the boot priority of the new slot to one above the running slot, emulating an in-place overwrite without the risk.
  3. 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.
The streamed CRC and the trampoline's CRC are two independent checks: the streamed one catches transport errors cheaply during transfer; the trampoline's is the final gate that guarantees the device never boots a bad 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.
The nano only accepts an encrypted image when its flash read-protection (SWD lock) is engaged - otherwise the just-written plaintext could be read back over the debug port. If your platform ships encrypted images, gate acceptance on the equivalent read-out protection being locked.
CRC32 is integrity, not authenticity - it stops corruption, not a malicious image. If authenticity matters, the long-term fix is transport-agnostic secure boot with signed images, so a forged image fails at boot regardless of how it arrived.

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 chunkChunksWall-clock
~130 KB image, vendorcast @ 55 ms55 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.

Next steps

Implementation Guide

A single step-by-step build plan for the responder and controller on an arbitrary MCU.

RDM basics

New to RDM? Brush up on discovery, UIDs and the request/response model before diving in.
Copyright © 2026