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