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