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//! default (ESP32 I2S / generic) interstate75 (RP2040/RP2350 PIO)
28//! 15 ─ (spare) 15 ─ (spare)
29//! 14 ─ B1 Blue – lower half 14 ─ (spare)
30//! 13 ─ G1 Green – lower half 13 ─ OE Output-Enable / Blank
31//! 12 ─ R1 Red – lower half 12 ─ LAT Latch / STB
32//! 11 ─ B0 Blue – upper half 11 ─ (spare)
33//! 10 ─ G0 Green – upper half 10 ─ E Row address
34//! 9 ─ R0 Red – upper half 9 ─ D Row address
35//! 8 ─ OE Output-Enable / Blank 8 ─ C Row address
36//! 7 ─ (spare) 7 ─ B Row address
37//! 6 ─ (spare) 6 ─ A Row address
38//! 5 ─ LAT Latch / STB 5 ─ B1 Blue – lower half
39//! 4-0 ─ A..E Row address lines 4 ─ G1 Green – lower half
40//! 3 ─ R1 Red – lower half
41//! 2 ─ B0 Blue – upper half
42//! 1 ─ G0 Green – upper half
43//! 0 ─ R0 Red – upper half
44//! ```
45//!
46//! The board-specific layout is selected by the `interstate75` feature; see
47//! [`crate::pinmap`].
48//!
49//! The pixel clock is generated by the peripheral that owns the DMA stream and
50//! is not part of the 16-bit word stored in the framebuffer.
51//!
52//! # Bitplane BCM Rendering
53//! Each framebuffer is organised into `PLANES` bit-planes, one per colour
54//! bit. To produce correct brightness via Binary Code Modulation, output each
55//! plane's data (scan it) a number of times equal to its bit-weight (2^7 for
56//! the MSB plane down to 2^0 for the LSB plane) — either with a DMA
57//! descriptor chain or by reprogramming the DMA registers per segment from a
58//! transfer-complete interrupt.
59//!
60//! Both layout variants store planes **LSB-first**: plane 0 carries bit 0
61//! and is displayed once per scan, plane 7 carries bit 7 and is displayed
62//! 128 times. Each BCM segment streams a contiguous *suffix* of planes,
63//! repeated just enough times to bring each plane's total coverage to its
64//! bit-weight — halving the number of DMA transfers per scan with identical
65//! brightness. LSB-first ordering also means the first plane after an
66//! address change displays stale data with minimal visual weight, avoiding
67//! a separate primer segment. See the [`frame`] and [`row`] module
68//! documentation for the exact scan order.
69//!
70//! See <https://www.batsocks.co.uk/readme/art_bcm_1.htm> for background on
71//! BCM.
72
73use bitfield::bitfield;
74
75pub mod frame;
76pub mod row;
77
78pub use frame::DmaFrameBuffer;
79
80#[cfg(feature = "tail-closes-latch")]
81pub(crate) use crate::pinmap::ADDR_MASK;
82pub(crate) use crate::pinmap::PINMAP;
83pub(crate) use crate::pinmap::{
84 ADDR_SHIFT, COLOR0_MASK, COLOR0_SHIFT, COLOR1_MASK, COLOR1_SHIFT, COLOR_MASK, LATCH_BIT, OE_BIT,
85};
86pub(crate) use crate::{INTER_ROW_BLANK, LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
87
88// The OE *level* constants stay local: their default polarity differs between
89// this module (standard active-low) and the deprecated `plain` module
90// (active-high). Only the bit position (`pinmap::OE_BIT`) is shared.
91#[cfg(feature = "invert-oe")]
92pub(crate) const OE_ACTIVE: u16 = OE_BIT;
93#[cfg(feature = "invert-oe")]
94pub(crate) const OE_BLANK: u16 = 0;
95
96#[cfg(not(feature = "invert-oe"))]
97pub(crate) const OE_ACTIVE: u16 = 0;
98#[cfg(not(feature = "invert-oe"))]
99pub(crate) const OE_BLANK: u16 = OE_BIT;
100
101// ---------------------------------------------------------------------------
102// Shared helper functions
103// ---------------------------------------------------------------------------
104
105#[inline]
106pub(crate) const fn map_index(i: usize) -> usize {
107 #[cfg(feature = "esp32-ordering")]
108 {
109 i ^ 1
110 }
111 #[cfg(not(feature = "esp32-ordering"))]
112 {
113 i
114 }
115}
116
117/// Builds the per-row pixel template with trail blank, active pixels, lead
118/// blank, and latch embedded within the COLS entries.
119#[allow(clippy::absurd_extreme_comparisons)]
120#[inline]
121pub(crate) const fn make_data_template<const COLS: usize>(addr: u8) -> [Entry; COLS] {
122 let mut data = [Entry::new(); COLS];
123 let mut i = 0;
124
125 while i < COLS {
126 let mut entry = Entry::new();
127 entry.0 = ((addr as u16) << ADDR_SHIFT) | OE_BLANK;
128
129 if i == COLS - 1 {
130 entry.0 |= LATCH_BIT;
131 } else if i >= TRAIL_BLANK_DELAY && i < COLS.saturating_sub(LEAD_BLANK_DELAY + 1) {
132 entry.0 = (entry.0 & !OE_BIT) | OE_ACTIVE;
133 }
134
135 data[map_index(i)] = entry;
136 i += 1;
137 }
138
139 data
140}
141
142// ---------------------------------------------------------------------------
143// Entry — the 16-bit word that rides on the HUB75 bus
144// ---------------------------------------------------------------------------
145
146// The bit layout is selected per board by `crate::pinmap`; this single
147// `bitfield!` definition reads the positions straight from `PINMAP` so the
148// accessors can never drift out of sync with the DMA templates. Note the
149// `red1/grn1/blu1` accessors are the *upper-half* triplet (`PINMAP.red0`...),
150// and `red2/grn2/blu2` the lower half.
151bitfield! {
152 #[derive(Clone, Copy, Default, PartialEq)]
153 #[repr(transparent)]
154 pub(crate) struct Entry(u16);
155 pub(crate) dummy2, set_dummy2: PINMAP.spare[2];
156 pub(crate) blu2, set_blu2: PINMAP.blu1;
157 pub(crate) grn2, set_grn2: PINMAP.grn1;
158 pub(crate) red2, set_red2: PINMAP.red1;
159 pub(crate) blu1, set_blu1: PINMAP.blu0;
160 pub(crate) grn1, set_grn1: PINMAP.grn0;
161 pub(crate) red1, set_red1: PINMAP.red0;
162 pub(crate) output_enable, set_output_enable: PINMAP.oe;
163 pub(crate) dummy1, set_dummy1: PINMAP.spare[1];
164 pub(crate) dummy0, set_dummy0: PINMAP.spare[0];
165 pub(crate) latch, set_latch: PINMAP.latch;
166 pub(crate) addr, set_addr: PINMAP.addr_msb(), PINMAP.addr_lsb;
167}
168
169impl core::fmt::Debug for Entry {
170 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171 f.debug_tuple("Entry")
172 .field(&format_args!("{:#x}", self.0))
173 .finish()
174 }
175}
176
177impl Entry {
178 pub(crate) const fn new() -> Self {
179 Self(OE_BLANK)
180 }
181
182 pub(crate) const fn from_raw(val: u16) -> Self {
183 Self(val)
184 }
185
186 #[allow(dead_code)]
187 pub(crate) const fn raw(self) -> u16 {
188 self.0
189 }
190
191 pub(crate) fn mask_raw(&mut self, mask: u16) {
192 self.0 &= mask;
193 }
194
195 // The colour bit positions/masks come from `crate::pinmap` so they follow
196 // the selected board layout.
197 #[inline]
198 pub(crate) fn set_color0_bits(&mut self, bits: u8) {
199 let bits16 = u16::from(bits) << COLOR0_SHIFT;
200 self.0 = (self.0 & !COLOR0_MASK) | (bits16 & COLOR0_MASK);
201 }
202
203 #[inline]
204 pub(crate) fn set_color1_bits(&mut self, bits: u8) {
205 let bits16 = u16::from(bits) << COLOR1_SHIFT;
206 self.0 = (self.0 & !COLOR1_MASK) | (bits16 & COLOR1_MASK);
207 }
208}