Skip to main content

Crate esp_hub75

Crate esp_hub75 

Source
Expand description

§ESP-HUB75

A no-std driver for HUB75-style LED matrix panels on ESP32-series microcontrollers.

The panel is refreshed over DMA with almost no CPU involvement, using whichever peripheral fits each chip best:

  • ESP32-S3: Uses the LCD_CAM peripheral
  • ESP32-C6: Uses the PARL_IO peripheral
  • ESP32-C5: Uses the PARL_IO peripheral (8-bit mode only; requires a latch circuit and Hub75Pins8)
  • ESP32: Uses the I2S peripheral in parallel mode

§Framebuffers

Use the bitplane framebuffers from the hub75-framebuffer crate. They come in two variants: direct-drive (16-bit, no external latch) and latched (8-bit, needs an external address-latch circuit). Both can be handed to the peripheral as-is; there is no extra formatting step.

Bitplane framebuffers (framebuffer::bitplane::plain::DmaFrameBuffer / framebuffer::bitplane::latched::DmaFrameBuffer) store only one bit per pixel per plane. The driver assembles the BCM (Binary Code Modulation) output on the fly with DMA descriptors, so RAM use stays low without losing visual quality.

§Usage

Example for ESP32-C6:

#![no_std]
#![no_main]

use embedded_graphics::Drawable;
use embedded_graphics::geometry::Point;
use embedded_graphics::mono_font::MonoTextStyleBuilder;
use embedded_graphics::mono_font::ascii::FONT_5X7;
use embedded_graphics::prelude::RgbColor;
use embedded_graphics::text::Alignment;
use embedded_graphics::text::Text;
use esp_backtrace as _;
use esp_hal::clock::CpuClock;
use esp_hal::gpio::Pin;
use esp_hal::main;
use esp_hub75::Color;
use esp_hub75::Hub75;
use esp_hub75::Hub75Pins16;
use esp_hub75::framebuffer::bitplane::plain::DmaFrameBuffer;
use esp_hub75::framebuffer::compute_rows;

esp_bootloader_esp_idf::esp_app_desc!();

const ROWS: usize = 64;
const COLS: usize = 64;
const NROWS: usize = compute_rows(ROWS);
const PLANES: usize = 4;

type FBType = DmaFrameBuffer<NROWS, COLS, PLANES>;

macro_rules! mk_static {
    ($t:ty,$val:expr) => {{
        static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
        #[deny(unused_attributes)]
        let x = STATIC_CELL.uninit().write($val);
        x
    }};
}

#[main]
fn main() -> ! {
    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::max()));

    let tx_descriptors = esp_hub75::hub75_dma_descriptors!(FBType);

    let pins = Hub75Pins16 {
        red1: peripherals.GPIO19.degrade(),
        grn1: peripherals.GPIO20.degrade(),
        blu1: peripherals.GPIO21.degrade(),
        red2: peripherals.GPIO22.degrade(),
        grn2: peripherals.GPIO23.degrade(),
        blu2: peripherals.GPIO15.degrade(),
        addr0: peripherals.GPIO10.degrade(),
        addr1: peripherals.GPIO8.degrade(),
        addr2: peripherals.GPIO1.degrade(),
        addr3: peripherals.GPIO0.degrade(),
        addr4: peripherals.GPIO11.degrade(),
        blank: peripherals.GPIO5.degrade(),
        clock: peripherals.GPIO7.degrade(),
        latch: peripherals.GPIO6.degrade(),
    };

    let fb = mk_static!(FBType, FBType::new());
    let text_style = MonoTextStyleBuilder::new()
        .font(&FONT_5X7)
        .text_color(Color::YELLOW)
        .background_color(Color::BLACK)
        .build();
    let point = Point::new(32, 32);
    Text::with_alignment("Hello, World!", point, text_style, Alignment::Center)
        .draw(fb)
        .expect("failed to draw text");

    let _hub75 = Hub75::new(
        peripherals.PARL_IO,
        pins,
        peripherals.DMA_CH0,
        tx_descriptors,
        Hub75Config::new(),
        &*fb,
    )
    .expect("failed to create Hub75");

    loop {
        core::hint::spin_loop();
    }
}

