Skip to main content

hub75_framebuffer/bitplane/plain/
mod.rs

1//! Bitplane framebuffers for a 16-bit plain HUB75 interface.
2//!
3//! All signals (row address, latch, OE, colour data) are packed into each
4//! 16-bit word -- no external latch circuit is required.
5//!
6//! Two layout variants are provided:
7//!
8//! - [`frame`] -- Plane-major: all rows for one bit-plane stored contiguously.
9//!   The DMA chain sends each plane's data separately.
10//! - [`row`] -- Row-major: all bit-planes for a single row stored contiguously.
11//!   Optimized for row-by-row BCM where the inter-row gap appears only once
12//!   per scan row instead of once per plane.
13//!
14//! Both variants share the same 16-bit `Entry` format and pin mapping.
15//!
16//! # Hardware Requirements
17//! Requires a parallel output peripheral capable of clocking 16 bits (though
18//! only 15 are actually used) at a time. The data is structured to directly
19//! match the HUB75 connector signals, so no external latch or address-decode
20//! hardware is needed.
21//!
22//! # HUB75 Signal Bit Mapping
23//! Each 16-bit `Entry` represents the logic levels driven onto the HUB75 bus
24//! during a single pixel-clock cycle:
25//!
26//! ```text
27//! 15 ─ (spare)
28//! 14 ─ B2       Blue  – lower half of the panel
29//! 13 ─ G2       Green – lower half of the panel
30//! 12 ─ R2       Red   – lower half of the panel
31//! 11 ─ B1       Blue  – upper half of the panel
32//! 10 ─ G1       Green – upper half of the panel
33//!  9 ─ R1       Red   – upper half of the panel
34//!  8 ─ OE       Output-Enable / Blank
35//!  7 ─ (spare)
36//!  6 ─ (spare)
37//!  5 ─ LAT      Latch / STB
38//! 4-0 ─ A..E    Row address lines
39//! ```
40//!
41//! The pixel clock is generated by the peripheral that owns the DMA stream and
42//! is not part of the 16-bit word stored in the framebuffer.
43//!
44//! # Bitplane BCM Rendering
45//! Each framebuffer is organised into `PLANES` bit-planes, one per colour
46//! bit. To produce correct brightness via Binary Code Modulation, output each
47//! plane's data (scan it) a number of times equal to its bit-weight (2^7 for
48//! the MSB plane down to 2^0 for the LSB plane) — either with a DMA
49//! descriptor chain or by reprogramming the DMA registers per segment from a
50//! transfer-complete interrupt.
51//!
52//! Both layout variants store planes **LSB-first**: plane 0 carries bit 0
53//! and is displayed once per scan, plane 7 carries bit 7 and is displayed
54//! 128 times. Each BCM segment streams a contiguous *suffix* of planes,
55//! repeated just enough times to bring each plane's total coverage to its
56//! bit-weight — halving the number of DMA transfers per scan with identical
57//! brightness. LSB-first ordering also means the first plane after an
58//! address change displays stale data with minimal visual weight, avoiding
59//! a separate primer segment. See the [`frame`] and [`row`] module
60//! documentation for the exact scan order.
61//!
62//! See <https://www.batsocks.co.uk/readme/art_bcm_1.htm> for background on
63//! BCM.
64
65use bitfield::bitfield;
66
67pub mod frame;
68pub mod row;
69
70pub use frame::DmaFrameBuffer;
71
72pub(crate) use crate::{INTER_ROW_BLANK, LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
73
74#[cfg(feature = "invert-oe")]
75pub(crate) const OE_ACTIVE: u16 = 0b1_0000_0000;
76#[cfg(feature = "invert-oe")]
77pub(crate) const OE_BLANK: u16 = 0;
78
79#[cfg(not(feature = "invert-oe"))]
80pub(crate) const OE_ACTIVE: u16 = 0;
81#[cfg(not(feature = "invert-oe"))]
82pub(crate) const OE_BLANK: u16 = 0b1_0000_0000;
83
84// ---------------------------------------------------------------------------
85// Shared helper functions
86// ---------------------------------------------------------------------------
87
88#[inline]
89pub(crate) const fn map_index(i: usize) -> usize {
90    #[cfg(feature = "esp32-ordering")]
91    {
92        i ^ 1
93    }
94    #[cfg(not(feature = "esp32-ordering"))]
95    {
96        i
97    }
98}
99
100/// Builds the per-row pixel template with trail blank, active pixels, lead
101/// blank, and latch embedded within the COLS entries.
102#[allow(clippy::absurd_extreme_comparisons)]
103#[inline]
104pub(crate) const fn make_data_template<const COLS: usize>(addr: u8) -> [Entry; COLS] {
105    let mut data = [Entry::new(); COLS];
106    let mut i = 0;
107
108    while i < COLS {
109        let mut entry = Entry::new();
110        entry.0 = addr as u16 | OE_BLANK;
111
112        if i == COLS - 1 {
113            entry.0 |= 0b0010_0000; // latch
114        } else if i >= TRAIL_BLANK_DELAY && i < COLS.saturating_sub(LEAD_BLANK_DELAY + 1) {
115            entry.0 = (entry.0 & !0b1_0000_0000) | OE_ACTIVE;
116        }
117
118        data[map_index(i)] = entry;
119        i += 1;
120    }
121
122    data
123}
124
125// ---------------------------------------------------------------------------
126// Entry — the 16-bit word that rides on the HUB75 bus
127// ---------------------------------------------------------------------------
128
129bitfield! {
130    #[derive(Clone, Copy, Default, PartialEq)]
131    #[repr(transparent)]
132    pub(crate) struct Entry(u16);
133    pub(crate) dummy2, set_dummy2: 15;
134    pub(crate) blu2, set_blu2: 14;
135    pub(crate) grn2, set_grn2: 13;
136    pub(crate) red2, set_red2: 12;
137    pub(crate) blu1, set_blu1: 11;
138    pub(crate) grn1, set_grn1: 10;
139    pub(crate) red1, set_red1: 9;
140    pub(crate) output_enable, set_output_enable: 8;
141    pub(crate) dummy1, set_dummy1: 7;
142    pub(crate) dummy0, set_dummy0: 6;
143    pub(crate) latch, set_latch: 5;
144    pub(crate) addr, set_addr: 4, 0;
145}
146
147impl core::fmt::Debug for Entry {
148    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149        f.debug_tuple("Entry")
150            .field(&format_args!("{:#x}", self.0))
151            .finish()
152    }
153}
154
155impl Entry {
156    pub(crate) const fn new() -> Self {
157        Self(OE_BLANK)
158    }
159
160    pub(crate) const fn from_raw(val: u16) -> Self {
161        Self(val)
162    }
163
164    #[allow(dead_code)]
165    pub(crate) const fn raw(self) -> u16 {
166        self.0
167    }
168
169    pub(crate) fn mask_raw(&mut self, mask: u16) {
170        self.0 &= mask;
171    }
172
173    const COLOR0_MASK: u16 = 0b0000_1110_0000_0000; // bits 9-11: R1, G1, B1
174    const COLOR1_MASK: u16 = 0b0111_0000_0000_0000; // bits 12-14: R2, G2, B2
175
176    #[inline]
177    pub(crate) fn set_color0_bits(&mut self, bits: u8) {
178        let bits16 = u16::from(bits) << 9;
179        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits16 & Self::COLOR0_MASK);
180    }
181
182    #[inline]
183    pub(crate) fn set_color1_bits(&mut self, bits: u8) {
184        let bits16 = u16::from(bits) << 12;
185        self.0 = (self.0 & !Self::COLOR1_MASK) | (bits16 & Self::COLOR1_MASK);
186    }
187}