Skip to main content

m5stack_core/driver/
sk6812.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! SK6812 addressable-RGB LED-strip driver (M5GO Battery Bottom light bars).
3//!
4//! The M5GO/FIRE Battery Bottom carries **10 SK6812 3535 RGB LEDs** (two bars of
5//! five) on a single one-wire data line. SK6812 uses the same NRZ bit encoding
6//! as WS2812 — 24 bits per LED in **G,R,B** order, MSB first — so this is driven
7//! by the RMT peripheral, one [`PulseCode`] per bit, like the 1-Wire driver.
8//!
9//! ## Pin (this is the subtle part across boards)
10//!
11//! The bottom's LED data line is wired to a fixed *physical* M-Bus pin (pin 23,
12//! the classic `I2S_OUT` position). That physical pin maps to a different GPIO
13//! per core, so pass the right one:
14//!   - **Fire27 (ESP32):**   `GPIO15`  (M-Bus pin 23)
15//!   - **CoreS3 (ESP32-S3):** `GPIO13`  (M-Bus pin 23)
16//!
17//! On **CoreS3** the bars are also unpowered until the M-Bus 5 V rail is enabled
18//! via the AW9523 — call [`crate::driver::aw9523b::Aw9523bDriver::enable_bus_5v`]
19//! first (see its guard notes). On the Fire that rail is always present.
20//!
21//! ## Timing
22//!
23//! RMT source clock 80 MHz, `clk_divider = 1` → 12.5 ns/tick. SK6812 nominal:
24//! T0H 0.3 µs / T0L 0.9 µs, T1H 0.6 µs / T1L 0.6 µs, reset ≥ 80 µs low.
25//!
26//! Refs: SK6812 datasheet; <https://docs.m5stack.com/en/base/m5go_bottom>
27use esp_hal::{
28    Async,
29    gpio::{AnyPin, Level},
30    peripherals::RMT,
31    rmt::{Channel, PulseCode, Rmt, Tx, TxChannelConfig, TxChannelCreator},
32    time::Rate,
33};
34use thiserror_no_std::Error;
35
36/// Max LEDs this driver will drive in one frame (M5GO bottom uses 10).
37/// Sets the size of the per-frame pulse buffer; bump if you chain more.
38pub const MAX_LEDS: usize = 16;
39/// 24 bits/LED + trailing reset code + end marker.
40const MAX_CODES: usize = MAX_LEDS * 24 + 2;
41
42// Bit timing in 12.5 ns ticks (80 MHz, divider 1).
43const T0H: u16 = 24; // 0.30 µs
44const T0L: u16 = 72; // 0.90 µs
45const T1H: u16 = 48; // 0.60 µs
46const T1L: u16 = 48; // 0.60 µs
47const RESET_HALF: u16 = 4000; // 2 × 50 µs = 100 µs low latch
48
49/// A single LED colour (8 bits per channel).
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub struct Rgb {
52    pub r: u8,
53    pub g: u8,
54    pub b: u8,
55}
56
57impl Rgb {
58    pub const OFF: Rgb = Rgb { r: 0, g: 0, b: 0 };
59
60    pub const fn new(r: u8, g: u8, b: u8) -> Self {
61        Self { r, g, b }
62    }
63}
64
65#[derive(Debug, Error)]
66pub enum Sk6812Error {
67    #[error("Failed to init RMT")]
68    Rmt(#[from] esp_hal::rmt::Error),
69    #[error("RMT config error")]
70    Config,
71    #[error("Too many LEDs (max {MAX_LEDS})")]
72    TooManyLeds,
73}
74
75pub struct Sk6812Driver {
76    channel: Channel<'static, Async, Tx>,
77}
78
79impl Sk6812Driver {
80    /// Claim the RMT peripheral (TX channel 0) and drive `pin` as the LED data
81    /// line. Note: the RMT peripheral is taken whole here — it cannot also be
82    /// handed to the 1-Wire temperature driver in the same app.
83    pub fn new(rmt: RMT<'static>, pin: AnyPin<'static>) -> Result<Self, Sk6812Error> {
84        let rmt = Rmt::new(rmt, Rate::from_mhz(80))
85            .map_err(|_| Sk6812Error::Config)?
86            .into_async();
87        let channel = rmt
88            .channel0
89            .configure_tx(
90                &TxChannelConfig::default()
91                    .with_clk_divider(1)
92                    .with_idle_output(true)
93                    .with_idle_output_level(Level::Low),
94            )
95            .map_err(|_| Sk6812Error::Config)?
96            .with_pin(pin);
97        Ok(Self { channel })
98    }
99
100    /// Push a frame of colours to the strip (LED 0 first). Holds the line low
101    /// afterwards to latch. Errors if `colors.len() > MAX_LEDS`.
102    pub async fn write(&mut self, colors: &[Rgb]) -> Result<(), Sk6812Error> {
103        if colors.len() > MAX_LEDS {
104            return Err(Sk6812Error::TooManyLeds);
105        }
106        let one = PulseCode::new(Level::High, T1H, Level::Low, T1L);
107        let zero = PulseCode::new(Level::High, T0H, Level::Low, T0L);
108
109        let mut buf: heapless::Vec<PulseCode, MAX_CODES> = heapless::Vec::new();
110        for c in colors {
111            // SK6812 wire order is G, R, B, MSB first.
112            for byte in [c.g, c.r, c.b] {
113                for bit in 0..8 {
114                    let code = if byte & (0x80 >> bit) != 0 { one } else { zero };
115                    // Buffer holds MAX_LEDS*24 + 2 and we checked the LED count.
116                    let _ = buf.push(code);
117                }
118            }
119        }
120        // Trailing low pulse → reset/latch (≥ 80 µs), then the required end
121        // marker (RMT errors with `EndMarkerMissing` without a final zero-length
122        // code; the marker idles the line low, holding the latch).
123        let _ = buf.push(PulseCode::new(
124            Level::Low,
125            RESET_HALF,
126            Level::Low,
127            RESET_HALF,
128        ));
129        let _ = buf.push(PulseCode::end_marker());
130
131        self.channel.transmit(&buf).await?;
132        Ok(())
133    }
134
135    /// Convenience: set every LED in a strip of `len` to the same colour.
136    pub async fn fill(&mut self, color: Rgb, len: usize) -> Result<(), Sk6812Error> {
137        let len = len.min(MAX_LEDS);
138        let mut frame: heapless::Vec<Rgb, MAX_LEDS> = heapless::Vec::new();
139        for _ in 0..len {
140            let _ = frame.push(color);
141        }
142        self.write(&frame).await
143    }
144}