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