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 duration 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//! | [`bitplane::plain`] | 16-bit | No | True bitplane |
43//! | [`bitplane::latched`] | 8-bit | Yes | True bitplane |
44//! | [`plain`] *(deprecated)* | 16-bit | No | Threshold frames |
45//! | [`latched`] *(deprecated)* | 8-bit | Yes | Threshold frames |
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
55//! DMA stream must be driven to render it. **The bitplane layouts are
56//! recommended for new designs**; the threshold layouts are deprecated.
57//!
58//! **True bitplane** (`bitplane::plain`, `bitplane::latched`) -- each of
59//! `PLANES` planes (typically 8) stores one bit of every colour channel
60//! directly. To render, stream each plane's data a number of times equal to
61//! its bit-weight (the MSB plane 128 times, the LSB plane once), either with
62//! a DMA descriptor chain or by reprogramming the DMA registers per segment.
63//! Memory scales linearly with the number of planes. Each module offers a
64//! plane-major (`frame`) and a row-major (`row`) layout — both LSB-first
65//! with suffix-coalesced BCM segments; see their documentation for the exact
66//! scan order.
67//!
68//! **Threshold frames** (`plain`, `latched`) -- **deprecated.** The driver
69//! compares each channel's 8-bit value against per-frame thresholds and
70//! stores the resulting on/off bits. For a colour depth of `BITS`, this
71//! produces `2^BITS - 1` frames. Frame *n* is displayed for a duration
72//! proportional to `2^n`. Memory grows exponentially with colour depth, but
73//! the whole refresh is one contiguous buffer that even interrupt-less
74//! circular DMA can stream.
75//!
76//! All four variants have configurable row and column dimensions, support
77//! `embedded-graphics` via the `DrawTarget` trait, and expose their BCM
78//! scan sequence through the [`FrameBuffer`] trait.
79//!
80//! ## Memory placement (⚠️ large buffers)
81//!
82//! Every framebuffer is a large inline array sized from its const generic
83//! parameters. Rough sizes:
84//!
85//! - `bitplane::plain`: `PLANES × NROWS × (COLS + gap) × 2` bytes.
86//! - `bitplane::latched`: `PLANES × NROWS × (COLS + gap)` bytes.
87//! - `plain`: `(2^BITS - 1) × NROWS × COLS × 2` bytes.
88//! - `latched`: `(2^BITS - 1) × NROWS × (COLS + 4)` bytes.
89//!
90//! For example, a 64×64 panel at 8-bit colour depth is already tens of
91//! kilobytes. **Never bind a framebuffer to a task-local `let`** — the whole
92//! buffer would be placed on that task's stack and overflow it.
93//!
94//! Framebuffers are **mutable** (drawing takes `&mut self`), so they need a
95//! `'static` home that hands out `&'static mut` **without** `unsafe`.
96//! `static mut` is *not* an option. Use `static_cell::StaticCell`, which
97//! guarantees the value is taken at most once and lives in `.bss`:
98//!
99//! ```rust,no_run
100//! use embedded_graphics::pixelcolor::RgbColor;
101//! use embedded_graphics::prelude::Point;
102//! use hub75_framebuffer::bitplane::plain::frame::DmaFrameBuffer;
103//! use hub75_framebuffer::Color;
104//! use static_cell::StaticCell;
105//!
106//! const COLS: usize = 64;
107//! const NROWS: usize = 32;
108//! const PLANES: usize = 8;
109//! type Fb = DmaFrameBuffer<NROWS, COLS, PLANES>;
110//!
111//! // `StaticCell` hands out `&'static mut` without `unsafe`; the value is
112//! // taken at most once and lives in .bss, not on the stack.
113//! static FB: StaticCell<Fb> = StaticCell::new();
114//! let fb = FB.uninit().write(Fb::new());
115//!
116//! fb.set_pixel(Point::new(0, 0), Color::RED);
117//! ```
118//!
119//! ## Driving the Panel (DMA Integration)
120//!
121//! This crate is **buffer-only**: it builds the memory image and describes
122//! exactly what to stream via the [`FrameBuffer`] trait — writing the
123//! peripheral driver (the refresh loop) is up to you. A driver only needs
124//! two things from a framebuffer:
125//!
126//! - [`FrameBuffer::Word`] — the stream element type: `u16` for the `plain`
127//!   variants, `u8` for the `latched` ones. The peripheral must output this
128//!   many bits in parallel and generates the pixel clock (`CLK`) itself.
129//! - [`FrameBuffer::BCM_SEQUENCE`] and [`FrameBuffer::bcm_segment`] — the
130//!   ordered `(ptr, len, reps)` segments that make up one complete panel
131//!   refresh.
132//!
133//! The framebuffer must live in DMA-capable memory and outlive the transfer:
134//! place it in a `static` (or a DMA RAM section on chips that require one)
135//! and apply whatever alignment and cache maintenance your MCU needs.
136//!
137//! ### DMA with descriptor chains (least CPU)
138//!
139//! The bitplane stream is non-uniform: each segment must be output `reps`
140//! times (the MSB plane up to `2^(PLANES-2)` times). This maps naturally
141//! onto DMA descriptor chains (e.g. ESP32 GPDMA linked-list mode) — use
142//! [`FrameBuffer::BCM_SEQUENCE`] / [`bcm_rep_count`] to size the descriptor
143//! table at compile time and [`FrameBuffer::bcm_segment`] to populate it; a
144//! segment whose `len` exceeds the DMA's maximum transfer size is simply
145//! split across several descriptors. Link the chain into a loop and the
146//! panel refreshes forever with zero CPU involvement. For free-running
147//! loops, consider the `tail-closes-latch` feature so `LAT` is not left
148//! asserted when the chain wraps.
149//!
150//! ### Simple DMA (registers only, no descriptors)
151//!
152//! If your DMA is just *source address + length* registers with a
153//! transfer-complete interrupt, walk the segments in the ISR: on each
154//! interrupt, program the address/length registers from
155//! [`FrameBuffer::bcm_segment`], re-arm the transfer, and re-issue the same
156//! segment until its `reps` count is exhausted before advancing. The cadence
157//! is modest — the plane-major (`frame`) layouts raise only `PLANES`
158//! interrupts per panel refresh (8 at full color depth); the row-major
159//! (`row`) layouts raise one per segment of every row
160//! ([`FrameBuffer::BCM_SEGMENT_COUNT`] per refresh).
161//!
162//! If the DMA cannot interrupt at all — truly circular-only hardware — the
163//! deprecated threshold framebuffers ([`plain`], [`latched`]) remain an
164//! option: their entire refresh is one contiguous buffer
165//! ([`FrameBuffer::BCM_SEQUENCE_LEN`] is `1`, `reps == 1`) with all BCM
166//! timing baked into the memory image, at an exponential memory cost.
167//!
168//! ## Multiple Panels / Scan-Pattern Remapping
169//! Use [`tiling::RemappedFrameBuffer`] to drive several HUB75 panels as one
170//! large virtual display, or to remap pixels for non-standard scan patterns
171//! (e.g. 1/16-scan on 64×64 panels). It works with all four framebuffer types
172//! and only requires two generic parameters (`F` and `M`).
173//!
174//! Combine it with a [`tiling::PixelRemapper`] implementation such as
175//! [`tiling::ChainTopRightDown`] (tiling) or [`tiling::QuarterScan`] (1/16-scan).
176//! The wrapper exposes a single `embedded-graphics` canvas, so for example a
177//! 3 × 3 stack of 64 × 32 panels simply looks like a 192 × 96 screen while
178//! all coordinate translation happens transparently.
179//!
180//! The older [`tiling::TiledFrameBuffer`] is still available but deprecated.
181//!
182//! ## Available Feature Flags
183//!
184//! ### `skip-black-pixels` Feature (disabled by default)
185//! When enabled, calls to `set_pixel()` with `Color::BLACK` return early without
186//! writing to the framebuffer. This provides a significant performance boost for
187//! UI applications that frequently draw black pixels (backgrounds, clearing, etc.)
188//! by assuming the framebuffer was already cleared.
189//!
190//! **Important**: This optimization assumes that black pixels represent "no change"
191//! rather than "explicitly set to black". By default, black pixels are written
192//! normally to ensure correct overwrite behavior. To enable the optimization:
193//!
194//! ```toml
195//! [dependencies]
196//! hub75-framebuffer = { version = "0.13.0", features = ["skip-black-pixels"] }
197//! ```
198//!
199//! ### `esp32-ordering` Feature (required for original ESP32 only)
200//! **Required** when targeting the original ESP32 chip (not ESP32-S3 or other variants).
201//! This feature adjusts byte ordering to accommodate the quirky requirements of the
202//! ESP32's I²S peripheral in 8-bit and 16-bit modes. The original ESP32 has different
203//! byte ordering requirements compared to other ESP32 variants (S2, S3, C3, etc.),
204//! which do **not** need this feature.
205//!
206//! ```toml
207//! [dependencies]
208//! hub75-framebuffer = { version = "0.13.0", features = ["esp32-ordering"] }
209//! ```
210//!
211//! ### `interstate75` Feature (Pimoroni Interstate 75 / 75 W only)
212//! Selects the 16-bit pin layout of the Pimoroni Interstate 75 and
213//! Interstate 75 W (`RP2040` / `RP2350`). The DMA word is pre-aligned to the
214//! board's contiguous GPIO mapping (`GPIO0..GPIO13`) so a PIO `out pins, 16`
215//! stream can clock out the panel with no run-time bit shuffling:
216//!
217//! ```text
218//! bit  13  12    11     10..6      5..3        2..0
219//!      OE  LAT  (spare)  E D C B A  B1 G1 R1   B0 G0 R0
220//! ```
221//!
222//! Bit 11 is unused by this crate and always left at 0.
223//!
224//! Applies to the 16-bit [`plain`] and [`bitplane::plain`] framebuffers only;
225//! the 8-bit `latched` framebuffers are unaffected. Mutually exclusive with
226//! `esp32-ordering`.
227//!
228//! ```toml
229//! [dependencies]
230//! hub75-framebuffer = { version = "0.13.0", features = ["interstate75"] }
231//! ```
232//!
233//! ### `tail-closes-latch` Feature (plain framebuffers only)
234//! Appends a single extra "tail" word at the end of the DMA buffer that drives the
235//! LATCH signal LOW (de-asserted) on the final clock edge. Without this feature the
236//! last word in each row asserts LATCH HIGH to latch shifted data into the LED
237//! drivers, and the GPIO pins remain in that state after the DMA transfer completes.
238//! Some hardware configurations (e.g. free-running DMA loops or peripherals that
239//! continue clocking after the descriptor chain ends) can re-latch stale data or
240//! glitch if LATCH is left asserted.
241//!
242//! Enabling `tail-closes-latch` adds one 16-bit `Entry` (for `plain`) or one entry
243//! per bit-plane (for `bitplane::plain`) that parks the bus with LATCH=0 and
244//! OE=BLANK, cleanly terminating the transfer. The cost is a single extra word per
245//! DMA chunk, which is negligible compared to the frame data.
246//!
247//! ```toml
248//! [dependencies]
249//! hub75-framebuffer = { version = "0.13.0", features = ["tail-closes-latch"] }
250//! ```
251//!
252//! ### `reverse-row-order` Feature (disabled by default)
253//! Stores the rows of every framebuffer layout in reverse scan order so that
254//! the DMA stream renders the last panel row first and row 0 last. The rows
255//! are physically reversed in the buffer (row addresses are written
256//! back-to-front while keeping the deferred address-change timing intact) and
257//! the pixel-setting paths map logical rows to the reversed slots, so logical
258//! coordinates are unchanged — only the scan order changes.
259//!
260//! ```toml
261//! [dependencies]
262//! hub75-framebuffer = { version = "0.13.0", features = ["reverse-row-order"] }
263//! ```
264//!
265//! ### Blanking delay features (`lead-blank-*` / `trail-blank-*`)
266//!
267//! Control the number of pixel-clock cycles of blanking (`OE` HIGH) inserted
268//! around row-address changes. The lead blank controls how many cycles the
269//! output is blanked *before* the row address is changed, and the trail blank
270//! controls blanking *after* the row address is changed. Together they give
271//! the address lines time to settle and prevent ghosting or "bleeding"
272//! artifacts caused by the panel briefly displaying data on the wrong row
273//! during the transition.
274//!
275//! | Feature          | Blanking cycles | Position              |
276//! |------------------|-----------------|-----------------------|
277//! | *(none)*         | 1 (default)     | before & after change |
278//! | `lead-blank-1`   | 1               | before address change |
279//! | `lead-blank-2`   | 2               | before address change |
280//! | `lead-blank-4`   | 4               | before address change |
281//! | `lead-blank-8`   | 8               | before address change |
282//! | `lead-blank-16`  | 16              | before address change |
283//! | `trail-blank-1`  | 1               | after address change  |
284//! | `trail-blank-2`  | 2               | after address change  |
285//! | `trail-blank-4`  | 4               | after address change  |
286//! | `trail-blank-8`  | 8               | after address change  |
287//! | `trail-blank-16` | 16              | after address change  |
288//!
289//! Higher values reduce ghosting at the cost of slightly less brightness (the
290//! LEDs are on for less time per scan line). Start with the default and increase
291//! only if you observe row-transition artifacts on your particular panel
292//! hardware.
293//!
294//! ```toml
295//! [dependencies]
296//! hub75-framebuffer = { version = "0.13.0", features = ["lead-blank-4", "trail-blank-2"] }
297//! ```
298//!
299//! **Note:** At most one `lead-blank-*` and one `trail-blank-*` feature may be
300//! enabled at a time. If multiple are enabled for the same edge, compile-time cfg
301//! conflicts will result.
302//!
303//! ### Inter-row blanking features (`inter-row-blank-*`)
304//!
305//! Insert additional dead clock cycles at the row transition, between the
306//! latch and the address-line change. In plain framebuffers the gap entries
307//! hold the previous row address with `OE` HIGH (blank), deferring the
308//! address change to the first pixel after the gap and giving slow panels
309//! more time to finish blanking before the address lines move. In latched
310//! framebuffers the latch and address change are inseparable in hardware,
311//! so the gap simply adds extra blanked cycles after the address change.
312//! Row-major bitplane framebuffers stream the gap between plane 0 (shifted
313//! out before the address change) and plane 1 (shifted out at/after it);
314//! all other framebuffers place it at the end of each row, between that
315//! row's latch and the next row's first pixel. The gap entries are
316//! invisible to all drawing primitives — they only appear in the DMA
317//! stream.
318//!
319//! | Feature              | Gap cycles | RAM cost per row       |
320//! |----------------------|------------|------------------------|
321//! | *(none)*             | 0          | 0 bytes                |
322//! | `inter-row-blank-4`  | 4          | 8 bytes (16-bit) / 4 bytes (8-bit) |
323//! | `inter-row-blank-8`  | 8          | 16 bytes / 8 bytes     |
324//! | `inter-row-blank-16` | 16         | 32 bytes / 16 bytes    |
325//! | `inter-row-blank-32` | 32         | 64 bytes / 32 bytes    |
326//!
327//! ```toml
328//! [dependencies]
329//! hub75-framebuffer = { version = "0.13.0", features = ["inter-row-blank-8"] }
330//! ```
331//!
332//! **Note:** At most one `inter-row-blank-*` feature may be enabled at a time.
333//! The `inter-row-blank` feature (no number) is an internal umbrella that is
334//! enabled automatically by any `inter-row-blank-*` feature — do not enable it
335//! directly.
336//! These are independent of the `lead-blank-*` / `trail-blank-*` features and
337//! can be combined with them.
338//!
339//! ### `defmt` Feature
340//! Implements `defmt::Format` for framebuffer types so they can be emitted with
341//! the `defmt` logging framework. No functional changes; purely adds a trait impl.
342//!
343//! ### `doc-images` Feature
344//! Embeds documentation images when building docs on docs.rs. Not needed for
345//! normal usage.
346#![no_std]
347#![warn(missing_docs)]
348#![warn(clippy::all)]
349#![warn(clippy::pedantic)]
350#![allow(clippy::cast_possible_truncation)]
351#![allow(clippy::cast_sign_loss)]
352
353use embedded_graphics::draw_target::DrawTarget;
354use embedded_graphics::pixelcolor::Rgb888;
355use embedded_graphics::prelude::Point;
356
357/// Compile-time HUB75-signal → DMA-word bit mapping.
358///
359/// Exposes [`pinmap::PINMAP`] and [`pinmap::LATCHED_PINMAP`] so a driver crate
360/// can validate at compile time that its own board wiring matches the bit
361/// assignments this crate assumes.
362pub mod pinmap;
363
364pub mod bitplane;
365pub mod latched;
366pub mod plain;
367pub mod tiling;
368
369/// Color type used in the framebuffer
370pub type Color = Rgb888;
371
372/// Word size configuration for the framebuffer
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum WordSize {
375    /// 8-bit word size
376    Eight,
377    /// 16-bit word size
378    Sixteen,
379}
380
381/// Computes the NROWS value from ROWS for `DmaFrameBuffer`
382///
383/// # Arguments
384///
385/// * `rows` - Total number of rows in the display
386///
387/// # Returns
388///
389/// Number of rows needed internally for `DmaFrameBuffer`
390#[must_use]
391pub const fn compute_rows(rows: usize) -> usize {
392    rows / 2
393}
394
395/// Computes the number of frames needed for a given bit depth
396///
397/// This is used to determine how many frames are needed to achieve
398/// the desired color depth through Binary Code Modulation (BCM).
399///
400/// # Arguments
401///
402/// * `bits` - Number of bits per color channel
403///
404/// # Returns
405///
406/// Number of frames required for the given bit depth
407#[must_use]
408pub const fn compute_frame_count(bits: u8) -> usize {
409    (1usize << bits) - 1
410}
411
412/// Maps a logical row-pair index to its physical memory slot.
413///
414/// With the `reverse-row-order` feature the rows are stored back-to-front so
415/// that the DMA stream renders the last panel row first and row 0 last; the
416/// pixel-setting paths use this to compensate, keeping logical coordinates
417/// unchanged. Without the feature this is the identity mapping.
418#[inline]
419pub(crate) const fn map_row_index<const NROWS: usize>(row_idx: usize) -> usize {
420    #[cfg(feature = "reverse-row-order")]
421    {
422        NROWS - 1 - row_idx
423    }
424    #[cfg(not(feature = "reverse-row-order"))]
425    {
426        row_idx
427    }
428}
429
430/// Returns the `(prev_addr, addr)` row addresses for the row stored in
431/// physical memory slot `slot` (slots are streamed in order `0..NROWS`).
432///
433/// `addr` is the panel row address rendered from that slot and `prev_addr` is
434/// the address of the row rendered immediately before it (the row displayed
435/// while this slot's data is shifted in). With the `reverse-row-order`
436/// feature the scan order is reversed: slot 0 renders row `NROWS - 1` first
437/// and the last slot renders row 0 last, so each slot's `prev_addr` is the
438/// *next* higher address (wrapping to 0 for slot 0, which follows the
439/// previous frame's final slot rendering row 0).
440#[inline]
441pub(crate) const fn slot_addresses<const NROWS: usize>(slot: usize) -> (u8, u8) {
442    #[cfg(feature = "reverse-row-order")]
443    {
444        let addr = (NROWS - 1 - slot) as u8;
445        let prev_addr = if slot == 0 { 0 } else { (NROWS - slot) as u8 };
446        (prev_addr, addr)
447    }
448    #[cfg(not(feature = "reverse-row-order"))]
449    {
450        let addr = slot as u8;
451        let prev_addr = if slot == 0 {
452            NROWS as u8 - 1
453        } else {
454            slot as u8 - 1
455        };
456        (prev_addr, addr)
457    }
458}
459
460/// Static `(len, reps)` pair describing one BCM segment without a pointer.
461///
462/// [`FrameBuffer::BCM_SEQUENCE`] is a compile-time array of these, one per
463/// segment in the repeating period. Drivers use it to size DMA descriptor
464/// tables and compute timing without a framebuffer instance.
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub struct BcmLenReps {
467    /// Byte length of the segment.
468    pub len: usize,
469    /// BCM repetition count: how many times the driver streams this segment
470    /// before advancing to the next one.
471    pub reps: usize,
472}
473
474impl BcmLenReps {
475    /// A zero-valued entry used for padding unused slots in
476    /// [`FrameBuffer::BCM_SEQUENCE`].
477    pub const ZERO: Self = Self { len: 0, reps: 0 };
478}
479
480/// Capacity of the [`FrameBuffer::BCM_SEQUENCE`] array.
481///
482/// Sized for the worst case sequence: 8 bit-planes plus an inter-row gap
483/// plus an end-of-row trailer.
484pub const BCM_SEQUENCE_CAPACITY: usize = 10;
485
486// ---------------------------------------------------------------------------
487// Feature-gated blanking-delay constants (shared across all framebuffer types)
488// ---------------------------------------------------------------------------
489
490// Compile‑time assertion: at most one lead-blank-* feature enabled.
491const _: () = assert!(
492    (cfg!(feature = "lead-blank-1") as usize)
493        + (cfg!(feature = "lead-blank-2") as usize)
494        + (cfg!(feature = "lead-blank-4") as usize)
495        + (cfg!(feature = "lead-blank-8") as usize)
496        + (cfg!(feature = "lead-blank-16") as usize)
497        + (cfg!(feature = "lead-blank-32") as usize)
498        <= 1,
499    "lead-blank-* features are mutually exclusive"
500);
501
502#[cfg(feature = "lead-blank-1")]
503pub(crate) const LEAD_BLANK_DELAY: usize = 1;
504#[cfg(feature = "lead-blank-2")]
505pub(crate) const LEAD_BLANK_DELAY: usize = 2;
506#[cfg(feature = "lead-blank-4")]
507pub(crate) const LEAD_BLANK_DELAY: usize = 4;
508#[cfg(feature = "lead-blank-8")]
509pub(crate) const LEAD_BLANK_DELAY: usize = 8;
510#[cfg(feature = "lead-blank-16")]
511pub(crate) const LEAD_BLANK_DELAY: usize = 16;
512#[cfg(feature = "lead-blank-32")]
513pub(crate) const LEAD_BLANK_DELAY: usize = 32;
514
515#[cfg(not(any(
516    feature = "lead-blank-1",
517    feature = "lead-blank-2",
518    feature = "lead-blank-4",
519    feature = "lead-blank-8",
520    feature = "lead-blank-16",
521    feature = "lead-blank-32"
522)))]
523pub(crate) const LEAD_BLANK_DELAY: usize = 0;
524
525// Compile‑time assertion: at most one trail-blank-* feature enabled.
526const _: () = assert!(
527    (cfg!(feature = "trail-blank-1") as usize)
528        + (cfg!(feature = "trail-blank-2") as usize)
529        + (cfg!(feature = "trail-blank-4") as usize)
530        + (cfg!(feature = "trail-blank-8") as usize)
531        + (cfg!(feature = "trail-blank-16") as usize)
532        + (cfg!(feature = "trail-blank-32") as usize)
533        <= 1,
534    "trail-blank-* features are mutually exclusive"
535);
536
537#[cfg(feature = "trail-blank-1")]
538pub(crate) const TRAIL_BLANK_DELAY: usize = 1;
539#[cfg(feature = "trail-blank-2")]
540pub(crate) const TRAIL_BLANK_DELAY: usize = 2;
541#[cfg(feature = "trail-blank-4")]
542pub(crate) const TRAIL_BLANK_DELAY: usize = 4;
543#[cfg(feature = "trail-blank-8")]
544pub(crate) const TRAIL_BLANK_DELAY: usize = 8;
545#[cfg(feature = "trail-blank-16")]
546pub(crate) const TRAIL_BLANK_DELAY: usize = 16;
547#[cfg(feature = "trail-blank-32")]
548pub(crate) const TRAIL_BLANK_DELAY: usize = 32;
549
550#[cfg(not(any(
551    feature = "trail-blank-1",
552    feature = "trail-blank-2",
553    feature = "trail-blank-4",
554    feature = "trail-blank-8",
555    feature = "trail-blank-16",
556    feature = "trail-blank-32"
557)))]
558pub(crate) const TRAIL_BLANK_DELAY: usize = 0;
559
560// Compile‑time assertion: at most one inter-row-blank-* feature enabled.
561const _: () = assert!(
562    (cfg!(feature = "inter-row-blank-4") as usize)
563        + (cfg!(feature = "inter-row-blank-8") as usize)
564        + (cfg!(feature = "inter-row-blank-16") as usize)
565        + (cfg!(feature = "inter-row-blank-32") as usize)
566        <= 1,
567    "inter-row-blank-* features are mutually exclusive"
568);
569
570// The `inter-row-blank` feature is an internal umbrella enabled by any
571// `inter-row-blank-*` feature; it must not be enabled on its own.
572#[cfg(all(
573    feature = "inter-row-blank",
574    not(any(
575        feature = "inter-row-blank-4",
576        feature = "inter-row-blank-8",
577        feature = "inter-row-blank-16",
578        feature = "inter-row-blank-32"
579    ))
580))]
581compile_error!("enable an inter-row-blank-* feature (4/8/16/32), not `inter-row-blank` directly");
582
583// `interstate75` selects an RP2xxx/PIO pin layout; `esp32-ordering` compensates
584// for the original ESP32's I²S byte order. The two target different boards and
585// cannot both be correct.
586#[cfg(all(feature = "interstate75", feature = "esp32-ordering"))]
587compile_error!(
588    "`interstate75` and `esp32-ordering` are mutually exclusive: they select \
589     different board pin/byte layouts"
590);
591
592#[cfg(feature = "inter-row-blank-4")]
593pub(crate) const INTER_ROW_BLANK: usize = 4;
594#[cfg(feature = "inter-row-blank-8")]
595pub(crate) const INTER_ROW_BLANK: usize = 8;
596#[cfg(feature = "inter-row-blank-16")]
597pub(crate) const INTER_ROW_BLANK: usize = 16;
598#[cfg(feature = "inter-row-blank-32")]
599pub(crate) const INTER_ROW_BLANK: usize = 32;
600
601#[cfg(not(any(
602    feature = "inter-row-blank-4",
603    feature = "inter-row-blank-8",
604    feature = "inter-row-blank-16",
605    feature = "inter-row-blank-32"
606)))]
607pub(crate) const INTER_ROW_BLANK: usize = 0;
608
609/// A single segment of the BCM scan sequence.
610///
611/// The driver walks an ordered sequence of segments, sending each one `reps`
612/// times, to produce correct BCM brightness weighting without needing to know
613/// the framebuffer's internal memory layout. See the [`FrameBuffer`]
614/// documentation for the segment/sequence terminology.
615///
616/// # Pointer validity
617///
618/// The [`ptr`](Self::ptr) field is a raw pointer borrowed from the
619/// framebuffer it was obtained from. It carries no lifetime, so the borrow
620/// checker cannot enforce the following requirements — the driver **must**:
621///
622/// - keep the source framebuffer borrowed (and **unmutated**) for the entire
623///   duration `ptr` is used by the DMA/PIO/parallel peripheral;
624/// - ensure the framebuffer is **not moved** while the transfer is live
625///   (allocate it in `static` memory, or an embassy `StaticCell`/`Mutex`);
626/// - treat `ptr` as aligned for the framebuffer's [`FrameBuffer::Word`], and
627///   read at most `len` bytes from it.
628#[derive(Debug, Clone, Copy)]
629pub struct BcmSegment {
630    /// Pointer to the segment data. See the [pointer validity
631    /// contract](Self#pointer-validity) before handing this to a DMA
632    /// peripheral.
633    pub ptr: *const u8,
634    /// Byte length of this segment.
635    pub len: usize,
636    /// BCM repetition count: how many times the driver streams this segment
637    /// before advancing to the next one.
638    pub reps: usize,
639}
640
641/// Framebuffer that exposes its BCM scan sequence as an ordered series of
642/// segments.
643///
644/// # Terminology
645///
646/// - **Segment** — the atomic unit, a [`BcmSegment`]: `len` bytes at
647///   `ptr`, streamed `reps` times before the driver advances to the next
648///   segment.
649/// - **Sequence** — the repeating period of the scan:
650///   [`BCM_SEQUENCE_LEN`](Self::BCM_SEQUENCE_LEN) consecutive segments
651///   whose `len` and `reps` are given by
652///   [`BCM_SEQUENCE`](Self::BCM_SEQUENCE). One complete panel refresh
653///   consists of this sequence repeated
654///   [`BCM_SEQUENCE_COUNT`](Self::BCM_SEQUENCE_COUNT) times.
655/// - **Group** — an optional driver hint:
656///   [`BCM_SEGMENTS_PER_GROUP`](Self::BCM_SEGMENTS_PER_GROUP) consecutive
657///   segments that a driver *may* chain into a single DMA transfer.
658///   Groups never straddle a sequence boundary.
659///
660/// ```text
661/// one refresh  = BCM_SEGMENT_COUNT segments
662///              = BCM_SEQUENCE_COUNT repetitions of one sequence
663/// one sequence = BCM_SEQUENCE_LEN segments (BCM_SEQUENCE)
664/// one segment  = len bytes, streamed reps times
665/// ```
666///
667/// | Layout | One sequence is… | `BCM_SEQUENCE_LEN` | `BCM_SEQUENCE_COUNT` |
668/// |--------|------------------|--------------------|----------------------|
669/// | Frame-major bitplane | the whole frame | `PLANES` | `1` |
670/// | Row-major bitplane | one row's BCM cycle | `PLANES + has_gap + has_tail` | `NROWS` |
671/// | Threshold (`plain` / `latched`) | the whole frame | `1` | `1` |
672///
673/// Row-major framebuffers repeat the same `(len, reps)` pattern `NROWS`
674/// times, so `BCM_SEQUENCE` holds one row's worth of entries instead of
675/// the whole frame's.
676///
677/// # Segment Ordering
678///
679/// **Frame-major** (bitplane) framebuffers describe the whole frame in a
680/// single sequence of `PLANES` segments (`BCM_SEQUENCE_LEN == PLANES`,
681/// `BCM_SEQUENCE_COUNT == 1`). Planes are **LSB-first** and stored
682/// contiguously, so each segment streams a whole *suffix* of planes: the
683/// segment for plane `k` starts at plane `k`, covers the remaining planes,
684/// and is repeated just enough times to bring plane `k`'s total coverage
685/// to `2^k`:
686///
687/// ```text
688/// segment 0: (plane0_ptr, PLANES*plane_bytes, 1)      // LSB; all planes
689/// segment 1: (plane1_ptr, (PLANES-1)*plane_bytes, 1)
690/// segment 2: (plane2_ptr, (PLANES-2)*plane_bytes, 2)
691/// …
692/// segment N: (planeN_ptr, plane_bytes, 2^(PLANES-2))  // MSB
693/// ```
694///
695/// **Row-major** framebuffers repeat one sequence per row
696/// (`BCM_SEQUENCE_COUNT == NROWS`). Planes are **LSB-first** within each
697/// row and stored contiguously. The inter-row gap segment sits between
698/// plane 0 and plane 1 — the point where the row address changes; with a
699/// gap enabled, plane 0 stands alone and plane 1's segment gets 2 reps:
700///
701/// ```text
702/// sequence 0 (row 0):
703///   (row0_plane0_ptr, PLANES*pixel_bytes, 1)
704///   (row0_gap_ptr, gap_bytes, 1)                    // if enabled
705///   (row0_plane1_ptr, (PLANES-1)*pixel_bytes, 1|2)  // 2 reps if gap
706///   …
707///   (row0_planeN_ptr, pixel_bytes, 2^(PLANES-2))
708///   (row0_trailer_ptr, trailer_bytes, 1)            // if enabled
709/// sequence 1 (row 1):
710///   …
711/// ```
712///
713/// **Threshold-based** framebuffers produce a single segment with
714/// `reps = 1` covering the entire buffer.
715pub trait FrameBuffer {
716    /// The DMA word type used by this framebuffer (`u8` for latched, `u16`
717    /// for direct-drive). Driver implementations can use this to enforce that pin
718    /// configurations match the framebuffer at compile time.
719    type Word;
720
721    /// Static `(len, reps)` of the segments in one **sequence**, in scan
722    /// order.
723    ///
724    /// This is instance-free data: `len` is the byte length of a segment
725    /// and `reps` is how many times it is streamed before advancing to the
726    /// next one. Drivers can use it at compile time to size transfer
727    /// resources (for example DMA descriptor tables) without needing a
728    /// framebuffer instance.
729    ///
730    /// Only the first [`BCM_SEQUENCE_LEN`](Self::BCM_SEQUENCE_LEN) entries
731    /// are meaningful; the remaining entries are
732    /// [`BcmLenReps::ZERO`] padding (stable Rust does not allow the array
733    /// length to be computed from the implementor's const generics).
734    const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY];
735
736    /// Number of BCM segments in one **sequence**, i.e. the number of
737    /// meaningful entries in [`BCM_SEQUENCE`](Self::BCM_SEQUENCE).
738    const BCM_SEQUENCE_LEN: usize;
739
740    /// Number of times the sequence repeats in one complete panel refresh:
741    /// `NROWS` for **row-major** framebuffers (one sequence per row), `1`
742    /// for **frame-major** and threshold framebuffers (the sequence covers
743    /// the whole frame).
744    const BCM_SEQUENCE_COUNT: usize;
745
746    /// Total number of BCM segments streamed for one complete panel
747    /// refresh (all rows, all planes).
748    ///
749    /// The default implementation is
750    /// `BCM_SEQUENCE_LEN * BCM_SEQUENCE_COUNT`.
751    const BCM_SEGMENT_COUNT: usize = Self::BCM_SEQUENCE_LEN * Self::BCM_SEQUENCE_COUNT;
752
753    /// Number of consecutive segments that a driver may chain into a
754    /// single DMA transfer. Must divide
755    /// [`BCM_SEQUENCE_LEN`](Self::BCM_SEQUENCE_LEN): groups never straddle
756    /// a sequence boundary.
757    ///
758    /// - **Frame-major** framebuffers use `1` (each plane is its own group).
759    /// - **Row-major** framebuffers use `PLANES + has_gap + has_tail` (all
760    ///   segments for one row form a single transfer).
761    const BCM_SEGMENTS_PER_GROUP: usize = 1;
762
763    /// Total number of BCM segments streamed for one complete panel
764    /// refresh.
765    ///
766    /// The default implementation returns [`Self::BCM_SEGMENT_COUNT`].
767    fn bcm_segment_count(&self) -> usize {
768        Self::BCM_SEGMENT_COUNT
769    }
770
771    /// Returns the byte pointer for the `index`-th BCM segment.
772    ///
773    /// The `len` and `reps` for this segment come from
774    /// [`BCM_SEQUENCE`](Self::BCM_SEQUENCE)`[index %`
775    /// [`BCM_SEQUENCE_LEN`](Self::BCM_SEQUENCE_LEN)`]`.
776    ///
777    /// # Pointer validity
778    ///
779    /// The returned pointer borrows from `self` but escapes as a raw pointer,
780    /// so the compiler cannot enforce how long it stays valid. The caller
781    /// **must** ensure that:
782    ///
783    /// - `self` stays borrowed and is **not mutated** for the whole time the
784    ///   pointer is used by the peripheral (hold the borrow — e.g. an embassy
785    ///   `Mutex<StaticCell<_>>` guarding a `&mut` — across the transfer);
786    /// - the framebuffer is **not moved** while the transfer is live, so it
787    ///   must live in `static`/`StaticCell` memory, never on a task stack;
788    /// - the pointer is aligned to `align_of::<Self::Word>()` before casting
789    ///   to `*const Self::Word` (a framebuffer in `static` memory satisfies
790    ///   this), and at most `len` bytes are read from it.
791    ///
792    /// [`bcm_segment`](Self::bcm_segment) wraps this together with the static
793    /// `(len, reps)` and is the preferred entry point.
794    ///
795    /// # Panics
796    /// Panics if `index >= BCM_SEGMENT_COUNT`.
797    fn bcm_segment_ptr(&self, index: usize) -> *const u8;
798
799    /// Returns the `index`-th BCM segment.
800    ///
801    /// The provided implementation combines
802    /// [`bcm_segment_ptr`](Self::bcm_segment_ptr) with the static
803    /// `(len, reps)` from [`BCM_SEQUENCE`](Self::BCM_SEQUENCE). The returned
804    /// [`BcmSegment`] inherits the [pointer validity
805    /// contract](Self::bcm_segment_ptr) of `bcm_segment_ptr`.
806    ///
807    /// # Panics
808    /// Panics if `index >= BCM_SEGMENT_COUNT`.
809    fn bcm_segment(&self, index: usize) -> BcmSegment {
810        assert!(
811            index < Self::BCM_SEGMENT_COUNT,
812            "segment index {index} out of range for {} segments",
813            Self::BCM_SEGMENT_COUNT
814        );
815        let BcmLenReps { len, reps } = Self::BCM_SEQUENCE[index % Self::BCM_SEQUENCE_LEN];
816        BcmSegment {
817            ptr: self.bcm_segment_ptr(index),
818            len,
819            reps,
820        }
821    }
822
823    /// Number of consecutive segments that form one transfer group.
824    ///
825    /// The default implementation returns
826    /// [`Self::BCM_SEGMENTS_PER_GROUP`].
827    fn bcm_segments_per_group(&self) -> usize {
828        Self::BCM_SEGMENTS_PER_GROUP
829    }
830}
831
832/// Sum of `reps` across one complete panel refresh.
833///
834/// Useful for sizing DMA descriptor tables when the hardware uses one
835/// descriptor per repetition (e.g. esp32 DMA and stm32 GPDMA linked-list mode).
836#[must_use]
837pub const fn bcm_rep_count<FB: FrameBuffer>() -> usize {
838    let mut total = 0;
839    let mut seq = 0;
840    while seq < FB::BCM_SEQUENCE_COUNT {
841        let mut i = 0;
842        while i < FB::BCM_SEQUENCE_LEN {
843            total += FB::BCM_SEQUENCE[i].reps;
844            i += 1;
845        }
846        seq += 1;
847    }
848    total
849}
850
851/// Trait for mutable framebuffers that support `embedded_graphics` drawing.
852pub trait MutableFrameBuffer:
853    FrameBuffer + DrawTarget<Color = Color, Error = core::convert::Infallible>
854{
855}
856
857/// Trait for all operations a user may want to call on a framebuffer.
858pub trait FrameBufferOperations: FrameBuffer {
859    /// Erase pixel colors while preserving control bits.
860    /// This is much faster than `format()` and is the typical way to clear the display.
861    fn erase(&mut self);
862
863    /// Set a pixel in the framebuffer.
864    fn set_pixel(&mut self, p: Point, color: Color);
865}
866
867#[cfg(test)]
868mod tests {
869    extern crate std;
870
871    use std::format;
872
873    use super::*;
874    use embedded_graphics::pixelcolor::RgbColor;
875
876    #[test]
877    fn test_compute_rows() {
878        // Test typical panel sizes
879        assert_eq!(compute_rows(32), 16);
880        assert_eq!(compute_rows(64), 32);
881        assert_eq!(compute_rows(16), 8);
882        assert_eq!(compute_rows(128), 64);
883
884        // Test edge cases
885        assert_eq!(compute_rows(2), 1);
886        assert_eq!(compute_rows(0), 0);
887
888        // Test that it always divides by 2
889        for rows in [8, 16, 24, 32, 48, 64, 96, 128, 256] {
890            assert_eq!(compute_rows(rows), rows / 2);
891        }
892    }
893
894    #[test]
895    fn test_compute_frame_count() {
896        // Test common bit depths
897        assert_eq!(compute_frame_count(1), 1); // 2^1 - 1 = 1
898        assert_eq!(compute_frame_count(2), 3); // 2^2 - 1 = 3
899        assert_eq!(compute_frame_count(3), 7); // 2^3 - 1 = 7
900        assert_eq!(compute_frame_count(4), 15); // 2^4 - 1 = 15
901        assert_eq!(compute_frame_count(5), 31); // 2^5 - 1 = 31
902        assert_eq!(compute_frame_count(6), 63); // 2^6 - 1 = 63
903        assert_eq!(compute_frame_count(7), 127); // 2^7 - 1 = 127
904        assert_eq!(compute_frame_count(8), 255); // 2^8 - 1 = 255
905
906        // Test the formula: (2^bits) - 1
907        for bits in 1..=8 {
908            let expected = (1usize << bits) - 1;
909            assert_eq!(compute_frame_count(bits), expected);
910        }
911    }
912
913    #[test]
914    #[cfg(not(feature = "reverse-row-order"))]
915    fn test_map_row_index_forward() {
916        for r in 0..16 {
917            assert_eq!(map_row_index::<16>(r), r);
918        }
919    }
920
921    #[test]
922    #[cfg(feature = "reverse-row-order")]
923    fn test_map_row_index_reversed() {
924        for r in 0..16 {
925            assert_eq!(map_row_index::<16>(r), 15 - r);
926        }
927    }
928
929    #[test]
930    #[cfg(not(feature = "reverse-row-order"))]
931    fn test_slot_addresses_forward() {
932        // Slot j renders panel row j; while it is shifted the panel displays
933        // row j-1 (wrapping to the previous frame's last row for slot 0).
934        for slot in 0..16usize {
935            let (prev_addr, addr) = slot_addresses::<16>(slot);
936            assert_eq!(addr, slot as u8);
937            assert_eq!(
938                prev_addr,
939                if slot == 0 { 15 } else { slot as u8 - 1 },
940                "prev_addr for slot {slot}"
941            );
942        }
943    }
944
945    #[test]
946    #[cfg(feature = "reverse-row-order")]
947    fn test_slot_addresses_reversed() {
948        // Slot j renders panel row 15-j; while it is shifted the panel
949        // displays the previously rendered row 15-(j-1) (row 0 from the
950        // previous frame's final slot for slot 0).
951        for slot in 0..16usize {
952            let (prev_addr, addr) = slot_addresses::<16>(slot);
953            assert_eq!(addr, 15 - slot as u8);
954            assert_eq!(
955                prev_addr,
956                if slot == 0 { 0 } else { 16 - slot as u8 },
957                "prev_addr for slot {slot}"
958            );
959        }
960    }
961
962    #[test]
963    fn test_compute_frame_count_properties() {
964        // Test that frame count grows exponentially
965        assert!(compute_frame_count(2) > compute_frame_count(1));
966        assert!(compute_frame_count(3) > compute_frame_count(2));
967        assert!(compute_frame_count(4) > compute_frame_count(3));
968
969        // Test doubling property: each additional bit approximately doubles frame count
970        for bits in 1..=7 {
971            let current_frames = compute_frame_count(bits);
972            let next_frames = compute_frame_count(bits + 1);
973            // next_frames should be approximately 2 * current_frames + 1
974            assert_eq!(next_frames, 2 * current_frames + 1);
975        }
976    }
977
978    #[test]
979    fn test_word_size_enum() {
980        // Test enum values
981        let eight = WordSize::Eight;
982        let sixteen = WordSize::Sixteen;
983
984        assert_ne!(eight, sixteen);
985        assert_eq!(eight, WordSize::Eight);
986        assert_eq!(sixteen, WordSize::Sixteen);
987    }
988
989    #[test]
990    fn test_word_size_debug() {
991        let eight = WordSize::Eight;
992        let sixteen = WordSize::Sixteen;
993
994        let eight_debug = format!("{:?}", eight);
995        let sixteen_debug = format!("{:?}", sixteen);
996
997        assert_eq!(eight_debug, "Eight");
998        assert_eq!(sixteen_debug, "Sixteen");
999    }
1000
1001    #[test]
1002    fn test_word_size_clone_copy() {
1003        let original = WordSize::Eight;
1004        let cloned = original.clone();
1005        let copied = original;
1006
1007        assert_eq!(original, cloned);
1008        assert_eq!(original, copied);
1009        assert_eq!(cloned, copied);
1010    }
1011
1012    #[test]
1013    fn test_color_type_alias() {
1014        // Test that Color is an alias for Rgb888
1015        let red_color: Color = Color::RED;
1016        let red_rgb888: Rgb888 = Rgb888::RED;
1017
1018        assert_eq!(red_color, red_rgb888);
1019        assert_eq!(red_color.r(), 255);
1020        assert_eq!(red_color.g(), 0);
1021        assert_eq!(red_color.b(), 0);
1022
1023        // Test various colors
1024        let colors = [
1025            (Color::RED, (255, 0, 0)),
1026            (Color::GREEN, (0, 255, 0)),
1027            (Color::BLUE, (0, 0, 255)),
1028            (Color::WHITE, (255, 255, 255)),
1029            (Color::BLACK, (0, 0, 0)),
1030            (Color::CYAN, (0, 255, 255)),
1031            (Color::MAGENTA, (255, 0, 255)),
1032            (Color::YELLOW, (255, 255, 0)),
1033        ];
1034
1035        for (color, (r, g, b)) in colors {
1036            assert_eq!(color.r(), r);
1037            assert_eq!(color.g(), g);
1038            assert_eq!(color.b(), b);
1039        }
1040    }
1041
1042    #[test]
1043    fn test_color_construction() {
1044        // Test Color construction from RGB values
1045        let custom_color = Color::new(128, 64, 192);
1046        assert_eq!(custom_color.r(), 128);
1047        assert_eq!(custom_color.g(), 64);
1048        assert_eq!(custom_color.b(), 192);
1049
1050        // Test that it behaves like Rgb888
1051        let rgb888_color = Rgb888::new(128, 64, 192);
1052        assert_eq!(custom_color, rgb888_color);
1053    }
1054
1055    #[test]
1056    fn test_helper_functions_const() {
1057        // Test that helper functions can be used in const contexts
1058        const ROWS: usize = 32;
1059        const COMPUTED_NROWS: usize = compute_rows(ROWS);
1060        const BITS: u8 = 4;
1061        const COMPUTED_FRAME_COUNT: usize = compute_frame_count(BITS);
1062
1063        assert_eq!(COMPUTED_NROWS, 16);
1064        assert_eq!(COMPUTED_FRAME_COUNT, 15);
1065    }
1066
1067    #[test]
1068    fn test_realistic_panel_configurations() {
1069        // Test common HUB75 panel configurations
1070        struct PanelConfig {
1071            rows: usize,
1072            cols: usize,
1073            bits: u8,
1074        }
1075
1076        let configs = [
1077            PanelConfig {
1078                rows: 32,
1079                cols: 64,
1080                bits: 3,
1081            }, // 32x64 panel, 3-bit color
1082            PanelConfig {
1083                rows: 64,
1084                cols: 64,
1085                bits: 4,
1086            }, // 64x64 panel, 4-bit color
1087            PanelConfig {
1088                rows: 32,
1089                cols: 32,
1090                bits: 5,
1091            }, // 32x32 panel, 5-bit color
1092            PanelConfig {
1093                rows: 16,
1094                cols: 32,
1095                bits: 6,
1096            }, // 16x32 panel, 6-bit color
1097        ];
1098
1099        for config in configs {
1100            let nrows = compute_rows(config.rows);
1101            let frame_count = compute_frame_count(config.bits);
1102
1103            // Basic sanity checks for rows
1104            assert!(nrows > 0);
1105            assert!(nrows <= config.rows);
1106            assert_eq!(nrows * 2, config.rows);
1107
1108            // Basic sanity checks for columns
1109            assert!(config.cols > 0);
1110            assert!(config.cols <= 256); // Reasonable upper limit for HUB75 panels
1111
1112            // Frame count checks
1113            assert!(frame_count > 0);
1114            assert!(frame_count < 256); // Should be reasonable for typical bit depths
1115
1116            // Frame count should grow with bit depth
1117            let prev_frame_count = compute_frame_count(config.bits - 1);
1118            assert!(frame_count > prev_frame_count);
1119        }
1120    }
1121
1122    #[test]
1123    fn test_memory_calculations() {
1124        // Test that we can calculate memory requirements using helper functions
1125        const ROWS: usize = 64;
1126        const COLS: usize = 64;
1127        const BITS: u8 = 4;
1128
1129        const NROWS: usize = compute_rows(ROWS);
1130        const FRAME_COUNT: usize = compute_frame_count(BITS);
1131
1132        // These should be compile-time constants
1133        assert_eq!(NROWS, 32);
1134        assert_eq!(FRAME_COUNT, 15);
1135
1136        // Verify the relationship between parameters
1137        assert_eq!(NROWS * 2, ROWS);
1138        assert_eq!(FRAME_COUNT, (1 << BITS) - 1);
1139
1140        // Verify COLS is reasonable for memory calculations
1141        assert!(COLS > 0);
1142        assert!(COLS <= 256); // Reasonable limit for HUB75 panels
1143    }
1144
1145    #[test]
1146    fn test_edge_cases() {
1147        // Test minimum values
1148        assert_eq!(compute_rows(2), 1);
1149        assert_eq!(compute_frame_count(1), 1);
1150
1151        // Test maximum reasonable values
1152        assert_eq!(compute_rows(512), 256);
1153        assert_eq!(compute_frame_count(8), 255);
1154
1155        // Test zero (though not practical)
1156        assert_eq!(compute_rows(0), 0);
1157    }
1158
1159    // Note: We can't easily test the traits directly since they're abstract,
1160    // but they are thoroughly tested through their implementations in
1161    // the plain and latched modules.
1162
1163    #[test]
1164    fn test_word_size_equality() {
1165        // Test all combinations of equality
1166        assert_eq!(WordSize::Eight, WordSize::Eight);
1167        assert_eq!(WordSize::Sixteen, WordSize::Sixteen);
1168        assert_ne!(WordSize::Eight, WordSize::Sixteen);
1169        assert_ne!(WordSize::Sixteen, WordSize::Eight);
1170    }
1171
1172    #[test]
1173    fn test_bit_depth_limits() {
1174        // Test that our bit depth calculations work for the full range
1175        for bits in 1..=8 {
1176            let frame_count = compute_frame_count(bits);
1177
1178            // Frame count should be positive
1179            assert!(frame_count > 0);
1180
1181            // Frame count should be less than 2^bits
1182            assert!(frame_count < (1 << bits));
1183
1184            // Frame count should be exactly (2^bits) - 1
1185            assert_eq!(frame_count, (1 << bits) - 1);
1186        }
1187    }
1188
1189    #[test]
1190    fn test_documentation_examples() {
1191        // Test the example values from the documentation
1192        const ROWS: usize = 32;
1193        const COLS: usize = 64;
1194        const NROWS: usize = ROWS / 2;
1195        const BITS: u8 = 8;
1196        const FRAME_COUNT: usize = (1 << BITS) - 1;
1197
1198        // Verify using our helper functions
1199        assert_eq!(compute_rows(ROWS), NROWS);
1200        assert_eq!(compute_frame_count(BITS), FRAME_COUNT);
1201
1202        // Verify the values match documentation
1203        assert_eq!(ROWS, 32);
1204        assert_eq!(COLS, 64);
1205        assert_eq!(NROWS, 16);
1206        assert_eq!(FRAME_COUNT, 255);
1207
1208        // Verify this matches typical panel dimensions
1209        assert!(COLS > 0);
1210        assert_eq!(NROWS * 2, ROWS);
1211    }
1212}