Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LED Matrix External Protocol

Wire protocol the RGB LED Matrix Display device (config type display/matrix) uses to stream its per-matrix pixel and palette data to an external peripheral process — the bundled emma65-led-matrix binary, or a replacement for it — over an attached transport, when running the plain emma65 CLI standalone. It’s unrelated to how the debugger renders the same device in-process, which needs no wire protocol at all (same address space, same process), and unrelated to the Character Display External Protocol — a different device with different needs, most notably that this device’s matrices swap per-matrix rather than in lockstep across the whole device on a single vsync. See Running the LED Matrix Peripheral for how to configure and launch emma65-led-matrix itself, and RGB LED Matrix Display for the device’s bus-facing register behavior — this page covers only what crosses the transport.

Transport requirements

Exactly one connection, outbound only (device → peripheral) — unlike the Character Display, this device has no input capability, so there is no inbound direction. The transport send must be atomic — either the whole buffer is written or none of it is — which in practice means only pipe: is supported; any other transport spec is rejected for a display/matrix device at configuration time.

Message framing

Unlike the Character Display protocol, messages here are tagged rather than shaped as one fixed-size frame — swaps happen per-matrix, at unpredictable intervals relative to each other, so there is no single per-tick “frame” to send as a unit. The header is sent exactly once, immediately when the transport is attached. Every subsequent message begins with a one-byte tag that determines its fixed total length; there are no length prefixes anywhere in this protocol. This is only safe because of the transport atomicity requirement above: a transport that could deliver a partial message would desync the stream permanently, with no way to resynchronize.

Header (sent once, on attach)

FieldTypeSize (bytes)Notes
magicASCII4"E65M" — distinct from the trace format’s "E65T" and the display protocol’s "E65D"
versionu812
matrix_countu81number of matrices configured, 1..=8
columnsu81the device’s configured arrangement’s column count; the peripheral derives row count as matrix_count / columns, which always divides evenly
frame_rate_hzu32 LE4auto-refresh cadence; informational only — a peripheral is not required to sync its own redraw to it

Total header size: 11 bytes. Unlike the Character Display protocol’s header, there is no palette or per-matrix dimension field: matrix dimensions are a fixed 32×32 constant that both sides already know, and the palette is never transferred at connection time (see Runtime palette updates).

columns was added in version 2, replacing the peripheral’s own --arrangement command-line flag: the peripheral’s on-screen layout now always mirrors the device’s actual bus-addressing arrangement rather than an independently chosen value that could disagree with it.

Messages (sent as they occur)

Every message after the header begins with a one-byte tag identifying its type and fixed length.

Block (MSG_BLOCK = 1, sent once per matrix swap)

FieldTypeSize (bytes)Notes
tagu811
matrix_indexu81which matrix this block belongs to, 0..matrix_count
pixelsraw1024one palette-index byte per pixel, row-major, top row first

Total message size: 1026 bytes. Sent whenever a matrix is swapped to its visible buffer — whether triggered by the SWAP command or by auto-refresh — carrying that matrix’s contents exactly as swapped. The peripheral is expected to composite these raw indices against its own copy of the current palette (see below), the same way the debugger’s in-process rendering does.

Palette (MSG_PALETTE = 2, sent only on an actual PALETTE_WRITE)

FieldTypeSize (bytes)Notes
tagu812
indexu81palette entry updated, 0..256
coloru16 LE2packed RGB565 (rrrrrggggggbbbbb), the entry’s new value

Total message size: 4 bytes. Sent whenever a PALETTE_WRITE command is applied, carrying the already-quantized RGB565 value stored in the device’s palette table — the same value a subsequent PALETTE_READ of that entry would report (scaled back up to 8-bit components), not the original pre-quantization write bytes.

Power (MSG_POWER = 3, sent only on an actual SET_POWER)

FieldTypeSize (bytes)Notes
tagu813
masku81new power-state bitmask, one bit per matrix

Total message size: 2 bytes. Sent whenever a SET_POWER command is applied. The peripheral must retain this mask and reapply it to every future composite of each affected matrix, the same way it already retains the palette — a powered-off matrix composites to fully black regardless of palette content.

Brightness (MSG_BRIGHTNESS = 4, sent only on an actual SET_BRIGHTNESS)

FieldTypeSize (bytes)Notes
tagu814
levelu81new global brightness level, 0..=255

Total message size: 2 bytes. Sent whenever a SET_BRIGHTNESS command is applied. The peripheral must retain this value and reapply it to every future composite of every matrix, the same way it already retains the palette.

Power and brightness messages are a pure addition to this tagged scheme, requiring no change to any existing message’s framing.

Startup state (device → peripheral)

The device never re-sends the full contents of every matrix or the whole palette at connection time. A peripheral that attaches after the device has already been running sees only messages for matrices swapped, palette entries written, and power/brightness changes made from that point forward; anything unset renders using the peripheral’s own reconstruction of the built-in default palette, all-zero (index 0) pixel data, and full power/brightness (power_mask = 0xFF, brightness = 0xFF), matching the device’s own defaults at startup.

Runtime palette updates

Unlike the Character Display, which resends its entire palette with every frame, this device’s palette is comparatively large (256 entries, RGB565) and changes independently of any single matrix’s swap cadence, so each write is sent as its own small message (see Palette above) instead. A peripheral must therefore retain every matrix’s most recently received raw pixel indices as well as its own copy of the palette, and recomposite every matrix’s stored pixels whenever a palette message arrives — palette changes are not re-sent per matrix.