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