Skip to main content

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}