Skip to main content

Crate hub75_framebuffer

Crate hub75_framebuffer 

Source
Expand description

Framebuffer implementation for HUB75 LED matrix displays.

§How HUB75 LED Displays Work

HUB75 RGB LED matrix panels are scanned, time-multiplexed displays that behave like a long daisy-chained shift register rather than a random-access framebuffer.

§Signal names

  • R1 G1 B1 / R2 G2 B2 – Serial colour data for the upper and lower halves of the active scan line
  • CLK – Shift-register clock; every rising edge pushes the six colour bits one pixel to the right
  • LAT / STB – Latch; copies the shift-register contents to the LED drivers for the row currently selected by the address lines
  • OE – Output-Enable (active LOW): LEDs are lit while OE is LOW and blanked when it is HIGH
  • A B C D (E) – Row-address select lines (choose which pair of rows is lit)
  • VCC & GND – 5 V power for panel logic and LED drivers

§Row-pair scanning workflow (e.g., 1/16-scan panel)

  1. While the panel is still displaying row pair N − 1, the controller shifts the six-bit colour data for row pair N into the chain (OE remains LOW so row N − 1 stays visible).
  2. After the last pixel is clocked in, the controller raises OE HIGH to blank the LEDs.
  3. With the panel blanked, it first changes the address lines to select row pair N, lets them settle for a few nanoseconds, and then pulses LAT to latch the freshly shifted data into the output drivers for that newly selected row.
  4. OE is immediately driven LOW again, lighting row pair N.
  5. Steps 1–4 repeat for every row pair fast enough (hundreds of Hz) that the human eye sees a steady image.
    • If the first row pair is being shifted, the panel continues showing the last row pair of the previous frame until the first blank-address-latch sequence occurs.

§Brightness and colour depth (Binary Code Modulation)

  • Full colour is typically achieved using Binary Code Modulation (BCM), also known as Bit-Angle Modulation (BAM). Each bit-plane is displayed for a duration proportional to its binary weight (1, 2, 4, 8 …), yielding 2ⁿ intensity levels per channel. See Batsocks – LED dimming using Binary Code Modulation for a deeper explanation.
  • Because each LED is on for only a fraction of the total frame time, the driver can use relatively high peak currents without overheating while average brightness is preserved.

§Implications for software / hardware drivers

  • You don’t simply “write a pixel” once; you must continuously stream the complete refresh data at MHz-range clock rates.
  • Precise timing of CLK, OE, address lines, and LAT is critical—especially the order: blank (OE HIGH) → set address → latch → un-blank (OE LOW).
  • Microcontrollers typically employ DMA, PIO, or parallel GPIO tricks, and FPGAs use dedicated logic, to sustain the data throughput while leaving processing resources free.

In short: a HUB75 panel is a high-speed shift-register chain that relies on rapid row-pair scanning and Binary Code Modulation (BCM) to create a bright, full-colour image. Keeping OE LOW almost all the time—blanking only long enough to change the address and pulse LAT—maximises brightness without visible artefacts.

§Framebuffer Implementations

Four framebuffer layouts are provided, covering two hardware variants and two BCM strategies:

ModuleWord sizeExternal latch?BCM strategy
bitplane::plain16-bitNoTrue bitplane
bitplane::latched8-bitYesTrue bitplane
plain (deprecated)16-bitNoThreshold frames
latched (deprecated)8-bitYesThreshold frames

§Plain vs. Latched

  • Plain packs all HUB75 signals (address, latch, OE, colour) into each 16-bit word. No extra hardware beyond a parallel output peripheral.
  • Latched uses 8-bit words with a separate external latch circuit to hold the row address and gate the pixel clock, halving per-entry memory.

§Threshold Frames vs. True Bitplane

The two BCM strategies differ in how they store colour data and how the DMA stream must be driven to render it. The bitplane layouts are recommended for new designs; the threshold layouts are deprecated.

True bitplane (bitplane::plain, bitplane::latched) – each of PLANES planes (typically 8) stores one bit of every colour channel directly. To render, stream each plane’s data a number of times equal to its bit-weight (the MSB plane 128 times, the LSB plane once), either with a DMA descriptor chain or by reprogramming the DMA registers per segment. Memory scales linearly with the number of planes. Each module offers a plane-major (frame) and a row-major (row) layout — both LSB-first with suffix-coalesced BCM segments; see their documentation for the exact scan order.

