hub75_framebuffer/
plain.rs

1//! DMA-friendly framebuffer implementation for HUB75 LED panels.
2//!
3//! This module provides a framebuffer implementation with memory
4//! layout optimized for efficient transfer to HUB75 LED panels. The data is
5//! structured for direct signal mapping, making it ideal for DMA transfers but
6//! also suitable for programmatic transfer. It supports RGB color and brightness
7//! control through multiple frames using Binary Code Modulation (BCM).
8//!
9//! # Hardware Requirements
10//! This implementation can be used by any microcontroller that has a peripheral
11//! capable of outputting a clock signal and 16 bits (though only 14 bits are
12//! actually needed) in parallel. The data is structured to directly match the
13//! HUB75 connector signals, making it suitable for various parallel output
14//! peripherals.
15//!
16//! # Features
17//! - Memory layout optimized for efficient data transfer
18//! - Direct signal mapping to HUB75 connector signals
19//! - Support for RGB color with brightness control
20//! - Multiple frame buffers for Binary Code Modulation (BCM)
21//! - Integration with embedded-graphics for easy drawing
22//!
23//! # Brightness Control
24//! Brightness is controlled through Binary Code Modulation (BCM):
25//! - The number of brightness levels is determined by the `BITS` parameter
26//! - Each additional bit doubles the number of brightness levels
27//! - More bits provide better brightness resolution but require more memory
28//! - Memory usage grows exponentially with the number of bits: `(2^BITS)-1`
29//!   frames
30//! - Example: 8 bits = 256 levels, 4 bits = 16 levels
31//!
32//! # Memory Usage
33//! The framebuffer's memory usage is determined by:
34//! - Panel size (ROWS × COLS)
35//! - Number of brightness bits (BITS)
36//! - Memory grows exponentially with bits: `(2^BITS)-1` frames
37//! - 16-bit entries provide direct signal mapping but use more memory
38//!
39//! # Example
40//! ```rust,no_run
41//! use embedded_graphics::pixelcolor::RgbColor;
42//! use embedded_graphics::prelude::*;
43//! use embedded_graphics::primitives::Circle;
44//! use embedded_graphics::primitives::Rectangle;
45//! use hub75_framebuffer::compute_frame_count;
46//! use hub75_framebuffer::compute_rows;
47//! use hub75_framebuffer::Color;
48//! use hub75_framebuffer::plain::DmaFrameBuffer;
49//! use embedded_graphics::primitives::PrimitiveStyle;
50//!
51//! // Create a framebuffer for a 64x32 panel with 3-bit color depth
52//! const ROWS: usize = 32;
53//! const COLS: usize = 64;
54//! const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
55//! const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
56//! const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
57//!
58//! let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
59//!
60//! // Draw a red rectangle
61//! Rectangle::new(Point::new(10, 10), Size::new(20, 20))
62//!     .into_styled(PrimitiveStyle::with_fill(Color::RED))
63//!     .draw(&mut framebuffer)
64//!     .unwrap();
65//!
66//! // Draw a blue circle
67//! Circle::new(Point::new(40, 20), 10)
68//!     .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
69//!     .draw(&mut framebuffer)
70//!     .unwrap();
71//! ```
72//!
73//! # Implementation Details
74//! The framebuffer is organized to directly match the HUB75 connector signals:
75//! - Each 16-bit word maps directly to the HUB75 control signals
76//! - Color data (R, G, B) for two sub-pixels is stored in dedicated bits
77//! - Control signals (output enable, latch, address) are mapped to specific
78//!   bits
79//! - Multiple frames are used to achieve Binary Code Modulation (BCM)
80//! - DMA transfers the data directly to the panel without any transformation
81//!
82//! # HUB75 Signal Bit Mapping
83//! Each 16-bit `Entry` represents the logic levels that will be driven onto the HUB75
84//! bus during a single pixel-clock cycle. Every panel signal—apart from the clock
85//! (`CLK`) itself—occupies a dedicated bit, allowing the word to be streamed via DMA
86//! with zero run-time manipulation.
87//!
88//! ```text
89//! 15 ─ Dummy2   (spare)
90//! 14 ─ B2       Blue  – lower half of the panel
91//! 13 ─ G2       Green – lower half of the panel
92//! 12 ─ R2       Red   – lower half of the panel
93//! 11 ─ B1       Blue  – upper half of the panel
94//! 10 ─ G1       Green – upper half of the panel
95//!  9 ─ R1       Red   – upper half of the panel
96//!  8 ─ OE       Output-Enable / Blank
97//!  7 ─ Dummy1   (spare)
98//!  6 ─ Dummy0   (spare)
99//!  5 ─ LAT      Latch / STB
100//! 4-0 ─ A..E    Row address lines
101//! ```
102//!
103//! The pixel clock is generated by the peripheral that owns the DMA stream and
104//! is therefore **not** part of the 16-bit word stored in the framebuffer.
105//!
106//! # Binary Code Modulation (BCM) Frames
107//! Brightness is achieved with Binary-Code-Modulation as outlined in
108//! <https://www.batsocks.co.uk/readme/art_bcm_1.htm>. For a colour depth of
109//! `BITS`, the driver allocates `FRAME_COUNT = 2^BITS - 1` frames. Frame *n*
110//! (0-based) is displayed for a duration proportional to `2^n`, mirroring the
111//! weight of that bit in a binary number.
112//!
113//! When a pixel is written its 8-bit RGB components are compared against a
114//! per-frame threshold:
115//!
116//! ```text
117//! brightness_step = 256 / 2^BITS
118//! threshold_n     = (n + 1) * brightness_step
119//! ```
120//!
121//! If a channel's value is greater than or equal to `threshold_n` the
122//! corresponding colour bit is set in frame *n*. Streaming the frames from the
123//! least-significant (shortest) to the most-significant (longest) time-slot
124//! produces the correct 8-bit brightness while keeping the refresh routine
125//! trivial.
126//!
127//! # Safety
128//! This implementation uses unsafe code for DMA operations. The framebuffer
129//! must be properly aligned in memory and the DMA configuration must match the
130//! buffer layout.
131
132use core::convert::Infallible;
133
134use bitfield::bitfield;
135#[cfg(not(feature = "esp-hal-dma"))]
136use embedded_dma::ReadBuffer;
137use embedded_graphics::pixelcolor::RgbColor;
138use embedded_graphics::prelude::Point;
139#[cfg(feature = "esp-hal-dma")]
140use esp_hal::dma::ReadBuffer;
141
142use super::Color;
143use super::FrameBuffer;
144use super::WordSize;
145
146const BLANKING_DELAY: usize = 1;
147
148// Pre-computed lookup table for all possible row addresses (0-31)
149// This eliminates the need to compute addresses at runtime
150const ADDR_TABLE: [u16; 32] = [
151    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
152    26, 27, 28, 29, 30, 31,
153];
154
155/// Creates a pre-computed data template for a row with the specified addresses.
156/// This template contains all the timing and control signals but no pixel data.
157#[inline]
158const fn make_data_template<const COLS: usize>(addr: u8, prev_addr: u8) -> [Entry; COLS] {
159    let mut data = [Entry::new(); COLS];
160    let mut i = 0;
161
162    while i < COLS {
163        let mut entry = Entry::new();
164        entry.0 = ADDR_TABLE[prev_addr as usize];
165
166        // Apply timing control based on position
167        if i == 1 {
168            entry.0 |= 0b1_0000_0000; // set output_enable bit
169        } else if i == COLS - BLANKING_DELAY - 1 {
170            // output_enable already false from initialization
171        } else if i == COLS - 1 {
172            entry.0 |= 0b0010_0000; // set latch bit
173            entry.0 = (entry.0 & !0b0001_1111) | ADDR_TABLE[addr as usize]; // set new address
174        } else if i > 1 && i < COLS - BLANKING_DELAY - 1 {
175            entry.0 |= 0b1_0000_0000; // set output_enable bit
176        }
177
178        data[map_index(i)] = entry;
179        i += 1;
180    }
181
182    data
183}
184
185bitfield! {
186    /// A 16-bit word representing the HUB75 control signals for a single pixel.
187    ///
188    /// This structure directly maps to the HUB75 connector signals:
189    /// - RGB color data for two sub-pixels (color0 and color1)
190    /// - Panel control signals (output enable, latch, address)
191    /// - Dummy bits for timing alignment
192    ///
193    /// The bit layout matches the HUB75 connector signals:
194    /// - Bit 15: Dummy bit 2
195    /// - Bit 14: Blue channel for color1
196    /// - Bit 13: Green channel for color1
197    /// - Bit 12: Red channel for color1
198    /// - Bit 11: Blue channel for color0
199    /// - Bit 10: Green channel for color0
200    /// - Bit 9: Red channel for color0
201    /// - Bit 8: Output enable
202    /// - Bit 7: Dummy bit 1
203    /// - Bit 6: Dummy bit 0
204    /// - Bit 5: Latch signal
205    /// - Bits 4-0: Row address
206    #[derive(Clone, Copy, Default, PartialEq)]
207    #[repr(transparent)]
208    struct Entry(u16);
209    dummy2, set_dummy2: 15;
210    blu2, set_blu2: 14;
211    grn2, set_grn2: 13;
212    red2, set_red2: 12;
213    blu1, set_blu1: 11;
214    grn1, set_grn1: 10;
215    red1, set_red1: 9;
216    output_enable, set_output_enable: 8;
217    dummy1, set_dummy1: 7;
218    dummy0, set_dummy0: 6;
219    latch, set_latch: 5;
220    addr, set_addr: 4, 0;
221}
222
223impl core::fmt::Debug for Entry {
224    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
225        f.debug_tuple("Entry")
226            .field(&format_args!("{:#x}", self.0))
227            .finish()
228    }
229}
230
231#[cfg(feature = "defmt")]
232impl defmt::Format for Entry {
233    fn format(&self, f: defmt::Formatter) {
234        defmt::write!(f, "Entry({=u16:#x})", self.0);
235    }
236}
237
238impl Entry {
239    const fn new() -> Self {
240        Self(0)
241    }
242
243    // Optimized color bit manipulation constants and methods
244    const COLOR0_MASK: u16 = 0b0000_1110_0000_0000; // bits 9-11: R1, G1, B1
245    const COLOR1_MASK: u16 = 0b0111_0000_0000_0000; // bits 12-14: R2, G2, B2
246
247    #[inline]
248    fn set_color0_bits(&mut self, bits: u8) {
249        let bits16 = u16::from(bits) << 9;
250        self.0 = (self.0 & !Self::COLOR0_MASK) | (bits16 & Self::COLOR0_MASK);
251    }
252
253    #[inline]
254    fn set_color1_bits(&mut self, bits: u8) {
255        let bits16 = u16::from(bits) << 12;
256        self.0 = (self.0 & !Self::COLOR1_MASK) | (bits16 & Self::COLOR1_MASK);
257    }
258}
259
260/// Represents a single row of pixels in the framebuffer.
261///
262/// Each row contains a fixed number of columns (`COLS`) and manages the timing
263/// and control signals for the HUB75 panel. The row handles:
264/// - Output enable timing to prevent ghosting
265/// - Latch signal generation for row updates
266/// - Row address management
267/// - Color data for both sub-pixels
268#[derive(Clone, Copy, PartialEq, Debug)]
269#[repr(C)]
270struct Row<const COLS: usize> {
271    data: [Entry; COLS],
272}
273
274const fn map_index(i: usize) -> usize {
275    #[cfg(feature = "esp32-ordering")]
276    {
277        i ^ 1
278    }
279    #[cfg(not(feature = "esp32-ordering"))]
280    {
281        i
282    }
283}
284
285impl<const COLS: usize> Default for Row<COLS> {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291impl<const COLS: usize> Row<COLS> {
292    pub const fn new() -> Self {
293        Self {
294            data: [Entry::new(); COLS],
295        }
296    }
297
298    pub fn format(&mut self, addr: u8, prev_addr: u8) {
299        // Use pre-computed template and bulk copy for maximum performance
300        let template = make_data_template::<COLS>(addr, prev_addr);
301        self.data.copy_from_slice(&template);
302    }
303
304    /// Fast clear method that preserves timing/control bits while clearing pixel data.
305    /// Uses bulk memory operations for maximum performance.
306    #[inline]
307    pub fn clear_colors(&mut self) {
308        // Clear color bits while preserving timing and control bits
309        const COLOR_CLEAR_MASK: u16 = !0b0111_1110_0000_0000; // Clear bits 9-14 (R1,G1,B1,R2,G2,B2)
310
311        for entry in &mut self.data {
312            entry.0 &= COLOR_CLEAR_MASK;
313        }
314    }
315
316    #[inline]
317    pub fn set_color0(&mut self, col: usize, r: bool, g: bool, b: bool) {
318        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
319        let col = map_index(col);
320        self.data[col].set_color0_bits(bits);
321    }
322
323    #[inline]
324    pub fn set_color1(&mut self, col: usize, r: bool, g: bool, b: bool) {
325        let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
326        let col = map_index(col);
327        self.data[col].set_color1_bits(bits);
328    }
329}
330
331#[derive(Copy, Clone, Debug)]
332#[repr(C)]
333struct Frame<const ROWS: usize, const COLS: usize, const NROWS: usize> {
334    rows: [Row<COLS>; NROWS],
335}
336
337impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Frame<ROWS, COLS, NROWS> {
338    pub const fn new() -> Self {
339        Self {
340            rows: [Row::new(); NROWS],
341        }
342    }
343
344    pub fn format(&mut self) {
345        for (addr, row) in self.rows.iter_mut().enumerate() {
346            let prev_addr = if addr == 0 {
347                NROWS as u8 - 1
348            } else {
349                addr as u8 - 1
350            };
351            row.format(addr as u8, prev_addr);
352        }
353    }
354
355    /// Fast clear method that preserves timing/control bits while clearing pixel data.
356    #[inline]
357    pub fn clear_colors(&mut self) {
358        for row in &mut self.rows {
359            row.clear_colors();
360        }
361    }
362
363    #[inline]
364    pub fn set_pixel(&mut self, y: usize, x: usize, red: bool, green: bool, blue: bool) {
365        let row = &mut self.rows[if y < NROWS { y } else { y - NROWS }];
366        if y < NROWS {
367            row.set_color0(x, red, green, blue);
368        } else {
369            row.set_color1(x, red, green, blue);
370        }
371    }
372}
373
374impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Default
375    for Frame<ROWS, COLS, NROWS>
376{
377    fn default() -> Self {
378        Self::new()
379    }
380}
381
382/// DMA-compatible framebuffer for HUB75 LED panels.
383///
384/// This is a framebuffer implementation that:
385/// - Manages multiple frames for Binary Code Modulation (BCM)
386/// - Provides DMA-compatible memory layout
387/// - Implements the embedded-graphics `DrawTarget` trait
388///
389/// # Type Parameters
390/// - `ROWS`: Total number of rows in the panel
391/// - `COLS`: Number of columns in the panel
392/// - `NROWS`: Number of rows per scan (typically half of ROWS)
393/// - `BITS`: Color depth (1-8 bits)
394/// - `FRAME_COUNT`: Number of frames used for Binary Code Modulation
395///
396/// # Helper Functions
397/// Use these functions to compute the correct values:
398/// - `esp_hub75::compute_frame_count(BITS)`: Computes the required number of
399///   frames
400/// - `esp_hub75::compute_rows(ROWS)`: Computes the number of rows per scan
401///
402/// # Memory Layout
403/// The buffer is aligned to ensure efficient DMA transfers and contains:
404/// - A 64-bit alignment field
405/// - An array of frames, each containing the full panel data
406#[derive(Copy, Clone)]
407#[repr(C)]
408pub struct DmaFrameBuffer<
409    const ROWS: usize,
410    const COLS: usize,
411    const NROWS: usize,
412    const BITS: u8,
413    const FRAME_COUNT: usize,
414> {
415    _align: u64,
416    frames: [Frame<ROWS, COLS, NROWS>; FRAME_COUNT],
417}
418
419impl<
420        const ROWS: usize,
421        const COLS: usize,
422        const NROWS: usize,
423        const BITS: u8,
424        const FRAME_COUNT: usize,
425    > Default for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
426{
427    fn default() -> Self {
428        Self::new()
429    }
430}
431
432impl<
433        const ROWS: usize,
434        const COLS: usize,
435        const NROWS: usize,
436        const BITS: u8,
437        const FRAME_COUNT: usize,
438    > DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
439{
440    /// Create a new, ready-to-use framebuffer.
441    ///
442    /// This creates a new framebuffer and automatically formats it with proper timing signals.
443    /// The framebuffer is immediately ready for pixel operations and DMA transfers.
444    ///
445    /// # Panics
446    ///
447    /// Panics if `BITS` is greater than 8, as only 1-8 bit color depths are supported.
448    ///
449    /// # Example
450    /// ```rust,no_run
451    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
452    ///
453    /// const ROWS: usize = 32;
454    /// const COLS: usize = 64;
455    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
456    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
457    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
458    ///
459    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
460    /// // No need to call format() - framebuffer is ready to use!
461    /// ```
462    #[must_use]
463    pub fn new() -> Self {
464        debug_assert!(BITS <= 8);
465
466        let mut instance = Self {
467            _align: 0,
468            frames: [Frame::new(); FRAME_COUNT],
469        };
470
471        // Pre-format the framebuffer so it's immediately ready for use
472        instance.format();
473        instance
474    }
475
476    /// This returns the size of the DMA buffer in bytes.  Its used to calculate
477    /// the number of DMA descriptors needed.
478    /// # Example
479    /// ```rust
480    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
481    ///
482    /// const ROWS: usize = 32;
483    /// const COLS: usize = 64;
484    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
485    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
486    /// const FRAME_COUNT: usize = compute_frame_count(BITS);
487    ///
488    /// type FBType = DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>;
489    /// let (_, tx_descriptors) = esp_hal::dma_descriptors!(0, FBType::dma_buffer_size_bytes());
490    /// ```
491    #[cfg(feature = "esp-hal-dma")]
492    pub const fn dma_buffer_size_bytes() -> usize {
493        core::mem::size_of::<[Frame<ROWS, COLS, NROWS>; FRAME_COUNT]>()
494    }
495
496    /// Perform full formatting of the framebuffer with timing and control signals.
497    ///
498    /// This sets up all the timing and control signals needed for proper HUB75 operation.
499    /// This is automatically called by `new()`, so you typically don't need to call this
500    /// unless you want to completely reinitialize the framebuffer.
501    ///
502    /// # Example
503    /// ```rust,no_run
504    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
505    ///
506    /// const ROWS: usize = 32;
507    /// const COLS: usize = 64;
508    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
509    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
510    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
511    ///
512    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
513    /// framebuffer.format(); // Reinitialize if needed
514    /// ```
515    #[inline]
516    pub fn format(&mut self) {
517        for frame in &mut self.frames {
518            frame.format();
519        }
520    }
521
522    /// Fast erase operation that clears all pixel data while preserving timing signals.
523    ///
524    /// This is much faster than `format()` when you just want to clear the display
525    /// since it preserves all the timing and control signals that are already set up.
526    /// Use this for clearing between frames or when you want to start drawing fresh content.
527    ///
528    /// # Example
529    /// ```rust,no_run
530    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
531    ///
532    /// const ROWS: usize = 32;
533    /// const COLS: usize = 64;
534    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
535    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
536    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
537    ///
538    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
539    /// framebuffer.erase();
540    /// ```
541    #[inline]
542    pub fn erase(&mut self) {
543        for frame in &mut self.frames {
544            frame.clear_colors();
545        }
546    }
547
548    /// Set a pixel in the framebuffer.
549    /// # Example
550    /// ```rust,no_run
551    /// use hub75_framebuffer::{Color,plain::DmaFrameBuffer,compute_rows,compute_frame_count};
552    /// use embedded_graphics::prelude::*;
553    ///
554    /// const ROWS: usize = 32;
555    /// const COLS: usize = 64;
556    /// const BITS: u8 = 3; // Color depth (8 brightness levels, 7 frames)
557    /// const NROWS: usize = compute_rows(ROWS); // Number of rows per scan
558    /// const FRAME_COUNT: usize = compute_frame_count(BITS); // Number of frames for BCM
559    ///
560    /// let mut framebuffer = DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::new();
561    /// framebuffer.set_pixel(Point::new(10, 10), Color::RED);
562    /// ```
563    pub fn set_pixel(&mut self, p: Point, color: Color) {
564        if p.x < 0 || p.y < 0 {
565            return;
566        }
567        self.set_pixel_internal(p.x as usize, p.y as usize, color);
568    }
569
570    #[inline]
571    fn frames_on(v: u8) -> usize {
572        // v / brightness_step but the compiler resolves the shift at build-time
573        (v as usize) >> (8 - BITS)
574    }
575
576    #[inline]
577    fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
578        if x >= COLS || y >= ROWS {
579            return;
580        }
581
582        // Early exit for black pixels - common in UI backgrounds
583        // Only enabled when skip-black-pixels feature is active
584        #[cfg(feature = "skip-black-pixels")]
585        if color == Color::BLACK {
586            return;
587        }
588
589        // Pre-compute how many frames each channel should be on
590        let red_frames = Self::frames_on(color.r());
591        let green_frames = Self::frames_on(color.g());
592        let blue_frames = Self::frames_on(color.b());
593
594        // Set the pixel in all frames based on pre-computed frame counts
595        for (frame_idx, frame) in self.frames.iter_mut().enumerate() {
596            frame.set_pixel(
597                y,
598                x,
599                frame_idx < red_frames,
600                frame_idx < green_frames,
601                frame_idx < blue_frames,
602            );
603        }
604    }
605}
606
607impl<
608        const ROWS: usize,
609        const COLS: usize,
610        const NROWS: usize,
611        const BITS: u8,
612        const FRAME_COUNT: usize,
613    > embedded_graphics::prelude::OriginDimensions
614    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
615{
616    fn size(&self) -> embedded_graphics::prelude::Size {
617        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
618    }
619}
620
621impl<
622        const ROWS: usize,
623        const COLS: usize,
624        const NROWS: usize,
625        const BITS: u8,
626        const FRAME_COUNT: usize,
627    > embedded_graphics::prelude::OriginDimensions
628    for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
629{
630    fn size(&self) -> embedded_graphics::prelude::Size {
631        embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
632    }
633}
634
635impl<
636        const ROWS: usize,
637        const COLS: usize,
638        const NROWS: usize,
639        const BITS: u8,
640        const FRAME_COUNT: usize,
641    > embedded_graphics::draw_target::DrawTarget
642    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
643{
644    type Color = Color;
645
646    type Error = Infallible;
647
648    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
649    where
650        I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
651    {
652        for pixel in pixels {
653            self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
654        }
655        Ok(())
656    }
657}
658
659unsafe impl<
660        const ROWS: usize,
661        const COLS: usize,
662        const NROWS: usize,
663        const BITS: u8,
664        const FRAME_COUNT: usize,
665    > ReadBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
666{
667    #[cfg(not(feature = "esp-hal-dma"))]
668    type Word = u8;
669
670    unsafe fn read_buffer(&self) -> (*const u8, usize) {
671        let ptr = (&raw const self.frames).cast::<u8>();
672        let len = core::mem::size_of_val(&self.frames);
673        (ptr, len)
674    }
675}
676
677unsafe impl<
678        const ROWS: usize,
679        const COLS: usize,
680        const NROWS: usize,
681        const BITS: u8,
682        const FRAME_COUNT: usize,
683    > ReadBuffer for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
684{
685    #[cfg(not(feature = "esp-hal-dma"))]
686    type Word = u8;
687
688    unsafe fn read_buffer(&self) -> (*const u8, usize) {
689        let ptr = (&raw const self.frames).cast::<u8>();
690        let len = core::mem::size_of_val(&self.frames);
691        (ptr, len)
692    }
693}
694
695impl<
696        const ROWS: usize,
697        const COLS: usize,
698        const NROWS: usize,
699        const BITS: u8,
700        const FRAME_COUNT: usize,
701    > core::fmt::Debug for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
702{
703    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
704        let brightness_step = 1 << (8 - BITS);
705        f.debug_struct("DmaFrameBuffer")
706            .field("size", &core::mem::size_of_val(&self.frames))
707            .field("frame_count", &self.frames.len())
708            .field("frame_size", &core::mem::size_of_val(&self.frames[0]))
709            .field("brightness_step", &&brightness_step)
710            .finish_non_exhaustive()
711    }
712}
713
714#[cfg(feature = "defmt")]
715impl<
716        const ROWS: usize,
717        const COLS: usize,
718        const NROWS: usize,
719        const BITS: u8,
720        const FRAME_COUNT: usize,
721    > defmt::Format for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
722{
723    fn format(&self, f: defmt::Formatter) {
724        let brightness_step = 1 << (8 - BITS);
725        defmt::write!(
726            f,
727            "DmaFrameBuffer<{}, {}, {}, {}, {}>",
728            ROWS,
729            COLS,
730            NROWS,
731            BITS,
732            FRAME_COUNT
733        );
734        defmt::write!(f, " size: {}", core::mem::size_of_val(&self.frames));
735        defmt::write!(
736            f,
737            " frame_size: {}",
738            core::mem::size_of_val(&self.frames[0])
739        );
740        defmt::write!(f, " brightness_step: {}", brightness_step);
741    }
742}
743
744impl<
745        const ROWS: usize,
746        const COLS: usize,
747        const NROWS: usize,
748        const BITS: u8,
749        const FRAME_COUNT: usize,
750    > FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
751    for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
752{
753    fn get_word_size(&self) -> WordSize {
754        WordSize::Sixteen
755    }
756}
757
758impl<
759        const ROWS: usize,
760        const COLS: usize,
761        const NROWS: usize,
762        const BITS: u8,
763        const FRAME_COUNT: usize,
764    > FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
765    for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
766{
767    fn get_word_size(&self) -> WordSize {
768        WordSize::Sixteen
769    }
770}
771
772#[cfg(test)]
773mod tests {
774    extern crate std;
775
776    use std::format;
777    use std::vec;
778
779    use super::*;
780    use crate::{FrameBuffer, WordSize};
781    use embedded_graphics::pixelcolor::RgbColor;
782    use embedded_graphics::prelude::*;
783    use embedded_graphics::primitives::{Circle, PrimitiveStyle, Rectangle};
784
785    const TEST_ROWS: usize = 32;
786    const TEST_COLS: usize = 64;
787    const TEST_NROWS: usize = TEST_ROWS / 2;
788    const TEST_BITS: u8 = 3;
789    const TEST_FRAME_COUNT: usize = (1 << TEST_BITS) - 1; // 7 frames for 3-bit depth
790
791    type TestFrameBuffer =
792        DmaFrameBuffer<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_BITS, TEST_FRAME_COUNT>;
793
794    // Helper function to get mapped index for ESP32
795    fn get_mapped_index(index: usize) -> usize {
796        map_index(index)
797    }
798
799    #[test]
800    fn test_entry_construction() {
801        let entry = Entry::new();
802        assert_eq!(entry.0, 0);
803        assert_eq!(entry.dummy2(), false);
804        assert_eq!(entry.blu2(), false);
805        assert_eq!(entry.grn2(), false);
806        assert_eq!(entry.red2(), false);
807        assert_eq!(entry.blu1(), false);
808        assert_eq!(entry.grn1(), false);
809        assert_eq!(entry.red1(), false);
810        assert_eq!(entry.output_enable(), false);
811        assert_eq!(entry.dummy1(), false);
812        assert_eq!(entry.dummy0(), false);
813        assert_eq!(entry.latch(), false);
814        assert_eq!(entry.addr(), 0);
815    }
816
817    #[test]
818    fn test_entry_setters() {
819        let mut entry = Entry::new();
820
821        entry.set_dummy2(true);
822        assert_eq!(entry.dummy2(), true);
823        assert_eq!(entry.0 & 0b1000000000000000, 0b1000000000000000);
824
825        entry.set_blu2(true);
826        assert_eq!(entry.blu2(), true);
827        assert_eq!(entry.0 & 0b0100000000000000, 0b0100000000000000);
828
829        entry.set_grn2(true);
830        assert_eq!(entry.grn2(), true);
831        assert_eq!(entry.0 & 0b0010000000000000, 0b0010000000000000);
832
833        entry.set_red2(true);
834        assert_eq!(entry.red2(), true);
835        assert_eq!(entry.0 & 0b0001000000000000, 0b0001000000000000);
836
837        entry.set_blu1(true);
838        assert_eq!(entry.blu1(), true);
839        assert_eq!(entry.0 & 0b0000100000000000, 0b0000100000000000);
840
841        entry.set_grn1(true);
842        assert_eq!(entry.grn1(), true);
843        assert_eq!(entry.0 & 0b0000010000000000, 0b0000010000000000);
844
845        entry.set_red1(true);
846        assert_eq!(entry.red1(), true);
847        assert_eq!(entry.0 & 0b0000001000000000, 0b0000001000000000);
848
849        entry.set_output_enable(true);
850        assert_eq!(entry.output_enable(), true);
851        assert_eq!(entry.0 & 0b0000000100000000, 0b0000000100000000);
852
853        entry.set_dummy1(true);
854        assert_eq!(entry.dummy1(), true);
855        assert_eq!(entry.0 & 0b0000000010000000, 0b0000000010000000);
856
857        entry.set_dummy0(true);
858        assert_eq!(entry.dummy0(), true);
859        assert_eq!(entry.0 & 0b0000000001000000, 0b0000000001000000);
860
861        entry.set_latch(true);
862        assert_eq!(entry.latch(), true);
863        assert_eq!(entry.0 & 0b0000000000100000, 0b0000000000100000);
864
865        entry.set_addr(0b11111);
866        assert_eq!(entry.addr(), 0b11111);
867        assert_eq!(entry.0 & 0b0000000000011111, 0b0000000000011111);
868    }
869
870    #[test]
871    fn test_entry_bit_isolation() {
872        let mut entry = Entry::new();
873
874        // Test that setting one field doesn't affect others
875        entry.set_addr(0b11111);
876        entry.set_latch(true);
877        assert_eq!(entry.addr(), 0b11111);
878        assert_eq!(entry.latch(), true);
879        assert_eq!(entry.output_enable(), false);
880        assert_eq!(entry.red1(), false);
881
882        entry.set_red1(true);
883        entry.set_grn2(true);
884        assert_eq!(entry.addr(), 0b11111);
885        assert_eq!(entry.latch(), true);
886        assert_eq!(entry.red1(), true);
887        assert_eq!(entry.grn2(), true);
888        assert_eq!(entry.blu1(), false);
889        assert_eq!(entry.red2(), false);
890    }
891
892    #[test]
893    fn test_entry_set_color0() {
894        let mut entry = Entry::new();
895
896        let bits = (u8::from(true) << 2) | (u8::from(false) << 1) | u8::from(true); // b=1, g=0, r=1 = 0b101
897        entry.set_color0_bits(bits);
898        assert_eq!(entry.red1(), true);
899        assert_eq!(entry.grn1(), false);
900        assert_eq!(entry.blu1(), true);
901        // Check that only the expected bits are set
902        assert_eq!(entry.0 & 0b0000101000000000, 0b0000101000000000); // Red1 and Blue1 bits
903    }
904
905    #[test]
906    fn test_entry_set_color1() {
907        let mut entry = Entry::new();
908
909        let bits = (u8::from(true) << 2) | (u8::from(true) << 1) | u8::from(false); // b=1, g=1, r=0 = 0b110
910        entry.set_color1_bits(bits);
911        assert_eq!(entry.red2(), false);
912        assert_eq!(entry.grn2(), true);
913        assert_eq!(entry.blu2(), true);
914        // Check that only the expected bits are set
915        assert_eq!(entry.0 & 0b0110000000000000, 0b0110000000000000); // Green2 and Blue2 bits
916    }
917
918    #[test]
919    fn test_entry_debug_formatting() {
920        let entry = Entry(0x1234);
921        let debug_str = format!("{:?}", entry);
922        assert_eq!(debug_str, "Entry(0x1234)");
923
924        let entry = Entry(0xabcd);
925        let debug_str = format!("{:?}", entry);
926        assert_eq!(debug_str, "Entry(0xabcd)");
927    }
928
929    #[test]
930    fn test_row_construction() {
931        let row: Row<TEST_COLS> = Row::new();
932        assert_eq!(row.data.len(), TEST_COLS);
933
934        // Check that all entries are initialized to zero
935        for entry in &row.data {
936            assert_eq!(entry.0, 0);
937        }
938    }
939
940    #[test]
941    fn test_row_format() {
942        let mut row: Row<TEST_COLS> = Row::new();
943        let test_addr = 5;
944        let prev_addr = 4;
945
946        row.format(test_addr, prev_addr);
947
948        // Check data entries configuration
949        for (physical_i, entry) in row.data.iter().enumerate() {
950            let logical_i = get_mapped_index(physical_i);
951
952            match logical_i {
953                i if i == TEST_COLS - BLANKING_DELAY - 1 => {
954                    // Second to last pixel should have output_enable false
955                    assert_eq!(entry.output_enable(), false);
956                    assert_eq!(entry.addr(), prev_addr as u16);
957                    assert_eq!(entry.latch(), false);
958                }
959                i if i == TEST_COLS - 1 => {
960                    // Last pixel should have latch true and new address
961                    assert_eq!(entry.latch(), true);
962                    assert_eq!(entry.addr(), test_addr as u16);
963                    assert_eq!(entry.output_enable(), false);
964                }
965                1 => {
966                    // First pixel after start should have output_enable true
967                    assert_eq!(entry.output_enable(), true);
968                    assert_eq!(entry.addr(), prev_addr as u16);
969                    assert_eq!(entry.latch(), false);
970                }
971                _ => {
972                    // Other pixels should have the previous address and no latch
973                    assert_eq!(entry.addr(), prev_addr as u16);
974                    assert_eq!(entry.latch(), false);
975                    if logical_i > 1 && logical_i < TEST_COLS - BLANKING_DELAY - 1 {
976                        assert_eq!(entry.output_enable(), true);
977                    }
978                }
979            }
980        }
981    }
982
983    #[test]
984    fn test_row_set_color0() {
985        let mut row: Row<TEST_COLS> = Row::new();
986
987        row.set_color0(0, true, false, true);
988
989        let mapped_col_0 = get_mapped_index(0);
990        assert_eq!(row.data[mapped_col_0].red1(), true);
991        assert_eq!(row.data[mapped_col_0].grn1(), false);
992        assert_eq!(row.data[mapped_col_0].blu1(), true);
993
994        // Test another column
995        row.set_color0(1, false, true, false);
996
997        let mapped_col_1 = get_mapped_index(1);
998        assert_eq!(row.data[mapped_col_1].red1(), false);
999        assert_eq!(row.data[mapped_col_1].grn1(), true);
1000        assert_eq!(row.data[mapped_col_1].blu1(), false);
1001    }
1002
1003    #[test]
1004    fn test_row_set_color1() {
1005        let mut row: Row<TEST_COLS> = Row::new();
1006
1007        row.set_color1(0, true, true, false);
1008
1009        let mapped_col_0 = get_mapped_index(0);
1010        assert_eq!(row.data[mapped_col_0].red2(), true);
1011        assert_eq!(row.data[mapped_col_0].grn2(), true);
1012        assert_eq!(row.data[mapped_col_0].blu2(), false);
1013    }
1014
1015    #[test]
1016    fn test_row_default() {
1017        let row1: Row<TEST_COLS> = Row::new();
1018        let row2: Row<TEST_COLS> = Row::default();
1019
1020        // Both should be equivalent
1021        assert_eq!(row1, row2);
1022        assert_eq!(row1.data.len(), row2.data.len());
1023
1024        // Check that all entries are initialized to zero
1025        for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1026            assert_eq!(entry1.0, entry2.0);
1027            assert_eq!(entry1.0, 0);
1028        }
1029    }
1030
1031    #[test]
1032    fn test_frame_construction() {
1033        let frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1034        assert_eq!(frame.rows.len(), TEST_NROWS);
1035    }
1036
1037    #[test]
1038    fn test_frame_format() {
1039        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1040
1041        frame.format();
1042
1043        // Check that each row was formatted with correct address parameters
1044        for addr in 0..TEST_NROWS {
1045            let prev_addr = if addr == 0 { TEST_NROWS - 1 } else { addr - 1 };
1046
1047            // Check some key pixels in each row
1048            let row = &frame.rows[addr];
1049
1050            // Check last pixel has correct new address
1051            let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
1052            assert_eq!(row.data[last_pixel_idx].addr(), addr as u16);
1053            assert_eq!(row.data[last_pixel_idx].latch(), true);
1054
1055            // Check non-last pixels have previous address
1056            let first_pixel_idx = get_mapped_index(0);
1057            assert_eq!(row.data[first_pixel_idx].addr(), prev_addr as u16);
1058            assert_eq!(row.data[first_pixel_idx].latch(), false);
1059        }
1060    }
1061
1062    #[test]
1063    fn test_frame_set_pixel() {
1064        let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1065
1066        // Test setting pixel in upper half (y < NROWS)
1067        frame.set_pixel(5, 10, true, false, true);
1068
1069        let mapped_col_10 = get_mapped_index(10);
1070        assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1071        assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1072        assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1073
1074        // Test setting pixel in lower half (y >= NROWS)
1075        frame.set_pixel(TEST_NROWS + 5, 15, false, true, false);
1076
1077        let mapped_col_15 = get_mapped_index(15);
1078        assert_eq!(frame.rows[5].data[mapped_col_15].red2(), false);
1079        assert_eq!(frame.rows[5].data[mapped_col_15].grn2(), true);
1080        assert_eq!(frame.rows[5].data[mapped_col_15].blu2(), false);
1081    }
1082
1083    #[test]
1084    fn test_frame_default() {
1085        let frame1: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
1086        let frame2: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::default();
1087
1088        // Both should be equivalent
1089        assert_eq!(frame1.rows.len(), frame2.rows.len());
1090
1091        // Check that all rows are equivalent
1092        for (row1, row2) in frame1.rows.iter().zip(frame2.rows.iter()) {
1093            assert_eq!(row1, row2);
1094
1095            // Verify all entries are zero-initialized
1096            for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
1097                assert_eq!(entry1.0, entry2.0);
1098                assert_eq!(entry1.0, 0);
1099            }
1100        }
1101    }
1102
1103    #[test]
1104    fn test_dma_framebuffer_construction() {
1105        let fb = TestFrameBuffer::new();
1106        assert_eq!(fb.frames.len(), TEST_FRAME_COUNT);
1107        assert_eq!(fb._align, 0);
1108    }
1109
1110    #[test]
1111    #[cfg(feature = "esp-hal-dma")]
1112    fn test_dma_framebuffer_dma_buffer_size() {
1113        let expected_size =
1114            core::mem::size_of::<[Frame<TEST_ROWS, TEST_COLS, TEST_NROWS>; TEST_FRAME_COUNT]>();
1115        assert_eq!(TestFrameBuffer::dma_buffer_size_bytes(), expected_size);
1116    }
1117
1118    #[test]
1119    fn test_dma_framebuffer_erase() {
1120        let fb = TestFrameBuffer::new();
1121
1122        // After erasing, all frames should be formatted
1123        for frame in &fb.frames {
1124            for addr in 0..TEST_NROWS {
1125                let prev_addr = if addr == 0 { TEST_NROWS - 1 } else { addr - 1 };
1126
1127                // Check some key pixels in each row
1128                let row = &frame.rows[addr];
1129
1130                // Check last pixel has correct new address
1131                let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
1132                assert_eq!(row.data[last_pixel_idx].addr(), addr as u16);
1133                assert_eq!(row.data[last_pixel_idx].latch(), true);
1134
1135                // Check non-last pixels have previous address
1136                let first_pixel_idx = get_mapped_index(0);
1137                assert_eq!(row.data[first_pixel_idx].addr(), prev_addr as u16);
1138                assert_eq!(row.data[first_pixel_idx].latch(), false);
1139            }
1140        }
1141    }
1142
1143    #[test]
1144    fn test_dma_framebuffer_set_pixel_bounds() {
1145        let mut fb = TestFrameBuffer::new();
1146
1147        // Test negative coordinates
1148        fb.set_pixel(Point::new(-1, 5), Color::RED);
1149        fb.set_pixel(Point::new(5, -1), Color::RED);
1150
1151        // Test coordinates out of bounds (should not panic)
1152        fb.set_pixel(Point::new(TEST_COLS as i32, 5), Color::RED);
1153        fb.set_pixel(Point::new(5, TEST_ROWS as i32), Color::RED);
1154    }
1155
1156    #[test]
1157    fn test_dma_framebuffer_set_pixel_internal() {
1158        let mut fb = TestFrameBuffer::new();
1159
1160        let red_color = Color::RED;
1161        fb.set_pixel_internal(10, 5, red_color);
1162
1163        // With 3-bit depth, brightness steps are 32 (256/8)
1164        // Frames represent thresholds: 32, 64, 96, 128, 160, 192, 224
1165        // Red value 255 should activate all frames
1166        for frame in &fb.frames {
1167            // Check upper half pixel
1168            let mapped_col_10 = get_mapped_index(10);
1169            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1170            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), false);
1171            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), false);
1172        }
1173    }
1174
1175    #[test]
1176    fn test_dma_framebuffer_brightness_modulation() {
1177        let mut fb = TestFrameBuffer::new();
1178
1179        // Test with a medium brightness value
1180        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1181        let test_brightness = brightness_step * 3; // 96
1182        let color = Color::from(embedded_graphics::pixelcolor::Rgb888::new(
1183            test_brightness,
1184            0,
1185            0,
1186        ));
1187
1188        fb.set_pixel_internal(0, 0, color);
1189
1190        // Should activate frames 0, 1, 2 (thresholds 32, 64, 96)
1191        // but not frames 3, 4, 5, 6 (thresholds 128, 160, 192, 224)
1192        for (frame_idx, frame) in fb.frames.iter().enumerate() {
1193            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1194            let should_be_active = test_brightness >= frame_threshold;
1195
1196            let mapped_col_0 = get_mapped_index(0);
1197            assert_eq!(frame.rows[0].data[mapped_col_0].red1(), should_be_active);
1198        }
1199    }
1200
1201    #[test]
1202    fn test_origin_dimensions() {
1203        let fb = TestFrameBuffer::new();
1204        let size = fb.size();
1205        assert_eq!(size.width, TEST_COLS as u32);
1206        assert_eq!(size.height, TEST_ROWS as u32);
1207
1208        // Test reference
1209        let size = (&fb).size();
1210        assert_eq!(size.width, TEST_COLS as u32);
1211        assert_eq!(size.height, TEST_ROWS as u32);
1212
1213        // Test mutable reference
1214        let mut fb = TestFrameBuffer::new();
1215        let fb_ref = &mut fb;
1216        let size = fb_ref.size();
1217        assert_eq!(size.width, TEST_COLS as u32);
1218        assert_eq!(size.height, TEST_ROWS as u32);
1219    }
1220
1221    #[test]
1222    fn test_draw_target() {
1223        let mut fb = TestFrameBuffer::new();
1224
1225        let pixels = vec![
1226            embedded_graphics::Pixel(Point::new(0, 0), Color::RED),
1227            embedded_graphics::Pixel(Point::new(1, 1), Color::GREEN),
1228            embedded_graphics::Pixel(Point::new(2, 2), Color::BLUE),
1229        ];
1230
1231        let result = fb.draw_iter(pixels);
1232        assert!(result.is_ok());
1233
1234        // Test mutable reference
1235        let result = (&mut fb).draw_iter(vec![embedded_graphics::Pixel(
1236            Point::new(3, 3),
1237            Color::WHITE,
1238        )]);
1239        assert!(result.is_ok());
1240    }
1241
1242    #[test]
1243    fn test_draw_iter_pixel_verification() {
1244        let mut fb = TestFrameBuffer::new();
1245
1246        // Create test pixels with specific colors and positions
1247        let pixels = vec![
1248            // Upper half pixels (y < NROWS) - should set color0
1249            embedded_graphics::Pixel(Point::new(5, 2), Color::RED), // (5, 2) -> red
1250            embedded_graphics::Pixel(Point::new(10, 5), Color::GREEN), // (10, 5) -> green
1251            embedded_graphics::Pixel(Point::new(15, 8), Color::BLUE), // (15, 8) -> blue
1252            embedded_graphics::Pixel(Point::new(20, 10), Color::WHITE), // (20, 10) -> white
1253            // Lower half pixels (y >= NROWS) - should set color1
1254            embedded_graphics::Pixel(Point::new(25, (TEST_NROWS + 3) as i32), Color::RED), // (25, 19) -> red
1255            embedded_graphics::Pixel(Point::new(30, (TEST_NROWS + 7) as i32), Color::GREEN), // (30, 23) -> green
1256            embedded_graphics::Pixel(Point::new(35, (TEST_NROWS + 12) as i32), Color::BLUE), // (35, 28) -> blue
1257            // Edge case: black pixel (should not be visible in first frame)
1258            embedded_graphics::Pixel(Point::new(40, 1), Color::BLACK), // (40, 1) -> black
1259        ];
1260
1261        let result = fb.draw_iter(pixels);
1262        assert!(result.is_ok());
1263
1264        // Check the first frame only
1265        let first_frame = &fb.frames[0];
1266        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1267        let first_frame_threshold = brightness_step; // 32
1268
1269        // Test upper half pixels (color0)
1270        // Red pixel at (5, 2) - should be red in first frame
1271        let col_idx = get_mapped_index(5);
1272        assert_eq!(
1273            first_frame.rows[2].data[col_idx].red1(),
1274            Color::RED.r() >= first_frame_threshold
1275        );
1276        assert_eq!(
1277            first_frame.rows[2].data[col_idx].grn1(),
1278            Color::RED.g() >= first_frame_threshold
1279        );
1280        assert_eq!(
1281            first_frame.rows[2].data[col_idx].blu1(),
1282            Color::RED.b() >= first_frame_threshold
1283        );
1284
1285        // Green pixel at (10, 5) - should be green in first frame
1286        let col_idx = get_mapped_index(10);
1287        assert_eq!(
1288            first_frame.rows[5].data[col_idx].red1(),
1289            Color::GREEN.r() >= first_frame_threshold
1290        );
1291        assert_eq!(
1292            first_frame.rows[5].data[col_idx].grn1(),
1293            Color::GREEN.g() >= first_frame_threshold
1294        );
1295        assert_eq!(
1296            first_frame.rows[5].data[col_idx].blu1(),
1297            Color::GREEN.b() >= first_frame_threshold
1298        );
1299
1300        // Blue pixel at (15, 8) - should be blue in first frame
1301        let col_idx = get_mapped_index(15);
1302        assert_eq!(
1303            first_frame.rows[8].data[col_idx].red1(),
1304            Color::BLUE.r() >= first_frame_threshold
1305        );
1306        assert_eq!(
1307            first_frame.rows[8].data[col_idx].grn1(),
1308            Color::BLUE.g() >= first_frame_threshold
1309        );
1310        assert_eq!(
1311            first_frame.rows[8].data[col_idx].blu1(),
1312            Color::BLUE.b() >= first_frame_threshold
1313        );
1314
1315        // White pixel at (20, 10) - should be white in first frame
1316        let col_idx = get_mapped_index(20);
1317        assert_eq!(
1318            first_frame.rows[10].data[col_idx].red1(),
1319            Color::WHITE.r() >= first_frame_threshold
1320        );
1321        assert_eq!(
1322            first_frame.rows[10].data[col_idx].grn1(),
1323            Color::WHITE.g() >= first_frame_threshold
1324        );
1325        assert_eq!(
1326            first_frame.rows[10].data[col_idx].blu1(),
1327            Color::WHITE.b() >= first_frame_threshold
1328        );
1329
1330        // Test lower half pixels (color1)
1331        // Red pixel at (25, TEST_NROWS + 3) -> row 3, color1
1332        let col_idx = get_mapped_index(25);
1333        assert_eq!(
1334            first_frame.rows[3].data[col_idx].red2(),
1335            Color::RED.r() >= first_frame_threshold
1336        );
1337        assert_eq!(
1338            first_frame.rows[3].data[col_idx].grn2(),
1339            Color::RED.g() >= first_frame_threshold
1340        );
1341        assert_eq!(
1342            first_frame.rows[3].data[col_idx].blu2(),
1343            Color::RED.b() >= first_frame_threshold
1344        );
1345
1346        // Green pixel at (30, TEST_NROWS + 7) -> row 7, color1
1347        let col_idx = get_mapped_index(30);
1348        assert_eq!(
1349            first_frame.rows[7].data[col_idx].red2(),
1350            Color::GREEN.r() >= first_frame_threshold
1351        );
1352        assert_eq!(
1353            first_frame.rows[7].data[col_idx].grn2(),
1354            Color::GREEN.g() >= first_frame_threshold
1355        );
1356        assert_eq!(
1357            first_frame.rows[7].data[col_idx].blu2(),
1358            Color::GREEN.b() >= first_frame_threshold
1359        );
1360
1361        // Blue pixel at (35, TEST_NROWS + 12) -> row 12, color1
1362        let col_idx = get_mapped_index(35);
1363        assert_eq!(
1364            first_frame.rows[12].data[col_idx].red2(),
1365            Color::BLUE.r() >= first_frame_threshold
1366        );
1367        assert_eq!(
1368            first_frame.rows[12].data[col_idx].grn2(),
1369            Color::BLUE.g() >= first_frame_threshold
1370        );
1371        assert_eq!(
1372            first_frame.rows[12].data[col_idx].blu2(),
1373            Color::BLUE.b() >= first_frame_threshold
1374        );
1375
1376        // Test black pixel - should not be visible in any frame
1377        let col_idx = get_mapped_index(40);
1378        assert_eq!(first_frame.rows[1].data[col_idx].red1(), false);
1379        assert_eq!(first_frame.rows[1].data[col_idx].grn1(), false);
1380        assert_eq!(first_frame.rows[1].data[col_idx].blu1(), false);
1381    }
1382
1383    #[test]
1384    fn test_embedded_graphics_integration() {
1385        let mut fb = TestFrameBuffer::new();
1386
1387        // Draw a rectangle
1388        let result = Rectangle::new(Point::new(5, 5), Size::new(10, 8))
1389            .into_styled(PrimitiveStyle::with_fill(Color::RED))
1390            .draw(&mut fb);
1391        assert!(result.is_ok());
1392
1393        // Draw a circle
1394        let result = Circle::new(Point::new(30, 15), 8)
1395            .into_styled(PrimitiveStyle::with_fill(Color::BLUE))
1396            .draw(&mut fb);
1397        assert!(result.is_ok());
1398    }
1399
1400    #[test]
1401    #[cfg(feature = "skip-black-pixels")]
1402    fn test_skip_black_pixels_enabled() {
1403        let mut fb = TestFrameBuffer::new();
1404
1405        // Set a red pixel first
1406        fb.set_pixel_internal(10, 5, Color::RED);
1407
1408        // Verify it's red in the first frame
1409        let mapped_col_10 = get_mapped_index(10);
1410        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1411        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1412        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1413
1414        // Now set it to black - with skip-black-pixels enabled, this should be ignored
1415        fb.set_pixel_internal(10, 5, Color::BLACK);
1416
1417        // The pixel should still be red (black write was skipped)
1418        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1419        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1420        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1421    }
1422
1423    #[test]
1424    #[cfg(not(feature = "skip-black-pixels"))]
1425    fn test_skip_black_pixels_disabled() {
1426        let mut fb = TestFrameBuffer::new();
1427
1428        // Set a red pixel first
1429        fb.set_pixel_internal(10, 5, Color::RED);
1430
1431        // Verify it's red in the first frame
1432        let mapped_col_10 = get_mapped_index(10);
1433        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1434        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1435        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1436
1437        // Now set it to black - with skip-black-pixels disabled, this should overwrite
1438        fb.set_pixel_internal(10, 5, Color::BLACK);
1439
1440        // The pixel should now be black (all bits false)
1441        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), false);
1442        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1443        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1444    }
1445
1446    #[test]
1447    fn test_bcm_frame_overwrite() {
1448        let mut fb = TestFrameBuffer::new();
1449
1450        // First write a white pixel (255, 255, 255)
1451        fb.set_pixel_internal(10, 5, Color::WHITE);
1452
1453        let mapped_col_10 = get_mapped_index(10);
1454
1455        // Verify white pixel is lit in all frames (255 >= all thresholds)
1456        for frame in fb.frames.iter() {
1457            // White (255) should be active in all frames since it's >= all thresholds
1458            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), true);
1459            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), true);
1460            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), true);
1461        }
1462
1463        // Now overwrite with 50% white (128, 128, 128)
1464        let half_white = Color::from(embedded_graphics::pixelcolor::Rgb888::new(128, 128, 128));
1465        fb.set_pixel_internal(10, 5, half_white);
1466
1467        // Verify only the correct frames are lit for 50% white
1468        // With 3-bit depth: thresholds are 32, 64, 96, 128, 160, 192, 224
1469        // 128 should activate frames 0, 1, 2, 3 (thresholds 32, 64, 96, 128)
1470        // but not frames 4, 5, 6 (thresholds 160, 192, 224)
1471        let brightness_step = 1 << (8 - TEST_BITS); // 32 for 3-bit
1472        for (frame_idx, frame) in fb.frames.iter().enumerate() {
1473            let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
1474            let should_be_active = 128 >= frame_threshold;
1475
1476            assert_eq!(frame.rows[5].data[mapped_col_10].red1(), should_be_active);
1477            assert_eq!(frame.rows[5].data[mapped_col_10].grn1(), should_be_active);
1478            assert_eq!(frame.rows[5].data[mapped_col_10].blu1(), should_be_active);
1479        }
1480
1481        // Specifically verify the expected pattern for 3-bit depth
1482        // Frames 0-3 should be active (thresholds 32, 64, 96, 128)
1483        for frame_idx in 0..4 {
1484            assert_eq!(
1485                fb.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1486                true
1487            );
1488        }
1489        // Frames 4-6 should be inactive (thresholds 160, 192, 224)
1490        for frame_idx in 4..TEST_FRAME_COUNT {
1491            assert_eq!(
1492                fb.frames[frame_idx].rows[5].data[mapped_col_10].red1(),
1493                false
1494            );
1495        }
1496    }
1497
1498    #[test]
1499    fn test_read_buffer_implementation() {
1500        // Test owned implementation - explicitly move the framebuffer to ensure we're testing the owned impl
1501        let fb = TestFrameBuffer::new();
1502        let expected_size = core::mem::size_of_val(&fb.frames);
1503
1504        // Test owned ReadBuffer implementation by calling ReadBuffer::read_buffer explicitly
1505        unsafe {
1506            let (ptr, len) = <TestFrameBuffer as ReadBuffer>::read_buffer(&fb);
1507            assert!(!ptr.is_null());
1508            assert_eq!(len, expected_size);
1509        }
1510
1511        // Test direct method call on owned value
1512        unsafe {
1513            let (ptr, len) = fb.read_buffer();
1514            assert!(!ptr.is_null());
1515            assert_eq!(len, expected_size);
1516        }
1517
1518        // Test reference implementation
1519        let fb = TestFrameBuffer::new();
1520        let fb_ref = &fb;
1521        unsafe {
1522            let (ptr, len) = fb_ref.read_buffer();
1523            assert!(!ptr.is_null());
1524            assert_eq!(len, core::mem::size_of_val(&fb.frames));
1525        }
1526
1527        // Test mutable reference implementation
1528        let mut fb = TestFrameBuffer::new();
1529        let fb_ref = &mut fb;
1530        unsafe {
1531            let (ptr, len) = fb_ref.read_buffer();
1532            assert!(!ptr.is_null());
1533            assert_eq!(len, core::mem::size_of_val(&fb.frames));
1534        }
1535    }
1536
1537    #[test]
1538    fn test_read_buffer_owned_implementation() {
1539        // This test specifically ensures the owned ReadBuffer implementation is tested
1540        // by consuming the framebuffer and testing the pointer validity
1541        fn test_owned_read_buffer(fb: TestFrameBuffer) -> (bool, usize) {
1542            unsafe {
1543                let (ptr, len) = fb.read_buffer();
1544                (!ptr.is_null(), len)
1545            }
1546        }
1547
1548        let fb = TestFrameBuffer::new();
1549        let expected_len = core::mem::size_of_val(&fb.frames);
1550
1551        let (ptr_valid, actual_len) = test_owned_read_buffer(fb);
1552        assert!(ptr_valid);
1553        assert_eq!(actual_len, expected_len);
1554    }
1555
1556    #[test]
1557    fn test_framebuffer_trait() {
1558        let fb = TestFrameBuffer::new();
1559        assert_eq!(fb.get_word_size(), WordSize::Sixteen);
1560
1561        let fb_ref = &fb;
1562        assert_eq!(fb_ref.get_word_size(), WordSize::Sixteen);
1563
1564        let mut fb = TestFrameBuffer::new();
1565        let fb_ref = &mut fb;
1566        assert_eq!(fb_ref.get_word_size(), WordSize::Sixteen);
1567    }
1568
1569    #[test]
1570    fn test_debug_formatting() {
1571        let fb = TestFrameBuffer::new();
1572        let debug_string = format!("{:?}", fb);
1573        assert!(debug_string.contains("DmaFrameBuffer"));
1574        assert!(debug_string.contains("frame_count"));
1575        assert!(debug_string.contains("frame_size"));
1576        assert!(debug_string.contains("brightness_step"));
1577    }
1578
1579    #[test]
1580    fn test_default_implementation() {
1581        let fb1 = TestFrameBuffer::new();
1582        let fb2 = TestFrameBuffer::default();
1583
1584        // Both should be equivalent in size, but may differ in content since new() calls format()
1585        assert_eq!(fb1.frames.len(), fb2.frames.len());
1586        assert_eq!(fb1._align, fb2._align);
1587    }
1588
1589    #[test]
1590    fn test_memory_alignment() {
1591        let fb = TestFrameBuffer::new();
1592        let ptr = &fb as *const _ as usize;
1593
1594        // Should be aligned to 64-bit boundary due to the _align field
1595        assert_eq!(ptr % 8, 0);
1596    }
1597
1598    #[test]
1599    fn test_color_values() {
1600        let mut fb = TestFrameBuffer::new();
1601
1602        // Test different color values
1603        let colors = [
1604            (Color::RED, (255, 0, 0)),
1605            (Color::GREEN, (0, 255, 0)),
1606            (Color::BLUE, (0, 0, 255)),
1607            (Color::WHITE, (255, 255, 255)),
1608            (Color::BLACK, (0, 0, 0)),
1609        ];
1610
1611        for (i, (color, (r, g, b))) in colors.iter().enumerate() {
1612            fb.set_pixel(Point::new(i as i32, 0), *color);
1613            assert_eq!(color.r(), *r);
1614            assert_eq!(color.g(), *g);
1615            assert_eq!(color.b(), *b);
1616        }
1617    }
1618
1619    #[test]
1620    fn test_blanking_delay() {
1621        let mut row: Row<TEST_COLS> = Row::new();
1622        let test_addr = 5;
1623        let prev_addr = 4;
1624
1625        row.format(test_addr, prev_addr);
1626
1627        // Test that the blanking delay is respected
1628        let blanking_pixel_idx = get_mapped_index(TEST_COLS - BLANKING_DELAY - 1);
1629        assert_eq!(row.data[blanking_pixel_idx].output_enable(), false);
1630
1631        // Test that pixels before blanking delay have output enabled (if after pixel 1)
1632        let before_blanking_idx = get_mapped_index(TEST_COLS - BLANKING_DELAY - 2);
1633        assert_eq!(row.data[before_blanking_idx].output_enable(), true);
1634    }
1635
1636    #[test]
1637    fn test_esp32_mapping() {
1638        // Test the ESP32-specific index mapping
1639        #[cfg(feature = "esp32-ordering")]
1640        {
1641            assert_eq!(map_index(0), 1);
1642            assert_eq!(map_index(1), 0);
1643            assert_eq!(map_index(2), 3);
1644            assert_eq!(map_index(3), 2);
1645            assert_eq!(map_index(4), 5);
1646            assert_eq!(map_index(5), 4);
1647        }
1648        #[cfg(not(feature = "esp32-ordering"))]
1649        {
1650            assert_eq!(map_index(0), 0);
1651            assert_eq!(map_index(1), 1);
1652            assert_eq!(map_index(2), 2);
1653            assert_eq!(map_index(3), 3);
1654        }
1655    }
1656
1657    #[test]
1658    fn test_bits_assertion() {
1659        // Test that BITS <= 8 assertion is enforced at compile time
1660        // This test mainly documents the constraint
1661        assert!(TEST_BITS <= 8);
1662    }
1663
1664    #[test]
1665    fn test_fast_clear_method() {
1666        let mut fb = TestFrameBuffer::new();
1667
1668        // Set some pixels
1669        fb.set_pixel_internal(10, 5, Color::RED);
1670        fb.set_pixel_internal(20, 10, Color::GREEN);
1671
1672        // Verify pixels are set
1673        let mapped_col_10 = get_mapped_index(10);
1674        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), true);
1675
1676        // Clear using fast method
1677        fb.erase();
1678
1679        // Verify pixels are cleared but timing signals remain
1680        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].red1(), false);
1681        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].grn1(), false);
1682        assert_eq!(fb.frames[0].rows[5].data[mapped_col_10].blu1(), false);
1683
1684        // Verify timing signals are still present (check last pixel has latch)
1685        let last_col = get_mapped_index(TEST_COLS - 1);
1686        assert_eq!(fb.frames[0].rows[5].data[last_col].latch(), true);
1687    }
1688
1689    // Remove the old `test_draw_char_bottom_right` and replace with a helper + combined test.
1690
1691    // Constants for FONT_6X10 glyph size
1692    const CHAR_W: i32 = 6;
1693    const CHAR_H: i32 = 10;
1694
1695    /// Draws glyph 'A' at `origin` and validates framebuffer pixels against a reference.
1696    fn verify_glyph_at(fb: &mut TestFrameBuffer, origin: Point) {
1697        use embedded_graphics::mock_display::MockDisplay;
1698        use embedded_graphics::mono_font::ascii::FONT_6X10;
1699        use embedded_graphics::mono_font::MonoTextStyle;
1700        use embedded_graphics::text::{Baseline, Text};
1701
1702        // Draw the glyph
1703        let style = MonoTextStyle::new(&FONT_6X10, Color::WHITE);
1704        Text::with_baseline("A", origin, style, Baseline::Top)
1705            .draw(fb)
1706            .unwrap();
1707
1708        // Reference glyph at (0,0)
1709        let mut reference: MockDisplay<Color> = MockDisplay::new();
1710        Text::with_baseline("A", Point::zero(), style, Baseline::Top)
1711            .draw(&mut reference)
1712            .unwrap();
1713
1714        for dy in 0..CHAR_H {
1715            for dx in 0..CHAR_W {
1716                let expected_on = reference
1717                    .get_pixel(Point::new(dx, dy))
1718                    .unwrap_or(Color::BLACK)
1719                    != Color::BLACK;
1720
1721                let gx = (origin.x + dx) as usize;
1722                let gy = (origin.y + dy) as usize;
1723
1724                // we have computed the origin to be within the panel, so we don't need to check for bounds
1725                // if gx >= TEST_COLS || gy >= TEST_ROWS {
1726                //     continue;
1727                // }
1728
1729                // Fetch Entry from frame 0
1730                let frame0 = &fb.frames[0];
1731                let e = if gy < TEST_NROWS {
1732                    &frame0.rows[gy].data[get_mapped_index(gx)]
1733                } else {
1734                    &frame0.rows[gy - TEST_NROWS].data[get_mapped_index(gx)]
1735                };
1736
1737                let (r, g, b) = if gy >= TEST_NROWS {
1738                    (e.red2(), e.grn2(), e.blu2())
1739                } else {
1740                    (e.red1(), e.grn1(), e.blu1())
1741                };
1742
1743                if expected_on {
1744                    assert!(r && g && b,);
1745                } else {
1746                    assert!(!r && !g && !b);
1747                }
1748            }
1749        }
1750    }
1751
1752    #[test]
1753    fn test_draw_char_corners() {
1754        let upper_left = Point::new(0, 0);
1755        let lower_right = Point::new(TEST_COLS as i32 - CHAR_W, TEST_ROWS as i32 - CHAR_H);
1756
1757        let mut fb = TestFrameBuffer::new();
1758
1759        verify_glyph_at(&mut fb, upper_left);
1760        verify_glyph_at(&mut fb, lower_right);
1761    }
1762}