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