Threshold frames (plain, latched) – deprecated. The driver compares each channel’s 8-bit value against per-frame thresholds and stores the resulting on/off bits. For a colour depth of BITS, this produces 2^BITS - 1 frames. Frame n is displayed for a duration proportional to 2^n. Memory grows exponentially with colour depth, but the whole refresh is one contiguous buffer that even interrupt-less circular DMA can stream.

All four variants have configurable row and column dimensions, support embedded-graphics via the DrawTarget trait, and expose their BCM scan sequence through the FrameBuffer trait.

§Memory placement (⚠️ large buffers)

Every framebuffer is a large inline array sized from its const generic parameters. Rough sizes:

  • bitplane::plain: PLANES × NROWS × (COLS + gap) × 2 bytes.
  • bitplane::latched: PLANES × NROWS × (COLS + gap) bytes.
  • plain: (2^BITS - 1) × NROWS × COLS × 2 bytes.
  • latched: (2^BITS - 1) × NROWS × (COLS + 4) bytes.

For example, a 64×64 panel at 8-bit colour depth is already tens of kilobytes. Never bind a framebuffer to a task-local let — the whole buffer would be placed on that task’s stack and overflow it.

Framebuffers are mutable (drawing takes &mut self), so they need a 'static home that hands out &'static mut without unsafe. static mut is not an option. Use static_cell::StaticCell, which guarantees the value is taken at most once and lives in .bss:

use embedded_graphics::pixelcolor::RgbColor;
use embedded_graphics::prelude::Point;
use hub75_framebuffer::bitplane::plain::frame::DmaFrameBuffer;
use hub75_framebuffer::Color;
use static_cell::StaticCell;

const COLS: usize = 64;
const NROWS: usize = 32;
const PLANES: usize = 8;
type Fb = DmaFrameBuffer<NROWS, COLS, PLANES>;

// `StaticCell` hands out `&'static mut` without `unsafe`; the value is
// taken at most once and lives in .bss, not on the stack.
static FB: StaticCell<Fb> = StaticCell::new();
let fb = FB.uninit().write(Fb::new());

fb.set_pixel(Point::new(0, 0), Color::RED);

§Driving the Panel (DMA Integration)

This crate is buffer-only: it builds the memory image and describes exactly what to stream via the FrameBuffer trait — writing the peripheral driver (the refresh loop) is up to you. A driver only needs two things from a framebuffer:

  • FrameBuffer::Word — the stream element type: u16 for the plain variants, u8 for the latched ones. The peripheral must output this many bits in parallel and generates the pixel clock (CLK) itself.
  • FrameBuffer::BCM_SEQUENCE and FrameBuffer::bcm_segment — the ordered (ptr, len, reps) segments that make up one complete panel refresh.

The framebuffer must live in DMA-capable memory and outlive the transfer: place it in a static (or a DMA RAM section on chips that require one) and apply whatever alignment and cache maintenance your MCU needs.

§DMA with descriptor chains (least CPU)

The bitplane stream is non-uniform: each segment must be output reps times (the MSB plane up to 2^(PLANES-2) times). This maps naturally onto DMA descriptor chains (e.g. ESP32 GPDMA linked-list mode) — use FrameBuffer::BCM_SEQUENCE / bcm_rep_count to size the descriptor table at compile time and FrameBuffer::bcm_segment to populate it; a segment whose len exceeds the DMA’s maximum transfer size is simply split across several descriptors. Link the chain into a loop and the panel refreshes forever with zero CPU involvement. For free-running loops, consider the tail-closes-latch feature so LAT is not left asserted when the chain wraps.

§Simple DMA (registers only, no descriptors)

If your DMA is just source address + length registers with a transfer-complete interrupt, walk the segments in the ISR: on each interrupt, program the address/length registers from FrameBuffer::bcm_segment, re-arm the transfer, and re-issue the same segment until its reps count is exhausted before advancing. The cadence is modest — the plane-major (frame) layouts raise only PLANES interrupts per panel refresh (8 at full color depth); the row-major (row) layouts raise one per segment of every row (FrameBuffer::BCM_SEGMENT_COUNT per refresh).

