Skip to main content

hub75_framebuffer/
plain.rs

1//! DMA-friendly framebuffer implementation for HUB75 LED panels.
2//!
3//! **Deprecated:** this threshold-frame layout is kept for legacy drivers
4//! whose DMA can stream a single circular buffer but cannot raise interrupts.
5//! New designs should use [`bitplane::plain`](crate::bitplane::plain), which
6//! scales linearly with color depth instead of exponentially.
7//!
8//! This module provides a framebuffer implementation with memory
9//! layout optimized for efficient transfer to HUB75 LED panels. The data is
10//! structured for direct signal mapping, making it ideal for DMA transfers but
11//! also suitable for programmatic transfer. It supports RGB color and brightness
12//! control through multiple frames using Binary Code Modulation (BCM).
13//!
14//! # Hardware Requirements
15//! This implementation can be used by any microcontroller that has a peripheral
16//! capable of outputting a clock signal and 16 bits (though only 14 bits are
17//! actually needed) in parallel. The data is structured to directly match the
18//! HUB75 connector signals, making it suitable for various parallel output
19//! peripherals.
20//!
21//! # Features
22//! - Memory layout optimized for efficient data transfer
23//! - Direct signal mapping to HUB75 connector signals
24//! - Support for RGB color with brightness control
25//! - Multiple frame buffers for Binary Code Modulation (BCM)
26//! - Integration with embedded-graphics for easy drawing
27//!
28//! # Brightness Control
29//! Brightness is controlled through Binary Code Modulation (BCM):
30//! - The number of brightness levels is determined by the `BITS` parameter
31//! - Each additional bit doubles the number of brightness levels
32//! - More bits provide better brightness resolution but require more memory
33//! - Memory usage grows exponentially with the number of bits: `(2^BITS)-1`
34//!   frames
35//! - Example: 8 bits = 256 levels, 4 bits = 16 levels
36//!
37//! # Memory Usage
38//! The framebuffer's memory usage is determined by:
39//! - Panel size (ROWS × COLS)
40//! - Number of brightness bits (BITS)
41//! - Memory grows exponentially with bits: `(2^BITS)-1` frames
42//! - 16-bit entries provide direct signal mapping but use more memory
43//!
44//! # Example
45//! ```rust,no_run
46//! use embedded_graphics::pixelcolor::RgbColor;
47//! use embedded_graphics::prelude::*;
48//! use embedded_graphics::primitives::Circle;
49//! use embedded_graphics::primitives::Rectangle;
50//! use hub75_framebuffer::compute_frame_count;
51//! use hub75_framebuffer::compute_rows;
52//! use hub75_framebuffer::Color;
53//! use hub75_framebuffer::plain::DmaFrameBuffer;
54//! use embedded_graphics::primitives::PrimitiveStyle;
55//!
56//! // Create a framebuffer for a 64x32 panel with 3-bit color depth
57//! const ROWS: usize = 32;
58//! const COLS: usize = 64;
59//! const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
60//! const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
61//! const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
62//!
63//! let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
64//!
65//! // Draw a red rectangle
66//! Rectangle::new(Point::new(10, 10), Size::new(20, 20))
67//!     .into_styled(PrimitiveStyle::with_fill(Color::RED))
68//!     .draw(&mut framebuffer)
69//!     .unwrap();
70//!
71//! // Draw a blue circle
72//! Circle::new(Point::new(40, 20), 10)
73//!     .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
74//!     .draw(&mut framebuffer)
75//!     .unwrap();
76//! ```
77//!
78//! # Implementation Details
79//! The framebuffer is organized to directly match the HUB75 connector signals:
80//! - Each 16-bit word maps directly to the HUB75 control signals
81//! - Color data (R, G, B) for two sub-pixels is stored in dedicated bits
82//! - Control signals (output enable, latch, address) are mapped to specific
83//!   bits
84//! - Multiple frames are used to achieve Binary Code Modulation (BCM)
85//! - DMA transfers the data directly to the panel without any transformation
86//!
87//! # HUB75 Signal Bit Mapping
88//! Each 16-bit `Entry` represents the logic levels that will be driven onto the HUB75
89//! bus during a single pixel-clock cycle. Every panel signal—apart from the clock
90//! (`CLK`) itself—occupies a dedicated bit, allowing the word to be streamed via DMA
91//! with zero run-time manipulation.
92//!
93//! ```text
94//! default (ESP32 I2S / generic)          interstate75 (RP2040/RP2350 PIO)
95//! 15 ─ Dummy2   (spare)                  15 ─ (spare)
96//! 14 ─ B2       Blue  – lower half       14 ─ (spare)
97//! 13 ─ G2       Green – lower half       13 ─ OE    Output-Enable / Blank
98//! 12 ─ R2       Red   – lower half       12 ─ LAT   Latch / STB
99//! 11 ─ B1       Blue  – upper half       11 ─ Dummy0 (spare)
100//! 10 ─ G1       Green – upper half       10 ─ E     Row address
101//!  9 ─ R1       Red   – upper half        9 ─ D     Row address
102//!  8 ─ OE       Output-Enable / Blank      8 ─ C     Row address
103//!  7 ─ Dummy1   (spare)                    7 ─ B     Row address
104//!  6 ─ Dummy0   (spare)                    6 ─ A     Row address
105//!  5 ─ LAT      Latch / STB                5 ─ B1    Blue  – lower half
106//! 4-0 ─ A..E    Row address lines          4 ─ G1    Green – lower half
107//!                                          3 ─ R1    Red   – lower half
108//!                                          2 ─ B0    Blue  – upper half
109//!                                          1 ─ G0    Green – upper half
110//!                                          0 ─ R0    Red   – upper half
111//! ```
112//!
113//! The board-specific layout is selected by the `interstate75` feature; see
114//! [`crate::pinmap`].
115//!
116//! The pixel clock is generated by the peripheral that owns the DMA stream and
117//! is therefore **not** part of the 16-bit word stored in the framebuffer.
118//!
119//! # Binary Code Modulation (BCM) Frames
120//! Brightness is achieved with Binary-Code-Modulation as outlined in
121//! <https://www.batsocks.co.uk/readme/art_bcm_1.htm>. For a colour depth of
122//! `BITS`, the driver allocates `FRAME_COUNT = 2^BITS - 1` frames. Frame *n*
123//! (0-based) is displayed for a duration proportional to `2^n`, mirroring the
124//! weight of that bit in a binary number.
125//!
126//! When a pixel is written its 8-bit RGB components are compared against a
127//! per-frame threshold:
128//!
129//! ```text
130//! brightness_step = 256 / 2^BITS
131//! threshold_n     = (n + 1) * brightness_step
132//! ```
133//!
134//! If a channel's value is greater than or equal to `threshold_n` the
135//! corresponding colour bit is set in frame *n*. Streaming the frames from the
136//! least-significant (shortest) to the most-significant (longest) time-slot
137//! produces the correct 8-bit brightness while keeping the refresh routine
138//! trivial.
139//!
140//! # Safety
141//! This implementation uses unsafe code for DMA operations. The framebuffer
142//! must be properly aligned in memory and the DMA configuration must match the
143//! buffer layout.
144
145use core::convert::Infallible;
146
147use crate::{
148    map_row_index, slot_addresses, BcmLenReps, FrameBuffer, FrameBufferOperations,
149    MutableFrameBuffer, BCM_SEQUENCE_CAPACITY,
150};
151use bitfield::bitfield;
152use embedded_graphics::pixelcolor::RgbColor;
153use embedded_graphics::prelude::Point;
154
155use super::Color;
156#[cfg(feature = "tail-closes-latch")]
157use crate::pinmap::ADDR_MASK;
158use crate::pinmap::PINMAP;
159use crate::pinmap::{
160    ADDR_SHIFT, COLOR0_MASK, COLOR0_SHIFT, COLOR1_MASK, COLOR1_SHIFT, COLOR_MASK, LATCH_BIT, OE_BIT,
161};
162use crate::{INTER_ROW_BLANK, LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
163
164// NOTE: this module's default OE polarity is the opposite of `bitplane::plain`
165// (active-high by default here); see the `invert-oe` feature. Only the bit
166// position (`pinmap::OE_BIT`) is shared, never the level.
167#[cfg(not(feature = "invert-oe"))]
168const OE_ACTIVE: u16 = OE_BIT;
169#[cfg(not(feature = "invert-oe"))]
170const OE_BLANK: u16 = 0;
171
172#[cfg(feature = "invert-oe")]
173const OE_ACTIVE: u16 = 0;
174#[cfg(feature = "invert-oe")]
175const OE_BLANK: u16 = OE_BIT;
176
177/// Creates a pre-computed data template for a row with the specified address.
178/// This template contains all the timing and control signals but no pixel data.
179/// The address remains constant across all entries (including the latch pixel)
180/// so that the address change is deferred to the first pixel of the next row.
181#[allow(clippy::absurd_extreme_comparisons)]
182#[inline]
183const fn make_data_template<const COLS: usize>(addr: u8) -> [Entry; COLS] {
184    let mut data = [Entry::new(); COLS];
185    let mut i = 0;
186
187    while i < COLS {
188        let mut entry = Entry::new();
189        entry.0 = ((addr as u16) << ADDR_SHIFT) | OE_BLANK;
190
191        if i == COLS - 1 {
192            entry.0 |= LATCH_BIT;
193        } else if i >= TRAIL_BLANK_DELAY && i < COLS.saturating_sub(LEAD_BLANK_DELAY + 1) {
194            entry.0 = (entry.0 & !OE_BIT) | OE_ACTIVE;
195        }
196
197        data[map_index(i)] = entry;
198        i += 1;
199    }
200
201    data
202}
203
204bitfield! {
205    /// A 16-bit word representing the HUB75 control signals for a single pixel.
206    ///
207    /// This structure directly maps to the HUB75 connector signals:
208    /// - RGB color data for two sub-pixels (color0 and color1)
209    /// - Panel control signals (output enable, latch, address)
210    /// - Spare bits for timing alignment
211    ///
212    /// The exact bit positions are selected per board by [`crate::pinmap`] and
213    /// consumed by a single `bitfield!` definition, so the accessors can never
214    /// drift out of sync with the DMA templates.
215    #[derive(Clone, Copy, Default, PartialEq)]
216    #[repr(transparent)]
217    struct Entry(u16);
218    dummy2, set_dummy2: PINMAP.spare[2];
219    blu2, set_blu2: PINMAP.blu1;
220    grn2, set_grn2: PINMAP.grn1;
221    red2, set_red2: PINMAP.red1;
222    blu1, set_blu1: PINMAP.blu0;
223    grn1, set_grn1: PINMAP.grn0;
224    red1, set_red1: PINMAP.red0;
225    output_enable, set_output_enable: PINMAP.oe;
226    dummy1, set_dummy1: PINMAP.spare[1];
227    dummy0, set_dummy0: PINMAP.spare[0];
228    latch, set_latch: PINMAP.latch;
229    addr, set_addr: PINMAP.addr_msb(), PINMAP.addr_lsb;
230}
231
232impl core::fmt::Debug for Entry {
233    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
234        f.debug_tuple("Entry")
235            .field(&format_args!("{:#x}", self.0))
236            .finish()
237    }
238}
239
240#[cfg(feature = "defmt")]
241impl defmt::Format for Entry {
242    fn format(&self, f: defmt::Formatter) {
243        defmt::write!(f, "Entry({=u16:#x})", self.0);
244    }
245}
246
247impl Entry {
248    const fn new() -> Self {
249        Self(OE_BLANK)
250    }
251
252    // Optimized color bit manipulation methods. The bit positions/masks come
253    // from `crate::pinmap` so they follow the selected board layout.
254    #[inline]
255    fn set_color0_bits(&mut self, bits: u8) {
256        let bits16 = u16::from(bits) << COLOR0_SHIFT;
257        self.0 = (self.0 & !COLOR0_MASK) | (bits16 & COLOR0_MASK);
258    }
259
260    #[inline]
261    fn set_color1_bits(&mut self, bits: u8) {
262        let bits16 = u16::from(bits) << COLOR1_SHIFT;
263        self.0 = (self.0 & !COLOR1_MASK) | (bits16 & COLOR1_MASK);
264    }
265}
266
267/// Represents a single row of pixels in the framebuffer.
268///
269/// Each row contains a fixed number of columns (`COLS`) and manages the timing
270/// and control signals for the HUB75 panel. The row handles:
271/// - Output enable timing to prevent ghosting
272/// - Latch signal generation for row updates
273/// - Row address management
274/// - Color data for both sub-pixels
275#[derive(Clone, Copy, PartialEq, Debug)]
276#[repr(C)]
277struct Row<const COLS: usize> {
278    data: [Entry; COLS],
279    gap: [Entry; INTER_ROW_BLANK],
280}
281
282const fn map_index(i: usize) -> usize {
283    #[cfg(feature = "esp32-ordering")]
284    {
285        i ^ 1
286    }
287    #[cfg(not(feature = "esp32-ordering"))]
288    {
289        i
290    }
291}
292
293impl<const COLS: usize> Default for Row<COLS> {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299impl<const COLS: usize> Row<COLS> {
300    pub const fn new() -> Self {
301        Self {
302            data: [Entry::new(); COLS],
303            gap: [Entry::new(); INTER_ROW_BLANK],
304        }
305    }
306
307    pub const fn format(&mut self, prev_addr: u8) {
308        let template = make_data_template::<COLS>(prev_addr);
309        let mut i = 0;
310        while i < COLS {
311            self.data[i] = template[i];
312            i += 1;
313        }
314
315        let gap_val = ((prev_addr as u16) << ADDR_SHIFT) | OE_BLANK;
316        let mut i = 0;
317        // INTER_ROW_BLANK is 0 unless an inter-row-blank-* feature is enabled.
318        #[allow(clippy::absurd_extreme_comparisons)]
319        while i < INTER_ROW_BLANK {
320            self.gap[i].0 = gap_val;
321            i += 1;
322        }
323    }
324
325    /// Fast clear method that preserves timing/control bits while clearing pixel data.
326    /// Uses bulk memory operations for maximum performance.
327    #[inline]
328    pub fn clear_colors(&mut self) {
329        // Clear the colour bits while preserving timing/control bits.
330        const COLOR_CLEAR_MASK: u16 = !COLOR_MASK;
331
332        for entry in &mut self.data {
333            entry.0 &= COLOR_CLEAR_MASK;
334        }
335    }
336
337    #[inline]
338    pub fn set_color0(&mut self, col: usize, r: bool, g: bool, b: bool) {
339        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
340        let col = map_index(col);
341        self.data[col].set_color0_bits(bits);
342    }
343
344    #[inline]
345    pub fn set_color1(&mut self, col: usize, r: bool, g: bool, b: bool) {
346        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
347        let col = map_index(col);
348        self.data[col].set_color1_bits(bits);
349    }
350}
351
352#[derive(Copy, Clone, Debug)]
353#[repr(C)]
354struct Frame<const ROWS: usize, const COLS: usize, const NROWS: usize> {
355    rows: [Row<COLS>; NROWS],
356}
357
358impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Frame<ROWS, COLS, NROWS> {
359    pub const fn new() -> Self {
360        Self {
361            rows: [Row::new(); NROWS],
362        }
363    }
364
365    pub const fn format(&mut self) {
366        let mut slot = 0;
367        while slot < NROWS {
368            let (prev_addr, _) = slot_addresses::<NROWS>(slot);
369            self.rows[slot].format(prev_addr);
370            slot += 1;
371        }
372    }
373
374    /// Fast clear method that preserves timing/control bits while clearing pixel data.
375    #[inline]
376    pub fn clear_colors(&mut self) {
377        for row in &mut self.rows {
378            row.clear_colors();
379        }
380    }
381
382    #[inline]
383    pub fn set_pixel(&mut self, y: usize, x: usize, red: bool, green: bool, blue: bool) {
384        let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
385        let row = &mut self.rows[row_idx];
386        if y < NROWS {
387            row.set_color0(x, red, green, blue);
388        } else {
389            row.set_color1(x, red, green, blue);
390        }
391    }
392}
393
394impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Default
395    for Frame<ROWS, COLS, NROWS>
396{
397    fn default() -> Self {
398        Self::new()
399    }
400}
401
402/// All BCM frames for this framebuffer, optionally followed by a tail word
403/// (see the `tail-closes-latch` feature).
404#[derive(Copy, Clone, Debug)]
405#[repr(C)]
406struct FrameData<const ROWS: usize, const COLS: usize, const NROWS: usize, const FRAME_COUNT: usize>
407{
408    frames: [Frame<ROWS, COLS, NROWS>; FRAME_COUNT],
409    #[cfg(all(feature = "tail-closes-latch", feature = "esp32-ordering"))]
410    padding: Entry,
411    #[cfg(feature = "tail-closes-latch")]
412    tail: Entry,
413}
414
415impl<const ROWS: usize, const COLS: usize, const NROWS: usize, const FRAME_COUNT: usize>
416    FrameData<ROWS, COLS, NROWS, FRAME_COUNT>
417{
418    const fn new() -> Self {
419        Self {
420            frames: [Frame::new(); FRAME_COUNT],
421            #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
422            padding: Entry::new(),
423            #[cfg(feature = "tail-closes-latch")]
424            tail: Entry::new(),
425        }
426    }
427}
428
429/// DMA-compatible framebuffer for HUB75 LED panels.
430///
431/// This is a framebuffer implementation that:
432/// - Manages multiple frames for Binary Code Modulation (BCM)
433/// - Provides DMA-compatible memory layout
434/// - Implements the embedded-graphics `DrawTarget` trait
435///
436/// # Type Parameters
437/// - `ROWS`: Total number of rows in the panel
438/// - `COLS`: Number of columns in the panel
439/// - `NROWS`: Number of rows per scan (typically half of ROWS)
440/// - `BITS`: Color depth (1-8 bits)
441/// - `FRAME_COUNT`: Number of frames used for Binary Code Modulation
442///
443/// # Helper Functions
444/// Use these functions to compute the correct values:
445/// - `esp_hub75::compute_frame_count(BITS)`: Computes the required number of
446///   frames
447/// - `esp_hub75::compute_rows(ROWS)`: Computes the number of rows per scan
448///
449/// # Memory Layout
450/// The buffer is aligned to ensure efficient DMA transfers and contains:
451/// - A 64-bit alignment field
452/// - An array of frames, each containing the full panel data
453#[derive(Copy, Clone)]
454#[repr(C)]
455pub struct DmaFrameBuffer<
456    const ROWS: usize,
457    const COLS: usize,
458    const NROWS: usize,
459    const BITS: u8,
460    const FRAME_COUNT: usize,
461> {
462    _align: u64,
463    data: FrameData<ROWS, COLS, NROWS, FRAME_COUNT>,
464}
465
466impl<
467        const ROWS: usize,
468        const COLS: usize,
469        const NROWS: usize,
470        const BITS: u8,
471        const FRAME_COUNT: usize,
472    > Default for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
473{
474    fn default() -> Self {
475        Self::new()
476    }
477}
478
479impl<
480        const ROWS: usize,
481        const COLS: usize,
482        const NROWS: usize,
483        const BITS: u8,
484        const FRAME_COUNT: usize,
485    > DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
486{
487    /// Create a new, ready-to-use framebuffer.
488    ///
489    /// This creates a new framebuffer and automatically formats it with proper timing signals.
490    /// The framebuffer is immediately ready for pixel operations and DMA transfers.
491    ///
492    /// # Panics
493    ///
494    /// Panics if `BITS` is greater than 8, as only 1-8 bit color depths are
495    /// supported. With the `esp32-ordering` feature, also panics if `COLS` is
496    /// not even (the ESP32's byte-order swap requires an even column count).
497    ///
498    /// # Example
499    /// ```rust,no_run
500    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
501    ///
502    /// const ROWS: usize = 32;
503    /// const COLS: usize = 64;
504    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
505    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
506    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
507    ///
508    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
509    /// // No need to call format() - framebuffer is ready to use!
510    /// ```
511    ///
512    /// # Memory placement (⚠️ large buffer)
513    ///
514    /// This framebuffer is a large inline array: one 16-bit word per pixel
515    /// clock per frame, for `(2^BITS)-1` frames. For a 64×64 panel at
516    /// 3-bit depth that is already tens of kilobytes, and it grows
517    /// exponentially with `BITS`. **Never bind it to a task-local `let`** —
518    /// that places the whole buffer on the task stack and overflows it.
519    ///
520    /// The framebuffer is mutable, so give it a `'static` home that hands
521    /// out `&'static mut` **without** `unsafe` — `static mut` is not an
522    /// option. Use `static_cell::StaticCell`. See the crate-level
523    /// **Memory placement** section for a worked example.
524    #[must_use]
525    pub const fn new() -> Self {
526        assert!(BITS <= 8);
527        #[cfg(feature = "esp32-ordering")]
528        assert!(
529            COLS % 2 == 0,
530            "esp32-ordering feature requires COLS to be even"
531        );
532
533        let mut instance = Self {
534            _align: 0,
535            data: FrameData::new(),
536        };
537
538        instance.format();
539        instance
540    }
541
542    /// Returns the number of BCM chunks in this framebuffer (always 1 for
543    /// single-plane framebuffers — the entire buffer is one contiguous chunk).
544    #[must_use]
545    #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
546    pub const fn bcm_chunk_count() -> usize {
547        1
548    }
549
550    /// Returns the byte size of one BCM chunk (for single-plane framebuffers
551    /// this equals the total DMA buffer size including the tail word, since BCM
552    /// weighting is baked in).
553    #[must_use]
554    #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
555    pub const fn bcm_chunk_bytes() -> usize {
556        core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>()
557    }
558
559    /// Perform full formatting of the framebuffer with timing and control signals.
560    ///
561    /// This sets up all the timing and control signals needed for proper HUB75 operation.
562    /// This is automatically called by `new()`, so you typically don't need to call this
563    /// unless you want to completely reinitialize the framebuffer.
564    ///
565    /// # Example
566    /// ```rust,no_run
567    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
568    ///
569    /// const ROWS: usize = 32;
570    /// const COLS: usize = 64;
571    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
572    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
573    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
574    ///
575    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
576    /// framebuffer.format(); // Reinitialize if needed
577    /// ```
578    #[inline]
579    pub const fn format(&mut self) {
580        let mut i = 0;
581        while i < FRAME_COUNT {
582            self.data.frames[i].format();
583            i += 1;
584        }
585        #[cfg(feature = "tail-closes-latch")]
586        {
587            self.data.tail.0 = ADDR_MASK | OE_BLANK;
588        }
589        #[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
590        {
591            self.data.padding.0 = ADDR_MASK | OE_BLANK;
592        }
593    }
594
595    /// Fast erase operation that clears all pixel data while preserving timing signals.
596    ///
597    /// This is much faster than `format()` when you just want to clear the display
598    /// since it preserves all the timing and control signals that are already set up.
599    /// Use this for clearing between frames or when you want to start drawing fresh content.
600    ///
601    /// # Example
602    /// ```rust,no_run
603    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
604    ///
605    /// const ROWS: usize = 32;
606    /// const COLS: usize = 64;
607    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
608    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
609    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
610    ///
611    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
612    /// framebuffer.erase();
613    /// ```
614    #[inline]
615    pub fn erase(&mut self) {
616        for frame in &mut self.data.frames {
617            frame.clear_colors();
618        }
619    }
620
621    /// Set a pixel in the framebuffer.
622    /// # Example
623    /// ```rust,no_run
624    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
625    /// use embedded_graphics::prelude::*;
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.set_pixel(Point::new(10, 10), Color::RED);
635    /// ```
636    pub fn set_pixel(&mut self, p: Point, color: Color) {
637        if p.x < 0 || p.y < 0 {
638            return;
639        }
640        self.set_pixel_internal(p.x as usize, p.y as usize, color);
641    }
642
643    #[inline]
644    fn frames_on(v: u8) -> usize {
645        // v / brightness_step but the compiler resolves the shift at build-time
646        (v as usize) >> (8 - BITS)
647    }
648
649    #[inline]
650    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
651        if x >= COLS || y >= ROWS {
652            return;
653        }
654
655        // Early exit for black pixels - common in UI backgrounds
656        // Only enabled when skip-black-pixels feature is active
657        #[cfg(feature = "skip-black-pixels")]
658        if color == Color::BLACK {
659            return;
660        }
661
662        // Pre-compute how many frames each channel should be on
663        let red_frames = Self::frames_on(color.r());
664        let green_frames = Self::frames_on(color.g());
665        let blue_frames = Self::frames_on(color.b());
666
667        // Set the pixel in all frames based on pre-computed frame counts
668        for (frame_idx, frame) in self.data.frames.iter_mut().enumerate() {
669            frame.set_pixel(
670                y,
671                x,
672                frame_idx < red_frames,
673                frame_idx < green_frames,
674                frame_idx < blue_frames,
675            );
676        }
677    }
678}
679
680impl<
681        const ROWS: usize,
682        const COLS: usize,
683        const NROWS: usize,
684        const BITS: u8,
685        const FRAME_COUNT: usize,
686    > FrameBufferOperations for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
687{
688    #[inline]
689    fn erase(&mut self) {
690        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::erase(self);
691    }
692
693    #[inline]
694    fn set_pixel(&mut self, p: Point, color: Color) {
695        DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::set_pixel(self, p, color);
696    }
697}
698
699impl<
700        const ROWS: usize,
701        const COLS: usize,
702        const NROWS: usize,
703        const BITS: u8,
704        const FRAME_COUNT: usize,
705    > embedded_graphics::prelude::OriginDimensions
706    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
707{
708    fn size(&self) -> embedded_graphics::prelude::Size {
709        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
710    }
711}
712
713impl<
714        const ROWS: usize,
715        const COLS: usize,
716        const NROWS: usize,
717        const BITS: u8,
718        const FRAME_COUNT: usize,
719    > embedded_graphics::prelude::OriginDimensions
720    for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
721{
722    fn size(&self) -> embedded_graphics::prelude::Size {
723        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
724    }
725}
726
727impl<
728        const ROWS: usize,
729        const COLS: usize,
730        const NROWS: usize,
731        const BITS: u8,
732        const FRAME_COUNT: usize,
733    > embedded_graphics::draw_target::DrawTarget
734    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
735{
736    type Color = Color;
737
738    type Error = Infallible;
739
740    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
741    where
742        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
743    {
744        for pixel in pixels {
745            if pixel.0.x < 0 || pixel.0.y < 0 {
746                continue;
747            }
748            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
749        }
750        Ok(())
751    }
752}
753
754impl<
755        const ROWS: usize,
756        const COLS: usize,
757        const NROWS: usize,
758        const BITS: u8,
759        const FRAME_COUNT: usize,
760    > core::fmt::Debug for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
761{
762    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
763        let brightness_step = 1 << (8 - BITS);
764        let size = core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>();
765        f.debug_struct("DmaFrameBuffer")
766            .field("size", &size)
767            .field("frame_count", &self.data.frames.len())
768            .field("frame_size", &core::mem::size_of_val(&self.data.frames[0]))
769            .field("brightness_step", &&brightness_step)
770            .finish_non_exhaustive()
771    }
772}
773
774#[cfg(feature = "defmt")]
775impl<
776        const ROWS: usize,
777        const COLS: usize,
778        const NROWS: usize,
779        const BITS: u8,
780        const FRAME_COUNT: usize,
781    > defmt::Format for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
782{
783    fn format(&self, f: defmt::Formatter) {
784        let brightness_step = 1 << (8 - BITS);
785        defmt::write!(
786            f,
787            "DmaFrameBuffer<{}, {}, {}, {}, {}>",
788            ROWS,
789            COLS,
790            NROWS,
791            BITS,
792            FRAME_COUNT
793        );
794        defmt::write!(
795            f,
796            " size: {}",
797            core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>()
798        );
799        defmt::write!(
800            f,
801            " frame_size: {}",
802            core::mem::size_of_val(&self.data.frames[0])
803        );
804        defmt::write!(f, " brightness_step: {}", brightness_step);
805    }
806}
807
808impl<
809        const ROWS: usize,
810        const COLS: usize,
811        const NROWS: usize,
812        const BITS: u8,
813        const FRAME_COUNT: usize,
814    > FrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
815{
816    type Word = u16;
817
818    const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = {
819        let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
820        seq[0] = BcmLenReps {
821            len: core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>(),
822            reps: 1,
823        };
824        seq
825    };
826
827    const BCM_SEQUENCE_LEN: usize = 1;
828
829    const BCM_SEQUENCE_COUNT: usize = 1;
830
831    fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
832        assert!(index == 0, "threshold DmaFrameBuffer has only 1 segment");
833        (&raw const self.data).cast::<u8>()
834    }
835}
836
837impl<
838        const ROWS: usize,
839        const COLS: usize,
840        const NROWS: usize,
841        const BITS: u8,
842        const FRAME_COUNT: usize,
843    > MutableFrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
844{
845}
846
847#[cfg(test)]
848mod tests {
849    extern crate std;
850
851    use std::format;
852    use std::vec;
853
854    use super::*;
855    use embedded_graphics::pixelcolor::RgbColor;
856    use embedded_graphics::prelude::*;
857    use embedded_graphics::primitives::{Circle, PrimitiveStyle, Rectangle};
858
859    const TEST_ROWS: usize = 32;
860    const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
861        128
862    } else {
863        64
864    };
865    const TEST_NROWS: usize = TEST_ROWS / 2;
866    const TEST_BITS: u8 = 3;
867    const TEST_FRAME_COUNT: usize = (1 << TEST_BITS) - 1; // 7 frames for 3-bit depth
868
869    type TestFrameBuffer =
870        DmaFrameBuffer<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_BITS, TEST_FRAME_COUNT>;
871
872    // Helper function to get mapped index for ESP32
873    fn get_mapped_index(index: usize) -> usize {
874        map_index(index)
875    }
876
877    // Helper function to get the physical memory slot for a logical row-pair
878    // index (reversed when the `reverse-row-order` feature is enabled)
879    fn get_mapped_row(row_idx: usize) -> usize {
880        map_row_index::<TEST_NROWS>(row_idx)
881    }
882
883    #[test]
884    fn test_entry_construction() {
885        let entry = Entry::new();
886        let expect_oe = cfg!(feature = "invert-oe");
887        assert_eq!(entry.dummy2(), false);
888        assert_eq!(entry.blu2(), false);
889        assert_eq!(entry.grn2(), false);
890        assert_eq!(entry.red2(), false);
891        assert_eq!(entry.blu1(), false);
892        assert_eq!(entry.grn1(), false);
893        assert_eq!(entry.red1(), false);
894        assert_eq!(entry.output_enable(), expect_oe);
895        assert_eq!(entry.dummy1(), false);
896        assert_eq!(entry.dummy0(), false);
897        assert_eq!(entry.latch(), false);
898        assert_eq!(entry.addr(), 0);
899    }
900
901    #[test]
902    fn test_entry_setters() {
903        let mut entry = Entry::new();
904
905        entry.set_dummy2(true);
906        assert_eq!(entry.dummy2(), true);
907
908        entry.set_blu2(true);
909        assert_eq!(entry.blu2(), true);
910
911        entry.set_grn2(true);
912        assert_eq!(entry.grn2(), true);
913
914        entry.set_red2(true);
915        assert_eq!(entry.red2(), true);
916
917        entry.set_blu1(true);
918        assert_eq!(entry.blu1(), true);
919
920        entry.set_grn1(true);
921        assert_eq!(entry.grn1(), true);
922
923        entry.set_red1(true);
924        assert_eq!(entry.red1(), true);
925        assert_eq!(entry.0 & COLOR0_MASK, COLOR0_MASK);
926
927        entry.set_output_enable(true);
928        assert_eq!(entry.output_enable(), true);
929        assert_eq!(entry.0 & OE_BIT, OE_BIT);
930
931        entry.set_dummy1(true);
932        assert_eq!(entry.dummy1(), true);
933
934        entry.set_dummy0(true);
935        assert_eq!(entry.dummy0(), true);
936
937        entry.set_latch(true);
938        assert_eq!(entry.latch(), true);
939        assert_eq!(entry.0 & LATCH_BIT, LATCH_BIT);
940
941        entry.set_addr(0b11111);
942        assert_eq!(entry.addr(), 0b11111);
943        assert_eq!(entry.0 & (0x1f_u16 << ADDR_SHIFT), 0x1f_u16 << ADDR_SHIFT);
944    }
945
946    #[test]
947    fn test_entry_bit_isolation() {
948        let mut entry = Entry::new();
949        let expect_oe = cfg!(feature = "invert-oe");
950
951        // Test that setting one field doesn't affect others
952        entry.set_addr(0b11111);
953        entry.set_latch(true);
954        assert_eq!(entry.addr(), 0b11111);
955        assert_eq!(entry.latch(), true);
956        assert_eq!(entry.output_enable(), expect_oe);
957        assert_eq!(entry.red1(), false);
958
959        entry.set_red1(true);
960        entry.set_grn2(true);
961        assert_eq!(entry.addr(), 0b11111);
962        assert_eq!(entry.latch(), true);
963        assert_eq!(entry.red1(), true);
964        assert_eq!(entry.grn2(), true);
965        assert_eq!(entry.blu1(), false);
966        assert_eq!(entry.red2(), false);
967    }
968
969    #[test]
970    fn test_entry_set_color0() {
971        let mut entry = Entry::new();
972
973        let bits = (u8::from(true) << 2) | (u8::from(false) << 1) | u8::from(true); // b=1, g=0, r=1 = 0b101
974        entry.set_color0_bits(bits);
975        assert_eq!(entry.red1(), true);
976        assert_eq!(entry.grn1(), false);
977        assert_eq!(entry.blu1(), true);
978        // Check that only the expected bits are set
979        assert_eq!(entry.0 & COLOR0_MASK, u16::from(bits) << COLOR0_SHIFT);
980    }
981
982    #[test]
983    fn test_entry_set_color1() {
984        let mut entry = Entry::new();
985
986        let bits = (u8::from(true) << 2) | (u8::from(true) << 1) | u8::from(false); // b=1, g=1, r=0 = 0b110
987        entry.set_color1_bits(bits);
988        assert_eq!(entry.red2(), false);
989        assert_eq!(entry.grn2(), true);
990        assert_eq!(entry.blu2(), true);
991        // Check that only the expected bits are set
992        assert_eq!(entry.0 & COLOR1_MASK, u16::from(bits) << COLOR1_SHIFT);
993    }
994
995    /// Nails the exact word layout the Interstate 75 W PIO expects:
996    /// `R0 G0 B0 R1 G1 B1 A B C D E - LAT OE` on bits 0..13, with the unused
997    /// bit 11 parked at 0. Mirrors `pimoroni-pico/drivers/hub75/hub75.hpp`.
998    #[cfg(feature = "interstate75")]
999    #[test]
1000    fn interstate75_word_layout_matches_pinout() {
1001        // `new()` blanks the panel: the OE bit must be at bit 13 (whichever
1002        // level `invert-oe` selects) and every other bit must be clear.
1003        let blanked = Entry::new();
1004        assert_eq!(blanked.0, OE_BLANK);
1005
1006        let mut e = Entry::new();
1007        e.set_red1(true); // R0 → bit 0
1008        e.set_blu1(true); // B0 → bit 2
1009        e.set_grn2(true); // G1 → bit 4
1010        e.set_addr(0b1_0101); // A..E → bits 6..10
1011        e.set_latch(true); // LAT → bit 12
1012
1013        let expected = 0b1u16 | (0b1 << 2) | (0b1 << 4) | (0b1_0101 << 6) | (1 << 12) | OE_BLANK;
1014        assert_eq!(e.0, expected);
1015        assert_eq!(e.0 & (1 << 11), 0, "unused bit 11 must stay 0");
1016        assert_eq!(e.0 & (0b11 << 14), 0, "bits 14/15 must stay 0");
1017    }
1018
1019    #[test]
1020    fn test_entry_debug_formatting() {
1021        let entry = Entry(0x1234);
1022        let debug_str = format!("{:?}", entry);
1023        assert_eq!(debug_str, "Entry(0x1234)");
1024
1025        let entry = Entry(0xabcd);
1026        let debug_str = format!("{:?}", entry);
1027        assert_eq!(debug_str, "Entry(0xabcd)");
1028    }
1029
1030    #[test]
1031    fn test_row_construction() {
1032        let row: Row<TEST_COLS> = Row::new();
1033        assert_eq!(row.data.len(), TEST_COLS);
1034
1035        let expected = Entry::new().0;
1036        for entry in &row.data {
1037            assert_eq!(entry.0, expected);
1038        }
1039    }
1040
1041    #[test]
1042    fn test_row_format() {
1043        let mut row: Row<TEST_COLS> = Row::new();
1044        let prev_addr = 4;
1045
1046        row.format(prev_addr);
1047
1048        let oe_active = !cfg!(feature = "invert-oe");
1049
1050        for (physical_i, entry) in row.data.iter().enumerate() {
1051            let logical_i = get_mapped_index(physical_i);
1052
1053            assert_eq!(entry.addr(), u16::from(prev_addr));
1054
1055            if logical_i == TEST_COLS - 1 {
1056                assert_eq!(entry.latch(), true);
1057                assert_eq!(entry.output_enable(), !oe_active);
1058            } else {
1059                assert_eq!(entry.latch(), false);
1060                if logical_i >= TRAIL_BLANK_DELAY && logical_i < TEST_COLS - LEAD_BLANK_DELAY - 1 {
1061                    assert_eq!(entry.output_enable(), oe_active);
1062                } else {
1063                    assert_eq!(entry.output_enable(), !oe_active);
1064                }
1065            }
1066        }
1067    }
1068
1069    #[test]
1070    fn test_row_set_color0() {
1071        let mut row: Row<TEST_COLS> = Row::new();
1072
1073        row.set_color0(0, true, false, true);
1074
1075        let mapped_col_0 = get_mapped_index(0);
1076        assert_eq!(row.data[mapped_col_0].red1(), true);
1077        assert_eq!(row.data[mapped_col_0].grn1(), false);
1078        assert_eq!(row.data[mapped_col_0].blu1(), true);
1079
1080        // Test another column
1081        row.set_color0(1, false, true, false);
1082
1083        let mapped_col_1 = get_mapped_index(1);
1084        assert_eq!(row.data[mapped_col_1].red1(), false);
1085        assert_eq!(row.data[mapped_col_1].grn1(), true);
1086        assert_eq!(row.data[mapped_col_1].blu1(), false);
1087    }
1088
1089    #[test]
1090    fn test_row_set_color1() {
1091        let mut row: Row<TEST_COLS> = Row::new();
1092
1093        row.set_color1(0, true, true, false);
1094
1095        let mapped_col_0 = get_mapped_index(0);
1096        assert_eq!(row.data[mapped_col_0].red2(), true);
1097        assert_eq!(row.data[mapped_col_0].grn2(), true);
1098        assert_eq!(row.data[mapped_col_0].blu2(), false);
1099    }
1100
1101    #[test]
1102    fn test_row_default() {
1103        let row1: Row<TEST_COLS> = Row::new();
1104        let row2: Row<TEST_COLS> = Row::default();
1105
1106        // Both should be equivalent
1107        assert_eq!(row1, row2);
1108        assert_eq!(row1.data.len(), row2.data.len());
1109
1110        let expected = Entry::new().0;
1111        for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1112            assert_eq!(entry1.0, entry2.0);
1113            assert_eq!(entry1.0, expected);
1114        }
1115    }
1116
1117    #[test]
1118    fn test_frame_construction() {
1119        let frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1120        assert_eq!(frame.rows.len(), TEST_NROWS);
1121    }
1122
1123    #[test]
1124    fn test_frame_format() {
1125        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1126
1127        frame.format();
1128
1129        for slot in 0..TEST_NROWS {
1130            let (prev_addr, _) = slot_addresses::<TEST_NROWS>(slot);
1131            let row = &frame.rows[slot];
1132
1133            let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
1134            assert_eq!(row.data[last_pixel_idx].addr(), u16::from(prev_addr));
1135            assert_eq!(row.data[last_pixel_idx].latch(), true);
1136
1137            let first_pixel_idx = get_mapped_index(0);
1138            assert_eq!(row.data[first_pixel_idx].addr(), u16::from(prev_addr));
1139            assert_eq!(row.data[first_pixel_idx].latch(), false);
1140        }
1141    }
1142
1143    #[test]
1144    fn test_frame_set_pixel() {
1145        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1146
1147        // Test setting pixel in upper half (y < NROWS)
1148        frame.set_pixel(5, 10, true, false, true);
1149
1150        let mapped_col_10 = get_mapped_index(10);
1151        assert_eq!(
1152            frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1153            true
1154        );
1155        assert_eq!(
1156            frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1157            false
1158        );
1159        assert_eq!(
1160            frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1161            true
1162        );
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 = get_mapped_index(15);
1168        assert_eq!(
1169            frame.rows[get_mapped_row(5)].data[mapped_col_15].red2(),
1170            false
1171        );
1172        assert_eq!(
1173            frame.rows[get_mapped_row(5)].data[mapped_col_15].grn2(),
1174            true
1175        );
1176        assert_eq!(
1177            frame.rows[get_mapped_row(5)].data[mapped_col_15].blu2(),
1178            false
1179        );
1180    }
1181
1182    #[test]
1183    #[cfg(feature = "reverse-row-order")]
1184    fn test_reverse_row_order() {
1185        let mut fb = TestFrameBuffer::new();
1186
1187        // Slot 0 is streamed first and renders the last panel row; while it
1188        // is shifted the panel still displays row 0 (latched by the previous
1189        // frame's final slot), so its entries carry prev_addr 0.
1190        let first = &fb.data.frames[0].rows[0];
1191        assert_eq!(first.data[get_mapped_index(0)].addr(), 0);
1192        // The final slot renders panel row 0 and carries prev_addr 1.
1193        let last = &fb.data.frames[0].rows[TEST_NROWS - 1];
1194        assert_eq!(last.data[get_mapped_index(0)].addr(), 1);
1195
1196        // Logical row 0 maps to the last memory slot.
1197        fb.set_pixel(Point::new(3, 0), Color::RED);
1198        let col3 = get_mapped_index(3);
1199        assert!(fb.data.frames[0].rows[TEST_NROWS - 1].data[col3].red1());
1200        assert!(!fb.data.frames[0].rows[0].data[col3].red1());
1201    }
1202
1203    #[test]
1204    fn test_frame_default() {
1205        let frame1: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1206        let frame2: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::default();
1207
1208        // Both should be equivalent
1209        assert_eq!(frame1.rows.len(), frame2.rows.len());
1210
1211        let expected = Entry::new().0;
1212        for (row1, row2) in frame1.rows.iter().zip(frame2.rows.iter()) {
1213            assert_eq!(row1, row2);
1214
1215            for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1216                assert_eq!(entry1.0, entry2.0);
1217                assert_eq!(entry1.0, expected);
1218            }
1219        }
1220    }
1221
1222    #[test]
1223    fn test_dma_framebuffer_construction() {
1224        let fb = TestFrameBuffer::new();
1225        assert_eq!(fb.data.frames.len(), TEST_FRAME_COUNT);
1226        assert_eq!(fb._align, 0);
1227    }
1228
1229    #[test]
1230    #[allow(deprecated)]
1231    fn test_bcm_chunk_info() {
1232        let expected_size =
1233            core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>();
1234        assert_eq!(TestFrameBuffer::bcm_chunk_bytes(), expected_size);
1235        assert_eq!(TestFrameBuffer::bcm_chunk_count(), 1);
1236    }
1237
1238    #[test]
1239    fn test_dma_framebuffer_erase() {
1240        let fb = TestFrameBuffer::new();
1241
1242        for frame in &fb.data.frames {
1243            for slot in 0..TEST_NROWS {
1244                let (prev_addr, _) = slot_addresses::<TEST_NROWS>(slot);
1245                let row = &frame.rows[slot];
1246
1247                let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
1248                assert_eq!(row.data[last_pixel_idx].addr(), u16::from(prev_addr));
1249                assert_eq!(row.data[last_pixel_idx].latch(), true);
1250
1251                let first_pixel_idx = get_mapped_index(0);
1252                assert_eq!(row.data[first_pixel_idx].addr(), u16::from(prev_addr));
1253                assert_eq!(row.data[first_pixel_idx].latch(), false);
1254            }
1255        }
1256    }
1257
1258    #[test]
1259    fn test_dma_framebuffer_set_pixel_bounds() {
1260        let mut fb = TestFrameBuffer::new();
1261
1262        // Test negative coordinates
1263        fb.set_pixel(Point::new(-1, 5), Color::RED);
1264        fb.set_pixel(Point::new(5, -1), Color::RED);
1265
1266        // Test coordinates out of bounds (should not panic)
1267        fb.set_pixel(Point::new(TEST_COLS as i32, 5), Color::RED);
1268        fb.set_pixel(Point::new(5, TEST_ROWS as i32), Color::RED);
1269    }
1270
1271    #[test]
1272    fn test_dma_framebuffer_set_pixel_internal() {
1273        let mut fb = TestFrameBuffer::new();
1274
1275        let red_color = Color::RED;
1276        fb.set_pixel_internal(10, 5, red_color);
1277
1278        // With 3-bit depth, brightness steps are 32 (256/8)
1279        // Frames represent thresholds: 32, 64, 96, 128, 160, 192, 224
1280        // Red value 255 should activate all frames
1281        for frame in &fb.data.frames {
1282            // Check upper half pixel
1283            let mapped_col_10 = get_mapped_index(10);
1284            assert_eq!(
1285                frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1286                true
1287            );
1288            assert_eq!(
1289                frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1290                false
1291            );
1292            assert_eq!(
1293                frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1294                false
1295            );
1296        }
1297    }
1298
1299    #[test]
1300    fn test_dma_framebuffer_brightness_modulation() {
1301        let mut fb = TestFrameBuffer::new();
1302
1303        // Test with a medium brightness value
1304        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1305        let test_brightness = brightness_step * 3; // 96
1306        let color = Color::from(embedded_graphics::pixelcolor::Rgb888::new(
1307            test_brightness,
1308            0,
1309            0,
1310        ));
1311
1312        fb.set_pixel_internal(0, 0, color);
1313
1314        // Should activate frames 0, 1, 2 (thresholds 32, 64, 96)
1315        // but not frames 3, 4, 5, 6 (thresholds 128, 160, 192, 224)
1316        for (frame_idx, frame) in fb.data.frames.iter().enumerate() {
1317            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1318            let should_be_active = test_brightness >= frame_threshold;
1319
1320            let mapped_col_0 = get_mapped_index(0);
1321            assert_eq!(
1322                frame.rows[get_mapped_row(0)].data[mapped_col_0].red1(),
1323                should_be_active
1324            );
1325        }
1326    }
1327
1328    #[test]
1329    fn test_origin_dimensions() {
1330        let fb = TestFrameBuffer::new();
1331        let size = fb.size();
1332        assert_eq!(size.width, TEST_COLS as u32);
1333        assert_eq!(size.height, TEST_ROWS as u32);
1334
1335        // Test reference
1336        let size = (&fb).size();
1337        assert_eq!(size.width, TEST_COLS as u32);
1338        assert_eq!(size.height, TEST_ROWS as u32);
1339
1340        // Test mutable reference
1341        let mut fb = TestFrameBuffer::new();
1342        let fb_ref = &mut fb;
1343        let size = fb_ref.size();
1344        assert_eq!(size.width, TEST_COLS as u32);
1345        assert_eq!(size.height, TEST_ROWS as u32);
1346    }
1347
1348    #[test]
1349    fn test_draw_target() {
1350        let mut fb = TestFrameBuffer::new();
1351
1352        let pixels = vec![
1353            embedded_graphics::Pixel(Point::new(0, 0), Color::RED),
1354            embedded_graphics::Pixel(Point::new(1, 1), Color::GREEN),
1355            embedded_graphics::Pixel(Point::new(2, 2), Color::BLUE),
1356        ];
1357
1358        let result = fb.draw_iter(pixels);
1359        assert!(result.is_ok());
1360
1361        // Test mutable reference
1362        let result = (&mut fb).draw_iter(vec![embedded_graphics::Pixel(
1363            Point::new(3, 3),
1364            Color::WHITE,
1365        )]);
1366        assert!(result.is_ok());
1367    }
1368
1369    #[test]
1370    fn test_draw_iter_pixel_verification() {
1371        let mut fb = TestFrameBuffer::new();
1372
1373        // Create test pixels with specific colors and positions
1374        let pixels = vec![
1375            // Upper half pixels (y < NROWS) - should set color0
1376            embedded_graphics::Pixel(Point::new(5, 2), Color::RED), // (5, 2) -> red
1377            embedded_graphics::Pixel(Point::new(10, 5), Color::GREEN), // (10, 5) -> green
1378            embedded_graphics::Pixel(Point::new(15, 8), Color::BLUE), // (15, 8) -> blue
1379            embedded_graphics::Pixel(Point::new(20, 10), Color::WHITE), // (20, 10) -> white
1380            // Lower half pixels (y >= NROWS) - should set color1
1381            embedded_graphics::Pixel(Point::new(25, (TEST_NROWS + 3) as i32), Color::RED), // (25, 19) -> red
1382            embedded_graphics::Pixel(Point::new(30, (TEST_NROWS + 7) as i32), Color::GREEN), // (30, 23) -> green
1383            embedded_graphics::Pixel(Point::new(35, (TEST_NROWS + 12) as i32), Color::BLUE), // (35, 28) -> blue
1384            // Edge case: black pixel (should not be visible in first frame)
1385            embedded_graphics::Pixel(Point::new(40, 1), Color::BLACK), // (40, 1) -> black
1386        ];
1387
1388        let result = fb.draw_iter(pixels);
1389        assert!(result.is_ok());
1390
1391        // Check the first frame only
1392        let first_frame = &fb.data.frames[0];
1393        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1394        let first_frame_threshold = brightness_step; // 32
1395
1396        // Test upper half pixels (color0)
1397        // Red pixel at (5, 2) - should be red in first frame
1398        let col_idx = get_mapped_index(5);
1399        assert_eq!(
1400            first_frame.rows[get_mapped_row(2)].data[col_idx].red1(),
1401            Color::RED.r() >= first_frame_threshold
1402        );
1403        assert_eq!(
1404            first_frame.rows[get_mapped_row(2)].data[col_idx].grn1(),
1405            Color::RED.g() >= first_frame_threshold
1406        );
1407        assert_eq!(
1408            first_frame.rows[get_mapped_row(2)].data[col_idx].blu1(),
1409            Color::RED.b() >= first_frame_threshold
1410        );
1411
1412        // Green pixel at (10, 5) - should be green in first frame
1413        let col_idx = get_mapped_index(10);
1414        assert_eq!(
1415            first_frame.rows[get_mapped_row(5)].data[col_idx].red1(),
1416            Color::GREEN.r() >= first_frame_threshold
1417        );
1418        assert_eq!(
1419            first_frame.rows[get_mapped_row(5)].data[col_idx].grn1(),
1420            Color::GREEN.g() >= first_frame_threshold
1421        );
1422        assert_eq!(
1423            first_frame.rows[get_mapped_row(5)].data[col_idx].blu1(),
1424            Color::GREEN.b() >= first_frame_threshold
1425        );
1426
1427        // Blue pixel at (15, 8) - should be blue in first frame
1428        let col_idx = get_mapped_index(15);
1429        assert_eq!(
1430            first_frame.rows[get_mapped_row(8)].data[col_idx].red1(),
1431            Color::BLUE.r() >= first_frame_threshold
1432        );
1433        assert_eq!(
1434            first_frame.rows[get_mapped_row(8)].data[col_idx].grn1(),
1435            Color::BLUE.g() >= first_frame_threshold
1436        );
1437        assert_eq!(
1438            first_frame.rows[get_mapped_row(8)].data[col_idx].blu1(),
1439            Color::BLUE.b() >= first_frame_threshold
1440        );
1441
1442        // White pixel at (20, 10) - should be white in first frame
1443        let col_idx = get_mapped_index(20);
1444        assert_eq!(
1445            first_frame.rows[get_mapped_row(10)].data[col_idx].red1(),
1446            Color::WHITE.r() >= first_frame_threshold
1447        );
1448        assert_eq!(
1449            first_frame.rows[get_mapped_row(10)].data[col_idx].grn1(),
1450            Color::WHITE.g() >= first_frame_threshold
1451        );
1452        assert_eq!(
1453            first_frame.rows[get_mapped_row(10)].data[col_idx].blu1(),
1454            Color::WHITE.b() >= first_frame_threshold
1455        );
1456
1457        // Test lower half pixels (color1)
1458        // Red pixel at (25, TEST_NROWS + 3) -> row 3, color1
1459        let col_idx = get_mapped_index(25);
1460        assert_eq!(
1461            first_frame.rows[get_mapped_row(3)].data[col_idx].red2(),
1462            Color::RED.r() >= first_frame_threshold
1463        );
1464        assert_eq!(
1465            first_frame.rows[get_mapped_row(3)].data[col_idx].grn2(),
1466            Color::RED.g() >= first_frame_threshold
1467        );
1468        assert_eq!(
1469            first_frame.rows[get_mapped_row(3)].data[col_idx].blu2(),
1470            Color::RED.b() >= first_frame_threshold
1471        );
1472
1473        // Green pixel at (30, TEST_NROWS + 7) -> row 7, color1
1474        let col_idx = get_mapped_index(30);
1475        assert_eq!(
1476            first_frame.rows[get_mapped_row(7)].data[col_idx].red2(),
1477            Color::GREEN.r() >= first_frame_threshold
1478        );
1479        assert_eq!(
1480            first_frame.rows[get_mapped_row(7)].data[col_idx].grn2(),
1481            Color::GREEN.g() >= first_frame_threshold
1482        );
1483        assert_eq!(
1484            first_frame.rows[get_mapped_row(7)].data[col_idx].blu2(),
1485            Color::GREEN.b() >= first_frame_threshold
1486        );
1487
1488        // Blue pixel at (35, TEST_NROWS + 12) -> row 12, color1
1489        let col_idx = get_mapped_index(35);
1490        assert_eq!(
1491            first_frame.rows[get_mapped_row(12)].data[col_idx].red2(),
1492            Color::BLUE.r() >= first_frame_threshold
1493        );
1494        assert_eq!(
1495            first_frame.rows[get_mapped_row(12)].data[col_idx].grn2(),
1496            Color::BLUE.g() >= first_frame_threshold
1497        );
1498        assert_eq!(
1499            first_frame.rows[get_mapped_row(12)].data[col_idx].blu2(),
1500            Color::BLUE.b() >= first_frame_threshold
1501        );
1502
1503        // Test black pixel - should not be visible in any frame
1504        let col_idx = get_mapped_index(40);
1505        assert_eq!(
1506            first_frame.rows[get_mapped_row(1)].data[col_idx].red1(),
1507            false
1508        );
1509        assert_eq!(
1510            first_frame.rows[get_mapped_row(1)].data[col_idx].grn1(),
1511            false
1512        );
1513        assert_eq!(
1514            first_frame.rows[get_mapped_row(1)].data[col_idx].blu1(),
1515            false
1516        );
1517    }
1518
1519    #[test]
1520    fn test_embedded_graphics_integration() {
1521        let mut fb = TestFrameBuffer::new();
1522
1523        // Draw a rectangle
1524        let result = Rectangle::new(Point::new(5, 5), Size::new(10, 8))
1525            .into_styled(PrimitiveStyle::with_fill(Color::RED))
1526            .draw(&mut fb);
1527        assert!(result.is_ok());
1528
1529        // Draw a circle
1530        let result = Circle::new(Point::new(30, 15), 8)
1531            .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
1532            .draw(&mut fb);
1533        assert!(result.is_ok());
1534    }
1535
1536    #[test]
1537    #[cfg(feature = "skip-black-pixels")]
1538    fn test_skip_black_pixels_enabled() {
1539        let mut fb = TestFrameBuffer::new();
1540
1541        // Set a red pixel first
1542        fb.set_pixel_internal(10, 5, Color::RED);
1543
1544        // Verify it's red in the first frame
1545        let mapped_col_10 = get_mapped_index(10);
1546        assert_eq!(
1547            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1548            true
1549        );
1550        assert_eq!(
1551            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1552            false
1553        );
1554        assert_eq!(
1555            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1556            false
1557        );
1558
1559        // Now set it to black - with skip-black-pixels enabled, this should be ignored
1560        fb.set_pixel_internal(10, 5, Color::BLACK);
1561
1562        // The pixel should still be red (black write was skipped)
1563        assert_eq!(
1564            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1565            true
1566        );
1567        assert_eq!(
1568            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1569            false
1570        );
1571        assert_eq!(
1572            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1573            false
1574        );
1575    }
1576
1577    #[test]
1578    #[cfg(not(feature = "skip-black-pixels"))]
1579    fn test_skip_black_pixels_disabled() {
1580        let mut fb = TestFrameBuffer::new();
1581
1582        // Set a red pixel first
1583        fb.set_pixel_internal(10, 5, Color::RED);
1584
1585        // Verify it's red in the first frame
1586        let mapped_col_10 = get_mapped_index(10);
1587        assert_eq!(
1588            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1589            true
1590        );
1591        assert_eq!(
1592            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1593            false
1594        );
1595        assert_eq!(
1596            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1597            false
1598        );
1599
1600        // Now set it to black - with skip-black-pixels disabled, this should overwrite
1601        fb.set_pixel_internal(10, 5, Color::BLACK);
1602
1603        // The pixel should now be black (all bits false)
1604        assert_eq!(
1605            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1606            false
1607        );
1608        assert_eq!(
1609            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1610            false
1611        );
1612        assert_eq!(
1613            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1614            false
1615        );
1616    }
1617
1618    #[test]
1619    fn test_bcm_frame_overwrite() {
1620        let mut fb = TestFrameBuffer::new();
1621
1622        // First write a white pixel (255, 255, 255)
1623        fb.set_pixel_internal(10, 5, Color::WHITE);
1624
1625        let mapped_col_10 = get_mapped_index(10);
1626
1627        // Verify white pixel is lit in all frames (255 >= all thresholds)
1628        for frame in fb.data.frames.iter() {
1629            // White (255) should be active in all frames since it's >= all thresholds
1630            assert_eq!(
1631                frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1632                true
1633            );
1634            assert_eq!(
1635                frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1636                true
1637            );
1638            assert_eq!(
1639                frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1640                true
1641            );
1642        }
1643
1644        // Now overwrite with 50% white (128, 128, 128)
1645        let half_white = Color::from(embedded_graphics::pixelcolor::Rgb888::new(128, 128, 128));
1646        fb.set_pixel_internal(10, 5, half_white);
1647
1648        // Verify only the correct frames are lit for 50% white
1649        // With 3-bit depth: thresholds are 32, 64, 96, 128, 160, 192, 224
1650        // 128 should activate frames 0, 1, 2, 3 (thresholds 32, 64, 96, 128)
1651        // but not frames 4, 5, 6 (thresholds 160, 192, 224)
1652        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1653        for (frame_idx, frame) in fb.data.frames.iter().enumerate() {
1654            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1655            let should_be_active = 128 >= frame_threshold;
1656
1657            assert_eq!(
1658                frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1659                should_be_active
1660            );
1661            assert_eq!(
1662                frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1663                should_be_active
1664            );
1665            assert_eq!(
1666                frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1667                should_be_active
1668            );
1669        }
1670
1671        // Specifically verify the expected pattern for 3-bit depth
1672        // Frames 0-3 should be active (thresholds 32, 64, 96, 128)
1673        for frame_idx in 0..4 {
1674            assert_eq!(
1675                fb.data.frames[frame_idx].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1676                true
1677            );
1678        }
1679        // Frames 4-6 should be inactive (thresholds 160, 192, 224)
1680        for frame_idx in 4..TEST_FRAME_COUNT {
1681            assert_eq!(
1682                fb.data.frames[frame_idx].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1683                false
1684            );
1685        }
1686    }
1687
1688    #[test]
1689    fn test_debug_formatting() {
1690        let fb = TestFrameBuffer::new();
1691        let debug_string = format!("{:?}", fb);
1692        assert!(debug_string.contains("DmaFrameBuffer"));
1693        assert!(debug_string.contains("frame_count"));
1694        assert!(debug_string.contains("frame_size"));
1695        assert!(debug_string.contains("brightness_step"));
1696    }
1697
1698    #[test]
1699    fn test_default_implementation() {
1700        let fb1 = TestFrameBuffer::new();
1701        let fb2 = TestFrameBuffer::default();
1702
1703        // Both should be equivalent in size, but may differ in content since new() calls format()
1704        assert_eq!(fb1.data.frames.len(), fb2.data.frames.len());
1705        assert_eq!(fb1._align, fb2._align);
1706    }
1707
1708    #[test]
1709    fn test_memory_alignment() {
1710        let fb = TestFrameBuffer::new();
1711        let ptr = &fb as *const _ as usize;
1712
1713        // Should be aligned to 64-bit boundary due to the _align field
1714        assert_eq!(ptr % 8, 0);
1715    }
1716
1717    #[test]
1718    fn test_color_values() {
1719        let mut fb = TestFrameBuffer::new();
1720
1721        // Test different color values
1722        let colors = [
1723            (Color::RED, (255, 0, 0)),
1724            (Color::GREEN, (0, 255, 0)),
1725            (Color::BLUE, (0, 0, 255)),
1726            (Color::WHITE, (255, 255, 255)),
1727            (Color::BLACK, (0, 0, 0)),
1728        ];
1729
1730        for (i, (color, (r, g, b))) in colors.iter().enumerate() {
1731            fb.set_pixel(Point::new(i as i32, 0), *color);
1732            assert_eq!(color.r(), *r);
1733            assert_eq!(color.g(), *g);
1734            assert_eq!(color.b(), *b);
1735        }
1736    }
1737
1738    #[test]
1739    fn test_blanking_delay() {
1740        let mut row: Row<TEST_COLS> = Row::new();
1741        let prev_addr = 4;
1742
1743        row.format(prev_addr);
1744
1745        let oe_active = !cfg!(feature = "invert-oe");
1746
1747        // Trail blank: indices 0..TRAIL_BLANK_DELAY-1 (DMA start = physical right edge)
1748        if TRAIL_BLANK_DELAY > 0 {
1749            let trail_blank_idx = get_mapped_index(TRAIL_BLANK_DELAY - 1);
1750            assert_eq!(row.data[trail_blank_idx].output_enable(), !oe_active);
1751        }
1752
1753        let after_trail_idx = get_mapped_index(TRAIL_BLANK_DELAY);
1754        assert_eq!(row.data[after_trail_idx].output_enable(), oe_active);
1755
1756        // Lead blank: indices before latch (DMA end = physical left edge)
1757        let before_lead_idx = get_mapped_index(TEST_COLS - LEAD_BLANK_DELAY - 2);
1758        assert_eq!(row.data[before_lead_idx].output_enable(), oe_active);
1759
1760        let lead_blank_idx = get_mapped_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
1761        assert_eq!(row.data[lead_blank_idx].output_enable(), !oe_active);
1762    }
1763
1764    #[test]
1765    fn test_esp32_mapping() {
1766        // Test the ESP32-specific index mapping
1767        #[cfg(feature = "esp32-ordering")]
1768        {
1769            assert_eq!(map_index(0), 1);
1770            assert_eq!(map_index(1), 0);
1771            assert_eq!(map_index(2), 3);
1772            assert_eq!(map_index(3), 2);
1773            assert_eq!(map_index(4), 5);
1774            assert_eq!(map_index(5), 4);
1775        }
1776        #[cfg(not(feature = "esp32-ordering"))]
1777        {
1778            assert_eq!(map_index(0), 0);
1779            assert_eq!(map_index(1), 1);
1780            assert_eq!(map_index(2), 2);
1781            assert_eq!(map_index(3), 3);
1782        }
1783    }
1784
1785    #[test]
1786    fn test_bits_assertion() {
1787        // Test that BITS <= 8 assertion is enforced at compile time
1788        // This test mainly documents the constraint
1789        assert!(TEST_BITS <= 8);
1790    }
1791
1792    #[test]
1793    fn test_fast_clear_method() {
1794        let mut fb = TestFrameBuffer::new();
1795
1796        // Set some pixels
1797        fb.set_pixel_internal(10, 5, Color::RED);
1798        fb.set_pixel_internal(20, 10, Color::GREEN);
1799
1800        // Verify pixels are set
1801        let mapped_col_10 = get_mapped_index(10);
1802        assert_eq!(
1803            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1804            true
1805        );
1806
1807        // Clear using fast method
1808        fb.erase();
1809
1810        // Verify pixels are cleared but timing signals remain
1811        assert_eq!(
1812            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
1813            false
1814        );
1815        assert_eq!(
1816            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
1817            false
1818        );
1819        assert_eq!(
1820            fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
1821            false
1822        );
1823
1824        // Verify timing signals are still present (check last pixel has latch)
1825        let last_col = get_mapped_index(TEST_COLS - 1);
1826        assert_eq!(
1827            fb.data.frames[0].rows[get_mapped_row(5)].data[last_col].latch(),
1828            true
1829        );
1830    }
1831
1832    // Remove the old `test_draw_char_bottom_right` and replace with a helper + combined test.
1833
1834    // Constants for FONT_6X10 glyph size
1835    const CHAR_W: i32 = 6;
1836    const CHAR_H: i32 = 10;
1837
1838    /// Draws glyph 'A' at `origin` and validates framebuffer pixels against a reference.
1839    fn verify_glyph_at(fb: &mut TestFrameBuffer, origin: Point) {
1840        use embedded_graphics::mock_display::MockDisplay;
1841        use embedded_graphics::mono_font::ascii::FONT_6X10;
1842        use embedded_graphics::mono_font::MonoTextStyle;
1843        use embedded_graphics::text::{Baseline, Text};
1844
1845        // Draw the glyph
1846        let style = MonoTextStyle::new(&FONT_6X10, Color::WHITE);
1847        Text::with_baseline("A", origin, style, Baseline::Top)
1848            .draw(fb)
1849            .unwrap();
1850
1851        // Reference glyph at (0,0)
1852        let mut reference: MockDisplay<Color> = MockDisplay::new();
1853        Text::with_baseline("A", Point::zero(), style, Baseline::Top)
1854            .draw(&mut reference)
1855            .unwrap();
1856
1857        for dy in 0..CHAR_H {
1858            for dx in 0..CHAR_W {
1859                let expected_on = reference
1860                    .get_pixel(Point::new(dx, dy))
1861                    .unwrap_or(Color::BLACK)
1862                    != Color::BLACK;
1863
1864                let gx = (origin.x + dx) as usize;
1865                let gy = (origin.y + dy) as usize;
1866
1867                // we have computed the origin to be within the panel, so we don't need to check for bounds
1868                // if gx >= TEST_COLS || gy >= TEST_ROWS {
1869                //     continue;
1870                // }
1871
1872                // Fetch Entry from frame 0
1873                let frame0 = &fb.data.frames[0];
1874                let e = if gy < TEST_NROWS {
1875                    &frame0.rows[get_mapped_row(gy)].data[get_mapped_index(gx)]
1876                } else {
1877                    &frame0.rows[get_mapped_row(gy - TEST_NROWS)].data[get_mapped_index(gx)]
1878                };
1879
1880                let (r, g, b) = if gy >= TEST_NROWS {
1881                    (e.red2(), e.grn2(), e.blu2())
1882                } else {
1883                    (e.red1(), e.grn1(), e.blu1())
1884                };
1885
1886                if expected_on {
1887                    assert!(r && g && b,);
1888                } else {
1889                    assert!(!r && !g && !b);
1890                }
1891            }
1892        }
1893    }
1894
1895    #[test]
1896    fn test_draw_char_corners() {
1897        let upper_left = Point::new(0, 0);
1898        let lower_right = Point::new(TEST_COLS as i32 - CHAR_W, TEST_ROWS as i32 - CHAR_H);
1899
1900        let mut fb = TestFrameBuffer::new();
1901
1902        verify_glyph_at(&mut fb, upper_left);
1903        verify_glyph_at(&mut fb, lower_right);
1904    }
1905
1906    #[test]
1907    fn test_framebuffer_operations_trait_erase() {
1908        let mut fb = TestFrameBuffer::new();
1909
1910        // Set pixels so erase has work to do
1911        fb.set_pixel_internal(10, 5, Color::RED);
1912        fb.set_pixel_internal(20, 10, Color::GREEN);
1913
1914        // Explicitly call trait method to hit the FrameBufferOperations impl
1915        <TestFrameBuffer as FrameBufferOperations>::erase(&mut fb);
1916
1917        // Verify colors cleared on frame 0
1918        let mc10 = get_mapped_index(10);
1919        let mc20 = get_mapped_index(20);
1920        assert_eq!(
1921            fb.data.frames[0].rows[get_mapped_row(5)].data[mc10].red1(),
1922            false
1923        );
1924        assert_eq!(
1925            fb.data.frames[0].rows[get_mapped_row(10)].data[mc20].grn1(),
1926            false
1927        );
1928
1929        // Timing signals preserved: last pixel should have latch
1930        let last_col = get_mapped_index(TEST_COLS - 1);
1931        assert!(fb.data.frames[0].rows[get_mapped_row(0)].data[last_col].latch());
1932    }
1933
1934    #[test]
1935    fn test_framebuffer_operations_trait_set_pixel() {
1936        let mut fb = TestFrameBuffer::new();
1937
1938        // Explicitly call trait method to hit the FrameBufferOperations impl
1939        <TestFrameBuffer as FrameBufferOperations>::set_pixel(
1940            &mut fb,
1941            Point::new(8, 3),
1942            Color::BLUE,
1943        );
1944
1945        let idx = get_mapped_index(8);
1946        assert_eq!(
1947            fb.data.frames[0].rows[get_mapped_row(3)].data[idx].blu1(),
1948            true
1949        );
1950        assert_eq!(
1951            fb.data.frames[0].rows[get_mapped_row(3)].data[idx].red1(),
1952            false
1953        );
1954        assert_eq!(
1955            fb.data.frames[0].rows[get_mapped_row(3)].data[idx].grn1(),
1956            false
1957        );
1958    }
1959
1960    #[test]
1961    fn test_entry_new_oe_matches_feature() {
1962        let entry = Entry::new();
1963        if cfg!(feature = "invert-oe") {
1964            assert!(entry.output_enable());
1965        } else {
1966            assert!(!entry.output_enable());
1967        }
1968    }
1969
1970    #[test]
1971    fn test_make_data_template_oe_polarity() {
1972        let mut row = Row::<TEST_COLS>::new();
1973        row.format(4);
1974
1975        let active_idx = get_mapped_index(TRAIL_BLANK_DELAY);
1976        let blank_idx = get_mapped_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
1977        let latch_idx = get_mapped_index(TEST_COLS - 1);
1978
1979        if cfg!(feature = "invert-oe") {
1980            assert!(!row.data[active_idx].output_enable());
1981            assert!(row.data[blank_idx].output_enable());
1982            assert!(row.data[latch_idx].output_enable());
1983        } else {
1984            assert!(row.data[active_idx].output_enable());
1985            assert!(!row.data[blank_idx].output_enable());
1986            assert!(!row.data[latch_idx].output_enable());
1987        }
1988    }
1989
1990    static STATIC_FB: TestFrameBuffer = TestFrameBuffer::new();
1991
1992    #[test]
1993    fn test_static_construction_is_formatted() {
1994        let runtime_fb = TestFrameBuffer::new();
1995
1996        for (fi, frame) in STATIC_FB.data.frames.iter().enumerate() {
1997            for (ri, row) in frame.rows.iter().enumerate() {
1998                assert_eq!(
1999                    row.data, runtime_fb.data.frames[fi].rows[ri].data,
2000                    "static vs runtime mismatch at frame {fi}, row {ri}"
2001                );
2002                assert_eq!(
2003                    row.gap, runtime_fb.data.frames[fi].rows[ri].gap,
2004                    "static vs runtime gap mismatch at frame {fi}, row {ri}"
2005                );
2006            }
2007        }
2008    }
2009
2010    #[test]
2011    fn test_format_reinitializes_at_runtime() {
2012        let mut fb = TestFrameBuffer::new();
2013        fb.erase();
2014        fb.format();
2015
2016        for (fi, frame) in fb.data.frames.iter().enumerate() {
2017            for (ri, row) in frame.rows.iter().enumerate() {
2018                assert_eq!(
2019                    row.data, STATIC_FB.data.frames[fi].rows[ri].data,
2020                    "re-formatted vs static mismatch at frame {fi}, row {ri}"
2021                );
2022            }
2023        }
2024    }
2025
2026    #[test]
2027    fn bcm_segment_count_is_one() {
2028        use crate::FrameBuffer;
2029        let fb = TestFrameBuffer::new();
2030        assert_eq!(fb.bcm_segment_count(), 1);
2031    }
2032
2033    #[test]
2034    fn bcm_segment_covers_entire_buffer() {
2035        use crate::FrameBuffer;
2036        let fb = TestFrameBuffer::new();
2037        let seg = fb.bcm_segment(0);
2038        let expected_ptr = (&raw const fb.data).cast::<u8>();
2039        let expected_len =
2040            core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>();
2041        assert_eq!(seg.ptr, expected_ptr);
2042        assert_eq!(seg.len, expected_len);
2043        assert_eq!(seg.reps, 1);
2044    }
2045
2046    #[test]
2047    #[should_panic(expected = "out of range")]
2048    fn bcm_segment_panics_for_invalid_index() {
2049        use crate::FrameBuffer;
2050        let fb = TestFrameBuffer::new();
2051        let _ = fb.bcm_segment(1);
2052    }
2053
2054    #[test]
2055    fn bcm_sequence_matches_runtime_segments() {
2056        use crate::FrameBuffer;
2057        let fb = TestFrameBuffer::new();
2058        assert_eq!(
2059            TestFrameBuffer::BCM_SEGMENT_COUNT,
2060            TestFrameBuffer::BCM_SEQUENCE_LEN * TestFrameBuffer::BCM_SEQUENCE_COUNT
2061        );
2062        assert_eq!(fb.bcm_segment_count(), TestFrameBuffer::BCM_SEGMENT_COUNT);
2063        for i in 0..TestFrameBuffer::BCM_SEGMENT_COUNT {
2064            let entry = TestFrameBuffer::BCM_SEQUENCE[i % TestFrameBuffer::BCM_SEQUENCE_LEN];
2065            let seg = fb.bcm_segment(i);
2066            assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
2067            assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
2068        }
2069        for entry in &TestFrameBuffer::BCM_SEQUENCE[TestFrameBuffer::BCM_SEQUENCE_LEN..] {
2070            assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
2071        }
2072    }
2073}