hub75_framebuffer/pinmap.rs
1//! Compile-time mapping of the HUB75 signals onto the bits of the DMA word.
2//!
3//! The [`plain`](crate::plain) and [`bitplane::plain`](crate::bitplane::plain)
4//! framebuffers store one 16-bit word per pixel-clock cycle, while
5//! [`latched`](crate::latched) and [`bitplane::latched`](crate::bitplane::latched)
6//! store 8-bit words. In every case each HUB75 signal occupies a fixed bit, and
7//! *which* bit depends on the board (for the 16-bit layouts) or on the external
8//! latch circuit (for the 8-bit layouts). This module is the single source of
9//! truth for those positions.
10//!
11//! ## The layouts
12//!
13//! ```text
14//! 16-bit plain / bitplane::plain
15//! default (ESP32 I2S / generic) interstate75 (RP2040/RP2350 PIO)
16//! 15 .. 13 spare (dummy2, B1, G1) 15 .. 14 spare
17//! 12 R1 Red - lower half 13 OE Output-Enable / Blank
18//! 11 B0 Blue - upper half 12 LAT Latch / STB
19//! 10 G0 Green - upper half 11 spare (unused)
20//! 9 R0 Red - upper half 10..6 E D C B A Row address
21//! 8 OE Output-Enable / Blank 5..3 B1 G1 R1 lower half
22//! 7 spare 2..0 B0 G0 R0 upper half
23//! 6 spare
24//! 5 LAT Latch / STB
25//! 4..0 A..E Row address
26//!
27//! 8-bit latched (fixed by the external latch circuit)
28//! 7 OE Output-Enable / Blank
29//! 6 LAT Latch / STB
30//! 5 B2 Blue - sub-pixel group 2
31//! 4 G2 Green - sub-pixel group 2
32//! 3 R2 Red - sub-pixel group 2
33//! 2 B1 Blue - sub-pixel group 1
34//! 1 G1 Green - sub-pixel group 1
35//! 0 R1 Red - sub-pixel group 1
36//! ```
37//!
38//! Everything is a `const`, so the layout is folded away during compilation:
39//! the generated code is identical to hand-written bit literals, with no
40//! runtime branch and no lookup table. [`PINMAP`](crate::pinmap::PINMAP) and
41//! [`LATCHED_PINMAP`](crate::pinmap::LATCHED_PINMAP) are
42//! exported so a calling crate (the driver that owns the DMA/PIO peripheral)
43//! can validate at compile time that it drives the panel with the same bit
44//! assignments this crate assumes — e.g.
45//!
46//! ```
47//! // The caller pins its own board definition to this crate's assumptions.
48//! # #[cfg(not(feature = "interstate75"))]
49//! const _: () = assert!(hub75_framebuffer::pinmap::PINMAP.oe == 8);
50//! ```
51
52// ---------------------------------------------------------------------------
53// The signal -> bit table
54// ---------------------------------------------------------------------------
55
56/// Bit position of every HUB75 signal within one framebuffer word.
57///
58/// Naming note: `color0_*` (the `red0`/`grn0`/`blu0` fields) carry the
59/// **upper-half** `R0/G0/B0` triplet and drive the `red1`/`grn1`/`blu1`
60/// accessors, while `color1_*` (`red1`/`grn1`/`blu1` fields) carry the
61/// **lower-half** `R1/G1/B1` triplet and drive `red2`/`grn2`/`blu2`.
62///
63/// The three colour signals of a triplet are assumed to be contiguous with red
64/// as the least-significant bit (true of every supported layout); the exact
65/// per-channel bits are recorded anyway so a driver can verify the wiring.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct PinMap {
68 /// Bit of the upper-half red signal (`R0`, `red1` accessor).
69 pub red0: usize,
70 /// Bit of the upper-half green signal (`G0`, `grn1` accessor).
71 pub grn0: usize,
72 /// Bit of the upper-half blue signal (`B0`, `blu1` accessor).
73 pub blu0: usize,
74 /// Bit of the lower-half red signal (`R1`, `red2` accessor).
75 pub red1: usize,
76 /// Bit of the lower-half green signal (`G1`, `grn2` accessor).
77 pub grn1: usize,
78 /// Bit of the lower-half blue signal (`B1`, `blu2` accessor).
79 pub blu1: usize,
80 /// Least-significant bit of the row-address field (`A`).
81 pub addr_lsb: usize,
82 /// Width of the row-address field in bits (`A..E` = 5).
83 pub addr_bits: usize,
84 /// Bit of the latch / strobe (`LAT`) signal.
85 pub latch: usize,
86 /// Bit of the output-enable (`OE`) signal.
87 pub oe: usize,
88 /// The unused ("spare") bits in the word. The 16-bit layouts expose these
89 /// through the `dummy0`/`dummy1`/`dummy2` accessors; set to [`usize::MAX`]
90 /// when unused.
91 pub spare: [usize; 3],
92}
93
94impl PinMap {
95 /// Mask covering the output-enable bit.
96 #[must_use]
97 #[inline]
98 pub const fn oe_mask(&self) -> u16 {
99 1u16 << self.oe
100 }
101
102 /// Mask covering the latch bit.
103 #[must_use]
104 #[inline]
105 pub const fn latch_mask(&self) -> u16 {
106 1u16 << self.latch
107 }
108
109 /// Mask covering the upper-half `R0/G0/B0` colour bits.
110 #[must_use]
111 #[inline]
112 pub const fn color0_mask(&self) -> u16 {
113 (1u16 << self.red0) | (1u16 << self.grn0) | (1u16 << self.blu0)
114 }
115
116 /// Mask covering the lower-half `R1/G1/B1` colour bits.
117 #[must_use]
118 #[inline]
119 pub const fn color1_mask(&self) -> u16 {
120 (1u16 << self.red1) | (1u16 << self.grn1) | (1u16 << self.blu1)
121 }
122
123 /// Mask covering every colour bit.
124 #[must_use]
125 #[inline]
126 pub const fn color_mask(&self) -> u16 {
127 self.color0_mask() | self.color1_mask()
128 }
129
130 /// Mask covering the whole row-address field.
131 #[must_use]
132 #[inline]
133 pub const fn addr_mask(&self) -> u16 {
134 (((1u32 << self.addr_bits) - 1) as u16) << self.addr_lsb
135 }
136
137 /// Most-significant bit of the row-address field.
138 #[must_use]
139 #[inline]
140 pub const fn addr_msb(&self) -> usize {
141 self.addr_lsb + self.addr_bits - 1
142 }
143}
144
145/// Layout for the 16-bit [`plain`](crate::plain) and
146/// [`bitplane::plain`](crate::bitplane::plain) framebuffers, selected per board.
147///
148/// The default matches any parallel-output peripheral wired in the historical
149/// HUB75 bit order used by the ESP32 I2S/PARLIO drivers; the `interstate75`
150/// feature selects the Pimoroni Interstate 75 / 75 W (RP2040 / RP2350) layout,
151/// which follows the board's contiguous `GPIO0..GPIO13` mapping so a PIO
152/// `out pins, 16` stream can drive the panel with no run-time bit shuffling.
153pub const PINMAP: PinMap = core::cfg_select! {
154 feature = "interstate75" => PinMap {
155 red0: 0,
156 grn0: 1,
157 blu0: 2,
158 red1: 3,
159 grn1: 4,
160 blu1: 5,
161 addr_lsb: 6,
162 addr_bits: 5,
163 latch: 12,
164 oe: 13,
165 // spare[0] is the unused bit 11; spare[1]/spare[2] are the top two bits.
166 spare: [11, 14, 15],
167 },
168 _ => PinMap {
169 red0: 9,
170 grn0: 10,
171 blu0: 11,
172 red1: 12,
173 grn1: 13,
174 blu1: 14,
175 addr_lsb: 0,
176 addr_bits: 5,
177 latch: 5,
178 oe: 8,
179 spare: [6, 7, 15],
180 },
181};
182
183/// Layout for the 8-bit [`latched`](crate::latched) and
184/// [`bitplane::latched`](crate::bitplane::latched) framebuffers.
185///
186/// This layout is fixed by the external latch circuit, not by the board, so it
187/// is not gated by a Cargo feature. It is exported for validation and to keep
188/// every signal position in one place.
189pub const LATCHED_PINMAP: PinMap = PinMap {
190 red0: 0,
191 grn0: 1,
192 blu0: 2,
193 red1: 3,
194 grn1: 4,
195 blu1: 5,
196 addr_lsb: 0,
197 addr_bits: 5,
198 latch: 6,
199 oe: 7,
200 // Bit 5 is the only unused slot; the dummy accessors are not used here.
201 spare: [usize::MAX; 3],
202};
203
204// ---------------------------------------------------------------------------
205// 16-bit layout: derived consts
206// ---------------------------------------------------------------------------
207
208/// Shift of the upper-half `R0/G0/B0` colour triplet.
209pub(crate) const COLOR0_SHIFT: u32 = PINMAP.red0 as u32;
210/// Shift of the lower-half `R1/G1/B1` colour triplet.
211pub(crate) const COLOR1_SHIFT: u32 = PINMAP.red1 as u32;
212/// Shift of the 5-bit row address (`A..E`).
213pub(crate) const ADDR_SHIFT: u32 = PINMAP.addr_lsb as u32;
214
215/// Mask covering the `R0/G0/B0` (upper-half) colour bits.
216pub(crate) const COLOR0_MASK: u16 = PINMAP.color0_mask();
217/// Mask covering the `R1/G1/B1` (lower-half) colour bits.
218pub(crate) const COLOR1_MASK: u16 = PINMAP.color1_mask();
219/// Mask covering every colour bit (`erase()` / `clear_colors()`).
220pub(crate) const COLOR_MASK: u16 = PINMAP.color_mask();
221
222/// Single-bit mask of the `LATCH` / `STB` signal.
223pub(crate) const LATCH_BIT: u16 = PINMAP.latch_mask();
224/// Single-bit mask of the output-enable signal.
225pub(crate) const OE_BIT: u16 = PINMAP.oe_mask();
226
227/// Every row-address bit set: the idle/sentinel address parked in the trailing
228/// word so the final latch does not select the first row again.
229#[cfg(feature = "tail-closes-latch")]
230pub(crate) const ADDR_MASK: u16 = PINMAP.addr_mask();
231
232// ---------------------------------------------------------------------------
233// 8-bit latched layout: derived consts
234// ---------------------------------------------------------------------------
235
236/// Single-bit mask of the latched layout's output-enable signal (bit position
237/// only; the active/blank *levels* stay local to each module).
238pub(crate) const LATCHED_OE_BIT: u8 = LATCHED_PINMAP.oe_mask() as u8;
239/// Single-bit mask of the latched layout's latch signal.
240pub(crate) const LATCHED_LATCH_BIT: u8 = LATCHED_PINMAP.latch_mask() as u8;
241/// Mask covering the `R1/G1/B1` colour bits.
242pub(crate) const LATCHED_COLOR0_MASK: u8 = LATCHED_PINMAP.color0_mask() as u8;
243/// Mask covering the `R2/G2/B2` colour bits.
244pub(crate) const LATCHED_COLOR1_MASK: u8 = LATCHED_PINMAP.color1_mask() as u8;
245/// Mask covering every colour bit.
246pub(crate) const LATCHED_COLOR_MASK: u8 = LATCHED_PINMAP.color_mask() as u8;
247
248// The `OE` *level* constants (`OE_ACTIVE` / `OE_BLANK`) deliberately stay local
249// to each framebuffer module: the deprecated `plain` framebuffer and
250// `bitplane::plain` use opposite default polarities (see `plain.rs` and
251// `bitplane/plain/mod.rs`), and the latched modules use a third fixed layout.
252// `pinmap` only owns the bit *position* (`OE_BIT` / `LATCHED_OE_BIT`), so
253// relocating `OE` for a board cannot silently flip any polarity.
254
255// ---------------------------------------------------------------------------
256// Compile-time sanity checks
257// ---------------------------------------------------------------------------
258
259const _: () = {
260 assert!(PINMAP.addr_lsb + PINMAP.addr_bits <= 16);
261 assert!(PINMAP.oe < 16);
262 assert!(PINMAP.latch < 16);
263
264 assert!(LATCHED_PINMAP.addr_lsb + LATCHED_PINMAP.addr_bits <= 8);
265 assert!(LATCHED_PINMAP.oe < 8);
266 assert!(LATCHED_PINMAP.latch < 8);
267};
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn masks_match_positions() {
275 for pm in [PINMAP, LATCHED_PINMAP] {
276 assert_eq!(pm.oe_mask(), 1u16 << pm.oe);
277 assert_eq!(pm.latch_mask(), 1u16 << pm.latch);
278 assert_eq!(
279 pm.color0_mask(),
280 (1u16 << pm.red0) | (1u16 << pm.grn0) | (1u16 << pm.blu0)
281 );
282 assert_eq!(
283 pm.color1_mask(),
284 (1u16 << pm.red1) | (1u16 << pm.grn1) | (1u16 << pm.blu1)
285 );
286 assert_eq!(pm.color_mask(), pm.color0_mask() | pm.color1_mask());
287 assert_eq!(pm.addr_mask(), ((1u16 << pm.addr_bits) - 1) << pm.addr_lsb);
288 assert_eq!(pm.addr_msb(), pm.addr_lsb + pm.addr_bits - 1);
289 }
290 }
291}