If the DMA cannot interrupt at all — truly circular-only hardware — the deprecated threshold framebuffers (plain, latched) remain an option: their entire refresh is one contiguous buffer (FrameBuffer::BCM_SEQUENCE_LEN is 1, reps == 1) with all BCM timing baked into the memory image, at an exponential memory cost.

§Multiple Panels / Scan-Pattern Remapping

Use tiling::RemappedFrameBuffer to drive several HUB75 panels as one large virtual display, or to remap pixels for non-standard scan patterns (e.g. 1/16-scan on 64×64 panels). It works with all four framebuffer types and only requires two generic parameters (F and M).

Combine it with a tiling::PixelRemapper implementation such as tiling::ChainTopRightDown (tiling) or tiling::QuarterScan (1/16-scan). The wrapper exposes a single embedded-graphics canvas, so for example a 3 × 3 stack of 64 × 32 panels simply looks like a 192 × 96 screen while all coordinate translation happens transparently.

The older tiling::TiledFrameBuffer is still available but deprecated.

§Available Feature Flags

§skip-black-pixels Feature (disabled by default)

When enabled, calls to set_pixel() with Color::BLACK return early without writing to the framebuffer. This provides a significant performance boost for UI applications that frequently draw black pixels (backgrounds, clearing, etc.) by assuming the framebuffer was already cleared.

Important: This optimization assumes that black pixels represent “no change” rather than “explicitly set to black”. By default, black pixels are written normally to ensure correct overwrite behavior. To enable the optimization:

[dependencies]
hub75-framebuffer = { version = "0.13.0", features = ["skip-black-pixels"] }

