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::{FrameBufferOperations, MutableFrameBuffer};
79
80bitfield! {
81    #[derive(Clone, Copy, Default, PartialEq, Eq)]
82    #[repr(transparent)]
83    pub(crate) struct Address(u8);
84    impl Debug;
85    pub(crate) output_enable, set_output_enable: 7;
86    pub(crate) latch, set_latch: 6;
87    pub(crate) addr, set_addr: 4, 0;
88}
89
90impl Address {
91    pub const fn new() -> Self {
92        Self(0)
93    }
94}
95
96bitfield! {
97    #[derive(Clone, Copy, Default, PartialEq)]
98    #[repr(transparent)]
99    pub(crate) struct Entry(u8);
100    impl Debug;
101    pub(crate) output_enable, set_output_enable: 7;
102    pub(crate) latch, set_latch: 6;
103    pub(crate) blu2, set_blu2: 5;
104    pub(crate) grn2, set_grn2: 4;
105    pub(crate) red2, set_red2: 3;
106    pub(crate) blu1, set_blu1: 2;
107    pub(crate) grn1, set_grn1: 1;
108    pub(crate) red1, set_red1: 0;
109}
110
111impl Entry {
112    pub const fn new() -> Self {
113        Self(0)
114    }
115
116    const COLOR0_MASK: u8 = 0b0000_0111;
117    const COLOR1_MASK: u8 = 0b0011_1000;
118
119    #[inline]
120    fn set_color0_bits(&mut self, bits: u8) {
121        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits & Self::COLOR0_MASK);
122    }
123
124    #[inline]
125    fn set_color1_bits(&mut self, bits: u8) {
126        self.0 = (self.0 & !Self::COLOR1_MASK) | ((bits << 3) & Self::COLOR1_MASK);
127    }
128}
129
130#[derive(Clone, Copy, PartialEq, Debug)]
131#[repr(C)]
132/// A single BCM row payload for 8-bit latched output.
133///
134/// Each row contains color-stream data for `COLS` pixels followed by four
135/// address/control bytes that clock the row address into the external latch.
136pub struct Row<const COLS: usize> {
137    pub(crate) data: [Entry; COLS],
138    pub(crate) address: [Address; 4],
139}
140
141#[inline]
142const fn map_index(index: usize) -> usize {
143    #[cfg(feature = "esp32-ordering")]
144    {
145        index ^ 2
146    }
147    #[cfg(not(feature = "esp32-ordering"))]
148    {
149        index
150    }
151}
152
153const fn make_addr_table() -> [[Address; 4]; 32] {
154    let mut tbl = [[Address::new(); 4]; 32];
155    let mut addr = 0;
156    while addr < 32 {
157        tbl[addr][map_index(0)].0 = 1u8 << 6 | addr as u8;
158        tbl[addr][map_index(1)].0 = 1u8 << 6 | addr as u8;
159        tbl[addr][map_index(2)].0 = addr as u8;
160        tbl[addr][map_index(3)].0 = 0;
161        addr += 1;
162    }
163    tbl
164}
165
166static ADDR_TABLE: [[Address; 4]; 32] = make_addr_table();
167
168const fn make_data_template<const COLS: usize>() -> [Entry; COLS] {
169    let mut data = [Entry::new(); COLS];
170    let mut i = 0;
171    while i < COLS {
172        let mapped_i = map_index(i);
173        data[mapped_i].0 = if i == COLS - 1 { 0 } else { 0b1000_0000 };
174        i += 1;
175    }
176    data
177}
178
179impl<const COLS: usize> Row<COLS> {
180    /// Creates a zero-initialized row.
181    ///
182    /// Call [`Self::format`] before first use to populate row address/control
183    /// metadata.
184    #[must_use]
185    pub const fn new() -> Self {
186        Self {
187            data: [Entry::new(); COLS],
188            address: [Address::new(); 4],
189        }
190    }
191
192    /// Formats this row for the provided multiplexed row address.
193    ///
194    /// This sets the trailing address bytes and initializes output-enable/latch
195    /// bits in the pixel stream template.
196    #[inline]
197    pub fn format(&mut self, addr: u8) {
198        debug_assert!((addr as usize) < ADDR_TABLE.len());
199        let src_addr = &ADDR_TABLE[addr as usize];
200        self.address[0] = src_addr[0];
201        self.address[1] = src_addr[1];
202        self.address[2] = src_addr[2];
203        self.address[3] = src_addr[3];
204
205        let data_template = make_data_template::<COLS>();
206        let mut i = 0;
207        while i < COLS {
208            self.data[i] = data_template[i];
209            i += 1;
210        }
211    }
212}
213
214impl<const COLS: usize> Default for Row<COLS> {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220/// The entire BCM Frame Buffer (Contiguous Memory)
221#[derive(Copy, Clone)]
222#[repr(C)]
223pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
224    pub(crate) planes: [[Row<COLS>; NROWS]; PLANES],
225}
226
227impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
228    DmaFrameBuffer<NROWS, COLS, PLANES>
229{
230    /// Creates a new frame buffer.
231    #[must_use]
232    pub fn new() -> Self {
233        let mut instance = Self {
234            planes: [[Row::new(); NROWS]; PLANES],
235        };
236        instance.format();
237        instance
238    }
239
240    /// Returns the number of BCM chunks (one per bit-plane).
241    #[must_use]
242    pub const fn bcm_chunk_count() -> usize {
243        PLANES
244    }
245
246    /// Returns the byte size of one BCM chunk (a single bit-plane).
247    #[must_use]
248    pub const fn bcm_chunk_bytes() -> usize {
249        NROWS * core::mem::size_of::<Row<COLS>>()
250    }
251
252    /// Formats the frame buffer with row addresses and control bits.
253    #[inline]
254    pub fn format(&mut self) {
255        for plane in &mut self.planes {
256            for (row_idx, row) in plane.iter_mut().enumerate() {
257                row.format(row_idx as u8);
258            }
259        }
260    }
261
262    /// Erase pixel colors while preserving row control data.
263    #[inline]
264    pub fn erase(&mut self) {
265        const MASK: u8 = !0b0011_1111;
266        for plane in &mut self.planes {
267            for row in plane {
268                for entry in &mut row.data {
269                    entry.0 &= MASK;
270                }
271            }
272        }
273    }
274
275    /// Set a pixel in the framebuffer.
276    #[inline]
277    pub fn set_pixel(&mut self, p: Point, color: Color) {
278        if p.x < 0 || p.y < 0 {
279            return;
280        }
281        self.set_pixel_internal(p.x as usize, p.y as usize, color);
282    }
283
284    #[inline]
285    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
286        if x >= COLS || y >= NROWS * 2 {
287            return;
288        }
289
290        let row_idx = if y < NROWS { y } else { y - NROWS };
291        let is_top = y < NROWS;
292        let red = color.r();
293        let green = color.g();
294        let blue = color.b();
295
296        for plane_idx in 0..PLANES {
297            let bit = 7_u32.saturating_sub(plane_idx as u32);
298            let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
299                | ((u8::from(((green >> bit) & 1) != 0)) << 1)
300                | u8::from(((red >> bit) & 1) != 0);
301            let col_idx = map_index(x);
302            let entry = &mut self.planes[plane_idx][row_idx].data[col_idx];
303            if is_top {
304                entry.set_color0_bits(bits);
305            } else {
306                entry.set_color1_bits(bits);
307            }
308        }
309    }
310}
311
312impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
313    for DmaFrameBuffer<NROWS, COLS, PLANES>
314{
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
321    for DmaFrameBuffer<NROWS, COLS, PLANES>
322{
323    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
324        f.debug_struct("DmaFrameBuffer")
325            .field("size", &core::mem::size_of_val(&self.planes))
326            .field("plane_count", &self.planes.len())
327            .field("plane_size", &core::mem::size_of_val(&self.planes[0]))
328            .finish()
329    }
330}
331
332#[cfg(feature = "defmt")]
333impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
334    for DmaFrameBuffer<NROWS, COLS, PLANES>
335{
336    fn format(&self, f: defmt::Formatter) {
337        defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
338        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
339        defmt::write!(
340            f,
341            " plane_size: {}",
342            core::mem::size_of_val(&self.planes[0])
343        );
344    }
345}
346
347impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
348    for DmaFrameBuffer<NROWS, COLS, PLANES>
349{
350    type Word = u8;
351
352    fn plane_count(&self) -> usize {
353        PLANES
354    }
355
356    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
357        assert!(
358            plane_idx < PLANES,
359            "plane_idx {plane_idx} out of range for {PLANES} planes"
360        );
361        let ptr = self.planes[plane_idx].as_ptr().cast::<u8>();
362        let len = NROWS * core::mem::size_of::<Row<COLS>>();
363        (ptr, len)
364    }
365}
366
367impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
368    for DmaFrameBuffer<NROWS, COLS, PLANES>
369{
370    #[inline]
371    fn erase(&mut self) {
372        DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
373    }
374
375    #[inline]
376    fn set_pixel(&mut self, p: Point, color: Color) {
377        DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
378    }
379}
380
381impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
382    for DmaFrameBuffer<NROWS, COLS, PLANES>
383{
384}
385
386impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
387    for DmaFrameBuffer<NROWS, COLS, PLANES>
388{
389    fn size(&self) -> Size {
390        Size::new(COLS as u32, (NROWS * 2) as u32)
391    }
392}
393
394impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
395    for DmaFrameBuffer<NROWS, COLS, PLANES>
396{
397    type Color = Color;
398    type Error = Infallible;
399
400    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
401    where
402        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
403    {
404        for pixel in pixels {
405            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
406        }
407        Ok(())
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    extern crate std;
414
415    use super::*;
416    use embedded_graphics::prelude::*;
417    use std::format;
418
419    type TestBuffer = DmaFrameBuffer<16, 64, 8>;
420
421    #[test]
422    fn row_format_sets_address_and_control_bits() {
423        let mut row = Row::<8>::new();
424        row.format(5);
425        let latch_count = row.address.iter().filter(|a| a.latch()).count();
426        assert_eq!(latch_count, 2);
427        assert_eq!(row.address[map_index(0)].addr(), 5);
428        assert_eq!(row.address[map_index(1)].addr(), 5);
429        assert_eq!(row.address[map_index(2)].addr(), 5);
430        assert_eq!(row.address[map_index(3)].0, 0);
431        let oe_false_count = row
432            .data
433            .iter()
434            .filter(|entry| !entry.output_enable())
435            .count();
436        assert_eq!(oe_false_count, 1);
437    }
438
439    #[test]
440    fn format_sets_expected_row_addresses_for_all_rows() {
441        let mut fb = TestBuffer::new();
442        fb.format();
443
444        for plane_idx in 0..8 {
445            for row_idx in 0..16 {
446                let row = &fb.planes[plane_idx][row_idx];
447                assert_eq!(row.address[map_index(0)].addr(), row_idx as u8);
448                assert_eq!(row.address[map_index(1)].addr(), row_idx as u8);
449                assert_eq!(row.address[map_index(2)].addr(), row_idx as u8);
450                assert_eq!(row.address[map_index(3)].0, 0);
451            }
452        }
453    }
454
455    #[test]
456    fn set_pixel_maps_top_half_bits_per_plane() {
457        let mut fb = TestBuffer::new();
458        let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
459        fb.set_pixel(Point::new(2, 3), color);
460
461        for plane_idx in 0..8 {
462            let bit = 7 - plane_idx;
463            let entry = fb.planes[plane_idx][3].data[map_index(2)];
464            assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
465            assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
466            assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
467        }
468    }
469
470    #[test]
471    fn set_pixel_maps_bottom_half_bits_per_plane() {
472        let mut fb = TestBuffer::new();
473        let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
474        fb.set_pixel(Point::new(4, 20), color);
475
476        for plane_idx in 0..8 {
477            let bit = 7 - plane_idx;
478            let entry = fb.planes[plane_idx][4].data[map_index(4)];
479            assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
480            assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
481            assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
482        }
483    }
484
485    #[test]
486    fn erase_clears_only_color_bits() {
487        let mut fb = TestBuffer::new();
488        let oe_before = fb.planes[0][0].data[0].output_enable();
489        fb.set_pixel(Point::new(0, 0), Color::WHITE);
490        fb.erase();
491
492        for plane in &fb.planes {
493            for row in plane {
494                for entry in &row.data {
495                    assert!(!entry.red1());
496                    assert!(!entry.grn1());
497                    assert!(!entry.blu1());
498                    assert!(!entry.red2());
499                    assert!(!entry.grn2());
500                    assert!(!entry.blu2());
501                }
502            }
503        }
504
505        assert_eq!(fb.planes[0][0].data[0].output_enable(), oe_before);
506    }
507
508    #[test]
509    fn draw_target_iter_sets_pixels() {
510        let mut fb = TestBuffer::new();
511        let pixels = [Pixel(Point::new(1, 1), Color::RED)];
512        let result = fb.draw_iter(pixels);
513        assert!(result.is_ok());
514
515        for plane_idx in 0..8 {
516            let bit = 7 - plane_idx;
517            let entry = fb.planes[plane_idx][1].data[map_index(1)];
518            assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
519            assert!(!entry.grn1());
520            assert!(!entry.blu1());
521        }
522    }
523
524    #[test]
525    fn set_pixel_ignores_out_of_bounds_and_negative() {
526        let mut fb = TestBuffer::new();
527        let before = fb.planes;
528        fb.set_pixel(Point::new(-1, 0), Color::WHITE);
529        fb.set_pixel(Point::new(0, -1), Color::WHITE);
530        fb.set_pixel(Point::new(64, 0), Color::WHITE);
531        fb.set_pixel(Point::new(0, 32), Color::WHITE);
532        assert_eq!(fb.planes, before);
533    }
534
535    #[test]
536    fn bcm_chunk_info_for_common_panel() {
537        assert_eq!(TestBuffer::bcm_chunk_count(), 8);
538        assert_eq!(
539            TestBuffer::bcm_chunk_bytes(),
540            16 * core::mem::size_of::<Row<64>>()
541        );
542    }
543
544    #[test]
545    fn frame_buffer_trait_accessors_report_expected_values() {
546        let fb = TestBuffer::new();
547        let as_trait: &dyn FrameBuffer<Word = u8> = &fb;
548        assert_eq!(as_trait.get_word_size(), crate::WordSize::Eight);
549        assert_eq!(as_trait.plane_count(), 8);
550
551        let (ptr, len) = as_trait.plane_ptr_len(0);
552        assert_eq!(len, 16 * core::mem::size_of::<Row<64>>());
553        assert_eq!(ptr, fb.planes[0].as_ptr().cast::<u8>());
554    }
555
556    #[test]
557    #[should_panic(expected = "out of range")]
558    fn plane_ptr_len_panics_for_invalid_plane() {
559        let fb = TestBuffer::new();
560        let _ = fb.plane_ptr_len(8);
561    }
562
563    #[test]
564    fn origin_dimensions_match_panel_geometry() {
565        let fb = TestBuffer::new();
566        assert_eq!(fb.size(), Size::new(64, 32));
567    }
568
569    #[test]
570    fn debug_impl_includes_shape_information() {
571        let fb = TestBuffer::new();
572        let s = format!("{fb:?}");
573        assert!(s.contains("DmaFrameBuffer"));
574        assert!(s.contains("plane_count"));
575        assert!(s.contains("plane_size"));
576    }
577
578    #[test]
579    fn row_format_sets_exactly_one_data_word_with_oe_low() {
580        let mut row = Row::<16>::new();
581        row.format(9);
582
583        let oe_low_indices: std::vec::Vec<_> = row
584            .data
585            .iter()
586            .enumerate()
587            .filter_map(|(i, entry)| (!entry.output_enable()).then_some(i))
588            .collect();
589        assert_eq!(oe_low_indices.len(), 1);
590        assert_eq!(oe_low_indices[0], map_index(15));
591    }
592
593    #[test]
594    fn default_constructors_match_new() {
595        let row_default = Row::<8>::default();
596        let row_new = Row::<8>::new();
597        assert_eq!(row_default, row_new);
598
599        let fb_default = TestBuffer::default();
600        let fb_new = TestBuffer::new();
601        assert_eq!(fb_default.planes, fb_new.planes);
602    }
603
604    #[test]
605    fn framebuffer_operations_trait_delegates_correctly() {
606        let mut fb = TestBuffer::new();
607        FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
608
609        assert!(fb.planes[0][5].data[map_index(3)].grn1());
610
611        FrameBufferOperations::erase(&mut fb);
612        for plane in &fb.planes {
613            for row in plane {
614                for entry in &row.data {
615                    assert!(!entry.red1());
616                    assert!(!entry.grn1());
617                    assert!(!entry.blu1());
618                    assert!(!entry.red2());
619                    assert!(!entry.grn2());
620                    assert!(!entry.blu2());
621                }
622            }
623        }
624    }
625
626    #[test]
627    fn addr_table_entries_are_consistent() {
628        let table = make_addr_table();
629        for addr in 0..32u8 {
630            let row = &table[addr as usize];
631            // First two clocks: latch asserted with row address
632            assert!(row[map_index(0)].latch());
633            assert_eq!(row[map_index(0)].addr(), addr);
634            assert!(row[map_index(1)].latch());
635            assert_eq!(row[map_index(1)].addr(), addr);
636            // Third clock: latch released, address still driven
637            assert!(!row[map_index(2)].latch());
638            assert_eq!(row[map_index(2)].addr(), addr);
639            // Fourth clock: clear cycle, all zero
640            assert!(!row[map_index(3)].latch());
641            assert_eq!(row[map_index(3)].0, 0);
642        }
643        assert_eq!(table, ADDR_TABLE);
644    }
645}