hub75-framebuffer 0.13.0

DMA-friendly Framebuffer implementation for HUB75 LED matrix displays
Documentation
//! Bitplane framebuffers for a 16-bit plain HUB75 interface.
//!
//! All signals (row address, latch, OE, colour data) are packed into each
//! 16-bit word -- no external latch circuit is required.
//!
//! Two layout variants are provided:
//!
//! - [`frame`] -- Plane-major: all rows for one bit-plane stored contiguously.
//!   The DMA chain sends each plane's data separately.
//! - [`row`] -- Row-major: all bit-planes for a single row stored contiguously.
//!   Optimized for row-by-row BCM where the inter-row gap appears only once
//!   per scan row instead of once per plane.
//!
//! Both variants share the same 16-bit `Entry` format and pin mapping.
//!
//! # Hardware Requirements
//! Requires a parallel output peripheral capable of clocking 16 bits (though
//! only 15 are actually used) at a time. The data is structured to directly
//! match the HUB75 connector signals, so no external latch or address-decode
//! hardware is needed.
//!
//! # HUB75 Signal Bit Mapping
//! Each 16-bit `Entry` represents the logic levels driven onto the HUB75 bus
//! during a single pixel-clock cycle:
//!
//! ```text
//! default (ESP32 I2S / generic)          interstate75 (RP2040/RP2350 PIO)
//! 15 ─ (spare)                           15 ─ (spare)
//! 14 ─ B1       Blue  – lower half       14 ─ (spare)
//! 13 ─ G1       Green – lower half       13 ─ OE    Output-Enable / Blank
//! 12 ─ R1       Red   – lower half       12 ─ LAT   Latch / STB
//! 11 ─ B0       Blue  – upper half       11 ─ (spare)
//! 10 ─ G0       Green – upper half       10 ─ E     Row address
//!  9 ─ R0       Red   – upper half        9 ─ D     Row address
//!  8 ─ OE       Output-Enable / Blank      8 ─ C     Row address
//!  7 ─ (spare)                             7 ─ B     Row address
//!  6 ─ (spare)                             6 ─ A     Row address
//!  5 ─ LAT      Latch / STB                5 ─ B1    Blue  – lower half
//! 4-0 ─ A..E    Row address lines          4 ─ G1    Green – lower half
//!                                          3 ─ R1    Red   – lower half
//!                                          2 ─ B0    Blue  – upper half
//!                                          1 ─ G0    Green – upper half
//!                                          0 ─ R0    Red   – upper half
//! ```
//!
//! The board-specific layout is selected by the `interstate75` feature; see
//! [`crate::pinmap`].
//!
//! The pixel clock is generated by the peripheral that owns the DMA stream and
//! is not part of the 16-bit word stored in the framebuffer.
//!
//! # Bitplane BCM Rendering
//! Each framebuffer is organised into `PLANES` bit-planes, one per colour
//! bit. To produce correct brightness via Binary Code Modulation, output each
//! plane's data (scan it) a number of times equal to its bit-weight (2^7 for
//! the MSB plane down to 2^0 for the LSB plane) — either with a DMA
//! descriptor chain or by reprogramming the DMA registers per segment from a
//! transfer-complete interrupt.
//!
//! Both layout variants store planes **LSB-first**: plane 0 carries bit 0
//! and is displayed once per scan, plane 7 carries bit 7 and is displayed
//! 128 times. Each BCM segment streams a contiguous *suffix* of planes,
//! repeated just enough times to bring each plane's total coverage to its
//! bit-weight — halving the number of DMA transfers per scan with identical
//! brightness. LSB-first ordering also means the first plane after an
//! address change displays stale data with minimal visual weight, avoiding
//! a separate primer segment. See the [`frame`] and [`row`] module
//! documentation for the exact scan order.
//!
//! See <https://www.batsocks.co.uk/readme/art_bcm_1.htm> for background on
//! BCM.

use bitfield::bitfield;

pub mod frame;
pub mod row;

pub use frame::DmaFrameBuffer;

#[cfg(feature = "tail-closes-latch")]
pub(crate) use crate::pinmap::ADDR_MASK;
pub(crate) use crate::pinmap::PINMAP;
pub(crate) use crate::pinmap::{
    ADDR_SHIFT, COLOR0_MASK, COLOR0_SHIFT, COLOR1_MASK, COLOR1_SHIFT, COLOR_MASK, LATCH_BIT, OE_BIT,
};
pub(crate) use crate::{INTER_ROW_BLANK, LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};

