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