Skip to main content

hub75_framebuffer/bitplane/
latched.rs

1//! Bitplane framebuffer for an 8-bit latched HUB75 interface.
2//!
3//! This module provides a framebuffer that stores colour data as separate
4//! bit-planes rather than the threshold-based frames used by
5//! [`crate::latched::DmaFrameBuffer`]. Each plane holds one bit of every
6//! colour channel, giving `PLANES` planes total (typically 8 for full 8-bit
7//! colour). Row addressing is carried by four trailing `Address` bytes per
8//! row, identical to the non-bitplane latched layout.
9//!
10//! # Hardware Requirements
11//! Requires a parallel output peripheral capable of clocking 8 bits at a time,
12//! plus an external latch circuit to hold the row address and gate the pixel
13//! clock (same circuit as the non-bitplane latched variant).
14//!
15//! # HUB75 Signal Bit Mapping (8-bit words)
16//! Two distinct 8-bit words are streamed to the panel:
17//!
18//! 1. **Address / Timing (`Address`)** -- row-select and latch control.
19//! 2. **Pixel Data (`Entry`)** -- RGB bits for two sub-pixels plus OE/LAT
20//!    shadow bits.
21//!
22//! ```text
23//! Address word (row select & timing)
24//! ┌──7─┬──6──┬─5─-┬─4─-┬─3-─┬─2-─┬─1-─┬─0-─┐
25//! │ OE │ LAT │    │ E  │ D  │ C  │ B  │ A  │
26//! └────┴─────┴───-┴───-┴───-┴───-┴───-┴───-┘
27//! ```
28//! ```text
29//! Entry word (pixel data)
30//! ┌──7─┬──6──┬─5──┬─4──┬─3──┬─2──┬─1──┬─0──┐
31//! │ OE │ LAT │ B2 │ G2 │ R2 │ B1 │ G1 │ R1 │
32//! └────┴─────┴────┴────┴────┴────┴────┴────┘
33//! ```
34//!
35//! Bits 7-6 (OE/LAT) occupy the same positions in both words so the control
36//! lines stay valid throughout the DMA stream.
37//!
38//! # Bitplane BCM Rendering
39//! The framebuffer is organised into `PLANES` bit-planes. Plane 0 carries the
40//! MSB (bit 7) of each colour channel, plane 1 carries bit 6, and so on down
41//! to plane 7 which carries the LSB (bit 0).
42//!
43//! To produce correct brightness via Binary Code Modulation, configure the DMA
44//! descriptor chain so that each plane's data is output (scanned) a number of
45//! times equal to its bit-weight:
46//!
47//! ```text
48//! plane 0 (bit 7) → output 2^7 = 128 times
49//! plane 1 (bit 6) → output 2^6 =  64 times
50//! plane 2 (bit 5) → output 2^5 =  32 times
51//!   …
52//! plane 7 (bit 0) → output 2^0 =   1 time
53//! ```
54//!
55//! That is, each plane is scanned `2^(7 - plane_index)` times. The weighted
56//! repetition counts sum to 255, reproducing the full 8-bit intensity range.
57//! See <https://www.batsocks.co.uk/readme/art_bcm_1.htm> for background on
58//! BCM.
59//!
60//! # Memory Usage
61//! Memory scales linearly with `PLANES`: the buffer contains `PLANES` copies
62//! of the row data (one per bit-plane). Unlike the threshold-based
63//! [`crate::latched::DmaFrameBuffer`] whose frame count grows as
64//! `2^BITS - 1`, this layout uses exactly `PLANES` planes regardless of
65//! colour depth.
66//!
67//! Each row is `COLS` data bytes plus 4 address bytes, so total size is
68//! `PLANES * NROWS * (COLS + 4)` bytes.
69
70use core::convert::Infallible;
71
72use bitfield::bitfield;
73use embedded_graphics::pixelcolor::RgbColor;
74use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
75
76use crate::Color;
77use crate::FrameBuffer;
78use crate::WordSize;
79use crate::{FrameBufferOperations, MutableFrameBuffer};
80
81bitfield! {
82    #[derive(Clone, Copy, Default, PartialEq, Eq)]
83    #[repr(transparent)]
84    pub(crate) struct Address(u8);
85    impl Debug;
86    pub(crate) output_enable, set_output_enable: 7;
87    pub(crate) latch, set_latch: 6;
88    pub(crate) addr, set_addr: 4, 0;
89}
90
91impl Address {
92    pub const fn new() -> Self {
93        Self(0)
94    }
95}
96
97bitfield! {
98    #[derive(Clone, Copy, Default, PartialEq)]
99    #[repr(transparent)]
100    pub(crate) struct Entry(u8);
101    impl Debug;
102    pub(crate) output_enable, set_output_enable: 7;
103    pub(crate) latch, set_latch: 6;
104    pub(crate) blu2, set_blu2: 5;
105    pub(crate) grn2, set_grn2: 4;
106    pub(crate) red2, set_red2: 3;
107    pub(crate) blu1, set_blu1: 2;
108    pub(crate) grn1, set_grn1: 1;
109    pub(crate) red1, set_red1: 0;
110}
111
112impl Entry {
113    pub const fn new() -> Self {
114        Self(0)
115    }
116
117    const COLOR0_MASK: u8 = 0b0000_0111;
118    const COLOR1_MASK: u8 = 0b0011_1000;
119
120    #[inline]
121    fn set_color0_bits(&mut self, bits: u8) {
122        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits & Self::COLOR0_MASK);
123    }
124
125    #[inline]
126    fn set_color1_bits(&mut self, bits: u8) {
127        self.0 = (self.0 & !Self::COLOR1_MASK) | ((bits << 3) & Self::COLOR1_MASK);
128    }
129}
130
131#[derive(Clone, Copy, PartialEq, Debug)]
132#[repr(C)]
133/// A single BCM row payload for 8-bit latched output.
134///
135/// Each row contains color-stream data for `COLS` pixels followed by four
136/// address/control bytes that clock the row address into the external latch.
137pub struct Row<const COLS: usize> {
138    pub(crate) data: [Entry; COLS],
139    pub(crate) address: [Address; 4],
140}
141
142#[inline]
143const fn map_index(index: usize) -> usize {
144    #[cfg(feature = "esp32-ordering")]
145    {
146        index ^ 2
147    }
148    #[cfg(not(feature = "esp32-ordering"))]
149    {
150        index
151    }
152}
153
154const fn make_addr_table() -> [[Address; 4]; 32] {
155    let mut tbl = [[Address::new(); 4]; 32];
156    let mut addr = 0;
157    while addr < 32 {
158        let mut i = 0;
159        while i < 4 {
160            let latch = i != 3;
161            let mapped_i = map_index(i);
162            let latch_bit = if latch { 1u8 << 6 } else { 0u8 };
163            tbl[addr][mapped_i].0 = latch_bit | addr as u8;
164            i += 1;
165        }
166        addr += 1;
167    }
168    tbl
169}
170
171static ADDR_TABLE: [[Address; 4]; 32] = make_addr_table();
172
173const fn make_data_template<const COLS: usize>() -> [Entry; COLS] {
174    let mut data = [Entry::new(); COLS];
175    let mut i = 0;
176    while i < COLS {
177        let mapped_i = map_index(i);
178        data[mapped_i].0 = if i == COLS - 1 { 0 } else { 0b1000_0000 };
179        i += 1;
180    }
181    data
182}
183
184impl<const COLS: usize> Row<COLS> {
185    /// Creates a zero-initialized row.
186    ///
187    /// Call [`Self::format`] before first use to populate row address/control
188    /// metadata.
189    #[must_use]
190    pub const fn new() -> Self {
191        Self {
192            data: [Entry::new(); COLS],
193            address: [Address::new(); 4],
194        }
195    }
196
197    /// Formats this row for the provided multiplexed row address.
198    ///
199    /// This sets the trailing address bytes and initializes output-enable/latch
200    /// bits in the pixel stream template.
201    #[inline]
202    pub fn format(&mut self, addr: u8) {
203        debug_assert!((addr as usize) < ADDR_TABLE.len());
204        let src_addr = &ADDR_TABLE[addr as usize];
205        self.address[0] = src_addr[0];
206        self.address[1] = src_addr[1];
207        self.address[2] = src_addr[2];
208        self.address[3] = src_addr[3];
209
210        let data_template = make_data_template::<COLS>();
211        let mut i = 0;
212        while i < COLS {
213            self.data[i] = data_template[i];
214            i += 1;
215        }
216    }
217}
218
219impl<const COLS: usize> Default for Row<COLS> {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225/// The entire BCM Frame Buffer (Contiguous Memory)
226#[derive(Copy, Clone)]
227#[repr(C)]
228pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
229    pub(crate) planes: [[Row<COLS>; NROWS]; PLANES],
230}
231
232impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
233    DmaFrameBuffer<NROWS, COLS, PLANES>
234{
235    /// Creates a new frame buffer.
236    #[must_use]
237    pub fn new() -> Self {
238        let mut instance = Self {
239            planes: [[Row::new(); NROWS]; PLANES],
240        };
241        instance.format();
242        instance
243    }
244
245    /// Returns the number of BCM chunks (one per bit-plane).
246    #[must_use]
247    pub const fn bcm_chunk_count() -> usize {
248        PLANES
249    }
250
251    /// Returns the byte size of one BCM chunk (a single bit-plane).
252    #[must_use]
253    pub const fn bcm_chunk_bytes() -> usize {
254        NROWS * core::mem::size_of::<Row<COLS>>()
255    }
256
257    /// Formats the frame buffer with row addresses and control bits.
258    #[inline]
259    pub fn format(&mut self) {
260        for plane in &mut self.planes {
261            for (row_idx, row) in plane.iter_mut().enumerate() {
262                row.format(row_idx as u8);
263            }
264        }
265    }
266
267    /// Erase pixel colors while preserving row control data.
268    #[inline]
269    pub fn erase(&mut self) {
270        const MASK: u8 = !0b0011_1111;
271        for plane in &mut self.planes {
272            for row in plane {
273                for entry in &mut row.data {
274                    entry.0 &= MASK;
275                }
276            }
277        }
278    }
279
280    /// Set a pixel in the framebuffer.
281    #[inline]
282    pub fn set_pixel(&mut self, p: Point, color: Color) {
283        if p.x < 0 || p.y < 0 {
284            return;
285        }
286        self.set_pixel_internal(p.x as usize, p.y as usize, color);
287    }
288
289    #[inline]
290    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
291        if x >= COLS || y >= NROWS * 2 {
292            return;
293        }
294
295        let row_idx = if y < NROWS { y } else { y - NROWS };
296        let is_top = y < NROWS;
297        let red = color.r();
298        let green = color.g();
299        let blue = color.b();
300
301        for plane_idx in 0..PLANES {
302            let bit = 7_u32.saturating_sub(plane_idx as u32);
303            let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
304                | ((u8::from(((green >> bit) & 1) != 0)) << 1)
305                | u8::from(((red >> bit) & 1) != 0);
306            let col_idx = map_index(x);
307            let entry = &mut self.planes[plane_idx][row_idx].data[col_idx];
308            if is_top {
309                entry.set_color0_bits(bits);
310            } else {
311                entry.set_color1_bits(bits);
312            }
313        }
314    }
315}
316
317impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
318    for DmaFrameBuffer<NROWS, COLS, PLANES>
319{
320    fn default() -> Self {
321        Self::new()
322    }
323}
324
325impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
326    for DmaFrameBuffer<NROWS, COLS, PLANES>
327{
328    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
329        f.debug_struct("DmaFrameBuffer")
330            .field("size", &core::mem::size_of_val(&self.planes))
331            .field("plane_count", &self.planes.len())
332            .field("plane_size", &core::mem::size_of_val(&self.planes[0]))
333            .finish()
334    }
335}
336
337#[cfg(feature = "defmt")]
338impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
339    for DmaFrameBuffer<NROWS, COLS, PLANES>
340{
341    fn format(&self, f: defmt::Formatter) {
342        defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
343        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
344        defmt::write!(
345            f,
346            " plane_size: {}",
347            core::mem::size_of_val(&self.planes[0])
348        );
349    }
350}
351
352impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
353    for DmaFrameBuffer<NROWS, COLS, PLANES>
354{
355    fn get_word_size(&self) -> WordSize {
356        WordSize::Eight
357    }
358
359    fn plane_count(&self) -> usize {
360        PLANES
361    }
362
363    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
364        assert!(
365            plane_idx < PLANES,
366            "plane_idx {plane_idx} out of range for {PLANES} planes"
367        );
368        let ptr = self.planes[plane_idx].as_ptr().cast::<u8>();
369        let len = NROWS * core::mem::size_of::<Row<COLS>>();
370        (ptr, len)
371    }
372}
373
374impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
375    for DmaFrameBuffer<NROWS, COLS, PLANES>
376{
377    #[inline]
378    fn erase(&mut self) {
379        DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
380    }
381
382    #[inline]
383    fn set_pixel(&mut self, p: Point, color: Color) {
384        DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
385    }
386}
387
388impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
389    for DmaFrameBuffer<NROWS, COLS, PLANES>
390{
391}
392
393impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
394    for DmaFrameBuffer<NROWS, COLS, PLANES>
395{
396    fn size(&self) -> Size {
397        Size::new(COLS as u32, (NROWS * 2) as u32)
398    }
399}
400
401impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
402    for DmaFrameBuffer<NROWS, COLS, PLANES>
403{
404    type Color = Color;
405    type Error = Infallible;
406
407    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
408    where
409        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
410    {
411        for pixel in pixels {
412            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
413        }
414        Ok(())
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    extern crate std;
421
422    use super::*;
423    use embedded_graphics::prelude::*;
424    use std::format;
425
426    type TestBuffer = DmaFrameBuffer<16, 64, 8>;
427
428    #[test]
429    fn row_format_sets_address_and_control_bits() {
430        let mut row = Row::<8>::new();
431        row.format(5);
432        let latch_false_count = row.address.iter().filter(|addr| !addr.latch()).count();
433        assert_eq!(latch_false_count, 1);
434        for addr in &row.address {
435            assert_eq!(addr.addr(), 5);
436        }
437        let oe_false_count = row
438            .data
439            .iter()
440            .filter(|entry| !entry.output_enable())
441            .count();
442        assert_eq!(oe_false_count, 1);
443    }
444
445    #[test]
446    fn format_sets_expected_row_addresses_for_all_rows() {
447        let mut fb = TestBuffer::new();
448        fb.format();
449
450        for plane_idx in 0..8 {
451            for row_idx in 0..16 {
452                for addr in &fb.planes[plane_idx][row_idx].address {
453                    assert_eq!(addr.addr(), row_idx as u8);
454                }
455            }
456        }
457    }
458
459    #[test]
460    fn set_pixel_maps_top_half_bits_per_plane() {
461        let mut fb = TestBuffer::new();
462        let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
463        fb.set_pixel(Point::new(2, 3), color);
464
465        for plane_idx in 0..8 {
466            let bit = 7 - plane_idx;
467            let entry = fb.planes[plane_idx][3].data[map_index(2)];
468            assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
469            assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
470            assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
471        }
472    }
473
474    #[test]
475    fn set_pixel_maps_bottom_half_bits_per_plane() {
476        let mut fb = TestBuffer::new();
477        let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
478        fb.set_pixel(Point::new(4, 20), color);
479
480        for plane_idx in 0..8 {
481            let bit = 7 - plane_idx;
482            let entry = fb.planes[plane_idx][4].data[map_index(4)];
483            assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
484            assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
485            assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
486        }
487    }
488
489    #[test]
490    fn erase_clears_only_color_bits() {
491        let mut fb = TestBuffer::new();
492        let oe_before = fb.planes[0][0].data[0].output_enable();
493        fb.set_pixel(Point::new(0, 0), Color::WHITE);
494        fb.erase();
495
496        for plane in &fb.planes {
497            for row in plane {
498                for entry in &row.data {
499                    assert!(!entry.red1());
500                    assert!(!entry.grn1());
501                    assert!(!entry.blu1());
502                    assert!(!entry.red2());
503                    assert!(!entry.grn2());
504                    assert!(!entry.blu2());
505                }
506            }
507        }
508
509        assert_eq!(fb.planes[0][0].data[0].output_enable(), oe_before);
510    }
511
512    #[test]
513    fn draw_target_iter_sets_pixels() {
514        let mut fb = TestBuffer::new();
515        let pixels = [Pixel(Point::new(1, 1), Color::RED)];
516        let result = fb.draw_iter(pixels);
517        assert!(result.is_ok());
518
519        for plane_idx in 0..8 {
520            let bit = 7 - plane_idx;
521            let entry = fb.planes[plane_idx][1].data[map_index(1)];
522            assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
523            assert!(!entry.grn1());
524            assert!(!entry.blu1());
525        }
526    }
527
528    #[test]
529    fn set_pixel_ignores_out_of_bounds_and_negative() {
530        let mut fb = TestBuffer::new();
531        let before = fb.planes;
532        fb.set_pixel(Point::new(-1, 0), Color::WHITE);
533        fb.set_pixel(Point::new(0, -1), Color::WHITE);
534        fb.set_pixel(Point::new(64, 0), Color::WHITE);
535        fb.set_pixel(Point::new(0, 32), Color::WHITE);
536        assert_eq!(fb.planes, before);
537    }
538
539    #[test]
540    fn bcm_chunk_info_for_common_panel() {
541        assert_eq!(TestBuffer::bcm_chunk_count(), 8);
542        assert_eq!(
543            TestBuffer::bcm_chunk_bytes(),
544            16 * core::mem::size_of::<Row<64>>()
545        );
546    }
547
548    #[test]
549    fn frame_buffer_trait_accessors_report_expected_values() {
550        let fb = TestBuffer::new();
551        let as_trait: &dyn FrameBuffer = &fb;
552        assert_eq!(as_trait.get_word_size(), WordSize::Eight);
553        assert_eq!(as_trait.plane_count(), 8);
554
555        let (ptr, len) = as_trait.plane_ptr_len(0);
556        assert_eq!(len, 16 * core::mem::size_of::<Row<64>>());
557        assert_eq!(ptr, fb.planes[0].as_ptr().cast::<u8>());
558    }
559
560    #[test]
561    #[should_panic(expected = "out of range")]
562    fn plane_ptr_len_panics_for_invalid_plane() {
563        let fb = TestBuffer::new();
564        let _ = fb.plane_ptr_len(8);
565    }
566
567    #[test]
568    fn origin_dimensions_match_panel_geometry() {
569        let fb = TestBuffer::new();
570        assert_eq!(fb.size(), Size::new(64, 32));
571    }
572
573    #[test]
574    fn debug_impl_includes_shape_information() {
575        let fb = TestBuffer::new();
576        let s = format!("{fb:?}");
577        assert!(s.contains("DmaFrameBuffer"));
578        assert!(s.contains("plane_count"));
579        assert!(s.contains("plane_size"));
580    }
581
582    #[test]
583    fn row_format_sets_exactly_one_data_word_with_oe_low() {
584        let mut row = Row::<16>::new();
585        row.format(9);
586
587        let oe_low_indices: std::vec::Vec<_> = row
588            .data
589            .iter()
590            .enumerate()
591            .filter_map(|(i, entry)| (!entry.output_enable()).then_some(i))
592            .collect();
593        assert_eq!(oe_low_indices.len(), 1);
594        assert_eq!(oe_low_indices[0], map_index(15));
595    }
596
597    #[test]
598    fn default_constructors_match_new() {
599        let row_default = Row::<8>::default();
600        let row_new = Row::<8>::new();
601        assert_eq!(row_default, row_new);
602
603        let fb_default = TestBuffer::default();
604        let fb_new = TestBuffer::new();
605        assert_eq!(fb_default.planes, fb_new.planes);
606    }
607
608    #[test]
609    fn framebuffer_operations_trait_delegates_correctly() {
610        let mut fb = TestBuffer::new();
611        FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
612
613        assert!(fb.planes[0][5].data[map_index(3)].grn1());
614
615        FrameBufferOperations::erase(&mut fb);
616        for plane in &fb.planes {
617            for row in plane {
618                for entry in &row.data {
619                    assert!(!entry.red1());
620                    assert!(!entry.grn1());
621                    assert!(!entry.blu1());
622                    assert!(!entry.red2());
623                    assert!(!entry.grn2());
624                    assert!(!entry.blu2());
625                }
626            }
627        }
628    }
629
630    #[test]
631    fn addr_table_entries_are_consistent() {
632        let table = make_addr_table();
633        for addr in 0..32u8 {
634            let row_addrs = &table[addr as usize];
635            for a in row_addrs {
636                assert_eq!(a.addr(), addr);
637            }
638            let latch_false_count = row_addrs.iter().filter(|a| !a.latch()).count();
639            assert_eq!(latch_false_count, 1);
640        }
641        assert_eq!(table, ADDR_TABLE);
642    }
643}