// The OE *level* constants stay local: their default polarity differs between
// this module (standard active-low) and the deprecated `plain` module
// (active-high). Only the bit position (`pinmap::OE_BIT`) is shared.
#[cfg(feature = "invert-oe")]
pub(crate) const OE_ACTIVE: u16 = OE_BIT;
#[cfg(feature = "invert-oe")]
pub(crate) const OE_BLANK: u16 = 0;

#[cfg(not(feature = "invert-oe"))]
pub(crate) const OE_ACTIVE: u16 = 0;
#[cfg(not(feature = "invert-oe"))]
pub(crate) const OE_BLANK: u16 = OE_BIT;

// ---------------------------------------------------------------------------
// Shared helper functions
// ---------------------------------------------------------------------------

#[inline]
pub(crate) const fn map_index(i: usize) -> usize {
    #[cfg(feature = "esp32-ordering")]
    {
        i ^ 1
    }
    #[cfg(not(feature = "esp32-ordering"))]
    {
        i
    }
}

/// Builds the per-row pixel template with trail blank, active pixels, lead
/// blank, and latch embedded within the COLS entries.
#[allow(clippy::absurd_extreme_comparisons)]
#[inline]
pub(crate) const fn make_data_template<const COLS: usize>(addr: u8) -> [Entry; COLS] {
    let mut data = [Entry::new(); COLS];
    let mut i = 0;

    while i < COLS {
        let mut entry = Entry::new();
        entry.0 = ((addr as u16) << ADDR_SHIFT) | OE_BLANK;

        if i == COLS - 1 {
            entry.0 |= LATCH_BIT;
        } else if i >= TRAIL_BLANK_DELAY && i < COLS.saturating_sub(LEAD_BLANK_DELAY + 1) {
            entry.0 = (entry.0 & !OE_BIT) | OE_ACTIVE;
        }

        data[map_index(i)] = entry;
        i += 1;
    }

    data
}

// ---------------------------------------------------------------------------
// Entry — the 16-bit word that rides on the HUB75 bus
// ---------------------------------------------------------------------------

// The bit layout is selected per board by `crate::pinmap`; this single
// `bitfield!` definition reads the positions straight from `PINMAP` so the
// accessors can never drift out of sync with the DMA templates. Note the
// `red1/grn1/blu1` accessors are the *upper-half* triplet (`PINMAP.red0`...),
// and `red2/grn2/blu2` the lower half.
bitfield! {
    #[derive(Clone, Copy, Default, PartialEq)]
    #[repr(transparent)]
    pub(crate) struct Entry(u16);
    pub(crate) dummy2, set_dummy2: PINMAP.spare[2];
    pub(crate) blu2, set_blu2: PINMAP.blu1;
    pub(crate) grn2, set_grn2: PINMAP.grn1;
    pub(crate) red2, set_red2: PINMAP.red1;
    pub(crate) blu1, set_blu1: PINMAP.blu0;
    pub(crate) grn1, set_grn1: PINMAP.grn0;
    pub(crate) red1, set_red1: PINMAP.red0;
    pub(crate) output_enable, set_output_enable: PINMAP.oe;
    pub(crate) dummy1, set_dummy1: PINMAP.spare[1];
    pub(crate) dummy0, set_dummy0: PINMAP.spare[0];
    pub(crate) latch, set_latch: PINMAP.latch;
    pub(crate) addr, set_addr: PINMAP.addr_msb(), PINMAP.addr_lsb;
}

impl core::fmt::Debug for Entry {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_tuple("Entry")
            .field(&format_args!("{:#x}", self.0))
            .finish()
    }
}

impl Entry {
    pub(crate) const fn new() -> Self {
        Self(OE_BLANK)
    }

    pub(crate) const fn from_raw(val: u16) -> Self {
        Self(val)
    }

    #[allow(dead_code)]
    pub(crate) const fn raw(self) -> u16 {
        self.0
    }

    pub(crate) fn mask_raw(&mut self, mask: u16) {
        self.0 &= mask;
    }

    // The colour bit positions/masks come from `crate::pinmap` so they follow
    // the selected board layout.
    #[inline]
    pub(crate) fn set_color0_bits(&mut self, bits: u8) {
        let bits16 = u16::from(bits) << COLOR0_SHIFT;
        self.0 = (self.0 & !COLOR0_MASK) | (bits16 & COLOR0_MASK);
    }

    #[inline]
    pub(crate) fn set_color1_bits(&mut self, bits: u8) {
        let bits16 = u16::from(bits) << COLOR1_SHIFT;
        self.0 = (self.0 & !COLOR1_MASK) | (bits16 & COLOR1_MASK);
    }
}