Skip to main content

hub75_framebuffer/bitplane/
plain.rs

1//! Bitplane framebuffer for a 16-bit plain 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::plain::DmaFrameBuffer`]. Each plane holds one bit of every colour
6//! channel, giving `PLANES` planes total (typically 8 for full 8-bit colour).
7//! Row addressing, latch, OE, and colour data are all packed into each 16-bit
8//! word -- no external latch circuit is required.
9//!
10//! # Hardware Requirements
11//! Requires a parallel output peripheral capable of clocking 16 bits (though
12//! only 15 are actually used) at a time. The data is structured to directly
13//! match the HUB75 connector signals, so no external latch or address-decode
14//! hardware is needed.
15//!
16//! # HUB75 Signal Bit Mapping
17//! Each 16-bit `Entry` represents the logic levels driven onto the HUB75 bus
18//! during a single pixel-clock cycle:
19//!
20//! ```text
21//! 15 ─ (spare)
22//! 14 ─ B2       Blue  – lower half of the panel
23//! 13 ─ G2       Green – lower half of the panel
24//! 12 ─ R2       Red   – lower half of the panel
25//! 11 ─ B1       Blue  – upper half of the panel
26//! 10 ─ G1       Green – upper half of the panel
27//!  9 ─ R1       Red   – upper half of the panel
28//!  8 ─ OE       Output-Enable / Blank
29//!  7 ─ (spare)
30//!  6 ─ (spare)
31//!  5 ─ LAT      Latch / STB
32//! 4-0 ─ A..E    Row address lines
33//! ```
34//!
35//! The pixel clock is generated by the peripheral that owns the DMA stream and
36//! is not part of the 16-bit word stored in the framebuffer.
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::plain::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` 16-bit entries, so total size is
68//! `PLANES * NROWS * COLS * 2` 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
81const BLANKING_DELAY: usize = 1;
82
83#[inline]
84const fn map_index(i: usize) -> usize {
85    #[cfg(feature = "esp32-ordering")]
86    {
87        i ^ 1
88    }
89    #[cfg(not(feature = "esp32-ordering"))]
90    {
91        i
92    }
93}
94
95#[inline]
96const fn make_data_template<const COLS: usize>(addr: u8, prev_addr: u8) -> [Entry; COLS] {
97    let mut data = [Entry::new(); COLS];
98    let mut i = 0;
99
100    while i < COLS {
101        let mut entry = Entry::new();
102        entry.0 = prev_addr as u16;
103
104        if i == 1 {
105            entry.0 |= 0b1_0000_0000; // OE
106        } else if i == COLS - BLANKING_DELAY - 1 {
107            // OE stays false
108        } else if i == COLS - 1 {
109            entry.0 |= 0b0010_0000; // latch
110            entry.0 = (entry.0 & !0b0001_1111) | (addr as u16); // new address
111        } else if i > 1 && i < COLS - BLANKING_DELAY - 1 {
112            entry.0 |= 0b1_0000_0000; // OE
113        }
114
115        data[map_index(i)] = entry;
116        i += 1;
117    }
118
119    data
120}
121
122bitfield! {
123    #[derive(Clone, Copy, Default, PartialEq)]
124    #[repr(transparent)]
125    pub(crate) struct Entry(u16);
126    pub(crate) dummy2, set_dummy2: 15;
127    pub(crate) blu2, set_blu2: 14;
128    pub(crate) grn2, set_grn2: 13;
129    pub(crate) red2, set_red2: 12;
130    pub(crate) blu1, set_blu1: 11;
131    pub(crate) grn1, set_grn1: 10;
132    pub(crate) red1, set_red1: 9;
133    pub(crate) output_enable, set_output_enable: 8;
134    pub(crate) dummy1, set_dummy1: 7;
135    pub(crate) dummy0, set_dummy0: 6;
136    pub(crate) latch, set_latch: 5;
137    pub(crate) addr, set_addr: 4, 0;
138}
139
140impl core::fmt::Debug for Entry {
141    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
142        f.debug_tuple("Entry")
143            .field(&format_args!("{:#x}", self.0))
144            .finish()
145    }
146}
147
148impl Entry {
149    const fn new() -> Self {
150        Self(0)
151    }
152
153    const COLOR0_MASK: u16 = 0b0000_1110_0000_0000; // bits 9-11: R1, G1, B1
154    const COLOR1_MASK: u16 = 0b0111_0000_0000_0000; // bits 12-14: R2, G2, B2
155
156    #[inline]
157    fn set_color0_bits(&mut self, bits: u8) {
158        let bits16 = u16::from(bits) << 9;
159        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits16 & Self::COLOR0_MASK);
160    }
161
162    #[inline]
163    fn set_color1_bits(&mut self, bits: u8) {
164        let bits16 = u16::from(bits) << 12;
165        self.0 = (self.0 & !Self::COLOR1_MASK) | (bits16 & Self::COLOR1_MASK);
166    }
167}
168
169#[derive(Clone, Copy, PartialEq, Debug)]
170#[repr(C)]
171/// A single BCM row payload for 16-bit plain output.
172///
173/// Row addressing, latch, OE, and pixel colour data are all encoded into the
174/// 16-bit `Entry` words -- no separate address bytes are needed.
175pub struct Row<const COLS: usize> {
176    pub(crate) data: [Entry; COLS],
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 control metadata.
183    #[must_use]
184    pub const fn new() -> Self {
185        Self {
186            data: [Entry::new(); COLS],
187        }
188    }
189
190    /// Formats this row for the provided multiplexed row address.
191    ///
192    /// Sets up blanking delay, output-enable, latch, and address bits in the
193    /// pixel stream template.
194    #[inline]
195    pub fn format(&mut self, addr: u8, prev_addr: u8) {
196        let template = make_data_template::<COLS>(addr, prev_addr);
197        self.data.copy_from_slice(&template);
198    }
199}
200
201impl<const COLS: usize> Default for Row<COLS> {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207/// The entire BCM Frame Buffer (per-plane storage).
208#[derive(Copy, Clone)]
209#[repr(C)]
210pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
211    pub(crate) planes: [[Row<COLS>; NROWS]; PLANES],
212}
213
214impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
215    DmaFrameBuffer<NROWS, COLS, PLANES>
216{
217    /// Creates a new frame buffer, pre-formatted and ready for use.
218    #[must_use]
219    pub fn new() -> Self {
220        let mut instance = Self {
221            planes: [[Row::new(); NROWS]; PLANES],
222        };
223        instance.format();
224        instance
225    }
226
227    /// Returns the number of BCM chunks (one per bit-plane).
228    #[must_use]
229    pub const fn bcm_chunk_count() -> usize {
230        PLANES
231    }
232
233    /// Returns the byte size of one BCM chunk (a single bit-plane).
234    #[must_use]
235    pub const fn bcm_chunk_bytes() -> usize {
236        NROWS * core::mem::size_of::<Row<COLS>>()
237    }
238
239    /// Formats the frame buffer with row addresses and control bits.
240    #[inline]
241    pub fn format(&mut self) {
242        for plane in &mut self.planes {
243            for (row_idx, row) in plane.iter_mut().enumerate() {
244                let prev_addr = if row_idx == 0 {
245                    NROWS as u8 - 1
246                } else {
247                    row_idx as u8 - 1
248                };
249                row.format(row_idx as u8, prev_addr);
250            }
251        }
252    }
253
254    /// Erase pixel colors while preserving row control data.
255    #[inline]
256    pub fn erase(&mut self) {
257        const MASK: u16 = !0b0111_1110_0000_0000; // clear bits 9-14 (R1,G1,B1,R2,G2,B2)
258        for plane in &mut self.planes {
259            for row in plane {
260                for entry in &mut row.data {
261                    entry.0 &= MASK;
262                }
263            }
264        }
265    }
266
267    /// Set a pixel in the framebuffer.
268    #[inline]
269    pub fn set_pixel(&mut self, p: Point, color: Color) {
270        if p.x < 0 || p.y < 0 {
271            return;
272        }
273        self.set_pixel_internal(p.x as usize, p.y as usize, color);
274    }
275
276    #[inline]
277    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
278        if x >= COLS || y >= NROWS * 2 {
279            return;
280        }
281
282        let row_idx = if y < NROWS { y } else { y - NROWS };
283        let is_top = y < NROWS;
284        let red = color.r();
285        let green = color.g();
286        let blue = color.b();
287
288        for plane_idx in 0..PLANES {
289            let bit = 7_u32.saturating_sub(plane_idx as u32);
290            let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
291                | ((u8::from(((green >> bit) & 1) != 0)) << 1)
292                | u8::from(((red >> bit) & 1) != 0);
293            let col_idx = map_index(x);
294            let entry = &mut self.planes[plane_idx][row_idx].data[col_idx];
295            if is_top {
296                entry.set_color0_bits(bits);
297            } else {
298                entry.set_color1_bits(bits);
299            }
300        }
301    }
302}
303
304impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
305    for DmaFrameBuffer<NROWS, COLS, PLANES>
306{
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
313    for DmaFrameBuffer<NROWS, COLS, PLANES>
314{
315    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
316        f.debug_struct("DmaFrameBuffer")
317            .field("size", &core::mem::size_of_val(&self.planes))
318            .field("plane_count", &self.planes.len())
319            .field("plane_size", &core::mem::size_of_val(&self.planes[0]))
320            .finish()
321    }
322}
323
324#[cfg(feature = "defmt")]
325impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
326    for DmaFrameBuffer<NROWS, COLS, PLANES>
327{
328    fn format(&self, f: defmt::Formatter) {
329        defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
330        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
331        defmt::write!(
332            f,
333            " plane_size: {}",
334            core::mem::size_of_val(&self.planes[0])
335        );
336    }
337}
338
339impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
340    for DmaFrameBuffer<NROWS, COLS, PLANES>
341{
342    fn get_word_size(&self) -> WordSize {
343        WordSize::Sixteen
344    }
345
346    fn plane_count(&self) -> usize {
347        PLANES
348    }
349
350    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
351        assert!(
352            plane_idx < PLANES,
353            "plane_idx {plane_idx} out of range for {PLANES} planes"
354        );
355        let ptr = self.planes[plane_idx].as_ptr().cast::<u8>();
356        let len = NROWS * core::mem::size_of::<Row<COLS>>();
357        (ptr, len)
358    }
359}
360
361impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
362    for DmaFrameBuffer<NROWS, COLS, PLANES>
363{
364    #[inline]
365    fn erase(&mut self) {
366        DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
367    }
368
369    #[inline]
370    fn set_pixel(&mut self, p: Point, color: Color) {
371        DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
372    }
373}
374
375impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
376    for DmaFrameBuffer<NROWS, COLS, PLANES>
377{
378}
379
380impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
381    for DmaFrameBuffer<NROWS, COLS, PLANES>
382{
383    fn size(&self) -> Size {
384        Size::new(COLS as u32, (NROWS * 2) as u32)
385    }
386}
387
388impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
389    for DmaFrameBuffer<NROWS, COLS, PLANES>
390{
391    type Color = Color;
392    type Error = Infallible;
393
394    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
395    where
396        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
397    {
398        for pixel in pixels {
399            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
400        }
401        Ok(())
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    extern crate std;
408
409    use super::*;
410    use embedded_graphics::prelude::*;
411    use std::format;
412
413    type TestBuffer = DmaFrameBuffer<16, 64, 8>;
414
415    #[test]
416    fn row_format_sets_address_and_control_bits() {
417        let mut row = Row::<8>::new();
418        row.format(5, 4);
419
420        let last_idx = map_index(7);
421        assert_eq!(row.data[last_idx].latch(), true);
422        assert_eq!(row.data[last_idx].addr(), 5);
423
424        let first_idx = map_index(0);
425        assert_eq!(row.data[first_idx].addr(), 4);
426        assert_eq!(row.data[first_idx].latch(), false);
427    }
428
429    #[test]
430    fn format_sets_expected_row_addresses_for_all_rows() {
431        let mut fb = TestBuffer::new();
432        fb.format();
433
434        for plane_idx in 0..8 {
435            for row_idx in 0..16 {
436                let last_col = map_index(63);
437                assert_eq!(
438                    fb.planes[plane_idx][row_idx].data[last_col].addr(),
439                    row_idx as u16
440                );
441                assert_eq!(fb.planes[plane_idx][row_idx].data[last_col].latch(), true);
442            }
443        }
444    }
445
446    #[test]
447    fn set_pixel_maps_top_half_bits_per_plane() {
448        let mut fb = TestBuffer::new();
449        let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
450        fb.set_pixel(Point::new(2, 3), color);
451
452        for plane_idx in 0..8 {
453            let bit = 7 - plane_idx;
454            let entry = fb.planes[plane_idx][3].data[map_index(2)];
455            assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
456            assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
457            assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
458        }
459    }
460
461    #[test]
462    fn set_pixel_maps_bottom_half_bits_per_plane() {
463        let mut fb = TestBuffer::new();
464        let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
465        fb.set_pixel(Point::new(4, 20), color);
466
467        for plane_idx in 0..8 {
468            let bit = 7 - plane_idx;
469            let entry = fb.planes[plane_idx][4].data[map_index(4)];
470            assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
471            assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
472            assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
473        }
474    }
475
476    #[test]
477    fn erase_clears_only_color_bits() {
478        let mut fb = TestBuffer::new();
479        let oe_before = fb.planes[0][0].data[map_index(1)].output_enable();
480        fb.set_pixel(Point::new(0, 0), Color::WHITE);
481        fb.erase();
482
483        for plane in &fb.planes {
484            for row in plane {
485                for entry in &row.data {
486                    assert!(!entry.red1());
487                    assert!(!entry.grn1());
488                    assert!(!entry.blu1());
489                    assert!(!entry.red2());
490                    assert!(!entry.grn2());
491                    assert!(!entry.blu2());
492                }
493            }
494        }
495
496        assert_eq!(
497            fb.planes[0][0].data[map_index(1)].output_enable(),
498            oe_before
499        );
500    }
501
502    #[test]
503    fn draw_target_iter_sets_pixels() {
504        let mut fb = TestBuffer::new();
505        let pixels = [Pixel(Point::new(1, 1), Color::RED)];
506        let result = fb.draw_iter(pixels);
507        assert!(result.is_ok());
508
509        for plane_idx in 0..8 {
510            let bit = 7 - plane_idx;
511            let entry = fb.planes[plane_idx][1].data[map_index(1)];
512            assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
513            assert!(!entry.grn1());
514            assert!(!entry.blu1());
515        }
516    }
517
518    #[test]
519    fn set_pixel_ignores_out_of_bounds_and_negative() {
520        let mut fb = TestBuffer::new();
521        let before = fb.planes;
522        fb.set_pixel(Point::new(-1, 0), Color::WHITE);
523        fb.set_pixel(Point::new(0, -1), Color::WHITE);
524        fb.set_pixel(Point::new(64, 0), Color::WHITE);
525        fb.set_pixel(Point::new(0, 32), Color::WHITE);
526        assert_eq!(fb.planes, before);
527    }
528
529    #[test]
530    fn bcm_chunk_info_for_common_panel() {
531        assert_eq!(TestBuffer::bcm_chunk_count(), 8);
532        assert_eq!(
533            TestBuffer::bcm_chunk_bytes(),
534            16 * core::mem::size_of::<Row<64>>()
535        );
536    }
537
538    #[test]
539    fn frame_buffer_trait_accessors_report_expected_values() {
540        let fb = TestBuffer::new();
541        let as_trait: &dyn FrameBuffer = &fb;
542        assert_eq!(as_trait.get_word_size(), WordSize::Sixteen);
543        assert_eq!(as_trait.plane_count(), 8);
544
545        let (ptr, len) = as_trait.plane_ptr_len(0);
546        assert_eq!(len, 16 * core::mem::size_of::<Row<64>>());
547        assert_eq!(ptr, fb.planes[0].as_ptr().cast::<u8>());
548    }
549
550    #[test]
551    #[should_panic(expected = "out of range")]
552    fn plane_ptr_len_panics_for_invalid_plane() {
553        let fb = TestBuffer::new();
554        let _ = fb.plane_ptr_len(8);
555    }
556
557    #[test]
558    fn origin_dimensions_match_panel_geometry() {
559        let fb = TestBuffer::new();
560        assert_eq!(fb.size(), Size::new(64, 32));
561    }
562
563    #[test]
564    fn debug_impl_includes_shape_information() {
565        let fb = TestBuffer::new();
566        let s = format!("{fb:?}");
567        assert!(s.contains("DmaFrameBuffer"));
568        assert!(s.contains("plane_count"));
569        assert!(s.contains("plane_size"));
570    }
571
572    #[test]
573    fn row_format_sets_expected_blank_and_latch_positions() {
574        let mut row = Row::<8>::new();
575        row.format(5, 4);
576
577        // i == 1 keeps OE high (visible) with previous address
578        let idx_1 = map_index(1);
579        assert!(row.data[idx_1].output_enable());
580        assert_eq!(row.data[idx_1].addr(), 4);
581
582        // i == COLS - BLANKING_DELAY - 1 blanks output before latch
583        let idx_blank = map_index(8 - BLANKING_DELAY - 1);
584        assert!(!row.data[idx_blank].output_enable());
585
586        // i == COLS - 1 latches and switches to new address
587        let idx_last = map_index(7);
588        assert!(row.data[idx_last].latch());
589        assert_eq!(row.data[idx_last].addr(), 5);
590    }
591
592    #[test]
593    fn default_constructors_match_new() {
594        let row_default = Row::<8>::default();
595        let row_new = Row::<8>::new();
596        assert_eq!(row_default, row_new);
597
598        let fb_default = TestBuffer::default();
599        let fb_new = TestBuffer::new();
600        assert_eq!(fb_default.planes, fb_new.planes);
601    }
602
603    #[test]
604    fn framebuffer_operations_trait_delegates_correctly() {
605        let mut fb = TestBuffer::new();
606        FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
607
608        assert!(fb.planes[0][5].data[map_index(3)].grn1());
609
610        FrameBufferOperations::erase(&mut fb);
611        for plane in &fb.planes {
612            for row in plane {
613                for entry in &row.data {
614                    assert!(!entry.red1());
615                    assert!(!entry.grn1());
616                    assert!(!entry.blu1());
617                    assert!(!entry.red2());
618                    assert!(!entry.grn2());
619                    assert!(!entry.blu2());
620                }
621            }
622        }
623    }
624
625    #[test]
626    fn entry_debug_shows_hex_value() {
627        let mut entry = Entry::new();
628        entry.set_red1(true);
629        let s = format!("{entry:?}");
630        assert!(s.contains("Entry"));
631        assert!(s.contains("0x"));
632    }
633}