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