Skip to main content

hub75_framebuffer/
lib.rs

1//! Framebuffer implementation for HUB75 LED matrix displays.
2//!
3//! ## How HUB75 LED Displays Work
4//!
5//! HUB75 RGB LED matrix panels are scanned, time-multiplexed displays that behave like a long
6//! daisy-chained shift register rather than a random-access framebuffer.
7//!
8//! ### Signal names
9//! - **R1 G1 B1 / R2 G2 B2** – Serial colour data for the upper and lower halves of the active scan line
10//! - **CLK** – Shift-register clock; every rising edge pushes the six colour bits one pixel to the right
11//! - **LAT / STB** – Latch; copies the shift-register contents to the LED drivers for the row currently selected by the address lines
12//! - **OE** – Output-Enable (active LOW): LEDs are lit while OE is LOW and blanked when it is HIGH
13//! - **A B C D (E)** – Row-address select lines (choose which pair of rows is lit)
14//! - **VCC & GND** – 5 V power for panel logic and LED drivers
15//!
16//! ### Row-pair scanning workflow (e.g., 1/16-scan panel)
17//! 1. While the panel is still displaying row pair N − 1, the controller shifts the six-bit colour data for row pair N into the chain (OE remains LOW so row N − 1 stays visible).
18//! 2. After the last pixel is clocked in, the controller raises OE HIGH to blank the LEDs.
19//! 3. With the panel blanked, it first changes the address lines to select row pair N, lets them settle for a few nanoseconds, and **then** pulses LAT to latch the freshly shifted data into the output drivers for that newly selected row.
20//! 4. OE is immediately driven LOW again, lighting row pair N.
21//! 5. Steps 1–4 repeat for every row pair fast enough (hundreds of Hz) that the human eye sees a steady image.
22//!    - If the first row pair is being shifted, the panel continues showing the last row pair of the previous frame until the first blank-address-latch sequence occurs.
23//!
24//! ### Brightness and colour depth (Binary Code Modulation)
25//! - Full colour is typically achieved using **Binary Code Modulation (BCM)**, also known as *Bit-Angle Modulation (BAM)*. Each bit-plane is displayed for a period proportional to its binary weight (1, 2, 4, 8 …), yielding 2ⁿ intensity levels per channel. See [Batsocks – LED dimming using Binary Code Modulation](https://www.batsocks.co.uk/readme/art_bcm_1.htm) for a deeper explanation.
26//! - Because each LED is on for only a fraction of the total frame time, the driver can use relatively high peak currents without overheating while average brightness is preserved.
27//!
28//! ### Implications for software / hardware drivers
29//! - You don't simply "write a pixel" once; you must continuously stream the complete refresh data at MHz-range clock rates.
30//! - Precise timing of CLK, OE, address lines, and LAT is critical—especially the order: blank (OE HIGH) → set address → latch → un-blank (OE LOW).
31//! - Microcontrollers typically employ DMA, PIO, or parallel GPIO tricks, and FPGAs use dedicated logic, to sustain the data throughput while leaving processing resources free.
32//!
33//! In short: a HUB75 panel is a high-speed shift-register chain that relies on rapid row-pair scanning and **Binary Code Modulation (BCM)** to create a bright, full-colour image. Keeping OE LOW almost all the time—blanking only long enough to change the address and pulse LAT—maximises brightness without visible artefacts.
34//!
35//! ## Framebuffer Implementations
36//!
37//! Four framebuffer layouts are provided, covering two hardware variants and
38//! two BCM strategies:
39//!
40//! | Module | Word size | External latch? | BCM strategy |
41//! |--------|-----------|-----------------|--------------|
42//! | [`plain`] | 16-bit | No | Threshold frames |
43//! | [`latched`] | 8-bit | Yes | Threshold frames |
44//! | [`bitplane::plain`] | 16-bit | No | True bitplane |
45//! | [`bitplane::latched`] | 8-bit | Yes | True bitplane |
46//!
47//! ### Plain vs. Latched
48//! - **Plain** packs all HUB75 signals (address, latch, OE, colour) into each
49//!   16-bit word. No extra hardware beyond a parallel output peripheral.
50//! - **Latched** uses 8-bit words with a separate external latch circuit to
51//!   hold the row address and gate the pixel clock, halving per-entry memory.
52//!
53//! ### Threshold Frames vs. True Bitplane
54//! The two BCM strategies differ in how they store colour data and how the DMA
55//! chain must be configured to render it.
56//!
57//! **Threshold frames** (`plain`, `latched`) -- the driver compares each
58//! channel's 8-bit value against per-frame thresholds and stores the resulting
59//! on/off bits. For a colour depth of `BITS`, this produces
60//! `2^BITS - 1` frames. Frame *n* is displayed for a duration proportional to
61//! `2^n`. Memory grows exponentially with colour depth.
62//!
63//! **True bitplane** (`bitplane::plain`, `bitplane::latched`) -- each of
64//! `PLANES` planes (typically 8) stores one bit of every colour channel
65//! directly. To render, configure the DMA descriptor chain so that each
66//! plane's data is output `2^(7 - plane_index)` times (plane 0 = MSB is
67//! scanned 128 times, plane 7 = LSB is scanned once). Memory scales linearly
68//! with the number of planes.
69//!
70//! All four variants have configurable row and column dimensions, support
71//! `embedded-graphics` via the `DrawTarget` trait, and expose per-plane
72//! pointers for DMA setup through the [`FrameBuffer`] trait.
73//!
74//! ## Multiple Panels / Scan-Pattern Remapping
75//! Use [`tiling::RemappedFrameBuffer`] to drive several HUB75 panels as one
76//! large virtual display, or to remap pixels for non-standard scan patterns
77//! (e.g. 1/16-scan on 64×64 panels). It works with all four framebuffer types
78//! and only requires two generic parameters (`F` and `M`).
79//!
80//! Combine it with a [`tiling::PixelRemapper`] implementation such as
81//! [`tiling::ChainTopRightDown`] (tiling) or [`tiling::QuarterScan`] (1/16-scan).
82//! The wrapper exposes a single `embedded-graphics` canvas, so for example a
83//! 3 × 3 stack of 64 × 32 panels simply looks like a 192 × 96 screen while
84//! all coordinate translation happens transparently.
85//!
86//! The older [`tiling::TiledFrameBuffer`] is still available but deprecated.
87//!
88//! ## Available Feature Flags
89//!
90//! ### `skip-black-pixels` Feature (disabled by default)
91//! When enabled, calls to `set_pixel()` with `Color::BLACK` return early without
92//! writing to the framebuffer. This provides a significant performance boost for
93//! UI applications that frequently draw black pixels (backgrounds, clearing, etc.)
94//! by assuming the framebuffer was already cleared.
95//!
96//! **Important**: This optimization assumes that black pixels represent "no change"
97//! rather than "explicitly set to black". By default, black pixels are written
98//! normally to ensure correct overwrite behavior. To enable the optimization:
99//!
100//! ```toml
101//! [dependencies]
102//! hub75-framebuffer = { version = "0.11.0", features = ["skip-black-pixels"] }
103//! ```
104//!
105//! ### `esp32-ordering` Feature (required for original ESP32 only)
106//! **Required** when targeting the original ESP32 chip (not ESP32-S3 or other variants).
107//! This feature adjusts byte ordering to accommodate the quirky requirements of the
108//! ESP32's I²S peripheral in 8-bit and 16-bit modes. The original ESP32 has different
109//! byte ordering requirements compared to other ESP32 variants (S2, S3, C3, etc.),
110//! which do **not** need this feature.
111//!
112//! ```toml
113//! [dependencies]
114//! hub75-framebuffer = { version = "0.11.0", features = ["esp32-ordering"] }
115//! ```
116//!
117//! ### `tail-closes-latch` Feature (plain framebuffers only)
118//! Appends a single extra "tail" word at the end of the DMA buffer that drives the
119//! LATCH signal LOW (de-asserted) on the final clock edge. Without this feature the
120//! last word in each row asserts LATCH HIGH to latch shifted data into the LED
121//! drivers, and the GPIO pins remain in that state after the DMA transfer completes.
122//! Some hardware configurations (e.g. free-running DMA loops or peripherals that
123//! continue clocking after the descriptor chain ends) can re-latch stale data or
124//! glitch if LATCH is left asserted.
125//!
126//! Enabling `tail-closes-latch` adds one 16-bit `Entry` (for `plain`) or one entry
127//! per bit-plane (for `bitplane::plain`) that parks the bus with LATCH=0 and
128//! OE=BLANK, cleanly terminating the transfer. The cost is a single extra word per
129//! DMA chunk, which is negligible compared to the frame data.
130//!
131//! ```toml
132//! [dependencies]
133//! hub75-framebuffer = { version = "0.11.0", features = ["tail-closes-latch"] }
134//! ```
135//!
136//! ### Blanking delay features (`lead-blank-*` / `trail-blank-*`)
137//!
138//! Control the number of pixel-clock cycles of blanking (`OE` HIGH) inserted
139//! around row-address changes. The lead blank controls how many cycles the
140//! output is blanked *before* the row address is changed, and the trail blank
141//! controls blanking *after* the row address is changed. Together they give
142//! the address lines time to settle and prevent ghosting or "bleeding"
143//! artifacts caused by the panel briefly displaying data on the wrong row
144//! during the transition.
145//!
146//! | Feature          | Blanking cycles | Position              |
147//! |------------------|-----------------|-----------------------|
148//! | *(none)*         | 1 (default)     | before & after change |
149//! | `lead-blank-1`   | 1               | before address change |
150//! | `lead-blank-2`   | 2               | before address change |
151//! | `lead-blank-4`   | 4               | before address change |
152//! | `lead-blank-8`   | 8               | before address change |
153//! | `lead-blank-16`  | 16              | before address change |
154//! | `trail-blank-1`  | 1               | after address change  |
155//! | `trail-blank-2`  | 2               | after address change  |
156//! | `trail-blank-4`  | 4               | after address change  |
157//! | `trail-blank-8`  | 8               | after address change  |
158//! | `trail-blank-16` | 16              | after address change  |
159//!
160//! Higher values reduce ghosting at the cost of slightly less brightness (the
161//! LEDs are on for less time per scan line). Start with the default and increase
162//! only if you observe row-transition artifacts on your particular panel
163//! hardware.
164//!
165//! ```toml
166//! [dependencies]
167//! hub75-framebuffer = { version = "0.11.0", features = ["lead-blank-4", "trail-blank-2"] }
168//! ```
169//!
170//! **Note:** At most one `lead-blank-*` and one `trail-blank-*` feature may be
171//! enabled at a time. If multiple are enabled for the same edge, compile-time cfg
172//! conflicts will result.
173//!
174//! ### Inter-row blanking features (`inter-row-blank-*`)
175//!
176//! Insert additional dead clock cycles at the end of each row. In plain
177//! framebuffers the gap entries hold the previous row address with `OE` HIGH
178//! (blank), deferring the address change to the first pixel of the next row
179//! and giving slow panels more time to finish blanking before the address
180//! lines move. In latched framebuffers the latch and address change are
181//! inseparable in hardware, so the gap simply adds extra blanked cycles after
182//! the address change. The gap entries are invisible to all drawing
183//! primitives — they only appear in the DMA stream.
184//!
185//! | Feature              | Gap cycles | RAM cost per row       |
186//! |----------------------|------------|------------------------|
187//! | *(none)*             | 0          | 0 bytes                |
188//! | `inter-row-blank-4`  | 4          | 8 bytes (16-bit) / 4 bytes (8-bit) |
189//! | `inter-row-blank-8`  | 8          | 16 bytes / 8 bytes     |
190//! | `inter-row-blank-16` | 16         | 32 bytes / 16 bytes    |
191//! | `inter-row-blank-32` | 32         | 64 bytes / 32 bytes    |
192//!
193//! ```toml
194//! [dependencies]
195//! hub75-framebuffer = { version = "0.11.0", features = ["inter-row-blank-8"] }
196//! ```
197//!
198//! **Note:** At most one `inter-row-blank-*` feature may be enabled at a time.
199//! These are independent of the `lead-blank-*` / `trail-blank-*` features and
200//! can be combined with them.
201//!
202//! ### `defmt` Feature
203//! Implements `defmt::Format` for framebuffer types so they can be emitted with
204//! the `defmt` logging framework. No functional changes; purely adds a trait impl.
205//!
206//! ### `doc-images` Feature
207//! Embeds documentation images when building docs on docs.rs. Not needed for
208//! normal usage.
209#![no_std]
210#![warn(missing_docs)]
211#![warn(clippy::all)]
212#![warn(clippy::pedantic)]
213#![allow(clippy::cast_possible_truncation)]
214#![allow(clippy::cast_sign_loss)]
215
216use embedded_graphics::draw_target::DrawTarget;
217use embedded_graphics::pixelcolor::Rgb888;
218use embedded_graphics::prelude::Point;
219
220pub mod bitplane;
221pub mod latched;
222pub mod plain;
223pub mod tiling;
224
225/// Color type used in the framebuffer
226pub type Color = Rgb888;
227
228/// Word size configuration for the framebuffer
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum WordSize {
231    /// 8-bit word size
232    Eight,
233    /// 16-bit word size
234    Sixteen,
235}
236
237/// Computes the NROWS value from ROWS for `DmaFrameBuffer`
238///
239/// # Arguments
240///
241/// * `rows` - Total number of rows in the display
242///
243/// # Returns
244///
245/// Number of rows needed internally for `DmaFrameBuffer`
246#[must_use]
247pub const fn compute_rows(rows: usize) -> usize {
248    rows / 2
249}
250
251/// Computes the number of frames needed for a given bit depth
252///
253/// This is used to determine how many frames are needed to achieve
254/// the desired color depth through Binary Code Modulation (BCM).
255///
256/// # Arguments
257///
258/// * `bits` - Number of bits per color channel
259///
260/// # Returns
261///
262/// Number of frames required for the given bit depth
263#[must_use]
264pub const fn compute_frame_count(bits: u8) -> usize {
265    (1usize << bits) - 1
266}
267
268/// Trait for read-only framebuffers.
269pub trait FrameBuffer {
270    /// The word type used by this framebuffer.
271    type Word;
272
273    /// Returns the word size configuration for this framebuffer
274    fn get_word_size(&self) -> WordSize {
275        match size_of::<Self::Word>() {
276            1 => WordSize::Eight,
277            2 => WordSize::Sixteen,
278            _ => panic!("Unsupported word size"),
279        }
280    }
281
282    /// Returns the number of BCM bit-planes in this framebuffer.
283    ///
284    /// Contiguous (threshold-based) framebuffers return `1` — the entire
285    /// buffer is treated as a single plane.  True bit-plane framebuffers
286    /// return the number of planes (typically equal to the colour depth in
287    /// bits).
288    fn plane_count(&self) -> usize;
289
290    /// Returns a raw pointer and byte length for the given plane.
291    ///
292    /// For a single-plane framebuffer (`plane_count() == 1`), `plane_idx`
293    /// must be `0` and the returned span covers the whole DMA-ready buffer.
294    ///
295    /// # Panics
296    ///
297    /// May panic if `plane_idx >= plane_count()`.
298    fn plane_ptr_len(&self, plane_idx: usize) -> (*const u8, usize);
299}
300
301/// Trait for mutable framebuffers
302///
303/// This trait extends `FrameBuffer` with the ability to draw to the framebuffer
304/// using the `embedded_graphics` drawing primitives.
305pub trait MutableFrameBuffer:
306    FrameBuffer + DrawTarget<Color = Color, Error = core::convert::Infallible>
307{
308}
309
310/// Trait for all operations a user may want to call on a framebuffer.
311pub trait FrameBufferOperations: FrameBuffer {
312    /// Erase pixel colors while preserving control bits.
313    /// This is much faster than `format()` and is the typical way to clear the display.
314    fn erase(&mut self);
315
316    /// Set a pixel in the framebuffer.
317    fn set_pixel(&mut self, p: Point, color: Color);
318}
319
320#[cfg(test)]
321mod tests {
322    extern crate std;
323
324    use std::format;
325
326    use super::*;
327    use embedded_graphics::pixelcolor::RgbColor;
328
329    #[test]
330    fn test_compute_rows() {
331        // Test typical panel sizes
332        assert_eq!(compute_rows(32), 16);
333        assert_eq!(compute_rows(64), 32);
334        assert_eq!(compute_rows(16), 8);
335        assert_eq!(compute_rows(128), 64);
336
337        // Test edge cases
338        assert_eq!(compute_rows(2), 1);
339        assert_eq!(compute_rows(0), 0);
340
341        // Test that it always divides by 2
342        for rows in [8, 16, 24, 32, 48, 64, 96, 128, 256] {
343            assert_eq!(compute_rows(rows), rows / 2);
344        }
345    }
346
347    #[test]
348    fn test_compute_frame_count() {
349        // Test common bit depths
350        assert_eq!(compute_frame_count(1), 1); // 2^1 - 1 = 1
351        assert_eq!(compute_frame_count(2), 3); // 2^2 - 1 = 3
352        assert_eq!(compute_frame_count(3), 7); // 2^3 - 1 = 7
353        assert_eq!(compute_frame_count(4), 15); // 2^4 - 1 = 15
354        assert_eq!(compute_frame_count(5), 31); // 2^5 - 1 = 31
355        assert_eq!(compute_frame_count(6), 63); // 2^6 - 1 = 63
356        assert_eq!(compute_frame_count(7), 127); // 2^7 - 1 = 127
357        assert_eq!(compute_frame_count(8), 255); // 2^8 - 1 = 255
358
359        // Test the formula: (2^bits) - 1
360        for bits in 1..=8 {
361            let expected = (1usize << bits) - 1;
362            assert_eq!(compute_frame_count(bits), expected);
363        }
364    }
365
366    #[test]
367    fn test_compute_frame_count_properties() {
368        // Test that frame count grows exponentially
369        assert!(compute_frame_count(2) > compute_frame_count(1));
370        assert!(compute_frame_count(3) > compute_frame_count(2));
371        assert!(compute_frame_count(4) > compute_frame_count(3));
372
373        // Test doubling property: each additional bit approximately doubles frame count
374        for bits in 1..=7 {
375            let current_frames = compute_frame_count(bits);
376            let next_frames = compute_frame_count(bits + 1);
377            // next_frames should be approximately 2 * current_frames + 1
378            assert_eq!(next_frames, 2 * current_frames + 1);
379        }
380    }
381
382    #[test]
383    fn test_word_size_enum() {
384        // Test enum values
385        let eight = WordSize::Eight;
386        let sixteen = WordSize::Sixteen;
387
388        assert_ne!(eight, sixteen);
389        assert_eq!(eight, WordSize::Eight);
390        assert_eq!(sixteen, WordSize::Sixteen);
391    }
392
393    #[test]
394    fn test_word_size_debug() {
395        let eight = WordSize::Eight;
396        let sixteen = WordSize::Sixteen;
397
398        let eight_debug = format!("{:?}", eight);
399        let sixteen_debug = format!("{:?}", sixteen);
400
401        assert_eq!(eight_debug, "Eight");
402        assert_eq!(sixteen_debug, "Sixteen");
403    }
404
405    #[test]
406    fn test_word_size_clone_copy() {
407        let original = WordSize::Eight;
408        let cloned = original.clone();
409        let copied = original;
410
411        assert_eq!(original, cloned);
412        assert_eq!(original, copied);
413        assert_eq!(cloned, copied);
414    }
415
416    #[test]
417    fn test_color_type_alias() {
418        // Test that Color is an alias for Rgb888
419        let red_color: Color = Color::RED;
420        let red_rgb888: Rgb888 = Rgb888::RED;
421
422        assert_eq!(red_color, red_rgb888);
423        assert_eq!(red_color.r(), 255);
424        assert_eq!(red_color.g(), 0);
425        assert_eq!(red_color.b(), 0);
426
427        // Test various colors
428        let colors = [
429            (Color::RED, (255, 0, 0)),
430            (Color::GREEN, (0, 255, 0)),
431            (Color::BLUE, (0, 0, 255)),
432            (Color::WHITE, (255, 255, 255)),
433            (Color::BLACK, (0, 0, 0)),
434            (Color::CYAN, (0, 255, 255)),
435            (Color::MAGENTA, (255, 0, 255)),
436            (Color::YELLOW, (255, 255, 0)),
437        ];
438
439        for (color, (r, g, b)) in colors {
440            assert_eq!(color.r(), r);
441            assert_eq!(color.g(), g);
442            assert_eq!(color.b(), b);
443        }
444    }
445
446    #[test]
447    fn test_color_construction() {
448        // Test Color construction from RGB values
449        let custom_color = Color::new(128, 64, 192);
450        assert_eq!(custom_color.r(), 128);
451        assert_eq!(custom_color.g(), 64);
452        assert_eq!(custom_color.b(), 192);
453
454        // Test that it behaves like Rgb888
455        let rgb888_color = Rgb888::new(128, 64, 192);
456        assert_eq!(custom_color, rgb888_color);
457    }
458
459    #[test]
460    fn test_helper_functions_const() {
461        // Test that helper functions can be used in const contexts
462        const ROWS: usize = 32;
463        const COMPUTED_NROWS: usize = compute_rows(ROWS);
464        const BITS: u8 = 4;
465        const COMPUTED_FRAME_COUNT: usize = compute_frame_count(BITS);
466
467        assert_eq!(COMPUTED_NROWS, 16);
468        assert_eq!(COMPUTED_FRAME_COUNT, 15);
469    }
470
471    #[test]
472    fn test_realistic_panel_configurations() {
473        // Test common HUB75 panel configurations
474        struct PanelConfig {
475            rows: usize,
476            cols: usize,
477            bits: u8,
478        }
479
480        let configs = [
481            PanelConfig {
482                rows: 32,
483                cols: 64,
484                bits: 3,
485            }, // 32x64 panel, 3-bit color
486            PanelConfig {
487                rows: 64,
488                cols: 64,
489                bits: 4,
490            }, // 64x64 panel, 4-bit color
491            PanelConfig {
492                rows: 32,
493                cols: 32,
494                bits: 5,
495            }, // 32x32 panel, 5-bit color
496            PanelConfig {
497                rows: 16,
498                cols: 32,
499                bits: 6,
500            }, // 16x32 panel, 6-bit color
501        ];
502
503        for config in configs {
504            let nrows = compute_rows(config.rows);
505            let frame_count = compute_frame_count(config.bits);
506
507            // Basic sanity checks for rows
508            assert!(nrows > 0);
509            assert!(nrows <= config.rows);
510            assert_eq!(nrows * 2, config.rows);
511
512            // Basic sanity checks for columns
513            assert!(config.cols > 0);
514            assert!(config.cols <= 256); // Reasonable upper limit for HUB75 panels
515
516            // Frame count checks
517            assert!(frame_count > 0);
518            assert!(frame_count < 256); // Should be reasonable for typical bit depths
519
520            // Frame count should grow with bit depth
521            let prev_frame_count = compute_frame_count(config.bits - 1);
522            assert!(frame_count > prev_frame_count);
523        }
524    }
525
526    #[test]
527    fn test_memory_calculations() {
528        // Test that we can calculate memory requirements using helper functions
529        const ROWS: usize = 64;
530        const COLS: usize = 64;
531        const BITS: u8 = 4;
532
533        const NROWS: usize = compute_rows(ROWS);
534        const FRAME_COUNT: usize = compute_frame_count(BITS);
535
536        // These should be compile-time constants
537        assert_eq!(NROWS, 32);
538        assert_eq!(FRAME_COUNT, 15);
539
540        // Verify the relationship between parameters
541        assert_eq!(NROWS * 2, ROWS);
542        assert_eq!(FRAME_COUNT, (1 << BITS) - 1);
543
544        // Verify COLS is reasonable for memory calculations
545        assert!(COLS > 0);
546        assert!(COLS <= 256); // Reasonable limit for HUB75 panels
547    }
548
549    #[test]
550    fn test_edge_cases() {
551        // Test minimum values
552        assert_eq!(compute_rows(2), 1);
553        assert_eq!(compute_frame_count(1), 1);
554
555        // Test maximum reasonable values
556        assert_eq!(compute_rows(512), 256);
557        assert_eq!(compute_frame_count(8), 255);
558
559        // Test zero (though not practical)
560        assert_eq!(compute_rows(0), 0);
561    }
562
563    // Note: We can't easily test the traits directly since they're abstract,
564    // but they are thoroughly tested through their implementations in
565    // the plain and latched modules.
566
567    #[test]
568    fn test_word_size_equality() {
569        // Test all combinations of equality
570        assert_eq!(WordSize::Eight, WordSize::Eight);
571        assert_eq!(WordSize::Sixteen, WordSize::Sixteen);
572        assert_ne!(WordSize::Eight, WordSize::Sixteen);
573        assert_ne!(WordSize::Sixteen, WordSize::Eight);
574    }
575
576    #[test]
577    fn test_bit_depth_limits() {
578        // Test that our bit depth calculations work for the full range
579        for bits in 1..=8 {
580            let frame_count = compute_frame_count(bits);
581
582            // Frame count should be positive
583            assert!(frame_count > 0);
584
585            // Frame count should be less than 2^bits
586            assert!(frame_count < (1 << bits));
587
588            // Frame count should be exactly (2^bits) - 1
589            assert_eq!(frame_count, (1 << bits) - 1);
590        }
591    }
592
593    #[test]
594    fn test_documentation_examples() {
595        // Test the example values from the documentation
596        const ROWS: usize = 32;
597        const COLS: usize = 64;
598        const NROWS: usize = ROWS / 2;
599        const BITS: u8 = 8;
600        const FRAME_COUNT: usize = (1 << BITS) - 1;
601
602        // Verify using our helper functions
603        assert_eq!(compute_rows(ROWS), NROWS);
604        assert_eq!(compute_frame_count(BITS), FRAME_COUNT);
605
606        // Verify the values match documentation
607        assert_eq!(ROWS, 32);
608        assert_eq!(COLS, 64);
609        assert_eq!(NROWS, 16);
610        assert_eq!(FRAME_COUNT, 255);
611
612        // Verify this matches typical panel dimensions
613        assert!(COLS > 0);
614        assert_eq!(NROWS * 2, ROWS);
615    }
616}