hub75_framebuffer/
lib.rs

1//! Framebuffer implementation for HUB75 LED matrix displays.
2//!
3//! ## How HUB75 LED Displays Work
4//!
5//! HUB75 RGB LED matrix panels are scanned, time-multiplexed displays that behave like a long
6//! daisy-chained shift register rather than a random-access framebuffer.
7//!
8//! ### Signal names
9//! - **R1 G1 B1 / R2 G2 B2** – Serial colour data for the upper and lower halves of the active scan line
10//! - **CLK** – Shift-register clock; every rising edge pushes the six colour bits one pixel to the right
11//! - **LAT / STB** – Latch; copies the shift-register contents to the LED drivers for the row currently selected by the address lines
12//! - **OE** – Output-Enable (active LOW): LEDs are lit while OE is LOW and blanked when it is HIGH
13//! - **A B C D (E)** – Row-address select lines (choose which pair of rows is lit)
14//! - **VCC & GND** – 5 V power for panel logic and LED drivers
15//!
16//! ### Row-pair scanning workflow (e.g., 1/16-scan panel)
17//! 1. While the panel is still displaying row pair N − 1, the controller shifts the six-bit colour data for row pair N into the chain (OE remains LOW so row N − 1 stays visible).
18//! 2. After the last pixel is clocked in, the controller raises OE HIGH to blank the LEDs.
19//! 3. With the panel blanked, it first changes the address lines to select row pair N, lets them settle for a few nanoseconds, and **then** pulses LAT to latch the freshly shifted data into the output drivers for that newly selected row.
20//! 4. OE is immediately driven LOW again, lighting row pair N.
21//! 5. Steps 1–4 repeat for every row pair fast enough (hundreds of Hz) that the human eye sees a steady image.
22//!    - If the first row pair is being shifted, the panel continues showing the last row pair of the previous frame until the first blank-address-latch sequence occurs.
23//!
24//! ### Brightness and colour depth (Binary Code Modulation)
25//! - Full colour is typically achieved using **Binary Code Modulation (BCM)**, also known as *Bit-Angle Modulation (BAM)*. Each bit-plane is displayed for a period proportional to its binary weight (1, 2, 4, 8 …), yielding 2ⁿ intensity levels per channel. See [Batsocks – LED dimming using Binary Code Modulation](https://www.batsocks.co.uk/readme/art_bcm_1.htm) for a deeper explanation.
26//! - Because each LED is on for only a fraction of the total frame time, the driver can use relatively high peak currents without overheating while average brightness is preserved.
27//!
28//! ### Implications for software / hardware drivers
29//! - You don't simply "write a pixel" once; you must continuously stream the complete refresh data at MHz-range clock rates.
30//! - Precise timing of CLK, OE, address lines, and LAT is critical—especially the order: blank (OE HIGH) → set address → latch → un-blank (OE LOW).
31//! - Microcontrollers typically employ DMA, PIO, or parallel GPIO tricks, and FPGAs use dedicated logic, to sustain the data throughput while leaving processing resources free.
32//!
33//! In short: a HUB75 panel is a high-speed shift-register chain that relies on rapid row-pair scanning and **Binary Code Modulation (BCM)** to create a bright, full-colour image. Keeping OE LOW almost all the time—blanking only long enough to change the address and pulse LAT—maximises brightness without visible artefacts.
34//!
35//! ## Framebuffer Implementations
36//!
37//! This module provides two different framebuffer implementations optimized for
38//! HUB75 LED matrix displays:
39//!
40//! 1. **Plain Implementation** (`plain` module)
41//!    - No additional hardware requirements
42//!    - Simpler implementation suitable for basic displays
43//!
44//! 2. **Latched Implementation** (`latched` module)
45//!    - Requires external latch hardware for address lines
46//!
47//! Both implementations:
48//! - Have configurable row and column dimensions
49//! - Support different color depths through Binary Code Modulation (BCM)
50//! - Implement the `ReadBuffer` trait for DMA compatibility
51//!
52//! ## Multiple Panels
53//! Use [`tiling::TiledFrameBuffer`] to drive several HUB75 panels as one large
54//! virtual display. Combine it with a pixel-remapping policy such as
55//! [`tiling::ChainTopRightDown`] and any of the framebuffer flavours above
56//! (`plain` or `latched`). The wrapper exposes a single `embedded-graphics`
57//! canvas, so for example a 3 × 3 stack of 64 × 32 panels simply looks like a
58//! 192 × 96 screen while all coordinate translation happens transparently.
59//!
60//! ## Available Feature Flags
61//!
62//! ### `skip-black-pixels` Feature (disabled by default)
63//! When enabled, calls to `set_pixel()` with `Color::BLACK` return early without
64//! writing to the framebuffer. This provides a significant performance boost for
65//! UI applications that frequently draw black pixels (backgrounds, clearing, etc.)
66//! by assuming the framebuffer was already cleared.
67//!
68//! **Important**: This optimization assumes that black pixels represent "no change"
69//! rather than "explicitly set to black". By default, black pixels are written
70//! normally to ensure correct overwrite behavior. To enable the optimization:
71//!
72//! ```toml
73//! [dependencies]
74//! hub75-framebuffer = { version = "0.4.2", features = ["skip-black-pixels"] }
75//! ```
76//!
77//! ### `esp-hal-dma` Feature (required when using `esp-hal`)
78//! **Required** when using the `esp-hal` crate for ESP32 development. This feature
79//! switches the `ReadBuffer` trait implementation from `embedded-dma` to `esp-hal::dma`.
80//! If you're targeting ESP32 devices with `esp-hal`, you **must** enable this feature
81//! for DMA compatibility.
82//!
83//! ```toml
84//! [dependencies]
85//! hub75-framebuffer = { version = "0.4.2", features = ["esp-hal-dma"] }
86//! ```
87//!
88//! ### `esp32-ordering` Feature (required for original ESP32 only)
89//! **Required** when targeting the original ESP32 chip (not ESP32-S3 or other variants).
90//! This feature adjusts byte ordering to accommodate the quirky requirements of the
91//! ESP32's I²S peripheral in 8-bit and 16-bit modes. The original ESP32 has different
92//! byte ordering requirements compared to other ESP32 variants (S2, S3, C3, etc.),
93//! which do **not** need this feature.
94//!
95//! ```toml
96//! [dependencies]
97//! hub75-framebuffer = { version = "0.4.2", features = ["esp32-ordering"] }
98//! ```
99//!
100//! ### `defmt` Feature
101//! Implements `defmt::Format` for framebuffer types so they can be emitted with
102//! the `defmt` logging framework. No functional changes; purely adds a trait impl.
103//!
104//! ### `doc-images` Feature
105//! Embeds documentation images when building docs on docs.rs. Not needed for
106//! normal usage.
107#![no_std]
108#![warn(missing_docs)]
109#![warn(clippy::all)]
110#![warn(clippy::pedantic)]
111#![allow(clippy::cast_possible_truncation)]
112#![allow(clippy::cast_sign_loss)]
113
114#[cfg(not(feature = "esp-hal-dma"))]
115use embedded_dma::ReadBuffer;
116use embedded_graphics::draw_target::DrawTarget;
117use embedded_graphics::pixelcolor::Rgb888;
118use embedded_graphics::prelude::Point;
119#[cfg(feature = "esp-hal-dma")]
120use esp_hal::dma::ReadBuffer;
121
122pub mod latched;
123pub mod plain;
124pub mod tiling;
125
126/// Color type used in the framebuffer
127pub type Color = Rgb888;
128
129/// Word size configuration for the framebuffer
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum WordSize {
132    /// 8-bit word size
133    Eight,
134    /// 16-bit word size
135    Sixteen,
136}
137
138/// Computes the NROWS value from ROWS for `DmaFrameBuffer`
139///
140/// # Arguments
141///
142/// * `rows` - Total number of rows in the display
143///
144/// # Returns
145///
146/// Number of rows needed internally for `DmaFrameBuffer`
147#[must_use]
148pub const fn compute_rows(rows: usize) -> usize {
149    rows / 2
150}
151
152/// Computes the number of frames needed for a given bit depth
153///
154/// This is used to determine how many frames are needed to achieve
155/// the desired color depth through Binary Code Modulation (BCM).
156///
157/// # Arguments
158///
159/// * `bits` - Number of bits per color channel
160///
161/// # Returns
162///
163/// Number of frames required for the given bit depth
164#[must_use]
165pub const fn compute_frame_count(bits: u8) -> usize {
166    (1usize << bits) - 1
167}
168
169/// Trait for read-only framebuffers
170///
171/// This trait defines the basic functionality required for a framebuffer
172/// that can be read from and transferred via DMA.
173///
174/// # Type Parameters
175///
176/// * `ROWS` - Total number of rows in the display
177/// * `COLS` - Number of columns in the display
178/// * `NROWS` - Number of rows processed in parallel
179/// * `BITS` - Number of bits per color channel
180/// * `FRAME_COUNT` - Number of frames needed for BCM
181pub trait FrameBuffer<
182    const ROWS: usize,
183    const COLS: usize,
184    const NROWS: usize,
185    const BITS: u8,
186    const FRAME_COUNT: usize,
187>: ReadBuffer
188{
189    /// Returns the word size configuration for this framebuffer
190    fn get_word_size(&self) -> WordSize;
191}
192
193/// Trait for mutable framebuffers
194///
195/// This trait extends `FrameBuffer` with the ability to draw to the framebuffer
196/// using the `embedded_graphics` drawing primitives.
197///
198/// # Type Parameters
199///
200/// * `ROWS` - Total number of rows in the display
201/// * `COLS` - Number of columns in the display
202/// * `NROWS` - Number of rows processed in parallel
203/// * `BITS` - Number of bits per color channel
204/// * `FRAME_COUNT` - Number of frames needed for BCM
205pub trait MutableFrameBuffer<
206    const ROWS: usize,
207    const COLS: usize,
208    const NROWS: usize,
209    const BITS: u8,
210    const FRAME_COUNT: usize,
211>:
212    FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
213    + DrawTarget<Color = Color, Error = core::convert::Infallible>
214{
215}
216
217/// Trait for all operations a user may want to call on a framebuffer.
218///
219/// # Type Parameters
220///
221/// * `ROWS` - Total number of rows in the display
222/// * `COLS` - Number of columns in the display
223/// * `NROWS` - Number of rows processed in parallel
224/// * `BITS` - Number of bits per color channel
225/// * `FRAME_COUNT` - Number of frames needed for BCM
226pub trait FrameBufferOperations<
227    const ROWS: usize,
228    const COLS: usize,
229    const NROWS: usize,
230    const BITS: u8,
231    const FRAME_COUNT: usize,
232>: FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
233{
234    /// Erase pixel colors while preserving control bits.
235    /// This is much faster than `format()` and is the typical way to clear the display.
236    fn erase(&mut self);
237
238    /// Set a pixel in the framebuffer.
239    fn set_pixel(&mut self, p: Point, color: Color);
240}
241
242#[cfg(test)]
243mod tests {
244    extern crate std;
245
246    use std::format;
247
248    use super::*;
249    use embedded_graphics::pixelcolor::RgbColor;
250
251    #[test]
252    fn test_compute_rows() {
253        // Test typical panel sizes
254        assert_eq!(compute_rows(32), 16);
255        assert_eq!(compute_rows(64), 32);
256        assert_eq!(compute_rows(16), 8);
257        assert_eq!(compute_rows(128), 64);
258
259        // Test edge cases
260        assert_eq!(compute_rows(2), 1);
261        assert_eq!(compute_rows(0), 0);
262
263        // Test that it always divides by 2
264        for rows in [8, 16, 24, 32, 48, 64, 96, 128, 256] {
265            assert_eq!(compute_rows(rows), rows / 2);
266        }
267    }
268
269    #[test]
270    fn test_compute_frame_count() {
271        // Test common bit depths
272        assert_eq!(compute_frame_count(1), 1); // 2^1 - 1 = 1
273        assert_eq!(compute_frame_count(2), 3); // 2^2 - 1 = 3
274        assert_eq!(compute_frame_count(3), 7); // 2^3 - 1 = 7
275        assert_eq!(compute_frame_count(4), 15); // 2^4 - 1 = 15
276        assert_eq!(compute_frame_count(5), 31); // 2^5 - 1 = 31
277        assert_eq!(compute_frame_count(6), 63); // 2^6 - 1 = 63
278        assert_eq!(compute_frame_count(7), 127); // 2^7 - 1 = 127
279        assert_eq!(compute_frame_count(8), 255); // 2^8 - 1 = 255
280
281        // Test the formula: (2^bits) - 1
282        for bits in 1..=8 {
283            let expected = (1usize << bits) - 1;
284            assert_eq!(compute_frame_count(bits), expected);
285        }
286    }
287
288    #[test]
289    fn test_compute_frame_count_properties() {
290        // Test that frame count grows exponentially
291        assert!(compute_frame_count(2) > compute_frame_count(1));
292        assert!(compute_frame_count(3) > compute_frame_count(2));
293        assert!(compute_frame_count(4) > compute_frame_count(3));
294
295        // Test doubling property: each additional bit approximately doubles frame count
296        for bits in 1..=7 {
297            let current_frames = compute_frame_count(bits);
298            let next_frames = compute_frame_count(bits + 1);
299            // next_frames should be approximately 2 * current_frames + 1
300            assert_eq!(next_frames, 2 * current_frames + 1);
301        }
302    }
303
304    #[test]
305    fn test_word_size_enum() {
306        // Test enum values
307        let eight = WordSize::Eight;
308        let sixteen = WordSize::Sixteen;
309
310        assert_ne!(eight, sixteen);
311        assert_eq!(eight, WordSize::Eight);
312        assert_eq!(sixteen, WordSize::Sixteen);
313    }
314
315    #[test]
316    fn test_word_size_debug() {
317        let eight = WordSize::Eight;
318        let sixteen = WordSize::Sixteen;
319
320        let eight_debug = format!("{:?}", eight);
321        let sixteen_debug = format!("{:?}", sixteen);
322
323        assert_eq!(eight_debug, "Eight");
324        assert_eq!(sixteen_debug, "Sixteen");
325    }
326
327    #[test]
328    fn test_word_size_clone_copy() {
329        let original = WordSize::Eight;
330        let cloned = original.clone();
331        let copied = original;
332
333        assert_eq!(original, cloned);
334        assert_eq!(original, copied);
335        assert_eq!(cloned, copied);
336    }
337
338    #[test]
339    fn test_color_type_alias() {
340        // Test that Color is an alias for Rgb888
341        let red_color: Color = Color::RED;
342        let red_rgb888: Rgb888 = Rgb888::RED;
343
344        assert_eq!(red_color, red_rgb888);
345        assert_eq!(red_color.r(), 255);
346        assert_eq!(red_color.g(), 0);
347        assert_eq!(red_color.b(), 0);
348
349        // Test various colors
350        let colors = [
351            (Color::RED, (255, 0, 0)),
352            (Color::GREEN, (0, 255, 0)),
353            (Color::BLUE, (0, 0, 255)),
354            (Color::WHITE, (255, 255, 255)),
355            (Color::BLACK, (0, 0, 0)),
356            (Color::CYAN, (0, 255, 255)),
357            (Color::MAGENTA, (255, 0, 255)),
358            (Color::YELLOW, (255, 255, 0)),
359        ];
360
361        for (color, (r, g, b)) in colors {
362            assert_eq!(color.r(), r);
363            assert_eq!(color.g(), g);
364            assert_eq!(color.b(), b);
365        }
366    }
367
368    #[test]
369    fn test_color_construction() {
370        // Test Color construction from RGB values
371        let custom_color = Color::new(128, 64, 192);
372        assert_eq!(custom_color.r(), 128);
373        assert_eq!(custom_color.g(), 64);
374        assert_eq!(custom_color.b(), 192);
375
376        // Test that it behaves like Rgb888
377        let rgb888_color = Rgb888::new(128, 64, 192);
378        assert_eq!(custom_color, rgb888_color);
379    }
380
381    #[test]
382    fn test_helper_functions_const() {
383        // Test that helper functions can be used in const contexts
384        const ROWS: usize = 32;
385        const COMPUTED_NROWS: usize = compute_rows(ROWS);
386        const BITS: u8 = 4;
387        const COMPUTED_FRAME_COUNT: usize = compute_frame_count(BITS);
388
389        assert_eq!(COMPUTED_NROWS, 16);
390        assert_eq!(COMPUTED_FRAME_COUNT, 15);
391    }
392
393    #[test]
394    fn test_realistic_panel_configurations() {
395        // Test common HUB75 panel configurations
396        struct PanelConfig {
397            rows: usize,
398            cols: usize,
399            bits: u8,
400        }
401
402        let configs = [
403            PanelConfig {
404                rows: 32,
405                cols: 64,
406                bits: 3,
407            }, // 32x64 panel, 3-bit color
408            PanelConfig {
409                rows: 64,
410                cols: 64,
411                bits: 4,
412            }, // 64x64 panel, 4-bit color
413            PanelConfig {
414                rows: 32,
415                cols: 32,
416                bits: 5,
417            }, // 32x32 panel, 5-bit color
418            PanelConfig {
419                rows: 16,
420                cols: 32,
421                bits: 6,
422            }, // 16x32 panel, 6-bit color
423        ];
424
425        for config in configs {
426            let nrows = compute_rows(config.rows);
427            let frame_count = compute_frame_count(config.bits);
428
429            // Basic sanity checks for rows
430            assert!(nrows > 0);
431            assert!(nrows <= config.rows);
432            assert_eq!(nrows * 2, config.rows);
433
434            // Basic sanity checks for columns
435            assert!(config.cols > 0);
436            assert!(config.cols <= 256); // Reasonable upper limit for HUB75 panels
437
438            // Frame count checks
439            assert!(frame_count > 0);
440            assert!(frame_count < 256); // Should be reasonable for typical bit depths
441
442            // Frame count should grow with bit depth
443            let prev_frame_count = compute_frame_count(config.bits - 1);
444            assert!(frame_count > prev_frame_count);
445        }
446    }
447
448    #[test]
449    fn test_memory_calculations() {
450        // Test that we can calculate memory requirements using helper functions
451        const ROWS: usize = 64;
452        const COLS: usize = 64;
453        const BITS: u8 = 4;
454
455        const NROWS: usize = compute_rows(ROWS);
456        const FRAME_COUNT: usize = compute_frame_count(BITS);
457
458        // These should be compile-time constants
459        assert_eq!(NROWS, 32);
460        assert_eq!(FRAME_COUNT, 15);
461
462        // Verify the relationship between parameters
463        assert_eq!(NROWS * 2, ROWS);
464        assert_eq!(FRAME_COUNT, (1 << BITS) - 1);
465
466        // Verify COLS is reasonable for memory calculations
467        assert!(COLS > 0);
468        assert!(COLS <= 256); // Reasonable limit for HUB75 panels
469    }
470
471    #[test]
472    fn test_edge_cases() {
473        // Test minimum values
474        assert_eq!(compute_rows(2), 1);
475        assert_eq!(compute_frame_count(1), 1);
476
477        // Test maximum reasonable values
478        assert_eq!(compute_rows(512), 256);
479        assert_eq!(compute_frame_count(8), 255);
480
481        // Test zero (though not practical)
482        assert_eq!(compute_rows(0), 0);
483    }
484
485    // Note: We can't easily test the traits directly since they're abstract,
486    // but they are thoroughly tested through their implementations in
487    // the plain and latched modules.
488
489    #[test]
490    fn test_word_size_equality() {
491        // Test all combinations of equality
492        assert_eq!(WordSize::Eight, WordSize::Eight);
493        assert_eq!(WordSize::Sixteen, WordSize::Sixteen);
494        assert_ne!(WordSize::Eight, WordSize::Sixteen);
495        assert_ne!(WordSize::Sixteen, WordSize::Eight);
496    }
497
498    #[test]
499    fn test_bit_depth_limits() {
500        // Test that our bit depth calculations work for the full range
501        for bits in 1..=8 {
502            let frame_count = compute_frame_count(bits);
503
504            // Frame count should be positive
505            assert!(frame_count > 0);
506
507            // Frame count should be less than 2^bits
508            assert!(frame_count < (1 << bits));
509
510            // Frame count should be exactly (2^bits) - 1
511            assert_eq!(frame_count, (1 << bits) - 1);
512        }
513    }
514
515    #[test]
516    fn test_documentation_examples() {
517        // Test the example values from the documentation
518        const ROWS: usize = 32;
519        const COLS: usize = 64;
520        const NROWS: usize = ROWS / 2;
521        const BITS: u8 = 8;
522        const FRAME_COUNT: usize = (1 << BITS) - 1;
523
524        // Verify using our helper functions
525        assert_eq!(compute_rows(ROWS), NROWS);
526        assert_eq!(compute_frame_count(BITS), FRAME_COUNT);
527
528        // Verify the values match documentation
529        assert_eq!(ROWS, 32);
530        assert_eq!(COLS, 64);
531        assert_eq!(NROWS, 16);
532        assert_eq!(FRAME_COUNT, 255);
533
534        // Verify this matches typical panel dimensions
535        assert!(COLS > 0);
536        assert_eq!(NROWS * 2, ROWS);
537    }
538}