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}