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