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