hub75-framebuffer
DMA-friendly framebuffer implementations for driving HUB75 RGB LED matrix
panels with Rust. The crate focuses on performance, correct timing,
and ergonomic drawing by integrating tightly with the embedded-graphics
ecosystem.
How HUB75 LED panels work (very short recap)
A HUB75 panel behaves like a long daisy-chained shift-register:
- Color data for one pair of rows is shifted in serially on every cycle of
CLK. - After the last pixel of the row pair has been clocked, the controller blanks
the LEDs (
OEHIGH), sets the address lines A–E, and produces a short pulse onLATto latch the freshly-shifted data into the LED drivers. OEgoes LOW again and the row pair lights up while the next one is already being shifted.
Color depth is achieved with Binary/Bit-Angle Code Modulation (BCM): lower bit-planes are shown for shorter times, higher ones for longer, yielding 2^n intensity levels per channel while keeping peak currents low.
If you want a deeper explanation, have a look inside src/lib.rs — the crate
documentation contains an extensive primer.
Two framebuffer flavors
| Module | Extra hardware | Word size | Memory use | Pros / Cons |
|---|---|---|---|---|
plain |
none | 16 bit (14 used) | high | Simplest, wires exactly like a standard HUB75 matrix. |
latched |
external latch gate (see below) | 8 bit | ×½ of plain |
Lower memory footprint, but needs a tiny glue-logic board. |
Multiple Panels
- Use
tiling::TiledFrameBufferto drive several HUB75 panels as one large display. - Combine it with a pixel-remapping policy like
ChainTopRightDownand any of the framebuffers above (plain or latched). - The wrapper exposes a single
embedded-graphicscanvas, so a 3 × 3 stack of 64 × 32 panels simply looks like a 192 × 96 screen while all coordinate translation happens transparently.
The latch circuit
The latched implementation assumes a small external circuit that holds the row address while gating the pixel clock. A typical solution uses a 74xx373 latch along with a few NAND gates:

The latch IC stores the address bits whilst one NAND gate blocks the CLK
signal during the latch interval. The remaining spare gate can be employed
to combine a global PWM signal with OE for fine-grained brightness control
as shown.
Getting started
Add the dependency to your Cargo.toml:
[]
= "0.8.1"
Choose your parameters
use ;
use DmaFrameBuffer;
// or ::plain::DmaFrameBuffer
const ROWS: usize = 32; // panel height
const COLS: usize = 64; // panel width
const BITS: u8 = 3; // colour depth ⇒ 7 BCM frames
const NROWS: usize = compute_rows; // 16
const FRAME_COUNT:usize = compute_frame_count; // (1<<BITS)-1 = 7
// Create a framebuffer (already initialized/cleared)
let mut framebuffer =
new;
You can now draw using any embedded-graphics primitive:
use *;
use ;
use Color;
new
.into_styled
.draw
.unwrap;
new
.into_styled
.draw
.unwrap;
Finally hand the raw DMA buffer off to your MCU's parallel peripheral.
Crate features
esp-hal-dma (required when using esp-hal)
Required when using the esp-hal crate for ESP32 development. This
feature switches the ReadBuffer trait implementation from embedded-dma
to esp-hal::dma. If you're targeting ESP32 devices with esp-hal, you
must enable this feature for DMA compatibility.
[]
= { = "0.8.1", = ["esp-hal-dma"] }
esp32-ordering (required for original ESP32 only)
Required when targeting the original ESP32 chip (not ESP32-S3 or other variants). This feature adjusts byte ordering to accommodate the quirky requirements of the ESP32's I²S peripheral in 8-bit and 16-bit modes. Other ESP32 variants (S2, S3, C3, etc.) do not need this feature.
[]
= { = "0.8.1", = ["esp32-ordering"] }
skip-black-pixels
Skip drawing black pixels for performance boost in UI applications. When
enabled, calls to set_pixel() with Color::BLACK return early without
writing to the framebuffer, assuming the framebuffer was already cleared.
blank-delay-1 / blank-delay-2 / blank-delay-4 / blank-delay-8
Control the number of pixel-clock cycles of blanking (OE HIGH) inserted around
row address changes in the plain framebuffers (plain and bitplane::plain).
The blanking delay gives the address lines time to settle before the new row is
latched and lit, preventing ghosting or "bleeding" artifacts between rows.
| Feature | Blanking cycles |
|---|---|
| (none) | 1 (default) |
blank-delay-1 |
1 |
blank-delay-2 |
2 |
blank-delay-4 |
4 |
blank-delay-8 |
8 |
Higher values reduce ghosting at the cost of slightly less brightness (the LEDs are on for less time per scan line). Start with the default and increase only if you observe row-transition artifacts on your particular panel hardware.
[]
= { = "0.8.1", = ["blank-delay-4"] }
Note: Only one blank-delay-* feature should be enabled at a time. If
multiple are enabled, compile-time cfg conflicts will result.
defmt
Implement the defmt::Format trait so framebuffer types can be logged with
the defmt ecosystem.
doc-images
Embed documentation images when building docs on docs.rs. Not needed for normal usage.
Enable features in your Cargo.toml:
[]
= { = "0.8.1",
features = ["esp-hal-dma", "esp32-ordering"] }
Running tests
cargo test
All logic including bitfields, address mapping, brightness modulation and
the embedded-graphics integration is covered by a comprehensive test-suite
(≈ 300 tests).
License
Licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.