§Crate Features

  • esp32: Enable support for the ESP32
  • esp32s3: Enable support for the ESP32-S3
  • esp32c5: Enable support for the ESP32-C5
  • esp32c6: Enable support for the ESP32-C6
  • defmt: Enable logging with defmt
  • log: Enable logging with the log crate
  • invert-blank: Invert the blank signal in hardware by enabling the GPIO output inverter on the blank pin. Applies to both 8-bit latched (Hub75Pins8) and 16-bit direct-drive (Hub75Pins16) configurations. Some latch controller boards include a hardware inverter on the blank line; enable this feature to compensate.
  • invert-clock: Invert the clock signal. By default the driver outputs data that changes on the falling edge of CLK so that it is stable when the panel latches on the rising edge. Enable this feature if your panel requires the opposite polarity.
  • invert-oe: Forwards to the hub75-framebuffer crate, inverting the output-enable (OE) signal in the generated data stream. Whereas invert-blank inverts the blank pin in hardware, this feature flips the OE polarity at the framebuffer level instead. The two features may seem redundant but are meant to be used together: where the peripheral drives all pins to 0 when a transfer completes, invert-blank turns that idle 0 into a 1 (blanked), and invert-oe compensates for the now-inverted pin.
  • full-chain-dma: Build the entire BCM repetition chain in a single DMA transfer instead of one plane per interrupt. This reduces interrupt frequency at the cost of more DMA descriptor RAM. Note that the ESP32-C6 PARL_IO peripheral has a 65,535-byte per-transfer limit, which constrains the maximum panel size and plane count when this feature is enabled.
  • circular-dma: Circular DMA descriptor chain (implies full-chain-dma). The DMA engine starts once and loops forever; buffer swaps are pointer-delta updates applied by the swap-boundary ISR at a pass boundary, so there is no DMA stop/restart and no mid-frame tearing. In steady state no interrupts are enabled: a swap temporarily arms the boundary detector (suc_eof on the last descriptor) and the ISR disarms it again after applying the swap. On ESP32-C5 (PARL_IO) a consumed suc_eof halts the DMA channel, so the ISR restarts the transfer after each swap; on ESP32/S3 the chain free-runs uninterrupted. Supported on ESP32 (I2S), ESP32-S3 (LCD_CAM), and ESP32-C5 (PARL_IO); on ESP32-C6 this is a compile-time error because PARL_IO cannot do circular chains.
  • skip-black-pixels: Forwards to the hub75-framebuffer crate, enabling an optimization that skips writing black pixels to the framebuffer.
  • tail-closes-latch: Forwards to the hub75-framebuffer crate. Appends a tail word at the end of each DMA buffer (plain framebuffers) or at the end of each bit-plane (bitplane::plain) that drives LATCH LOW when the transfer completes. Does not apply to latched framebuffers.
  • iram: Place the driver’s hot path — the refresh ISR, the DMA start/finish/wait path, and the BCM segment and descriptor bookkeeping, including the framebuffer pointer-delta swap — in Instruction RAM (IRAM) to avoid flash-cache stalls (for example during Wi-Fi, PSRAM, or SPI-flash activity) that can cause visible flicker. Drawing (set_pixel) stays in flash. Costs roughly 1–2 KiB of IRAM (about 4 KiB at opt-level = 0).
  • lead-blank-1/2/4/8/16 / trail-blank-1/2/4/8/16: Forwards to hub75-framebuffer. Control the number of pixel-clock cycles of blanking (OE HIGH) inserted around row address changes. The lead blank controls blanking before the address change, and the trail blank controls blanking after. Higher values reduce ghosting at the cost of slightly less brightness.
  • inter-row-blank-4/8/16/32: Forwards to hub75-framebuffer. Insert additional dead clock cycles at the end of each row. In plain framebuffers the gap defers the address change to the first pixel of the next row, giving slow panels more time to finish blanking. In latched framebuffers the gap adds extra blanked cycles after the address change.
  • reverse-row-order: Forwards to hub75-framebuffer. Stores the rows of the framebuffer in reverse scan order so that the DMA stream renders the last panel row first and row 0 last.

§Safety

This crate uses unsafe code to interface with hardware peripherals, but it exposes a safe, high-level API.

Re-exports§

pub use hub75_framebuffer as framebuffer;

Macros§

hub75_dma_descriptors
Allocates static DMA descriptors sized for the given framebuffer type.

Structs§

Hub75
HUB75 display controller driven by an interrupt-based BCM refresh loop.
Hub75Config
Configuration for creating a Hub75 instance.
Hub75DmaDescriptors
DMA descriptor storage bound to a specific framebuffer type.
Hub75Pins8
Pin configuration for a HUB75 panel with an external address latch.
Hub75Pins16
Pin configuration for a HUB75 panel without an external address latch.
Hub75Swap
A pending framebuffer swap.

Enums§

Hub75Error
Errors returned by the HUB75 driver.

Traits§

Hub75Pins
Describes the pins used to drive a HUB75 panel.

Functions§

dma_descriptor_count
Computes the number of DMA descriptors this driver needs for a framebuffer of type FB.
frame_clock_cycles
Number of pixel-clock cycles the DMA streams for one complete panel refresh of framebuffer type FB.
refresh_hz
Theoretical refresh rate (in Hz) for framebuffer type FB at the given HUB75 pixel-clock frequency.

Type Aliases§

Color
The color type used by the HUB75 driver. Color type used in the framebuffer