Skip to main content

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}