Skip to main content

hub75_framebuffer/
latched.rs

1//! DMA-friendly framebuffer implementation for HUB75 LED panels with external
2//! latch circuit support.
3//!
4//! This module provides a framebuffer implementation with memory
5//! layout optimized for efficient transfer to HUB75 LED panels. The data is
6//! structured for direct signal mapping, making it ideal for DMA transfers but
7//! also suitable for programmatic transfer. It supports RGB color and brightness
8//! control through multiple frames using Binary Code Modulation (BCM).
9//!
10//! # Hardware Requirements
11//! This implementation can be used by any microcontroller that has a peripheral
12//! capable of outputting a clock signal and 8 bits in parallel. A latch circuit
13//! similar to the one shown below can be used to hold the row address. The clock
14//! is gated so it does not reach the HUB75 interface when the latch is open.
15//! Since there is typically 4 2 input nand gates on a chip the 4th is used to allow
16//! PWM to gate the output enable providing much finer grained overall brightness control.
17//!
18// Important: note the blank line of documentation on each side of the image lookup table.
19// The "image lookup table" can be placed anywhere, but we place it here together with the
20// warning if the `doc-images` feature is not enabled.
21#![cfg_attr(feature = "doc-images",
22cfg_attr(all(),
23doc = ::embed_doc_image::embed_image!("latch-circuit", "images/latch-circuit.png")))]
24#![cfg_attr(
25    not(feature = "doc-images"),
26    doc = "**Doc images not enabled**. Compile with feature `doc-images` and Rust version >= 1.54 \
27           to enable."
28)]
29//!
30//! ![Latch Circuit][latch-circuit]
31//!
32//! # Key Differences from Plain Implementation
33//! - Uses an external latch circuit to hold the row address and gate the pixel
34//!   clock, reducing memory usage
35//! - 8-bit entries instead of 16-bit, halving memory requirements
36//! - Separate address and data words for better control
37//! - Requires an external latch circuit; not compatible with plain HUB75 wiring
38//!
39//! # Features
40//! - Support for RGB color with brightness control
41//! - Multiple frame buffers for Binary Code Modulation (BCM)
42//! - Integration with embedded-graphics for easy drawing
43//! - Memory-efficient 8-bit format
44//!
45//! # Brightness Control
46//! Brightness is controlled through Binary Code Modulation (BCM):
47//! - The number of brightness levels is determined by the `BITS` parameter
48//! - Each additional bit doubles the number of brightness levels
49//! - More bits provide better brightness resolution but require more memory
50//! - Memory usage grows exponentially with the number of bits: `(2^BITS)-1`
51//!   frames
52//! - Example: 8 bits = 256 levels, 4 bits = 16 levels
53//!
54//! # Memory Usage
55//! The framebuffer's memory usage is determined by:
56//! - Panel size (ROWS × COLS)
57//! - Number of brightness bits (BITS)
58//! - Memory grows exponentially with bits: `(2^BITS)-1` frames
59//! - 8-bit entries reduce memory usage compared to 16-bit implementations
60//!
61//! # Example
62//! ```rust
63//! use embedded_graphics::pixelcolor::RgbColor;
64//! use embedded_graphics::prelude::*;
65//! use embedded_graphics::primitives::Circle;
66//! use embedded_graphics::primitives::Rectangle;
67//! use embedded_graphics::primitives::PrimitiveStyle;
68//! use hub75_framebuffer::compute_frame_count;
69//! use hub75_framebuffer::compute_rows;
70//! use hub75_framebuffer::Color;
71//! use hub75_framebuffer::latched::DmaFrameBuffer;
72//!
73//! // Create a framebuffer for a 64x32 panel with 3-bit color depth
74//! const ROWS: usize = 32;
75//! const COLS: usize = 64;
76//! const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
77//! const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
78//! const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
79//!
80//! let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
81//!
82//! // Draw a red rectangle
83//! Rectangle::new(Point::new(10, 10), Size::new(20, 20))
84//!     .into_styled(PrimitiveStyle::with_fill(Color::RED))
85//!     .draw(&mut framebuffer)
86//!     .unwrap();
87//!
88//! // Draw a blue circle
89//! Circle::new(Point::new(40, 20), 10)
90//!     .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
91//!     .draw(&mut framebuffer)
92//!     .unwrap();
93//! ```
94//!
95//! # Implementation Details
96//! The framebuffer is organized to efficiently use memory while maintaining
97//! HUB75 compatibility:
98//! - Each row contains both data and address words
99//! - 8-bit entries store RGB data for two sub-pixels
100//! - Separate address words control row selection and timing
101//! - Multiple frames are used to achieve Binary Code Modulation (BCM)
102//! - DMA transfers the data directly to the panel without
103//!   transformation
104//!
105//! # HUB75 Signal Bit Mapping (8-bit words)
106//! Two distinct 8-bit words are streamed to the panel:
107//!
108//! 1. **Address / Timing (`Address`)** – row-select and latch control.
109//! 2. **Pixel Data (`Entry`)**       – RGB bits for two sub-pixels plus OE/LAT shadow bits.
110//!
111//! The bit layouts intentionally overlap so that *the very same GPIO lines*
112//! can transmit either word without any run-time bit twiddling:
113//!
114//! ```text
115//! Address word (row select & timing)
116//! ┌──7─┬──6──┬─5─┬─4─┬─3─┬─2─┬─1─┬─0─┐
117//! │ OE │ LAT │   │ E │ D │ C │ B │ A │
118//! └────┴─────┴───┴───┴───┴───┴───┴───┘
119//!        ^                ^
120//!        |                └── Row-address lines (LSB = A)
121//!        └── Latch pulse – when HIGH the current address is latched and
122//!            external glue logic gates the pixel clock (`CLK`).
123//! ````
124//! ```text
125//! Entry word (pixel data for two sub-pixels)
126//! ┌──7─┬──6──┬─5──┬─4──┬─3──┬─2──┬─1──┬─0──┐
127//! │ OE │ LAT │ B2 │ G2 │ R2 │ B1 │ G1 │ R1 │
128//! └────┴─────┴────┴────┴────┴────┴────┴────┘
129//! ```
130//!
131//! *Bits 7–6* (OE/LAT) mirror those in the `Address` word so the control lines
132//! remain valid throughout the entire DMA stream.
133//!
134//! # External Latch Timing Sequence
135//! 1. Pixel data for row *N* is clocked out while `OE` is LOW.
136//! 2. `OE` is raised **HIGH** – LEDs blank.
137//! 3. An **`Address` word** with the new row index is transmitted while
138//!    `LAT` is HIGH; the CPLD/logic also blocks `CLK` during this period.
139//! 4. `LAT` returns LOW and `OE` is driven LOW again.
140//!
141//! This keeps visual artefacts to a minimum while allowing the framebuffer to
142//! use just 8 data bits.
143//!
144//! # Binary Code Modulation (BCM) Frames
145//! Brightness is realised with Binary-Code-Modulation just like the *plain*
146//! implementation—see <https://www.batsocks.co.uk/readme/art_bcm_1.htm>.
147//! With a colour depth of `BITS` the driver allocates
148//! `FRAME_COUNT = 2^BITS − 1` frames. Frame *n* (0-based) is displayed for a
149//! time slice proportional to `2^n`.
150//!
151//! For each channel the driver compares the 8-bit colour value against a per-frame
152//! threshold:
153//!
154//! ```text
155//! brightness_step = 256 / 2^BITS
156//! threshold_n     = (n + 1) * brightness_step
157//! ```
158//!
159//! The channel bit is set in frame *n* iff `value >= threshold_n`. Streaming the
160//! frames from LSB to MSB therefore reproduces the intended 8-bit intensity
161//! without extra processing.
162//!
163//! # Memory Layout
164//! Each row consists of:
165//! - 4 address words (8 bits each) for row selection and timing
166//! - COLS data words (8 bits each) for pixel data
167//!
168//! # Safety
169//! This implementation uses unsafe code for DMA operations. The framebuffer
170//! must be properly aligned in memory and the DMA configuration must match the
171//! buffer layout.
172use core::convert::Infallible;
173
174use super::Color;
175use crate::{FrameBufferOperations, MutableFrameBuffer};
176use bitfield::bitfield;
177use embedded_dma::ReadBuffer;
178use embedded_graphics::pixelcolor::Rgb888;
179use embedded_graphics::pixelcolor::RgbColor;
180use embedded_graphics::prelude::Point;
181
182#[cfg(feature = "blank-delay-1")]
183const BLANKING_DELAY: usize = 1;
184#[cfg(feature = "blank-delay-2")]
185const BLANKING_DELAY: usize = 2;
186#[cfg(feature = "blank-delay-4")]
187const BLANKING_DELAY: usize = 4;
188#[cfg(feature = "blank-delay-8")]
189const BLANKING_DELAY: usize = 8;
190
191#[cfg(not(any(
192    feature = "blank-delay-1",
193    feature = "blank-delay-2",
194    feature = "blank-delay-4",
195    feature = "blank-delay-8"
196)))]
197const BLANKING_DELAY: usize = 0;
198
199#[cfg(not(feature = "invert-oe"))]
200const OE_ACTIVE: u8 = 0b1000_0000;
201#[cfg(not(feature = "invert-oe"))]
202const OE_BLANK: u8 = 0;
203
204#[cfg(feature = "invert-oe")]
205const OE_ACTIVE: u8 = 0;
206#[cfg(feature = "invert-oe")]
207const OE_BLANK: u8 = 0b1000_0000;
208
209bitfield! {
210    /// 8-bit word carrying the row-address and timing control signals that are
211    /// driven on a HUB75 connector.
212    ///
213    /// Relationship to [`Entry`]
214    /// -------------------------
215    /// The control bits—output-enable (`OE`) and latch (`LAT`)—occupy **exactly**
216    /// the same bit positions as in [`Entry`].
217    /// This deliberate overlap allows both structures to be streamed through the
218    /// same GPIO/DMA path without any run-time bit remapping.
219    ///
220    /// Field summary
221    /// -------------
222    /// - Row-address lines `A`–`E` (5 bits)
223    /// - Latch signal `LAT`        (1 bit)
224    /// - Output-enable `OE`        (1 bit)
225    ///
226    /// Bit layout
227    /// ----------
228    /// - Bit 7 `OE`  : Output enable
229    /// - Bit 6 `LAT` : Row-latch strobe
230    ///   When asserted:
231    ///   1. The address bits (`A`–`E`) are latched by the panel driver.
232    ///   2. External glue logic gates the pixel clock (`CLK`), preventing any
233    ///      new pixel data from being shifted into the display while the latch
234    ///      is open.
235    /// - Bits 4–0 `A`–`E` : Row address (LSB =`A`)
236    ///
237    /// Behaviour notes
238    /// ---------------
239    /// * The address bits take effect only while `LAT` is high; they may be
240    ///   changed safely at any other time.
241    /// * Because `CLK` is inhibited during the latch interval, the pixel data
242    ///   stream produced from [`Entry`] words is paused until the latch is
243    ///   released.
244    #[derive(Clone, Copy, Default, PartialEq, Eq)]
245    #[repr(transparent)]
246    struct Address(u8);
247    impl Debug;
248    pub output_enable, set_output_enable: 7;
249    pub latch, set_latch: 6;
250    pub addr, set_addr: 4, 0;
251}
252
253impl Address {
254    pub const fn new() -> Self {
255        Self(0)
256    }
257}
258
259bitfield! {
260    /// 8-bit word representing the pixel data and control signals.
261    ///
262    /// This structure contains the RGB data for two sub-pixels and control signals:
263    /// - RGB data for two sub-pixels (color0 and color1)
264    /// - Output enable signal
265    /// - Latch signal
266    ///
267    /// The bit layout is as follows:
268    /// - Bit 7: Output enable
269    /// - Bit 6: Latch signal
270    /// - Bit 5: Blue channel for color1
271    /// - Bit 4: Green channel for color1
272    /// - Bit 3: Red channel for color1
273    /// - Bit 2: Blue channel for color0
274    /// - Bit 1: Green channel for color0
275    /// - Bit 0: Red channel for color0
276    #[derive(Clone, Copy, Default, PartialEq)]
277    #[repr(transparent)]
278    struct Entry(u8);
279    impl Debug;
280    pub output_enable, set_output_enable: 7;
281    pub latch, set_latch: 6;
282    pub blu2, set_blu2: 5;
283    pub grn2, set_grn2: 4;
284    pub red2, set_red2: 3;
285    pub blu1, set_blu1: 2;
286    pub grn1, set_grn1: 1;
287    pub red1, set_red1: 0;
288}
289
290impl Entry {
291    pub const fn new() -> Self {
292        Self(0)
293    }
294
295    // Optimized color bit manipulation constants and methods
296    const COLOR0_MASK: u8 = 0b0000_0111; // bits 0-2: R1, G1, B1
297    const COLOR1_MASK: u8 = 0b0011_1000; // bits 3-5: R2, G2, B2
298
299    #[inline]
300    fn set_color0_bits(&mut self, bits: u8) {
301        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits & Self::COLOR0_MASK);
302    }
303
304    #[inline]
305    fn set_color1_bits(&mut self, bits: u8) {
306        self.0 = (self.0 & !Self::COLOR1_MASK) | ((bits << 3) & Self::COLOR1_MASK);
307    }
308}
309
310/// Represents a single row of pixels with external latch circuit support.
311///
312/// Each row contains both pixel data and address information:
313/// - 4 address words for row selection and timing
314/// - COLS data words for pixel data
315///
316/// The address words are arranged to match the external latch circuit's
317/// timing requirements. When the `esp32` feature is enabled, a specific
318/// mapping (2, 3, 0, 1) is applied to correct for the strange byte ordering
319/// required for the ESP32's I2S peripheral.
320#[derive(Clone, Copy, PartialEq, Debug)]
321#[repr(C)]
322struct Row<const COLS: usize> {
323    data: [Entry; COLS],
324    address: [Address; 4],
325}
326
327// bytes are output in the order 2, 3, 0, 1
328#[inline]
329const fn map_index(index: usize) -> usize {
330    #[cfg(feature = "esp32-ordering")]
331    {
332        index ^ 2
333    }
334    #[cfg(not(feature = "esp32-ordering"))]
335    {
336        index
337    }
338}
339
340/// Pre-computed address table for all possible row addresses (0-31).
341/// Each entry contains the 4 address words needed for that row.
342const fn make_addr_table() -> [[Address; 4]; 32] {
343    let mut tbl = [[Address::new(); 4]; 32];
344    let mut addr = 0;
345    while addr < 32 {
346        let mut i = 0;
347        while i < 4 {
348            let latch = i != 3;
349            let mapped_i = map_index(i);
350            let latch_bit = if latch { 1u8 << 6 } else { 0u8 };
351            tbl[addr][mapped_i].0 = OE_BLANK | latch_bit | addr as u8;
352            i += 1;
353        }
354        addr += 1;
355    }
356    tbl
357}
358
359static ADDR_TABLE: [[Address; 4]; 32] = make_addr_table();
360
361/// Pre-computed data template for a row with the given number of columns.
362/// This template has the correct OE/LAT bits set for each column position.
363#[cfg_attr(
364    not(any(
365        feature = "blank-delay-1",
366        feature = "blank-delay-2",
367        feature = "blank-delay-4",
368        feature = "blank-delay-8"
369    )),
370    allow(clippy::absurd_extreme_comparisons)
371)]
372const fn make_data_template<const COLS: usize>() -> [Entry; COLS] {
373    let mut data = [Entry::new(); COLS];
374    let mut i = 0;
375    while i < COLS {
376        let mapped_i = map_index(i);
377        data[mapped_i].0 = if i >= BLANKING_DELAY && i < COLS - BLANKING_DELAY - 1 {
378            OE_ACTIVE
379        } else {
380            OE_BLANK
381        };
382        i += 1;
383    }
384    data
385}
386
387impl<const COLS: usize> Row<COLS> {
388    pub const fn new() -> Self {
389        Self {
390            address: [Address::new(); 4],
391            data: [Entry::new(); COLS],
392        }
393    }
394
395    #[inline]
396    pub fn format(&mut self, addr: u8) {
397        // Use pre-computed address table
398        self.address.copy_from_slice(&ADDR_TABLE[addr as usize]);
399
400        // Use pre-computed data template - create it each time since we can't use generics in static
401        let data_template = make_data_template::<COLS>();
402        self.data.copy_from_slice(&data_template);
403    }
404
405    /// Fast clear that only zeros the color bits, preserving OE/LAT control bits
406    #[inline]
407    pub fn clear_colors(&mut self) {
408        // Clear color bits while preserving timing and control bits
409        const COLOR_CLEAR_MASK: u8 = !0b0011_1111; // Clear bits 0-5 (R1,G1,B1,R2,G2,B2)
410
411        for entry in &mut self.data {
412            entry.0 &= COLOR_CLEAR_MASK;
413        }
414    }
415
416    #[inline]
417    pub fn set_color0(&mut self, col: usize, r: bool, g: bool, b: bool) {
418        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
419        let col = map_index(col);
420        self.data[col].set_color0_bits(bits);
421    }
422
423    #[inline]
424    pub fn set_color1(&mut self, col: usize, r: bool, g: bool, b: bool) {
425        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
426        let col = map_index(col);
427        self.data[col].set_color1_bits(bits);
428    }
429}
430
431impl<const COLS: usize> Default for Row<COLS> {
432    fn default() -> Self {
433        Self::new()
434    }
435}
436
437#[derive(Copy, Clone, Debug)]
438#[repr(C)]
439struct Frame<const ROWS: usize, const COLS: usize, const NROWS: usize> {
440    rows: [Row<COLS>; NROWS],
441}
442
443impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Frame<ROWS, COLS, NROWS> {
444    pub const fn new() -> Self {
445        Self {
446            rows: [Row::new(); NROWS],
447        }
448    }
449
450    #[inline]
451    pub fn format(&mut self) {
452        for (addr, row) in self.rows.iter_mut().enumerate() {
453            row.format(addr as u8);
454        }
455    }
456
457    /// Fast clear that only zeros the color bits, preserving control bits
458    #[inline]
459    pub fn clear_colors(&mut self) {
460        for row in &mut self.rows {
461            row.clear_colors();
462        }
463    }
464
465    #[inline]
466    pub fn set_pixel(&mut self, y: usize, x: usize, red: bool, green: bool, blue: bool) {
467        let row = &mut self.rows[if y < NROWS { y } else { y - NROWS }];
468        if y < NROWS {
469            row.set_color0(x, red, green, blue);
470        } else {
471            row.set_color1(x, red, green, blue);
472        }
473    }
474}
475
476impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Default
477    for Frame<ROWS, COLS, NROWS>
478{
479    fn default() -> Self {
480        Self::new()
481    }
482}
483
484/// DMA-compatible framebuffer for HUB75 LED panels with external latch circuit
485/// support.
486///
487/// This implementation is optimized for memory usage and external latch circuit
488/// support:
489/// - Uses 8-bit entries instead of 16-bit
490/// - Separates address and data words
491/// - Supports the external latch circuit for row selection
492/// - Implements the embedded-graphics `DrawTarget` trait
493///
494/// # Type Parameters
495/// - `ROWS`: Total number of rows in the panel
496/// - `COLS`: Number of columns in the panel
497/// - `NROWS`: Number of rows per scan (typically half of ROWS)
498/// - `BITS`: Color depth (1-8 bits)
499/// - `FRAME_COUNT`: Number of frames used for Binary Code Modulation
500///
501/// # Helper Functions
502/// Use these functions to compute the correct values:
503/// - `esp_hub75::compute_frame_count(BITS)`: Computes the required number of
504///   frames
505/// - `esp_hub75::compute_rows(ROWS)`: Computes the number of rows per scan
506///
507/// # Memory Layout
508/// The buffer is aligned to ensure efficient DMA transfers and contains:
509/// - An array of frames, each containing the full panel data
510/// - Each frame contains NROWS rows
511/// - Each row contains both data and address words
512#[derive(Copy, Clone)]
513#[repr(C)]
514#[repr(align(4))]
515pub struct DmaFrameBuffer<
516    const ROWS: usize,
517    const COLS: usize,
518    const NROWS: usize,
519    const BITS: u8,
520    const FRAME_COUNT: usize,
521> {
522    frames: [Frame<ROWS, COLS, NROWS>; FRAME_COUNT],
523}
524
525impl<
526        const ROWS: usize,
527        const COLS: usize,
528        const NROWS: usize,
529        const BITS: u8,
530        const FRAME_COUNT: usize,
531    > Default for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
532{
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538impl<
539        const ROWS: usize,
540        const COLS: usize,
541        const NROWS: usize,
542        const BITS: u8,
543        const FRAME_COUNT: usize,
544    > DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
545{
546    /// Create a new framebuffer with the given number of frames.
547    /// The framebuffer is automatically formatted and ready to use.
548    /// # Example
549    /// ```rust,no_run
550    /// use hub75_framebuffer::{latched::DmaFrameBuffer,compute_rows,compute_frame_count};
551    ///
552    /// const ROWS: usize = 32;
553    /// const COLS: usize = 64;
554    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
555    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
556    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
557    ///
558    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
559    /// // Ready to use immediately
560    /// ```
561    #[must_use]
562    pub fn new() -> Self {
563        let mut fb = Self {
564            frames: [Frame::new(); FRAME_COUNT],
565        };
566        fb.format();
567        fb
568    }
569
570    /// Returns the number of BCM chunks in this framebuffer (always 1 for
571    /// single-plane framebuffers — the entire buffer is one contiguous chunk).
572    #[must_use]
573    pub const fn bcm_chunk_count() -> usize {
574        1
575    }
576
577    /// Returns the byte size of one BCM chunk (for single-plane framebuffers
578    /// this equals the total DMA buffer size, since BCM weighting is baked in).
579    #[must_use]
580    pub const fn bcm_chunk_bytes() -> usize {
581        core::mem::size_of::<[Frame<ROWS, COLS, NROWS>; FRAME_COUNT]>()
582    }
583
584    /// Format the framebuffer, setting up all control bits and clearing pixel data.
585    /// This method does a full format of all control bits and clears all pixel data.
586    /// Normally you don't need to call this as `new()` automatically formats the framebuffer.
587    /// # Example
588    /// ```rust,no_run
589    /// use hub75_framebuffer::{Color,latched::DmaFrameBuffer,compute_rows,compute_frame_count};
590    ///
591    /// const ROWS: usize = 32;
592    /// const COLS: usize = 64;
593    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
594    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
595    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
596    ///
597    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
598    /// // framebuffer.format(); // Not needed - new() already calls this
599    /// ```
600    pub fn format(&mut self) {
601        for frame in &mut self.frames {
602            frame.format();
603        }
604    }
605
606    /// Erase pixel colors while preserving control bits.
607    /// This is much faster than `format()` and is the typical way to clear the display.
608    /// # Example
609    /// ```rust,no_run
610    /// use hub75_framebuffer::{Color,latched::DmaFrameBuffer,compute_rows,compute_frame_count};
611    ///
612    /// const ROWS: usize = 32;
613    /// const COLS: usize = 64;
614    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
615    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
616    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
617    ///
618    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
619    /// // ... draw some pixels ...
620    /// framebuffer.erase();
621    /// ```
622    #[inline]
623    pub fn erase(&mut self) {
624        for frame in &mut self.frames {
625            frame.clear_colors();
626        }
627    }
628
629    /// Set a pixel in the framebuffer.
630    /// # Example
631    /// ```rust,no_run
632    /// use hub75_framebuffer::{Color,latched::DmaFrameBuffer,compute_rows,compute_frame_count};
633    /// use embedded_graphics::prelude::*;
634    ///
635    /// const ROWS: usize = 32;
636    /// const COLS: usize = 64;
637    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
638    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
639    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
640    ///
641    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
642    /// framebuffer.set_pixel(Point::new(10, 10), Color::RED);
643    /// ```
644    pub fn set_pixel(&mut self, p: Point, color: Color) {
645        if p.x < 0 || p.y < 0 {
646            return;
647        }
648        self.set_pixel_internal(p.x as usize, p.y as usize, color);
649    }
650
651    #[inline]
652    fn frames_on(v: u8) -> usize {
653        // v / brightness_step but the compiler resolves the shift at build-time
654        (v as usize) >> (8 - BITS)
655    }
656
657    #[inline]
658    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Rgb888) {
659        if x >= COLS || y >= ROWS {
660            return;
661        }
662
663        // Early exit for black pixels - common in UI backgrounds
664        // Only enabled when skip-black-pixels feature is active
665        #[cfg(feature = "skip-black-pixels")]
666        if color == Rgb888::BLACK {
667            return;
668        }
669
670        // Pre-compute how many frames each channel should be on
671        let red_frames = Self::frames_on(color.r());
672        let green_frames = Self::frames_on(color.g());
673        let blue_frames = Self::frames_on(color.b());
674
675        // Set the pixel in all frames based on pre-computed frame counts
676        for (frame_idx, frame) in self.frames.iter_mut().enumerate() {
677            frame.set_pixel(
678                y,
679                x,
680                frame_idx < red_frames,
681                frame_idx < green_frames,
682                frame_idx < blue_frames,
683            );
684        }
685    }
686}
687
688impl<
689        const ROWS: usize,
690        const COLS: usize,
691        const NROWS: usize,
692        const BITS: u8,
693        const FRAME_COUNT: usize,
694    > FrameBufferOperations for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
695{
696    #[inline]
697    fn erase(&mut self) {
698        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::erase(self);
699    }
700
701    #[inline]
702    fn set_pixel(&mut self, p: Point, color: Color) {
703        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::set_pixel(self, p, color);
704    }
705}
706
707impl<
708        const ROWS: usize,
709        const COLS: usize,
710        const NROWS: usize,
711        const BITS: u8,
712        const FRAME_COUNT: usize,
713    > embedded_graphics::prelude::OriginDimensions
714    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
715{
716    fn size(&self) -> embedded_graphics::prelude::Size {
717        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
718    }
719}
720
721impl<
722        const ROWS: usize,
723        const COLS: usize,
724        const NROWS: usize,
725        const BITS: u8,
726        const FRAME_COUNT: usize,
727    > embedded_graphics::draw_target::DrawTarget
728    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
729{
730    type Color = Color;
731
732    type Error = Infallible;
733
734    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
735    where
736        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
737    {
738        for pixel in pixels {
739            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
740        }
741        Ok(())
742    }
743}
744
745unsafe impl<
746        const ROWS: usize,
747        const COLS: usize,
748        const NROWS: usize,
749        const BITS: u8,
750        const FRAME_COUNT: usize,
751    > ReadBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
752{
753    type Word = u8;
754
755    unsafe fn read_buffer(&self) -> (*const u8, usize) {
756        let ptr = (&raw const self.frames).cast::<u8>();
757        let len = core::mem::size_of_val(&self.frames);
758        (ptr, len)
759    }
760}
761
762unsafe impl<
763        const ROWS: usize,
764        const COLS: usize,
765        const NROWS: usize,
766        const BITS: u8,
767        const FRAME_COUNT: usize,
768    > ReadBuffer for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
769{
770    type Word = u8;
771
772    unsafe fn read_buffer(&self) -> (*const u8, usize) {
773        let ptr = (&raw const self.frames).cast::<u8>();
774        let len = core::mem::size_of_val(&self.frames);
775        (ptr, len)
776    }
777}
778
779impl<
780        const ROWS: usize,
781        const COLS: usize,
782        const NROWS: usize,
783        const BITS: u8,
784        const FRAME_COUNT: usize,
785    > core::fmt::Debug for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
786{
787    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
788        let brightness_step = 1 << (8 - BITS);
789        f.debug_struct("DmaFrameBuffer")
790            .field("size", &core::mem::size_of_val(&self.frames))
791            .field("frame_count", &self.frames.len())
792            .field("frame_size", &core::mem::size_of_val(&self.frames[0]))
793            .field("brightness_step", &&brightness_step)
794            .finish()
795    }
796}
797
798#[cfg(feature = "defmt")]
799impl<
800        const ROWS: usize,
801        const COLS: usize,
802        const NROWS: usize,
803        const BITS: u8,
804        const FRAME_COUNT: usize,
805    > defmt::Format for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
806{
807    fn format(&self, f: defmt::Formatter) {
808        let brightness_step = 1 << (8 - BITS);
809        defmt::write!(
810            f,
811            "DmaFrameBuffer<{}, {}, {}, {}, {}>",
812            ROWS,
813            COLS,
814            NROWS,
815            BITS,
816            FRAME_COUNT
817        );
818        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.frames));
819        defmt::write!(
820            f,
821            " frame_size: {}",
822            core::mem::size_of_val(&self.frames[0])
823        );
824        defmt::write!(f, " brightness_step: {}", brightness_step);
825    }
826}
827
828impl<
829        const ROWS: usize,
830        const COLS: usize,
831        const NROWS: usize,
832        const BITS: u8,
833        const FRAME_COUNT: usize,
834    > super::FrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
835{
836    type Word = u8;
837
838    fn plane_count(&self) -> usize {
839        1
840    }
841
842    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize) {
843        assert!(plane_idx == 0, "latched DmaFrameBuffer has only 1 plane");
844        let ptr = (&raw const self.frames).cast::<u8>();
845        let len = core::mem::size_of_val(&self.frames);
846        (ptr, len)
847    }
848}
849
850impl<
851        const ROWS: usize,
852        const COLS: usize,
853        const NROWS: usize,
854        const BITS: u8,
855        const FRAME_COUNT: usize,
856    > embedded_graphics::prelude::OriginDimensions
857    for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
858{
859    fn size(&self) -> embedded_graphics::prelude::Size {
860        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
861    }
862}
863
864impl<
865        const ROWS: usize,
866        const COLS: usize,
867        const NROWS: usize,
868        const BITS: u8,
869        const FRAME_COUNT: usize,
870    > MutableFrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
871{
872}
873
874#[cfg(test)]
875mod tests {
876    extern crate std;
877
878    use std::format;
879    use std::vec;
880
881    use super::*;
882    use crate::{FrameBuffer, WordSize};
883    use embedded_graphics::pixelcolor::RgbColor;
884    use embedded_graphics::prelude::*;
885    use embedded_graphics::primitives::{Circle, PrimitiveStyle, Rectangle};
886
887    const TEST_ROWS: usize = 32;
888    const TEST_COLS: usize = 64;
889    const TEST_NROWS: usize = TEST_ROWS / 2;
890    const TEST_BITS: u8 = 3;
891    const TEST_FRAME_COUNT: usize = (1 << TEST_BITS) - 1; // 7 frames for 3-bit depth
892
893    type TestFrameBuffer =
894        DmaFrameBuffer<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_BITS, TEST_FRAME_COUNT>;
895
896    #[test]
897    fn test_address_construction() {
898        let addr = Address::new();
899        assert_eq!(addr.0, 0);
900        assert_eq!(addr.latch(), false);
901        assert_eq!(addr.addr(), 0);
902    }
903
904    #[test]
905    fn test_address_setters() {
906        let mut addr = Address::new();
907
908        addr.set_latch(true);
909        assert_eq!(addr.latch(), true);
910        assert_eq!(addr.0 & 0b01000000, 0b01000000);
911
912        addr.set_addr(0b11111);
913        assert_eq!(addr.addr(), 0b11111);
914        assert_eq!(addr.0 & 0b00011111, 0b00011111);
915    }
916
917    #[test]
918    fn test_address_bit_isolation() {
919        let mut addr = Address::new();
920
921        // Test that setting one field doesn't affect others
922        addr.set_addr(0b11111);
923        addr.set_latch(true);
924        assert_eq!(addr.addr(), 0b11111);
925        assert_eq!(addr.latch(), true);
926    }
927
928    #[test]
929    fn test_entry_construction() {
930        let entry = Entry::new();
931        assert_eq!(entry.0, 0);
932        assert_eq!(entry.output_enable(), false);
933        assert_eq!(entry.latch(), false);
934        assert_eq!(entry.red1(), false);
935        assert_eq!(entry.grn1(), false);
936        assert_eq!(entry.blu1(), false);
937        assert_eq!(entry.red2(), false);
938        assert_eq!(entry.grn2(), false);
939        assert_eq!(entry.blu2(), false);
940    }
941
942    #[test]
943    fn test_entry_setters() {
944        let mut entry = Entry::new();
945
946        entry.set_output_enable(true);
947        assert_eq!(entry.output_enable(), true);
948        assert_eq!(entry.0 & 0b10000000, 0b10000000);
949
950        entry.set_latch(true);
951        assert_eq!(entry.latch(), true);
952        assert_eq!(entry.0 & 0b01000000, 0b01000000);
953
954        // Test RGB channels for color0 (bits 0-2)
955        entry.set_red1(true);
956        entry.set_grn1(true);
957        entry.set_blu1(true);
958        assert_eq!(entry.red1(), true);
959        assert_eq!(entry.grn1(), true);
960        assert_eq!(entry.blu1(), true);
961        assert_eq!(entry.0 & 0b00000111, 0b00000111);
962
963        // Test RGB channels for color1 (bits 3-5)
964        entry.set_red2(true);
965        entry.set_grn2(true);
966        entry.set_blu2(true);
967        assert_eq!(entry.red2(), true);
968        assert_eq!(entry.grn2(), true);
969        assert_eq!(entry.blu2(), true);
970        assert_eq!(entry.0 & 0b00111000, 0b00111000);
971    }
972
973    #[test]
974    fn test_entry_set_color0() {
975        let mut entry = Entry::new();
976
977        let bits = (u8::from(true) << 2) | (u8::from(false) << 1) | u8::from(true); // b=1, g=0, r=1 = 0b101
978        entry.set_color0_bits(bits);
979        assert_eq!(entry.red1(), true);
980        assert_eq!(entry.grn1(), false);
981        assert_eq!(entry.blu1(), true);
982        assert_eq!(entry.0 & 0b00000111, 0b00000101); // Red and blue bits set
983    }
984
985    #[test]
986    fn test_entry_set_color1() {
987        let mut entry = Entry::new();
988
989        let bits = (u8::from(true) << 2) | (u8::from(true) << 1) | u8::from(false); // b=1, g=1, r=0 = 0b110
990        entry.set_color1_bits(bits);
991        assert_eq!(entry.red2(), false);
992        assert_eq!(entry.grn2(), true);
993        assert_eq!(entry.blu2(), true);
994        assert_eq!(entry.0 & 0b00111000, 0b00110000); // Green and blue bits set
995    }
996
997    #[test]
998    fn test_row_construction() {
999        let row: Row<TEST_COLS> = Row::new();
1000        assert_eq!(row.data.len(), TEST_COLS);
1001        assert_eq!(row.address.len(), 4);
1002
1003        // Check that all entries are initialized to zero
1004        for entry in &row.data {
1005            assert_eq!(entry.0, 0);
1006        }
1007        for addr in &row.address {
1008            assert_eq!(addr.0, 0);
1009        }
1010    }
1011
1012    #[test]
1013    fn test_row_format() {
1014        let mut row: Row<TEST_COLS> = Row::new();
1015        let test_addr = 5;
1016
1017        row.format(test_addr);
1018
1019        // Check address words configuration
1020        for addr in &row.address {
1021            assert_eq!(addr.addr(), test_addr);
1022            // The latch values are pre-computed in the address table based on the logical
1023            // arrangement, so we don't need to reverse-map. Just verify the table matches
1024            // what we expect from the make_addr_table function.
1025        }
1026        // Since the address table is complex with ESP32 mapping, let's just verify
1027        // that exactly one address has latch=false (from logical index 3) and the
1028        // rest have latch=true
1029        let latch_false_count = row.address.iter().filter(|addr| !addr.latch()).count();
1030        assert_eq!(latch_false_count, 1);
1031
1032        // Check data entries configuration
1033        for entry in &row.data {
1034            assert_eq!(entry.latch(), false);
1035        }
1036        let oe_active = !cfg!(feature = "invert-oe");
1037        let active_count = TEST_COLS.saturating_sub(2 * BLANKING_DELAY + 1);
1038        let blank_count = TEST_COLS - active_count;
1039        let oe_blank_count = row
1040            .data
1041            .iter()
1042            .filter(|entry| entry.output_enable() != oe_active)
1043            .count();
1044        assert_eq!(oe_blank_count, blank_count);
1045    }
1046
1047    #[test]
1048    fn test_row_set_color0() {
1049        let mut row: Row<TEST_COLS> = Row::new();
1050
1051        row.set_color0(0, true, false, true);
1052
1053        let mapped_col_0 = map_index(0);
1054        assert_eq!(row.data[mapped_col_0].red1(), true);
1055        assert_eq!(row.data[mapped_col_0].grn1(), false);
1056        assert_eq!(row.data[mapped_col_0].blu1(), true);
1057
1058        // Test another column
1059        row.set_color0(1, false, true, false);
1060
1061        let mapped_col_1 = map_index(1);
1062        assert_eq!(row.data[mapped_col_1].red1(), false);
1063        assert_eq!(row.data[mapped_col_1].grn1(), true);
1064        assert_eq!(row.data[mapped_col_1].blu1(), false);
1065    }
1066
1067    #[test]
1068    fn test_row_set_color1() {
1069        let mut row: Row<TEST_COLS> = Row::new();
1070
1071        row.set_color1(0, true, true, false);
1072
1073        let mapped_col_0 = map_index(0);
1074        assert_eq!(row.data[mapped_col_0].red2(), true);
1075        assert_eq!(row.data[mapped_col_0].grn2(), true);
1076        assert_eq!(row.data[mapped_col_0].blu2(), false);
1077    }
1078
1079    #[test]
1080    fn test_frame_construction() {
1081        let frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1082        assert_eq!(frame.rows.len(), TEST_NROWS);
1083    }
1084
1085    #[test]
1086    fn test_frame_format() {
1087        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1088
1089        frame.format();
1090
1091        for (addr, row) in frame.rows.iter().enumerate() {
1092            // Check that each row was formatted with its address
1093            for address in &row.address {
1094                assert_eq!(address.addr() as usize, addr);
1095            }
1096        }
1097    }
1098
1099    #[test]
1100    fn test_frame_set_pixel() {
1101        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1102
1103        // Test setting pixel in upper half (y < NROWS)
1104        frame.set_pixel(5, 10, true, false, true);
1105
1106        let mapped_col_10 = map_index(10);
1107        assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1108        assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1109        assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1110
1111        // Test setting pixel in lower half (y >= NROWS)
1112        frame.set_pixel(TEST_NROWS + 5, 15, false, true, false);
1113
1114        let mapped_col_15 = map_index(15);
1115        assert_eq!(frame.rows[5].data[mapped_col_15].red2(), false);
1116        assert_eq!(frame.rows[5].data[mapped_col_15].grn2(), true);
1117        assert_eq!(frame.rows[5].data[mapped_col_15].blu2(), false);
1118    }
1119
1120    #[test]
1121    fn test_row_default() {
1122        let row1: Row<TEST_COLS> = Row::new();
1123        let row2: Row<TEST_COLS> = Row::default();
1124
1125        // Both should be equivalent
1126        assert_eq!(row1, row2);
1127        assert_eq!(row1.data.len(), row2.data.len());
1128        assert_eq!(row1.address.len(), row2.address.len());
1129
1130        // Check that all entries are initialized to zero
1131        for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1132            assert_eq!(entry1.0, entry2.0);
1133            assert_eq!(entry1.0, 0);
1134        }
1135        for (addr1, addr2) in row1.address.iter().zip(row2.address.iter()) {
1136            assert_eq!(addr1.0, addr2.0);
1137            assert_eq!(addr1.0, 0);
1138        }
1139    }
1140
1141    #[test]
1142    fn test_frame_default() {
1143        let frame1: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1144        let frame2: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::default();
1145
1146        // Both should be equivalent
1147        assert_eq!(frame1.rows.len(), frame2.rows.len());
1148
1149        // Check that all rows are equivalent
1150        for (row1, row2) in frame1.rows.iter().zip(frame2.rows.iter()) {
1151            assert_eq!(row1, row2);
1152
1153            // Verify all entries are zero-initialized
1154            for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1155                assert_eq!(entry1.0, entry2.0);
1156                assert_eq!(entry1.0, 0);
1157            }
1158            for (addr1, addr2) in row1.address.iter().zip(row2.address.iter()) {
1159                assert_eq!(addr1.0, addr2.0);
1160                assert_eq!(addr1.0, 0);
1161            }
1162        }
1163    }
1164
1165    #[test]
1166    fn test_dma_framebuffer_construction() {
1167        let fb = TestFrameBuffer::new();
1168        assert_eq!(fb.frames.len(), TEST_FRAME_COUNT);
1169    }
1170
1171    #[test]
1172    fn test_bcm_chunk_info() {
1173        let expected_size =
1174            core::mem::size_of::<[Frame<TEST_ROWS, TEST_COLS, TEST_NROWS>; TEST_FRAME_COUNT]>();
1175        assert_eq!(TestFrameBuffer::bcm_chunk_bytes(), expected_size);
1176        assert_eq!(TestFrameBuffer::bcm_chunk_count(), 1);
1177    }
1178
1179    #[test]
1180    fn test_dma_framebuffer_format() {
1181        let mut fb = TestFrameBuffer {
1182            frames: [Frame::new(); TEST_FRAME_COUNT],
1183        };
1184        fb.format();
1185
1186        // After formatting, all frames should be formatted
1187        for frame in &fb.frames {
1188            for (addr, row) in frame.rows.iter().enumerate() {
1189                for address in &row.address {
1190                    assert_eq!(address.addr() as usize, addr);
1191                }
1192            }
1193        }
1194    }
1195
1196    #[test]
1197    fn test_dma_framebuffer_set_pixel_bounds() {
1198        let mut fb = TestFrameBuffer::new();
1199
1200        // Test negative coordinates
1201        fb.set_pixel(Point::new(-1, 5), Color::RED);
1202        fb.set_pixel(Point::new(5, -1), Color::RED);
1203
1204        // Test coordinates out of bounds (should not panic)
1205        fb.set_pixel(Point::new(TEST_COLS as i32, 5), Color::RED);
1206        fb.set_pixel(Point::new(5, TEST_ROWS as i32), Color::RED);
1207    }
1208
1209    #[test]
1210    fn test_dma_framebuffer_set_pixel_internal() {
1211        let mut fb = TestFrameBuffer::new();
1212
1213        let red_color = Rgb888::new(255, 0, 0);
1214        fb.set_pixel_internal(10, 5, red_color);
1215
1216        // With 3-bit depth, brightness steps are 32 (256/8)
1217        // Frames represent thresholds: 32, 64, 96, 128, 160, 192, 224
1218        // Red value 255 should activate all frames
1219        for frame in &fb.frames {
1220            // Check upper half pixel
1221            let mapped_col_10 = map_index(10);
1222            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1223            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1224            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), false);
1225        }
1226    }
1227
1228    #[test]
1229    fn test_dma_framebuffer_brightness_modulation() {
1230        let mut fb = TestFrameBuffer::new();
1231
1232        // Test with a medium brightness value
1233        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1234        let test_brightness = brightness_step * 3; // 96
1235        let color = Rgb888::new(test_brightness, 0, 0);
1236
1237        fb.set_pixel_internal(0, 0, color);
1238
1239        // Should activate frames 0, 1, 2 (thresholds 32, 64, 96)
1240        // but not frames 3, 4, 5, 6 (thresholds 128, 160, 192, 224)
1241        for (frame_idx, frame) in fb.frames.iter().enumerate() {
1242            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1243            let should_be_active = test_brightness >= frame_threshold;
1244
1245            let mapped_col_0 = map_index(0);
1246            assert_eq!(frame.rows[0].data[mapped_col_0].red1(), should_be_active);
1247        }
1248    }
1249
1250    #[test]
1251    fn test_origin_dimensions() {
1252        let fb = TestFrameBuffer::new();
1253        let size = fb.size();
1254        assert_eq!(size.width, TEST_COLS as u32);
1255        assert_eq!(size.height, TEST_ROWS as u32);
1256
1257        // Test mutable reference
1258        let mut fb = TestFrameBuffer::new();
1259        let fb_ref = &mut fb;
1260        let size = fb_ref.size();
1261        assert_eq!(size.width, TEST_COLS as u32);
1262        assert_eq!(size.height, TEST_ROWS as u32);
1263    }
1264
1265    #[test]
1266    fn test_draw_target() {
1267        let mut fb = TestFrameBuffer::new();
1268
1269        let pixels = vec![
1270            embedded_graphics::Pixel(Point::new(0, 0), Color::RED),
1271            embedded_graphics::Pixel(Point::new(1, 1), Color::GREEN),
1272            embedded_graphics::Pixel(Point::new(2, 2), Color::BLUE),
1273        ];
1274
1275        let result = fb.draw_iter(pixels);
1276        assert!(result.is_ok());
1277    }
1278
1279    #[test]
1280    fn test_draw_iter_pixel_verification() {
1281        let mut fb = TestFrameBuffer::new();
1282
1283        // Create test pixels with specific colors and positions
1284        let pixels = vec![
1285            // Upper half pixels (y < NROWS) - should set color0
1286            embedded_graphics::Pixel(Point::new(5, 2), Color::RED), // (5, 2) -> red
1287            embedded_graphics::Pixel(Point::new(10, 5), Color::GREEN), // (10, 5) -> green
1288            embedded_graphics::Pixel(Point::new(15, 8), Color::BLUE), // (15, 8) -> blue
1289            embedded_graphics::Pixel(Point::new(20, 10), Color::WHITE), // (20, 10) -> white
1290            // Lower half pixels (y >= NROWS) - should set color1
1291            embedded_graphics::Pixel(Point::new(25, (TEST_NROWS + 3) as i32), Color::RED), // (25, 19) -> red
1292            embedded_graphics::Pixel(Point::new(30, (TEST_NROWS + 7) as i32), Color::GREEN), // (30, 23) -> green
1293            embedded_graphics::Pixel(Point::new(35, (TEST_NROWS + 12) as i32), Color::BLUE), // (35, 28) -> blue
1294            // Edge case: black pixel (should not be visible in first frame)
1295            embedded_graphics::Pixel(Point::new(40, 1), Color::BLACK), // (40, 1) -> black
1296            // Low brightness pixel that should not appear in first frame
1297            embedded_graphics::Pixel(Point::new(45, 3), Rgb888::new(16, 16, 16)), // Below threshold
1298        ];
1299
1300        let result = fb.draw_iter(pixels);
1301        assert!(result.is_ok());
1302
1303        // Check the first frame only
1304        let first_frame = &fb.frames[0];
1305        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1306        let first_frame_threshold = brightness_step; // 32
1307
1308        // Test upper half pixels (color0)
1309        // Red pixel at (5, 2) - should be red in first frame
1310        let col_idx = map_index(5);
1311        assert_eq!(
1312            first_frame.rows[2].data[col_idx].red1(),
1313            Color::RED.r() >= first_frame_threshold
1314        );
1315        assert_eq!(
1316            first_frame.rows[2].data[col_idx].grn1(),
1317            Color::RED.g() >= first_frame_threshold
1318        );
1319        assert_eq!(
1320            first_frame.rows[2].data[col_idx].blu1(),
1321            Color::RED.b() >= first_frame_threshold
1322        );
1323
1324        // Green pixel at (10, 5) - should be green in first frame
1325        let col_idx = map_index(10);
1326        assert_eq!(
1327            first_frame.rows[5].data[col_idx].red1(),
1328            Color::GREEN.r() >= first_frame_threshold
1329        );
1330        assert_eq!(
1331            first_frame.rows[5].data[col_idx].grn1(),
1332            Color::GREEN.g() >= first_frame_threshold
1333        );
1334        assert_eq!(
1335            first_frame.rows[5].data[col_idx].blu1(),
1336            Color::GREEN.b() >= first_frame_threshold
1337        );
1338
1339        // Blue pixel at (15, 8) - should be blue in first frame
1340        let col_idx = map_index(15);
1341        assert_eq!(
1342            first_frame.rows[8].data[col_idx].red1(),
1343            Color::BLUE.r() >= first_frame_threshold
1344        );
1345        assert_eq!(
1346            first_frame.rows[8].data[col_idx].grn1(),
1347            Color::BLUE.g() >= first_frame_threshold
1348        );
1349        assert_eq!(
1350            first_frame.rows[8].data[col_idx].blu1(),
1351            Color::BLUE.b() >= first_frame_threshold
1352        );
1353
1354        // White pixel at (20, 10) - should be white in first frame
1355        let col_idx = map_index(20);
1356        assert_eq!(
1357            first_frame.rows[10].data[col_idx].red1(),
1358            Color::WHITE.r() >= first_frame_threshold
1359        );
1360        assert_eq!(
1361            first_frame.rows[10].data[col_idx].grn1(),
1362            Color::WHITE.g() >= first_frame_threshold
1363        );
1364        assert_eq!(
1365            first_frame.rows[10].data[col_idx].blu1(),
1366            Color::WHITE.b() >= first_frame_threshold
1367        );
1368
1369        // Test lower half pixels (color1)
1370        // Red pixel at (25, TEST_NROWS + 3) -> row 3, color1
1371        let col_idx = map_index(25);
1372        assert_eq!(
1373            first_frame.rows[3].data[col_idx].red2(),
1374            Color::RED.r() >= first_frame_threshold
1375        );
1376        assert_eq!(
1377            first_frame.rows[3].data[col_idx].grn2(),
1378            Color::RED.g() >= first_frame_threshold
1379        );
1380        assert_eq!(
1381            first_frame.rows[3].data[col_idx].blu2(),
1382            Color::RED.b() >= first_frame_threshold
1383        );
1384
1385        // Green pixel at (30, TEST_NROWS + 7) -> row 7, color1
1386        let col_idx = map_index(30);
1387        assert_eq!(
1388            first_frame.rows[7].data[col_idx].red2(),
1389            Color::GREEN.r() >= first_frame_threshold
1390        );
1391        assert_eq!(
1392            first_frame.rows[7].data[col_idx].grn2(),
1393            Color::GREEN.g() >= first_frame_threshold
1394        );
1395        assert_eq!(
1396            first_frame.rows[7].data[col_idx].blu2(),
1397            Color::GREEN.b() >= first_frame_threshold
1398        );
1399
1400        // Blue pixel at (35, TEST_NROWS + 12) -> row 12, color1
1401        let col_idx = map_index(35);
1402        assert_eq!(
1403            first_frame.rows[12].data[col_idx].red2(),
1404            Color::BLUE.r() >= first_frame_threshold
1405        );
1406        assert_eq!(
1407            first_frame.rows[12].data[col_idx].grn2(),
1408            Color::BLUE.g() >= first_frame_threshold
1409        );
1410        assert_eq!(
1411            first_frame.rows[12].data[col_idx].blu2(),
1412            Color::BLUE.b() >= first_frame_threshold
1413        );
1414
1415        // Test black pixel - should not be visible in any frame
1416        let col_idx = map_index(40);
1417        assert_eq!(first_frame.rows[1].data[col_idx].red1(), false);
1418        assert_eq!(first_frame.rows[1].data[col_idx].grn1(), false);
1419        assert_eq!(first_frame.rows[1].data[col_idx].blu1(), false);
1420
1421        // Test low brightness pixel (16, 16, 16) - should not be visible in first frame (threshold 32)
1422        let col_idx = map_index(45);
1423        assert_eq!(
1424            first_frame.rows[3].data[col_idx].red1(),
1425            16 >= first_frame_threshold
1426        ); // false
1427        assert_eq!(
1428            first_frame.rows[3].data[col_idx].grn1(),
1429            16 >= first_frame_threshold
1430        ); // false
1431        assert_eq!(
1432            first_frame.rows[3].data[col_idx].blu1(),
1433            16 >= first_frame_threshold
1434        ); // false
1435    }
1436
1437    #[test]
1438    fn test_embedded_graphics_integration() {
1439        let mut fb = TestFrameBuffer::new();
1440
1441        // Draw a rectangle
1442        let result = Rectangle::new(Point::new(5, 5), Size::new(10, 8))
1443            .into_styled(PrimitiveStyle::with_fill(Color::RED))
1444            .draw(&mut fb);
1445        assert!(result.is_ok());
1446
1447        // Draw a circle
1448        let result = Circle::new(Point::new(30, 15), 8)
1449            .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
1450            .draw(&mut fb);
1451        assert!(result.is_ok());
1452    }
1453
1454    #[test]
1455    fn test_read_buffer_implementation() {
1456        let fb = TestFrameBuffer::new();
1457
1458        // Test direct implementation
1459        unsafe {
1460            let (ptr, len) = fb.read_buffer();
1461            assert!(!ptr.is_null());
1462            assert_eq!(len, core::mem::size_of_val(&fb.frames));
1463        }
1464
1465        // Test mutable reference implementation
1466        let mut fb = TestFrameBuffer::new();
1467        let fb_ref = &mut fb;
1468        unsafe {
1469            let (ptr, len) = fb_ref.read_buffer();
1470            assert!(!ptr.is_null());
1471            assert_eq!(len, core::mem::size_of_val(&fb.frames));
1472        }
1473    }
1474
1475    #[test]
1476    fn test_framebuffer_trait() {
1477        let fb = TestFrameBuffer::new();
1478        assert_eq!(fb.get_word_size(), WordSize::Eight);
1479
1480        let mut fb = TestFrameBuffer::new();
1481        let fb_ref = &mut fb;
1482        assert_eq!(fb_ref.get_word_size(), WordSize::Eight);
1483    }
1484
1485    #[test]
1486    fn test_debug_formatting() {
1487        let fb = TestFrameBuffer::new();
1488        let debug_string = format!("{:?}", fb);
1489        assert!(debug_string.contains("DmaFrameBuffer"));
1490        assert!(debug_string.contains("frame_count"));
1491        assert!(debug_string.contains("frame_size"));
1492        assert!(debug_string.contains("brightness_step"));
1493    }
1494
1495    #[test]
1496    fn test_default_implementation() {
1497        let fb1 = TestFrameBuffer::new();
1498        let fb2 = TestFrameBuffer::default();
1499
1500        // Both should be equivalent
1501        assert_eq!(fb1.frames.len(), fb2.frames.len());
1502    }
1503
1504    #[cfg(feature = "esp32-ordering")]
1505    #[test]
1506    fn test_esp32_mapping() {
1507        // Test the ESP32-specific index mapping
1508        assert_eq!(map_index(0), 2);
1509        assert_eq!(map_index(1), 3);
1510        assert_eq!(map_index(2), 0);
1511        assert_eq!(map_index(3), 1);
1512        assert_eq!(map_index(4), 6); // 4 & !0b11 | 2 = 4 | 2 = 6
1513        assert_eq!(map_index(5), 7); // 5 & !0b11 | 3 = 4 | 3 = 7
1514    }
1515
1516    #[test]
1517    fn test_memory_alignment() {
1518        let fb = TestFrameBuffer::new();
1519        let ptr = &fb as *const _ as usize;
1520
1521        // Should be 4-byte aligned as specified in repr(align(4))
1522        assert_eq!(ptr % 4, 0);
1523    }
1524
1525    #[test]
1526    fn test_color_values() {
1527        let mut fb = TestFrameBuffer::new();
1528
1529        // Test different color values
1530        let colors = [
1531            (Color::RED, (255, 0, 0)),
1532            (Color::GREEN, (0, 255, 0)),
1533            (Color::BLUE, (0, 0, 255)),
1534            (Color::WHITE, (255, 255, 255)),
1535            (Color::BLACK, (0, 0, 0)),
1536        ];
1537
1538        for (i, (color, (r, g, b))) in colors.iter().enumerate() {
1539            fb.set_pixel(Point::new(i as i32, 0), *color);
1540            assert_eq!(color.r(), *r);
1541            assert_eq!(color.g(), *g);
1542            assert_eq!(color.b(), *b);
1543        }
1544    }
1545
1546    #[test]
1547    fn test_bits_assertion() {
1548        // Test that BITS <= 8 assertion is enforced at compile time
1549        // This test mainly documents the constraint
1550        assert!(TEST_BITS <= 8);
1551    }
1552
1553    #[test]
1554    #[cfg(feature = "skip-black-pixels")]
1555    fn test_skip_black_pixels_enabled() {
1556        let mut fb = TestFrameBuffer::new();
1557
1558        // Set a red pixel first
1559        fb.set_pixel_internal(10, 5, Color::RED);
1560
1561        // Verify it's red in the first frame
1562        let mapped_col_10 = map_index(10);
1563        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1564        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1565        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1566
1567        // Now set it to black - with skip-black-pixels enabled, this should be ignored
1568        fb.set_pixel_internal(10, 5, Color::BLACK);
1569
1570        // The pixel should still be red (black write was skipped)
1571        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1572        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1573        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1574    }
1575
1576    #[test]
1577    #[cfg(not(feature = "skip-black-pixels"))]
1578    fn test_skip_black_pixels_disabled() {
1579        let mut fb = TestFrameBuffer::new();
1580
1581        // Set a red pixel first
1582        fb.set_pixel_internal(10, 5, Color::RED);
1583
1584        // Verify it's red in the first frame
1585        let mapped_col_10 = map_index(10);
1586        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1587        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1588        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1589
1590        // Now set it to black - with skip-black-pixels disabled, this should overwrite
1591        fb.set_pixel_internal(10, 5, Color::BLACK);
1592
1593        // The pixel should now be black (all bits false)
1594        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), false);
1595        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1596        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1597    }
1598
1599    #[test]
1600    fn test_bcm_frame_overwrite() {
1601        let mut fb = TestFrameBuffer::new();
1602
1603        // First write a white pixel (255, 255, 255)
1604        fb.set_pixel_internal(10, 5, Color::WHITE);
1605
1606        let mapped_col_10 = map_index(10);
1607
1608        // Verify white pixel is lit in all frames (255 >= all thresholds)
1609        for frame in fb.frames.iter() {
1610            // White (255) should be active in all frames since it's >= all thresholds
1611            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1612            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), true);
1613            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1614        }
1615
1616        // Now overwrite with 50% white (128, 128, 128)
1617        let half_white = embedded_graphics::pixelcolor::Rgb888::new(128, 128, 128);
1618        fb.set_pixel_internal(10, 5, half_white);
1619
1620        // Verify only the correct frames are lit for 50% white
1621        // With 3-bit depth: thresholds are 32, 64, 96, 128, 160, 192, 224
1622        // 128 should activate frames 0, 1, 2, 3 (thresholds 32, 64, 96, 128)
1623        // but not frames 4, 5, 6 (thresholds 160, 192, 224)
1624        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1625        for (frame_idx, frame) in fb.frames.iter().enumerate() {
1626            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1627            let should_be_active = 128 >= frame_threshold;
1628
1629            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), should_be_active);
1630            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), should_be_active);
1631            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), should_be_active);
1632        }
1633
1634        // Specifically verify the expected pattern for 3-bit depth
1635        // Frames 0-3 should be active (thresholds 32, 64, 96, 128)
1636        for frame_idx in 0..4 {
1637            assert_eq!(
1638                fb.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1639                true
1640            );
1641        }
1642        // Frames 4-6 should be inactive (thresholds 160, 192, 224)
1643        for frame_idx in 4..TEST_FRAME_COUNT {
1644            assert_eq!(
1645                fb.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1646                false
1647            );
1648        }
1649    }
1650
1651    #[test]
1652    fn test_new_auto_formats() {
1653        let fb = TestFrameBuffer::new();
1654
1655        // After new(), all frames should be formatted
1656        for frame in &fb.frames {
1657            for (addr, row) in frame.rows.iter().enumerate() {
1658                for address in &row.address {
1659                    assert_eq!(address.addr() as usize, addr);
1660                }
1661            }
1662        }
1663    }
1664
1665    #[test]
1666    fn test_erase() {
1667        let mut fb = TestFrameBuffer::new();
1668
1669        // Set some pixels
1670        fb.set_pixel_internal(10, 5, Color::RED);
1671        fb.set_pixel_internal(20, 10, Color::GREEN);
1672
1673        let mapped_col_10 = map_index(10);
1674        let mapped_col_20 = map_index(20);
1675
1676        // Verify pixels are set
1677        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1678        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].grn1(), true);
1679
1680        // erase
1681        fb.erase();
1682
1683        // Verify pixels are cleared but control bits are preserved
1684        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), false);
1685        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1686        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1687        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].red1(), false);
1688        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].grn1(), false);
1689        assert_eq!(fb.frames[0].rows[10].data[mapped_col_20].blu1(), false);
1690
1691        // Verify control bits are still correct
1692        let oe_active = !cfg!(feature = "invert-oe");
1693        let active_count = TEST_COLS.saturating_sub(2 * BLANKING_DELAY + 1);
1694        let blank_count = TEST_COLS - active_count;
1695        for frame in &fb.frames {
1696            for (addr, row) in frame.rows.iter().enumerate() {
1697                // Check address words
1698                for address in &row.address {
1699                    assert_eq!(address.addr() as usize, addr);
1700                }
1701                let oe_blank_count = row
1702                    .data
1703                    .iter()
1704                    .filter(|entry| entry.output_enable() != oe_active)
1705                    .count();
1706                assert_eq!(oe_blank_count, blank_count);
1707            }
1708        }
1709    }
1710
1711    #[test]
1712    fn test_row_clear_colors() {
1713        let mut row: Row<TEST_COLS> = Row::new();
1714        row.format(5);
1715
1716        // Set some colors
1717        row.set_color0(0, true, false, true);
1718        row.set_color1(1, false, true, false);
1719
1720        let mapped_col_0 = map_index(0);
1721        let mapped_col_1 = map_index(1);
1722
1723        // Verify colors are set
1724        assert_eq!(row.data[mapped_col_0].red1(), true);
1725        assert_eq!(row.data[mapped_col_0].blu1(), true);
1726        assert_eq!(row.data[mapped_col_1].grn2(), true);
1727
1728        // Store original control bits
1729        let original_oe_0 = row.data[mapped_col_0].output_enable();
1730        let original_latch_0 = row.data[mapped_col_0].latch();
1731        let original_oe_1 = row.data[mapped_col_1].output_enable();
1732        let original_latch_1 = row.data[mapped_col_1].latch();
1733
1734        // Clear colors
1735        row.clear_colors();
1736
1737        // Verify colors are cleared
1738        assert_eq!(row.data[mapped_col_0].red1(), false);
1739        assert_eq!(row.data[mapped_col_0].grn1(), false);
1740        assert_eq!(row.data[mapped_col_0].blu1(), false);
1741        assert_eq!(row.data[mapped_col_1].red2(), false);
1742        assert_eq!(row.data[mapped_col_1].grn2(), false);
1743        assert_eq!(row.data[mapped_col_1].blu2(), false);
1744
1745        // Verify control bits are preserved
1746        assert_eq!(row.data[mapped_col_0].output_enable(), original_oe_0);
1747        assert_eq!(row.data[mapped_col_0].latch(), original_latch_0);
1748        assert_eq!(row.data[mapped_col_1].output_enable(), original_oe_1);
1749        assert_eq!(row.data[mapped_col_1].latch(), original_latch_1);
1750    }
1751
1752    #[test]
1753    fn test_make_addr_table_function() {
1754        // Test the make_addr_table function directly to ensure code coverage
1755        let table = make_addr_table();
1756
1757        // Verify basic properties of the generated table
1758        assert_eq!(table.len(), 32); // Should have 32 address entries (0-31)
1759
1760        // Check first address (0)
1761        let addr_0 = &table[0];
1762        assert_eq!(addr_0.len(), 4); // Should have 4 address words
1763
1764        // Verify that exactly one address word has latch=false (index 3 in logical order)
1765        let latch_false_count = addr_0.iter().filter(|addr| !addr.latch()).count();
1766        assert_eq!(latch_false_count, 1);
1767
1768        // All addresses should have addr field set to 0 for the first entry
1769        for addr in addr_0 {
1770            assert_eq!(addr.addr(), 0);
1771        }
1772
1773        // Check last address (31)
1774        let addr_31 = &table[31];
1775        let latch_false_count = addr_31.iter().filter(|addr| !addr.latch()).count();
1776        assert_eq!(latch_false_count, 1);
1777
1778        // All addresses should have addr field set to 31 for the last entry
1779        for addr in addr_31 {
1780            assert_eq!(addr.addr(), 31);
1781        }
1782    }
1783
1784    #[test]
1785    fn test_make_data_template_function() {
1786        // Test the make_data_template function directly to ensure code coverage
1787        let template = make_data_template::<TEST_COLS>();
1788
1789        // Verify basic properties
1790        assert_eq!(template.len(), TEST_COLS);
1791
1792        // All entries should have latch=false
1793        for entry in &template {
1794            assert_eq!(entry.latch(), false);
1795        }
1796
1797        let oe_active = !cfg!(feature = "invert-oe");
1798        let active_count = TEST_COLS.saturating_sub(2 * BLANKING_DELAY + 1);
1799        let blank_count = TEST_COLS - active_count;
1800        let oe_blank_count = template
1801            .iter()
1802            .filter(|entry| entry.output_enable() != oe_active)
1803            .count();
1804        assert_eq!(oe_blank_count, blank_count);
1805
1806        // Test with a small template size to verify edge cases
1807        let small_template = make_data_template::<4>();
1808        assert_eq!(small_template.len(), 4);
1809
1810        let small_active = 4_usize.saturating_sub(2 * BLANKING_DELAY + 1);
1811        let small_blank = 4 - small_active;
1812        let oe_blank_count = small_template
1813            .iter()
1814            .filter(|entry| entry.output_enable() != oe_active)
1815            .count();
1816        assert_eq!(oe_blank_count, small_blank);
1817
1818        // Test with single column - but skip this test if ESP32 ordering is enabled
1819        // because the mapping function assumes at least 4 columns for proper mapping
1820        #[cfg(not(feature = "esp32-ordering"))]
1821        {
1822            let single_template = make_data_template::<1>();
1823            assert_eq!(single_template.len(), 1);
1824            assert_eq!(single_template[0].output_enable(), !oe_active);
1825            assert_eq!(single_template[0].latch(), false);
1826        }
1827    }
1828
1829    #[test]
1830    fn test_addr_table_correctness() {
1831        // Test that the pre-computed address table matches the original logic
1832        for addr in 0..32 {
1833            let mut expected_addresses = [Address::new(); 4];
1834
1835            // Original logic
1836            for i in 0..4 {
1837                let latch = !matches!(i, 3);
1838                #[cfg(feature = "esp32-ordering")]
1839                let mapped_i = map_index(i);
1840                #[cfg(not(feature = "esp32-ordering"))]
1841                let mapped_i = i;
1842
1843                expected_addresses[mapped_i].set_latch(latch);
1844                expected_addresses[mapped_i].set_addr(addr);
1845                expected_addresses[mapped_i].0 |= OE_BLANK;
1846            }
1847
1848            // Compare with table
1849            let table_addresses = &ADDR_TABLE[addr as usize];
1850            for i in 0..4 {
1851                assert_eq!(table_addresses[i].0, expected_addresses[i].0);
1852            }
1853        }
1854    }
1855
1856    // Helper constants for the glyph dimensions used by FONT_6X10
1857    const CHAR_W: i32 = 6;
1858    const CHAR_H: i32 = 10;
1859
1860    /// Draws the glyph 'A' at `origin` and verifies every pixel against a software reference.
1861    /// Re-usable for different panel locations.
1862    fn verify_glyph_at(fb: &mut TestFrameBuffer, origin: Point) {
1863        use embedded_graphics::mock_display::MockDisplay;
1864        use embedded_graphics::mono_font::ascii::FONT_6X10;
1865        use embedded_graphics::mono_font::MonoTextStyle;
1866        use embedded_graphics::text::{Baseline, Text};
1867
1868        // Draw the character on the framebuffer.
1869        let style = MonoTextStyle::new(&FONT_6X10, Color::WHITE);
1870        Text::with_baseline("A", origin, style, Baseline::Top)
1871            .draw(fb)
1872            .unwrap();
1873
1874        // Reference bitmap for the glyph at (0,0)
1875        let mut reference: MockDisplay<Color> = MockDisplay::new();
1876        Text::with_baseline("A", Point::zero(), style, Baseline::Top)
1877            .draw(&mut reference)
1878            .unwrap();
1879
1880        // Iterate over the glyph's bounding box and compare pixel states.
1881        for dy in 0..CHAR_H {
1882            for dx in 0..CHAR_W {
1883                let expected_on = reference
1884                    .get_pixel(Point::new(dx, dy))
1885                    .unwrap_or(Color::BLACK)
1886                    != Color::BLACK;
1887
1888                let gx = (origin.x + dx) as usize;
1889                let gy = (origin.y + dy) as usize;
1890
1891                // we have computed the origin to be within the panel, so we don't need to check for bounds
1892                // if gx >= TEST_COLS || gy >= TEST_ROWS {
1893                //     continue;
1894                // }
1895
1896                // Fetch the entry from frame 0 directly.
1897                let frame0 = &fb.frames[0];
1898                let e = if gy < TEST_NROWS {
1899                    &frame0.rows[gy].data[map_index(gx)]
1900                } else {
1901                    &frame0.rows[gy - TEST_NROWS].data[map_index(gx)]
1902                };
1903
1904                let (r, g, b) = if gy >= TEST_NROWS {
1905                    (e.red2(), e.grn2(), e.blu2())
1906                } else {
1907                    (e.red1(), e.grn1(), e.blu1())
1908                };
1909
1910                if expected_on {
1911                    assert!(r && g && b);
1912                } else {
1913                    assert!(!r && !g && !b);
1914                }
1915            }
1916        }
1917    }
1918
1919    #[test]
1920    fn test_draw_char_corners() {
1921        // Upper-left and lower-right character placement.
1922        let upper_left = Point::new(0, 0);
1923        let lower_right = Point::new(TEST_COLS as i32 - CHAR_W, TEST_ROWS as i32 - CHAR_H);
1924
1925        let mut fb = TestFrameBuffer::new();
1926
1927        // Verify glyph in the upper-left corner.
1928        verify_glyph_at(&mut fb, upper_left);
1929        // Verify glyph in the lower-right corner.
1930        verify_glyph_at(&mut fb, lower_right);
1931    }
1932
1933    #[test]
1934    fn test_framebuffer_operations_trait_erase() {
1935        let mut fb = TestFrameBuffer::new();
1936
1937        // Set a couple of pixels so erase has an effect to clear
1938        fb.set_pixel_internal(10, 5, Color::RED);
1939        fb.set_pixel_internal(20, 10, Color::GREEN);
1940
1941        // Call the trait method explicitly to exercise the impl
1942        <TestFrameBuffer as FrameBufferOperations>::erase(&mut fb);
1943
1944        // Verify colors are cleared but control bits/timing remain intact on frame 0
1945        let mc10 = map_index(10);
1946        let mc20 = map_index(20);
1947        assert_eq!(fb.frames[0].rows[5].data[mc10].red1(), false);
1948        assert_eq!(fb.frames[0].rows[10].data[mc20].grn1(), false);
1949
1950        // Data entries should still have the same OE pattern and latch should remain false for all
1951        let oe_active = !cfg!(feature = "invert-oe");
1952        let active_count = TEST_COLS.saturating_sub(2 * BLANKING_DELAY + 1);
1953        let blank_count = TEST_COLS - active_count;
1954        let row0 = &fb.frames[0].rows[0];
1955        let oe_blank_count = row0
1956            .data
1957            .iter()
1958            .filter(|entry| entry.output_enable() != oe_active)
1959            .count();
1960        assert_eq!(oe_blank_count, blank_count);
1961        assert!(row0.data.iter().all(|e| !e.latch()));
1962
1963        // Address words should remain precomputed table values
1964        for (i, addr) in row0.address.iter().enumerate() {
1965            assert_eq!(addr.0, ADDR_TABLE[0][i].0);
1966        }
1967    }
1968
1969    #[test]
1970    fn test_framebuffer_operations_trait_set_pixel() {
1971        let mut fb = TestFrameBuffer::new();
1972
1973        // Call the trait method explicitly to exercise the impl
1974        <TestFrameBuffer as FrameBufferOperations>::set_pixel(
1975            &mut fb,
1976            Point::new(8, 3),
1977            Color::BLUE,
1978        );
1979
1980        // For BITS=3, BLUE should light blue channel in early frames
1981        let idx = map_index(8);
1982        assert_eq!(fb.frames[0].rows[3].data[idx].blu1(), true);
1983        // Red/Green should be off for BLUE at frame 0
1984        assert_eq!(fb.frames[0].rows[3].data[idx].red1(), false);
1985        assert_eq!(fb.frames[0].rows[3].data[idx].grn1(), false);
1986    }
1987
1988    #[test]
1989    fn test_blanking_delay() {
1990        let mut row: Row<TEST_COLS> = Row::new();
1991        row.format(5);
1992
1993        let oe_active = !cfg!(feature = "invert-oe");
1994
1995        if BLANKING_DELAY > 0 {
1996            let first_blanked_idx = map_index(0);
1997            assert_eq!(row.data[first_blanked_idx].output_enable(), !oe_active);
1998
1999            let first_active_idx = map_index(BLANKING_DELAY);
2000            assert_eq!(row.data[first_active_idx].output_enable(), oe_active);
2001        }
2002
2003        let last_active_idx = map_index(TEST_COLS - BLANKING_DELAY - 2);
2004        assert_eq!(row.data[last_active_idx].output_enable(), oe_active);
2005
2006        let blanking_pixel_idx = map_index(TEST_COLS - BLANKING_DELAY - 1);
2007        assert_eq!(row.data[blanking_pixel_idx].output_enable(), !oe_active);
2008
2009        let last_pixel_idx = map_index(TEST_COLS - 1);
2010        assert_eq!(row.data[last_pixel_idx].output_enable(), !oe_active);
2011    }
2012}