§esp32-ordering Feature (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. The original ESP32 has different byte ordering requirements compared to other ESP32 variants (S2, S3, C3, etc.), which do not need this feature.

[dependencies]
hub75-framebuffer = { version = "0.13.0", features = ["esp32-ordering"] }

§interstate75 Feature (Pimoroni Interstate 75 / 75 W only)

Selects the 16-bit pin layout of the Pimoroni Interstate 75 and Interstate 75 W (RP2040 / RP2350). The DMA word is pre-aligned to the board’s contiguous GPIO mapping (GPIO0..GPIO13) so a PIO out pins, 16 stream can clock out the panel with no run-time bit shuffling:

bit  13  12    11     10..6      5..3        2..0
     OE  LAT  (spare)  E D C B A  B1 G1 R1   B0 G0 R0

Bit 11 is unused by this crate and always left at 0.

Applies to the 16-bit plain and bitplane::plain framebuffers only; the 8-bit latched framebuffers are unaffected. Mutually exclusive with esp32-ordering.

[dependencies]
hub75-framebuffer = { version = "0.13.0", features = ["interstate75"] }

§tail-closes-latch Feature (plain framebuffers only)

Appends a single extra “tail” word at the end of the DMA buffer that drives the LATCH signal LOW (de-asserted) on the final clock edge. Without this feature the last word in each row asserts LATCH HIGH to latch shifted data into the LED drivers, and the GPIO pins remain in that state after the DMA transfer completes. Some hardware configurations (e.g. free-running DMA loops or peripherals that continue clocking after the descriptor chain ends) can re-latch stale data or glitch if LATCH is left asserted.

Enabling tail-closes-latch adds one 16-bit Entry (for plain) or one entry per bit-plane (for bitplane::plain) that parks the bus with LATCH=0 and OE=BLANK, cleanly terminating the transfer. The cost is a single extra word per DMA chunk, which is negligible compared to the frame data.

[dependencies]
hub75-framebuffer = { version = "0.13.0", features = ["tail-closes-latch"] }

§reverse-row-order Feature (disabled by default)

Stores the rows of every framebuffer layout in reverse scan order so that the DMA stream renders the last panel row first and row 0 last. The rows are physically reversed in the buffer (row addresses are written back-to-front while keeping the deferred address-change timing intact) and the pixel-setting paths map logical rows to the reversed slots, so logical coordinates are unchanged — only the scan order changes.

[dependencies]
hub75-framebuffer = { version = "0.13.0", features = ["reverse-row-order"] }

§Blanking delay features (lead-blank-* / trail-blank-*)

Control the number of pixel-clock cycles of blanking (OE HIGH) inserted around row-address changes. The lead blank controls how many cycles the output is blanked before the row address is changed, and the trail blank controls blanking after the row address is changed. Together they give the address lines time to settle and prevent ghosting or “bleeding” artifacts caused by the panel briefly displaying data on the wrong row during the transition.

FeatureBlanking cyclesPosition
(none)1 (default)before & after change
lead-blank-11before address change
lead-blank-22before address change
lead-blank-44before address change
lead-blank-88before address change
lead-blank-1616before address change
trail-blank-11after address change
trail-blank-22after address change
trail-blank-44after address change
trail-blank-88after address change
trail-blank-1616after address change

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.

[dependencies]
hub75-framebuffer = { version = "0.13.0", features = ["lead-blank-4", "trail-blank-2"] }

Note: At most one lead-blank-* and one trail-blank-* feature may be enabled at a time. If multiple are enabled for the same edge, compile-time cfg conflicts will result.

§Inter-row blanking features (inter-row-blank-*)

Insert additional dead clock cycles at the row transition, between the latch and the address-line change. In plain framebuffers the gap entries hold the previous row address with OE HIGH (blank), deferring the address change to the first pixel after the gap and giving slow panels more time to finish blanking before the address lines move. In latched framebuffers the latch and address change are inseparable in hardware, so the gap simply adds extra blanked cycles after the address change. Row-major bitplane framebuffers stream the gap between plane 0 (shifted out before the address change) and plane 1 (shifted out at/after it); all other framebuffers place it at the end of each row, between that row’s latch and the next row’s first pixel. The gap entries are invisible to all drawing primitives — they only appear in the DMA stream.

FeatureGap cyclesRAM cost per row
(none)00 bytes
inter-row-blank-448 bytes (16-bit) / 4 bytes (8-bit)
inter-row-blank-8816 bytes / 8 bytes
inter-row-blank-161632 bytes / 16 bytes
inter-row-blank-323264 bytes / 32 bytes
[dependencies]
hub75-framebuffer = { version = "0.13.0", features = ["inter-row-blank-8"] }

Note: At most one inter-row-blank-* feature may be enabled at a time. The inter-row-blank feature (no number) is an internal umbrella that is enabled automatically by any inter-row-blank-* feature — do not enable it directly. These are independent of the lead-blank-* / trail-blank-* features and can be combined with them.

§defmt Feature

Implements defmt::Format for framebuffer types so they can be emitted with the defmt logging framework. No functional changes; purely adds a trait impl.

§doc-images Feature

Embeds documentation images when building docs on docs.rs. Not needed for normal usage.

Modules§

bitplane
Bitplane-oriented framebuffer implementations for HUB75 LED panels.
latched
DMA-friendly framebuffer implementation for HUB75 LED panels with external latch circuit support.
pinmap
Compile-time HUB75-signal → DMA-word bit mapping.
plain
DMA-friendly framebuffer implementation for HUB75 LED panels.
tiling
Coordinate remapping for tiled and scan-pattern LED panel arrangements

Structs§

BcmLenReps
Static (len, reps) pair describing one BCM segment without a pointer.
BcmSegment
A single segment of the BCM scan sequence.

Enums§

WordSize
Word size configuration for the framebuffer

Constants§

BCM_SEQUENCE_CAPACITY
Capacity of the FrameBuffer::BCM_SEQUENCE array.

Traits§

FrameBuffer
Framebuffer that exposes its BCM scan sequence as an ordered series of segments.
FrameBufferOperations
Trait for all operations a user may want to call on a framebuffer.
MutableFrameBuffer
Trait for mutable framebuffers that support embedded_graphics drawing.

Functions§

bcm_rep_count
Sum of reps across one complete panel refresh.
compute_frame_count
Computes the number of frames needed for a given bit depth
compute_rows
Computes the NROWS value from ROWS for DmaFrameBuffer

Type Aliases§

Color
Color type used in the framebuffer