Skip to main content

hub75_framebuffer/
latched.rs

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