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#![no_std]
52#![warn(missing_docs)]
53#![warn(clippy::all)]
54#![warn(clippy::pedantic)]
55#![allow(clippy::cast_possible_truncation)]
56#![allow(clippy::cast_sign_loss)]
57
58#[cfg(not(feature = "esp-dma"))]
59use embedded_dma::ReadBuffer;
60use embedded_graphics::draw_target::DrawTarget;
61use embedded_graphics::pixelcolor::Rgb888;
62#[cfg(feature = "esp-dma")]
63use esp_hal::dma::ReadBuffer;
64
65pub mod latched;
66pub mod plain;
67
68/// Color type used in the framebuffer
69pub type Color = Rgb888;
70
71/// Word size configuration for the framebuffer
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum WordSize {
74    /// 8-bit word size
75    Eight,
76    /// 16-bit word size
77    Sixteen,
78}
79
80/// Computes the NROWS value from ROWS for `DmaFrameBuffer`
81///
82/// # Arguments
83///
84/// * `rows` - Total number of rows in the display
85///
86/// # Returns
87///
88/// Number of rows needed internally for `DmaFrameBuffer`
89#[must_use]
90pub const fn compute_rows(rows: usize) -> usize {
91    rows / 2
92}
93
94/// Computes the number of frames needed for a given bit depth
95///
96/// This is used to determine how many frames are needed to achieve
97/// the desired color depth through Binary Code Modulation (BCM).
98///
99/// # Arguments
100///
101/// * `bits` - Number of bits per color channel
102///
103/// # Returns
104///
105/// Number of frames required for the given bit depth
106#[must_use]
107pub const fn compute_frame_count(bits: u8) -> usize {
108    (1usize << bits) - 1
109}
110
111/// Trait for read-only framebuffers
112///
113/// This trait defines the basic functionality required for a framebuffer
114/// that can be read from and transferred via DMA.
115///
116/// # Type Parameters
117///
118/// * `ROWS` - Total number of rows in the display
119/// * `COLS` - Number of columns in the display
120/// * `NROWS` - Number of rows processed in parallel
121/// * `BITS` - Number of bits per color channel
122/// * `FRAME_COUNT` - Number of frames needed for BCM
123pub trait FrameBuffer<
124    const ROWS: usize,
125    const COLS: usize,
126    const NROWS: usize,
127    const BITS: u8,
128    const FRAME_COUNT: usize,
129>: ReadBuffer
130{
131    /// Returns the word size configuration for this framebuffer
132    fn get_word_size(&self) -> WordSize;
133}
134
135/// Trait for mutable framebuffers
136///
137/// This trait extends `FrameBuffer` with the ability to draw to the framebuffer
138/// using the `embedded_graphics` drawing primitives.
139///
140/// # Type Parameters
141///
142/// * `ROWS` - Total number of rows in the display
143/// * `COLS` - Number of columns in the display
144/// * `NROWS` - Number of rows processed in parallel
145/// * `BITS` - Number of bits per color channel
146/// * `FRAME_COUNT` - Number of frames needed for BCM
147pub trait MutableFrameBuffer<
148    const ROWS: usize,
149    const COLS: usize,
150    const NROWS: usize,
151    const BITS: u8,
152    const FRAME_COUNT: usize,
153>:
154    FrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
155    + DrawTarget<Color = Color, Error = core::convert::Infallible>
156{
157}
158
159#[cfg(test)]
160mod tests {
161    extern crate std;
162
163    use std::format;
164
165    use super::*;
166    use embedded_graphics::pixelcolor::RgbColor;
167
168    #[test]
169    fn test_compute_rows() {
170        // Test typical panel sizes
171        assert_eq!(compute_rows(32), 16);
172        assert_eq!(compute_rows(64), 32);
173        assert_eq!(compute_rows(16), 8);
174        assert_eq!(compute_rows(128), 64);
175
176        // Test edge cases
177        assert_eq!(compute_rows(2), 1);
178        assert_eq!(compute_rows(0), 0);
179
180        // Test that it always divides by 2
181        for rows in [8, 16, 24, 32, 48, 64, 96, 128, 256] {
182            assert_eq!(compute_rows(rows), rows / 2);
183        }
184    }
185
186    #[test]
187    fn test_compute_frame_count() {
188        // Test common bit depths
189        assert_eq!(compute_frame_count(1), 1); // 2^1 - 1 = 1
190        assert_eq!(compute_frame_count(2), 3); // 2^2 - 1 = 3
191        assert_eq!(compute_frame_count(3), 7); // 2^3 - 1 = 7
192        assert_eq!(compute_frame_count(4), 15); // 2^4 - 1 = 15
193        assert_eq!(compute_frame_count(5), 31); // 2^5 - 1 = 31
194        assert_eq!(compute_frame_count(6), 63); // 2^6 - 1 = 63
195        assert_eq!(compute_frame_count(7), 127); // 2^7 - 1 = 127
196        assert_eq!(compute_frame_count(8), 255); // 2^8 - 1 = 255
197
198        // Test the formula: (2^bits) - 1
199        for bits in 1..=8 {
200            let expected = (1usize << bits) - 1;
201            assert_eq!(compute_frame_count(bits), expected);
202        }
203    }
204
205    #[test]
206    fn test_compute_frame_count_properties() {
207        // Test that frame count grows exponentially
208        assert!(compute_frame_count(2) > compute_frame_count(1));
209        assert!(compute_frame_count(3) > compute_frame_count(2));
210        assert!(compute_frame_count(4) > compute_frame_count(3));
211
212        // Test doubling property: each additional bit approximately doubles frame count
213        for bits in 1..=7 {
214            let current_frames = compute_frame_count(bits);
215            let next_frames = compute_frame_count(bits + 1);
216            // next_frames should be approximately 2 * current_frames + 1
217            assert_eq!(next_frames, 2 * current_frames + 1);
218        }
219    }
220
221    #[test]
222    fn test_word_size_enum() {
223        // Test enum values
224        let eight = WordSize::Eight;
225        let sixteen = WordSize::Sixteen;
226
227        assert_ne!(eight, sixteen);
228        assert_eq!(eight, WordSize::Eight);
229        assert_eq!(sixteen, WordSize::Sixteen);
230    }
231
232    #[test]
233    fn test_word_size_debug() {
234        let eight = WordSize::Eight;
235        let sixteen = WordSize::Sixteen;
236
237        let eight_debug = format!("{:?}", eight);
238        let sixteen_debug = format!("{:?}", sixteen);
239
240        assert_eq!(eight_debug, "Eight");
241        assert_eq!(sixteen_debug, "Sixteen");
242    }
243
244    #[test]
245    fn test_word_size_clone_copy() {
246        let original = WordSize::Eight;
247        let cloned = original.clone();
248        let copied = original;
249
250        assert_eq!(original, cloned);
251        assert_eq!(original, copied);
252        assert_eq!(cloned, copied);
253    }
254
255    #[test]
256    fn test_color_type_alias() {
257        // Test that Color is an alias for Rgb888
258        let red_color: Color = Color::RED;
259        let red_rgb888: Rgb888 = Rgb888::RED;
260
261        assert_eq!(red_color, red_rgb888);
262        assert_eq!(red_color.r(), 255);
263        assert_eq!(red_color.g(), 0);
264        assert_eq!(red_color.b(), 0);
265
266        // Test various colors
267        let colors = [
268            (Color::RED, (255, 0, 0)),
269            (Color::GREEN, (0, 255, 0)),
270            (Color::BLUE, (0, 0, 255)),
271            (Color::WHITE, (255, 255, 255)),
272            (Color::BLACK, (0, 0, 0)),
273            (Color::CYAN, (0, 255, 255)),
274            (Color::MAGENTA, (255, 0, 255)),
275            (Color::YELLOW, (255, 255, 0)),
276        ];
277
278        for (color, (r, g, b)) in colors {
279            assert_eq!(color.r(), r);
280            assert_eq!(color.g(), g);
281            assert_eq!(color.b(), b);
282        }
283    }
284
285    #[test]
286    fn test_color_construction() {
287        // Test Color construction from RGB values
288        let custom_color = Color::new(128, 64, 192);
289        assert_eq!(custom_color.r(), 128);
290        assert_eq!(custom_color.g(), 64);
291        assert_eq!(custom_color.b(), 192);
292
293        // Test that it behaves like Rgb888
294        let rgb888_color = Rgb888::new(128, 64, 192);
295        assert_eq!(custom_color, rgb888_color);
296    }
297
298    #[test]
299    fn test_helper_functions_const() {
300        // Test that helper functions can be used in const contexts
301        const ROWS: usize = 32;
302        const COMPUTED_NROWS: usize = compute_rows(ROWS);
303        const BITS: u8 = 4;
304        const COMPUTED_FRAME_COUNT: usize = compute_frame_count(BITS);
305
306        assert_eq!(COMPUTED_NROWS, 16);
307        assert_eq!(COMPUTED_FRAME_COUNT, 15);
308    }
309
310    #[test]
311    fn test_realistic_panel_configurations() {
312        // Test common HUB75 panel configurations
313        struct PanelConfig {
314            rows: usize,
315            cols: usize,
316            bits: u8,
317        }
318
319        let configs = [
320            PanelConfig {
321                rows: 32,
322                cols: 64,
323                bits: 3,
324            }, // 32x64 panel, 3-bit color
325            PanelConfig {
326                rows: 64,
327                cols: 64,
328                bits: 4,
329            }, // 64x64 panel, 4-bit color
330            PanelConfig {
331                rows: 32,
332                cols: 32,
333                bits: 5,
334            }, // 32x32 panel, 5-bit color
335            PanelConfig {
336                rows: 16,
337                cols: 32,
338                bits: 6,
339            }, // 16x32 panel, 6-bit color
340        ];
341
342        for config in configs {
343            let nrows = compute_rows(config.rows);
344            let frame_count = compute_frame_count(config.bits);
345
346            // Basic sanity checks for rows
347            assert!(nrows > 0);
348            assert!(nrows <= config.rows);
349            assert_eq!(nrows * 2, config.rows);
350
351            // Basic sanity checks for columns
352            assert!(config.cols > 0);
353            assert!(config.cols <= 256); // Reasonable upper limit for HUB75 panels
354
355            // Frame count checks
356            assert!(frame_count > 0);
357            assert!(frame_count < 256); // Should be reasonable for typical bit depths
358
359            // Frame count should grow with bit depth
360            let prev_frame_count = compute_frame_count(config.bits - 1);
361            assert!(frame_count > prev_frame_count);
362        }
363    }
364
365    #[test]
366    fn test_memory_calculations() {
367        // Test that we can calculate memory requirements using helper functions
368        const ROWS: usize = 64;
369        const COLS: usize = 64;
370        const BITS: u8 = 4;
371
372        const NROWS: usize = compute_rows(ROWS);
373        const FRAME_COUNT: usize = compute_frame_count(BITS);
374
375        // These should be compile-time constants
376        assert_eq!(NROWS, 32);
377        assert_eq!(FRAME_COUNT, 15);
378
379        // Verify the relationship between parameters
380        assert_eq!(NROWS * 2, ROWS);
381        assert_eq!(FRAME_COUNT, (1 << BITS) - 1);
382
383        // Verify COLS is reasonable for memory calculations
384        assert!(COLS > 0);
385        assert!(COLS <= 256); // Reasonable limit for HUB75 panels
386    }
387
388    #[test]
389    fn test_edge_cases() {
390        // Test minimum values
391        assert_eq!(compute_rows(2), 1);
392        assert_eq!(compute_frame_count(1), 1);
393
394        // Test maximum reasonable values
395        assert_eq!(compute_rows(512), 256);
396        assert_eq!(compute_frame_count(8), 255);
397
398        // Test zero (though not practical)
399        assert_eq!(compute_rows(0), 0);
400    }
401
402    // Note: We can't easily test the traits directly since they're abstract,
403    // but they are thoroughly tested through their implementations in
404    // the plain and latched modules.
405
406    #[test]
407    fn test_word_size_equality() {
408        // Test all combinations of equality
409        assert_eq!(WordSize::Eight, WordSize::Eight);
410        assert_eq!(WordSize::Sixteen, WordSize::Sixteen);
411        assert_ne!(WordSize::Eight, WordSize::Sixteen);
412        assert_ne!(WordSize::Sixteen, WordSize::Eight);
413    }
414
415    #[test]
416    fn test_bit_depth_limits() {
417        // Test that our bit depth calculations work for the full range
418        for bits in 1..=8 {
419            let frame_count = compute_frame_count(bits);
420
421            // Frame count should be positive
422            assert!(frame_count > 0);
423
424            // Frame count should be less than 2^bits
425            assert!(frame_count < (1 << bits));
426
427            // Frame count should be exactly (2^bits) - 1
428            assert_eq!(frame_count, (1 << bits) - 1);
429        }
430    }
431
432    #[test]
433    fn test_documentation_examples() {
434        // Test the example values from the documentation
435        const ROWS: usize = 32;
436        const COLS: usize = 64;
437        const NROWS: usize = ROWS / 2;
438        const BITS: u8 = 8;
439        const FRAME_COUNT: usize = (1 << BITS) - 1;
440
441        // Verify using our helper functions
442        assert_eq!(compute_rows(ROWS), NROWS);
443        assert_eq!(compute_frame_count(BITS), FRAME_COUNT);
444
445        // Verify the values match documentation
446        assert_eq!(ROWS, 32);
447        assert_eq!(COLS, 64);
448        assert_eq!(NROWS, 16);
449        assert_eq!(FRAME_COUNT, 255);
450
451        // Verify this matches typical panel dimensions
452        assert!(COLS > 0);
453        assert_eq!(NROWS * 2, ROWS);
454    }
455}