hub75/
lib.rs

1#![no_std]
2use embedded_hal::blocking::delay::DelayUs;
3use embedded_hal::digital::v2::OutputPin;
4// Inspired by
5// - https://github.com/polyfloyd/ledcat/blob/master/src/device/hub75.rs
6// - https://github.com/mmou/led-marquee/blob/8c88531a6938edff6db829ca21c15304515874ea/src/hub.rs
7// - https://github.com/adafruit/RGB-matrix-Panel/blob/master/RGBmatrixPanel.cpp
8// - https://www.mikrocontroller.net/topic/452187 (sorry, german only)
9
10/// # Theory of Operation
11/// This display is essentially split in half, with the top 16 rows being
12/// controlled by one set of shift registers (r1, g1, b1) and the botton 16
13/// rows by another set (r2, g2, b2). So, the best way to update it is to
14/// show one of the botton and top rows in tandem. The row (between 0-15) is then
15/// selected by the A, B, C, D pins, which are just, as one might expect, the bits 0 to 3.
16///
17/// The display doesn't really do brightness, so we have to do it ourselves, by
18/// rendering the same frame multiple times, with some pixels being turned of if
19/// they are darker (pwm)
20
21pub struct Hub75<PINS> {
22    //       r1, g1, b1, r2, g2, b2, column, row
23    data: [[(u8, u8, u8, u8, u8, u8); 64]; 16],
24    brightness_step: u8,
25    brightness_count: u8,
26    pins: PINS,
27}
28
29/// A trait, so that it's easier to reason about the pins
30/// Implemented for a tuple `(r1, g1, b1, r2, g2, b2, a, b, c, d, clk, lat, oe)`
31/// with every element implementing `OutputPin`
32pub trait Outputs {
33    type R1: OutputPin;
34    type G1: OutputPin;
35    type B1: OutputPin;
36    type R2: OutputPin;
37    type G2: OutputPin;
38    type B2: OutputPin;
39    type A: OutputPin;
40    type B: OutputPin;
41    type C: OutputPin;
42    type D: OutputPin;
43    type CLK: OutputPin;
44    type LAT: OutputPin;
45    type OE: OutputPin;
46    fn r1(&mut self) -> &mut Self::R1;
47    fn g1(&mut self) -> &mut Self::G1;
48    fn b1(&mut self) -> &mut Self::B1;
49    fn r2(&mut self) -> &mut Self::R2;
50    fn g2(&mut self) -> &mut Self::G2;
51    fn b2(&mut self) -> &mut Self::B2;
52    fn a(&mut self) -> &mut Self::A;
53    fn b(&mut self) -> &mut Self::B;
54    fn c(&mut self) -> &mut Self::C;
55    fn d(&mut self) -> &mut Self::D;
56    fn clk(&mut self) -> &mut Self::CLK;
57    fn lat(&mut self) -> &mut Self::LAT;
58    fn oe(&mut self) -> &mut Self::OE;
59}
60
61impl<
62        R1: OutputPin,
63        G1: OutputPin,
64        B1: OutputPin,
65        R2: OutputPin,
66        G2: OutputPin,
67        B2: OutputPin,
68        A: OutputPin,
69        B: OutputPin,
70        C: OutputPin,
71        D: OutputPin,
72        CLK: OutputPin,
73        LAT: OutputPin,
74        OE: OutputPin,
75    > Outputs for (R1, G1, B1, R2, G2, B2, A, B, C, D, CLK, LAT, OE)
76{
77    type R1 = R1;
78    type G1 = G1;
79    type B1 = B1;
80    type R2 = R2;
81    type G2 = G2;
82    type B2 = B2;
83    type A = A;
84    type B = B;
85    type C = C;
86    type D = D;
87    type CLK = CLK;
88    type LAT = LAT;
89    type OE = OE;
90    fn r1(&mut self) -> &mut R1 {
91        &mut self.0
92    }
93    fn g1(&mut self) -> &mut G1 {
94        &mut self.1
95    }
96    fn b1(&mut self) -> &mut B1 {
97        &mut self.2
98    }
99    fn r2(&mut self) -> &mut R2 {
100        &mut self.3
101    }
102    fn g2(&mut self) -> &mut G2 {
103        &mut self.4
104    }
105    fn b2(&mut self) -> &mut B2 {
106        &mut self.5
107    }
108    fn a(&mut self) -> &mut A {
109        &mut self.6
110    }
111    fn b(&mut self) -> &mut B {
112        &mut self.7
113    }
114    fn c(&mut self) -> &mut C {
115        &mut self.8
116    }
117    fn d(&mut self) -> &mut D {
118        &mut self.9
119    }
120    fn clk(&mut self) -> &mut CLK {
121        &mut self.10
122    }
123    fn lat(&mut self) -> &mut LAT {
124        &mut self.11
125    }
126    fn oe(&mut self) -> &mut OE {
127        &mut self.12
128    }
129}
130
131impl<PINS: Outputs> Hub75<PINS> {
132    /// Create a new hub instance
133    ///
134    /// Takes an implementation of the Outputs trait,
135    /// using a tuple `(r1, g1, b1, r2, g2, b2, a, b, c, d, clk, lat, oe)`,
136    /// with every member implementing `OutputPin` is usually the right choice.
137    ///
138    /// `brightness_bits` provides the number of brightness_bits for each color (1-8).
139    /// More bits allow for much more colors, especially in combination with the gamma correction,
140    /// but each extra bit doubles the time `output` will take. This might lead to noticable flicker.
141    ///
142    /// 3-4 bits are usually a good choice.
143    pub fn new(pins: PINS, brightness_bits: u8) -> Self {
144        assert!(brightness_bits < 9 && brightness_bits > 0);
145        let data = [[(0, 0, 0, 0, 0, 0); 64]; 16];
146        let brightness_step = 1 << (8 - brightness_bits);
147        let brightness_count = ((1 << brightness_bits as u16) - 1) as u8;
148        Self {
149            data,
150            brightness_step,
151            brightness_count,
152            pins,
153        }
154    }
155
156    /// Output the buffer to the display
157    ///
158    /// Takes some time and should be called quite often, otherwise the output
159    /// will flicker
160    pub fn output<DELAY: DelayUs<u8>>(&mut self, delay: &mut DELAY) {
161        // Enable the output
162        // The previous last row will continue to display
163        self.pins.oe().set_low().ok();
164        // PWM cycle
165        for mut brightness in 0..self.brightness_count {
166            brightness = (brightness + 1).saturating_mul(self.brightness_step);
167            for (count, row) in self.data.iter().enumerate() {
168                for element in row.iter() {
169                    if element.0 >= brightness {
170                        self.pins.r1().set_high().ok();
171                    } else {
172                        self.pins.r1().set_low().ok();
173                    }
174                    if element.1 >= brightness {
175                        self.pins.g1().set_high().ok();
176                    } else {
177                        self.pins.g1().set_low().ok();
178                    }
179                    if element.2 >= brightness {
180                        self.pins.b1().set_high().ok();
181                    } else {
182                        self.pins.b1().set_low().ok();
183                    }
184                    if element.3 >= brightness {
185                        self.pins.r2().set_high().ok();
186                    } else {
187                        self.pins.r2().set_low().ok();
188                    }
189                    if element.4 >= brightness {
190                        self.pins.g2().set_high().ok();
191                    } else {
192                        self.pins.g2().set_low().ok();
193                    }
194                    if element.5 >= brightness {
195                        self.pins.b2().set_high().ok();
196                    } else {
197                        self.pins.b2().set_low().ok();
198                    }
199                    self.pins.clk().set_high().ok();
200                    self.pins.clk().set_low().ok();
201                }
202                self.pins.oe().set_high().ok();
203                // Prevents ghosting, no idea why
204                delay.delay_us(2);
205                self.pins.lat().set_low().ok();
206                delay.delay_us(2);
207                self.pins.lat().set_high().ok();
208                // Select row
209                if count & 1 != 0 {
210                    self.pins.a().set_high().ok();
211                } else {
212                    self.pins.a().set_low().ok();
213                }
214                if count & 2 != 0 {
215                    self.pins.b().set_high().ok();
216                } else {
217                    self.pins.b().set_low().ok();
218                }
219                if count & 4 != 0 {
220                    self.pins.c().set_high().ok();
221                } else {
222                    self.pins.c().set_low().ok();
223                }
224                if count & 8 != 0 {
225                    self.pins.d().set_high().ok();
226                } else {
227                    self.pins.d().set_low().ok();
228                }
229                delay.delay_us(2);
230                self.pins.oe().set_low().ok();
231            }
232        }
233        // Disable the output
234        // Prevents one row from being much brighter than the others
235        self.pins.oe().set_high().ok();
236    }
237    /// Clear the output
238    ///
239    /// It's a bit faster than using the embedded_graphics interface
240    /// to do the same
241    pub fn clear(&mut self) {
242        for row in self.data.iter_mut() {
243            for e in row.iter_mut() {
244                e.0 = 0;
245                e.1 = 0;
246                e.2 = 0;
247                e.3 = 0;
248                e.4 = 0;
249                e.5 = 0;
250            }
251        }
252    }
253}
254
255use embedded_graphics::{
256    drawable::{Dimensions, Pixel},
257    pixelcolor::Rgb565,
258    Drawing, SizedDrawing,
259};
260impl<PINS: Outputs> Drawing<Rgb565> for Hub75<PINS> {
261    fn draw<T>(&mut self, item_pixels: T)
262    where
263        T: IntoIterator<Item = Pixel<Rgb565>>,
264    {
265        // This table remaps linear input values
266        // (the numbers we’d like to use; e.g. 127 = half brightness)
267        // to nonlinear gamma-corrected output values
268        // (numbers producing the desired effect on the LED;
269        // e.g. 36 = half brightness).
270        const GAMMA8: [u8; 256] = [
271            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
272            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4,
273            4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11,
274            12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22,
275            22, 23, 24, 24, 25, 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, 37,
276            38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, 51, 52, 54, 55, 56, 57, 58,
277            59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85,
278            86, 87, 89, 90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 109, 110, 112, 114,
279            115, 117, 119, 120, 122, 124, 126, 127, 129, 131, 133, 135, 137, 138, 140, 142, 144,
280            146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 167, 169, 171, 173, 175, 177, 180,
281            182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208, 210, 213, 215, 218, 220,
282            223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 255,
283        ];
284        for Pixel(coord, color) in item_pixels {
285            let row = coord[1] % 16;
286            let data = &mut self.data[row as usize][coord[0] as usize];
287            if coord[1] >= 16 {
288                data.3 = GAMMA8[color.r() as usize];
289                data.4 = GAMMA8[color.g() as usize];
290                data.5 = GAMMA8[color.b() as usize];
291            } else {
292                data.0 = GAMMA8[color.r() as usize];
293                data.1 = GAMMA8[color.g() as usize];
294                data.2 = GAMMA8[color.b() as usize];
295            }
296        }
297    }
298}
299
300// TODO Does it make sense to include this?
301impl<PINS: Outputs> SizedDrawing<Rgb565> for Hub75<PINS> {
302    fn draw_sized<T>(&mut self, item_pixels: T)
303    where
304        T: IntoIterator<Item = Pixel<Rgb565>> + Dimensions,
305    {
306        self.draw(item_pixels);
307    }
308}