rustyfarian_avr_ws2812/lib.rs
1#![no_std]
2#![cfg_attr(
3 all(feature = "bitbang", target_arch = "avr"),
4 feature(asm_experimental_arch)
5)]
6//! WS2812 (NeoPixel) LED driver for AVR with two backends.
7//!
8//! Both backends share the same `&[RGB8]`-based public API and run every
9//! [`ferriswheel`](https://crates.io/crates/ferriswheel) effect unchanged.
10//! All animation logic stays in `ferriswheel` / `bunting`; these drivers are
11//! thin hardware wrappers.
12//!
13//! # Choosing a Backend
14//!
15//! | Aspect | [`Ws2812Spi`] (SPI prerendered) | [`Ws2812BitBang`] (cycle-counted asm) |
16//! |:-------------------|:--------------------------------|:--------------------------------------|
17//! | Cargo feature | always available | `bitbang` (opt-in) |
18//! | Status | works on tolerant strips | **recommended; hardware-validated** |
19//! | Hardware | any AVR with SPI peripheral | ATmega328P @ 16 MHz |
20//! | Pin | the SPI MOSI pin | any pin on PORTB / PORTC / PORTD |
21//! | WS2812 timing | `T0H = 500 ns`, `T1H = 1500 ns` (relies on chip tolerance) | `T0H = 250 ns`, `T1H = 812 ns` (in WS2812B spec) |
22//! | Interrupts | caller wraps `write` in `avr_device::interrupt::free` | wrapped internally — timing is mandatory |
23//! | Other peripherals | SPI bus is owned by the driver | only the chosen GPIO pin is owned |
24//!
25//! [ADR 007](https://github.com/datenkollektiv/rustyfarian-ws2812/blob/main/docs/adr/007-avr-ws2812-driver-strategy.md)
26//! records the empirical evidence: a strip that works correctly on the ESP32 RMT drivers
27//! produced stable white-ish output and chain leakage on the SPI prerendered backend
28//! across both genuine and clone Arduino Nanos.
29//! The bit-bang backend renders correctly on the same hardware.
30//!
31//! Use the SPI backend if your strip is known to tolerate the encoding's
32//! out-of-spec `T1H`, or if you need other peripherals to keep operating during the LED
33//! write. Use the bit-bang backend everywhere else.
34//!
35//! # SPI Prerendered Backend
36//!
37//! [`Ws2812Spi`] drives WS2812/NeoPixel LEDs over SPI using the prerendered encoding
38//! from [`bunting`](https://crates.io/crates/bunting) — 4 SPI bits per WS2812 bit,
39//! 12 SPI bytes per LED. The encoding is byte-for-byte compatible with
40//! [`ws2812-spi`](https://crates.io/crates/ws2812-spi) v0.5.1's prerendered module.
41//!
42//! ## Buffer sizing
43//!
44//! Const-generic buffer `[u8; N]` where `N = spi_data_len(num_leds) + SPI_RESET_BYTES_2MHZ`.
45//! Use [`spi_buffer_size`] to compute `N` at compile time:
46//!
47//! ```ignore
48//! use rustyfarian_avr_ws2812::spi_buffer_size;
49//! const N: usize = spi_buffer_size(8); // 8-LED ring
50//! ```
51//!
52//! ## SPI clock configuration
53//!
54//! The SPI peripheral **must** be configured at 2 MHz before the first call to
55//! [`Ws2812Spi::write`]. On a 16 MHz ATmega328P, use a clock prescaler of ÷8.
56//!
57//! ## Interrupt safety
58//!
59//! The SPI backend is **not** interrupt-safe by itself; the caller wraps each
60//! `write` in a critical section:
61//!
62//! ```ignore
63//! avr_device::interrupt::free(|_| {
64//! ws.write(&colors).unwrap();
65//! });
66//! ```
67//!
68//! ## Example
69//!
70//! ```ignore
71//! use rustyfarian_avr_ws2812::{Ws2812Spi, spi_buffer_size};
72//! use rgb::RGB8;
73//!
74//! const NUM_LEDS: usize = 8;
75//! const N: usize = spi_buffer_size(NUM_LEDS);
76//!
77//! let mut ws: Ws2812Spi<_, N> = Ws2812Spi::new(spi_bus);
78//! let colors = [RGB8::new(255, 0, 0); NUM_LEDS];
79//! avr_device::interrupt::free(|_| {
80//! ws.write(&colors).unwrap();
81//! });
82//! ```
83//!
84//! # Bit-Bang Backend (recommended)
85//!
86//! [`Ws2812BitBang`] uses cycle-counted inline `asm!` to drive any GPIO pin in low
87//! I/O space (PORTB / PORTC / PORTD on ATmega328P) at WS2812-spec timing.
88//! The `write` method wraps the asm loop in `avr_device::interrupt::free` internally —
89//! the caller does **not** need to add a critical section.
90//!
91//! Enable the `bitbang` feature in `Cargo.toml`:
92//!
93//! ```toml
94//! rustyfarian-avr-ws2812 = { version = "0.1", features = ["bitbang"] }
95//! ```
96//!
97//! ## Pin selection
98//!
99//! The driver is generic over the port-register address ([`ports::PORTB`], [`ports::PORTC`],
100//! [`ports::PORTD`]) and the pin bit number (0–7). Both are compile-time constants so
101//! the asm uses single-instruction `sbi`/`cbi` operations.
102//!
103//! ## Interrupt safety
104//!
105//! Cycle-accurate WS2812 timing requires interrupts to stay disabled for the full frame
106//! window (≈ 30 µs per LED). The driver does this internally; user code remains free of
107//! `interrupt::free` boilerplate. The `millis()` timer and serial UART will lose ticks
108//! during the write window — standard tradeoff documented in `docs/avr-getting-started.md`.
109//!
110//! ## Example
111//!
112//! ```ignore
113//! use rustyfarian_avr_ws2812::{ports, Ws2812BitBang};
114//! use rgb::RGB8;
115//!
116//! let pin = pins.d11.into_output();
117//! let mut driver: Ws2812BitBang<_, { ports::PORTB }, 3> = Ws2812BitBang::new(pin);
118//!
119//! let colors = [RGB8::new(8, 0, 0); 10];
120//! driver.write(&colors).ok(); // no `interrupt::free` needed — handled inside
121//! ```
122//!
123//! # Runnable Examples
124//!
125//! Standalone, flashable Arduino Nano examples live at
126//! [`examples/avr-nano-rainbow/`](https://github.com/datenkollektiv/rustyfarian-ws2812/tree/main/examples/avr-nano-rainbow)
127//! in the workspace root (separate AVR toolchain, target, and `arduino-hal` git dependency).
128//! See [`docs/avr-getting-started.md`](https://github.com/datenkollektiv/rustyfarian-ws2812/blob/main/docs/avr-getting-started.md)
129//! for wiring, toolchain setup, and `just` recipes:
130//!
131//! - `just flash-avr-example` — bit-bang `RainbowEffect`, the recommended demo (`src/main.rs`)
132//! - `just flash-avr-bitbang-demo` — bit-bang `PulseEffect` red breath (`bin/bitbang_demo`)
133//! - `just flash-avr-spi-rainbow` — SPI prerendered comparison, **diagnostic only** (`bin/spi_rainbow`)
134//! - `just flash-avr-bitbang-spike` — frozen low-level reference, no driver crate (`bin/bitbang_spike`)
135
136use bunting::{prerender_spi, spi_data_len, SpiEncodeError, SPI_RESET_BYTES_2MHZ};
137use core::fmt;
138use embedded_hal::spi::SpiBus;
139use rgb::RGB8;
140
141/// Errors that can occur during WS2812 SPI operations.
142#[derive(Debug)]
143pub enum SpiError<E> {
144 /// The color slice was too large for the buffer.
145 Encode(SpiEncodeError),
146 /// The underlying SPI bus returned an error.
147 Spi(E),
148}
149
150impl<E: fmt::Debug> fmt::Display for SpiError<E> {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 match self {
153 Self::Encode(e) => write!(f, "SPI encode error: {e}"),
154 Self::Spi(e) => write!(f, "SPI bus error: {e:?}"),
155 }
156 }
157}
158
159impl<E> From<SpiEncodeError> for SpiError<E> {
160 fn from(e: SpiEncodeError) -> Self {
161 Self::Encode(e)
162 }
163}
164
165/// Returns the total SPI buffer size for `num_leds` LEDs (data + reset bytes).
166///
167/// Use this as the const generic `N` for [`Ws2812Spi`]:
168///
169/// ```
170/// use rustyfarian_avr_ws2812::spi_buffer_size;
171/// const N: usize = spi_buffer_size(8); // 8-LED ring → 176
172/// assert_eq!(N, 176);
173/// ```
174pub const fn spi_buffer_size(num_leds: usize) -> usize {
175 spi_data_len(num_leds) + SPI_RESET_BYTES_2MHZ
176}
177
178/// WS2812 LED driver using SPI prerendered encoding (`no_std`, `embedded-hal` 1.0).
179///
180/// `N` is the total SPI buffer size in bytes.
181/// Compute it with [`spi_buffer_size`]: `N = spi_data_len(num_leds) + SPI_RESET_BYTES_2MHZ`.
182///
183/// The struct is generic over any [`SpiBus`] implementation — no AVR-specific
184/// dependencies are introduced here. The caller is responsible for:
185/// - Configuring SPI at 2 MHz before calling [`write`](Ws2812Spi::write).
186/// - Wrapping the `write` call in a critical section on interrupt-driven systems.
187///
188/// # Type Parameters
189///
190/// - `SPI` — any type implementing [`embedded_hal::spi::SpiBus`].
191/// - `N` — total SPI buffer size (`spi_data_len(num_leds) + SPI_RESET_BYTES_2MHZ`).
192pub struct Ws2812Spi<SPI, const N: usize> {
193 spi: SPI,
194 buf: [u8; N],
195}
196
197impl<SPI: SpiBus, const N: usize> Ws2812Spi<SPI, N> {
198 /// Creates a new WS2812 SPI driver wrapping the given SPI bus.
199 ///
200 /// The internal buffer is zero-initialised. The SPI bus **must** already be
201 /// configured at 2 MHz before the first call to [`write`](Self::write).
202 pub fn new(spi: SPI) -> Self {
203 Self { spi, buf: [0u8; N] }
204 }
205
206 /// Encodes `colors` into the internal buffer and sends it over SPI.
207 ///
208 /// The first `spi_data_len(colors.len())` bytes of the buffer are filled with
209 /// prerendered WS2812 SPI data.
210 /// The remaining bytes are zeroed to provide the WS2812 reset pulse
211 /// (`SPI_RESET_BYTES_2MHZ` = 80 bytes at 2 MHz).
212 ///
213 /// # Errors
214 ///
215 /// - [`SpiError::Encode`] if `colors.len() > (N - SPI_RESET_BYTES_2MHZ) / 12`
216 /// (the color slice is too large for the buffer).
217 /// - [`SpiError::Spi`] if the underlying SPI bus returns an error.
218 pub fn write(&mut self, colors: &[RGB8]) -> Result<(), SpiError<SPI::Error>> {
219 let data_len = spi_data_len(colors.len());
220 prerender_spi(colors, &mut self.buf[..data_len])?;
221 // Zero the reset tail so the WS2812 latches the frame.
222 self.buf[data_len..].fill(0);
223 self.spi.write(&self.buf).map_err(SpiError::Spi)?;
224 Ok(())
225 }
226
227 /// Releases the inner SPI bus, consuming the driver.
228 pub fn release(self) -> SPI {
229 self.spi
230 }
231}
232
233/// `SmartLedsWrite` adapter for [`Ws2812Spi`].
234///
235/// Provides ecosystem parity with [`smart-leds`] consumers (sister ESP drivers
236/// implement the same trait). Internally, the iterator is collected into a
237/// stack-allocated `[RGB8; 256]` and forwarded to [`Ws2812Spi::write`].
238///
239/// # Stack cost
240///
241/// **Each call uses ≈ 768 bytes of stack** (`256 × size_of::<RGB8>()`).
242/// On ATmega328P (2 KB SRAM total) this is a meaningful fraction of available
243/// memory — depending on what else is on the call stack, it can leave little
244/// headroom for arrays, `core::fmt` machinery, or interrupt-context frames.
245///
246/// If you don't need the iterator-based ergonomics, prefer the inherent
247/// [`Ws2812Spi::write`] which takes a `&[RGB8]` directly and **allocates no
248/// adapter buffer**: pass a `[RGB8; NUM_LEDS]` you already own.
249///
250/// # Truncation
251///
252/// Iterators with more than `256` items are silently truncated to the first
253/// 256 colors — the cap matches [`ferriswheel::effect::MAX_LEDS`]. This avoids
254/// dynamic allocation while staying within the workspace's pure-logic contract.
255/// If you need longer chains, use the inherent `write` with a slice of any length.
256///
257/// [`smart-leds`]: https://crates.io/crates/smart-leds
258/// [`ferriswheel::effect::MAX_LEDS`]: https://docs.rs/ferriswheel/latest/ferriswheel/effect/constant.MAX_LEDS.html
259#[cfg(feature = "smart-leds-trait")]
260impl<SPI: SpiBus, const N: usize> smart_leds_trait::SmartLedsWrite for Ws2812Spi<SPI, N> {
261 type Error = SpiError<SPI::Error>;
262 type Color = RGB8;
263
264 fn write<T, I>(&mut self, iterator: T) -> Result<(), Self::Error>
265 where
266 T: IntoIterator<Item = I>,
267 I: Into<Self::Color>,
268 {
269 // 256 × 3 bytes = 768 bytes on stack — see the impl docs above.
270 let mut buf = [RGB8::default(); 256];
271 let mut count = 0usize;
272 for color in iterator {
273 if count >= buf.len() {
274 break;
275 }
276 buf[count] = color.into();
277 count += 1;
278 }
279 Self::write(self, &buf[..count])
280 }
281}
282
283#[cfg(feature = "bitbang")]
284mod bitbang;
285#[cfg(all(feature = "bitbang", target_arch = "avr"))]
286mod bitbang_avr;
287
288#[cfg(feature = "bitbang")]
289pub use bitbang::{ports, BitBangError, Ws2812BitBang};
290
291#[cfg(test)]
292mod tests {
293 extern crate std;
294
295 use super::*;
296 use bunting::SpiEncodeError;
297 use std::string::ToString;
298
299 // --- spi_buffer_size tests -----------------------------------------------
300
301 #[test]
302 fn spi_buffer_size_zero_leds() {
303 assert_eq!(spi_buffer_size(0), SPI_RESET_BYTES_2MHZ);
304 }
305
306 #[test]
307 fn spi_buffer_size_one_led() {
308 // 1 LED: 12 data bytes + 80 reset bytes = 92
309 assert_eq!(spi_buffer_size(1), 92);
310 }
311
312 #[test]
313 fn spi_buffer_size_eight_leds() {
314 // 8 LEDs: 96 data bytes + 80 reset bytes = 176
315 assert_eq!(spi_buffer_size(8), 176);
316 }
317
318 #[test]
319 fn spi_buffer_size_twelve_leds() {
320 // 12 LEDs: 144 data bytes + 80 reset bytes = 224
321 assert_eq!(spi_buffer_size(12), 224);
322 }
323
324 // --- SpiError Display tests ----------------------------------------------
325
326 #[test]
327 fn spi_error_encode_display() {
328 let e: SpiError<()> = SpiError::Encode(SpiEncodeError::BufferTooSmall);
329 assert!(e.to_string().contains("encode"));
330 }
331
332 #[test]
333 fn spi_error_spi_display() {
334 let e: SpiError<&str> = SpiError::Spi("bus failure");
335 assert!(e.to_string().contains("SPI bus error"));
336 }
337
338 // --- From<SpiEncodeError> ------------------------------------------------
339
340 #[test]
341 fn from_spi_encode_error() {
342 let e: SpiError<()> = SpiEncodeError::BufferTooSmall.into();
343 assert!(matches!(
344 e,
345 SpiError::Encode(SpiEncodeError::BufferTooSmall)
346 ));
347 }
348}