From fe9eca0d0fec927bf122de00ee11b45bf1a7dc13 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Wed, 7 Oct 2026 13:00:01 +1100 Subject: [PATCH 01/36] Add generalised spi driver for clockless LEDs This addresses https://github.com/ahdinosaur/blinksy/issues/12 The implementation is currently pretty long winded for what it actually does (to my taste anyway). Most of this is due to hoops we're having to jump through to calculate buffer sizes at compile time (See https://github.com/ahdinosaur/blinksy/issues/107) Documentation is still to come --- Cargo.lock | 42 ++++ blinksy/Cargo.toml | 3 + blinksy/src/driver/clockless/mod.rs | 21 +- blinksy/src/driver/clockless/spi/builder.rs | 150 +++++++++++++ blinksy/src/driver/clockless/spi/encoding.rs | 124 +++++++++++ blinksy/src/driver/clockless/spi/mod.rs | 207 ++++++++++++++++++ .../examples/ws2812-spi-matrix-8x8.rs | 114 ++++++++++ 7 files changed, 653 insertions(+), 8 deletions(-) create mode 100644 blinksy/src/driver/clockless/spi/builder.rs create mode 100644 blinksy/src/driver/clockless/spi/encoding.rs create mode 100644 blinksy/src/driver/clockless/spi/mod.rs create mode 100644 esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs diff --git a/Cargo.lock b/Cargo.lock index 3347126..e2f1fee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,10 +48,22 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bitvec" +version = "1.1.0" +source = "git+https://github.com/ferrilab/ferrilab.git?rev=6af4ab0a33233573326f4b27f0193a2c956aada5#6af4ab0a33233573326f4b27f0193a2c956aada5" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blinksy" version = "0.11.0" dependencies = [ + "bitvec", "defmt 0.3.100", "embedded-hal 1.0.0", "embedded-hal-async", @@ -458,6 +470,12 @@ dependencies = [ "gcd", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-core" version = "0.3.33" @@ -929,6 +947,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1775bc532a9bfde46e26eba441ca1171b91608d14a3bae71fea371f18a00cffe" +dependencies = [ + "cfg-if", +] + [[package]] name = "rand" version = "0.10.2" @@ -1078,6 +1105,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "thiserror" version = "1.0.69" @@ -1560,6 +1593,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-clipboard" version = "0.9.3" diff --git a/blinksy/Cargo.toml b/blinksy/Cargo.toml index e978178..e411fa2 100644 --- a/blinksy/Cargo.toml +++ b/blinksy/Cargo.toml @@ -12,6 +12,9 @@ keywords.workspace = true categories.workspace = true [dependencies] +# radium 0.7 is a dependency of the latest released version of bitvec which breaks on esp32. So using an unreleased +# version for now. See https://github.com/ferrilab/ferrilab/issues/1#issuecomment-4243321551 +bitvec = { git = "https://github.com/ferrilab/ferrilab.git", rev = "6af4ab0a33233573326f4b27f0193a2c956aada5", default-features = false } defmt = { version = "0.3.10", optional = true } embedded-hal = "1.0.0" embedded-hal-async = { version = "1.0.0", optional = true } diff --git a/blinksy/src/driver/clockless/mod.rs b/blinksy/src/driver/clockless/mod.rs index 7bfd500..f9fafb5 100644 --- a/blinksy/src/driver/clockless/mod.rs +++ b/blinksy/src/driver/clockless/mod.rs @@ -73,8 +73,10 @@ use crate::{ }; mod delay; +mod spi; pub use self::delay::*; +pub use self::spi::*; /// Trait that defines the timing parameters and protocol specifics for a clockless LED chipset. /// @@ -126,14 +128,6 @@ pub trait ClocklessLed { /// Different LED chipsets may expect data in different channel orders (e.g., RGB, GRB, RGBW). const LED_CHANNELS: LedChannels; - /// Calculates the total cycle time for a bit transmission. - /// - /// Returns the maximum of (T_0H + T_0L) and (T_1H + T_1L) to ensure - /// timing is correct regardless of bit value. - fn t_cycle() -> Nanoseconds { - (Self::T_0H + Self::T_0L).max(Self::T_1H + Self::T_1L) - } - /// Encodes a buffer to represent the next frame update. /// /// This method: @@ -174,6 +168,17 @@ pub trait ClocklessLed { } } +/// Calculates the total cycle time for a bit transmission. +/// +/// Returns the maximum of (T_0H + T_0L) and (T_1H + T_1L) to ensure +/// timing is correct regardless of bit value. +pub const fn t_cycle() -> Nanoseconds { + let t0_ns = Led::T_0H.to_nanos() + Led::T_0L.to_nanos(); + let t1_ns = Led::T_1H.to_nanos() + Led::T_1L.to_nanos(); + let t = if t0_ns > t1_ns { t0_ns } else { t1_ns }; + Nanoseconds::nanos(t) +} + /// Trait for types that can write data words to a clockless protocol. pub trait ClocklessWriter { type Error; diff --git a/blinksy/src/driver/clockless/spi/builder.rs b/blinksy/src/driver/clockless/spi/builder.rs new file mode 100644 index 0000000..f9fb4ff --- /dev/null +++ b/blinksy/src/driver/clockless/spi/builder.rs @@ -0,0 +1,150 @@ +use core::marker::PhantomData; + +use embedded_hal::spi::SpiBus; +#[cfg(feature = "async")] +use embedded_hal_async::spi::SpiBus as SpiBusAsync; + +use crate::driver::ClocklessLed; +use crate::driver::ClocklessSpi; +#[cfg(feature = "async")] +use crate::driver::ClocklessSpiAsync; +use crate::markers::{Async, Blocking}; + +pub struct ClocklessSpiBuilder< + const BUFFER_SIZE: usize, + const PULSE_SIZE: usize, + Spi, + Led, + SpiWord, + Freq, + Dm, +> { + spi: Spi, + freq_hz: Freq, + _led: PhantomData, + _spi_word: PhantomData, + _dm: PhantomData, +} + +impl + ClocklessSpiBuilder +{ + pub fn with_async( + self, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz: self.freq_hz, + _led: PhantomData, + _spi_word: PhantomData, + _dm: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_freq_hz( + self, + freq_hz: u32, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz, + _led: PhantomData, + _spi_word: PhantomData, + _dm: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_pulse_size( + self, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz: self.freq_hz, + _led: PhantomData, + _spi_word: PhantomData, + _dm: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_buffer_size( + self, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz: self.freq_hz, + _led: PhantomData, + _spi_word: PhantomData, + _dm: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_spi( + self, + spi: Spi, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi, + freq_hz: self.freq_hz, + _led: PhantomData, + _spi_word: PhantomData, + _dm: PhantomData, + } + } +} + +impl Default for ClocklessSpiBuilder<0, 0, (), Led, SpiWord, (), Blocking> { + fn default() -> Self { + Self { + spi: (), + freq_hz: (), + _led: PhantomData, + _spi_word: PhantomData, + _dm: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +where + Spi: SpiBus, + SpiWord: Copy + 'static, + Led: ClocklessLed, +{ + pub fn build(self) -> ClocklessSpi { + assert!(BUFFER_SIZE > 0, "set buffer_size before calling build"); + assert!(PULSE_SIZE > 0, "set pulse_size before calling build"); + ClocklessSpi::new(self.spi, self.freq_hz) + } +} + +#[cfg(feature = "async")] +impl + ClocklessSpiBuilder +where + Spi: SpiBusAsync, + SpiWord: Copy + 'static, + Led: ClocklessLed, +{ + pub fn build(self) -> ClocklessSpiAsync { + assert!(BUFFER_SIZE > 0, "set buffer_size before calling build"); + assert!(PULSE_SIZE > 0, "set pulse_size before calling build"); + ClocklessSpiAsync::new(self.spi, self.freq_hz) + } +} diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs new file mode 100644 index 0000000..381efe1 --- /dev/null +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -0,0 +1,124 @@ +use crate::driver::ClocklessLed; +use bitvec::{array::BitArray, order::Msb0, slice::BitSlice}; +use core::marker::PhantomData; + +pub const fn freq_hz_to_duration_ns(freq_hz: u32) -> u32 { + 1_000_000_000 / freq_hz +} + +pub const fn duration_ns_to_freq_hz(duration_ns: u32) -> u32 { + 1_000_000_000 / duration_ns +} + +pub struct Timing { + clock_period_ns: u32, + pub t_0h: u32, + pub t_0l: u32, + pub t_1h: u32, + pub t_1l: u32, + pub t_reset: u32, + _led: PhantomData, +} + +impl Timing { + pub const fn new(clock_period_ns: u32) -> Self { + Self { + clock_period_ns, + t_0h: Led::T_0H.to_nanos() / clock_period_ns, + t_0l: Led::T_0L.to_nanos() / clock_period_ns, + t_1h: Led::T_1H.to_nanos() / clock_period_ns, + t_1l: Led::T_1L.to_nanos() / clock_period_ns, + t_reset: Led::T_RESET.to_nanos() / clock_period_ns, + _led: PhantomData, + } + } + + const fn t0(&self) -> u32 { + self.t_0h + self.t_0l + } + + const fn t1(&self) -> u32 { + self.t_1h + self.t_1l + } + + pub const fn duty_cycle_bits(&self) -> u32 { + let t0 = self.t0(); + let t1 = self.t1(); + // We can't yet use max in const function + if t0 > t1 { + t0 + } else { + t1 + } + } + + pub const fn max_error_ns(&self) -> u32 { + let error_0h_ns = Led::T_0H.to_nanos() - self.t_0h * self.clock_period_ns; + let error_0l_ns = Led::T_0L.to_nanos() - self.t_0l * self.clock_period_ns; + let error_1h_ns = Led::T_1H.to_nanos() - self.t_1h * self.clock_period_ns; + let error_1l_ns = Led::T_1L.to_nanos() - self.t_1l * self.clock_period_ns; + + let mut max_error_ns = 0; + if error_0h_ns > max_error_ns { + max_error_ns = error_0h_ns + }; + if error_0l_ns > max_error_ns { + max_error_ns = error_0l_ns + }; + if error_1h_ns > max_error_ns { + max_error_ns = error_1h_ns + }; + if error_1l_ns > max_error_ns { + max_error_ns = error_1l_ns + }; + max_error_ns + } +} + +pub struct PulseCode { + buffer: BitArray<[u8; N], Msb0>, + len: usize, +} + +impl PulseCode { + pub fn new(high: usize, low: usize) -> Self { + let mut buffer = BitArray::new([0u8; N]); + for mut v in &mut buffer[..high] { + v.set(true); + } + Self { + buffer, + len: high + low, + } + } + + pub fn bits(&self) -> &BitSlice { + &self.buffer[..self.len] + } + + pub fn len(&self) -> usize { + self.len + } +} + +pub struct Pulses { + zero: PulseCode, + one: PulseCode, +} + +impl Pulses { + pub fn new(timing: &Timing) -> Pulses { + Self { + zero: PulseCode::new(timing.t_0h as usize, timing.t_0l as usize), + one: PulseCode::new(timing.t_1h as usize, timing.t_1l as usize), + } + } + + pub fn get(&self, value: bool) -> &PulseCode { + if value { + &self.one + } else { + &self.zero + } + } +} diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs new file mode 100644 index 0000000..a4e52b1 --- /dev/null +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -0,0 +1,207 @@ +use core::marker::PhantomData; + +use bitvec::prelude::*; +use bitvec::view::BitView; +use bitvec::view::BitViewSized; +use embedded_hal::spi::SpiBus; +#[cfg(feature = "async")] +use embedded_hal_async::spi::SpiBus as SpiBusAsync; + +use crate::driver::t_cycle; +#[cfg(feature = "async")] +use crate::driver::ClocklessWriterAsync; +use crate::driver::{ClocklessLed, ClocklessWriter}; + +mod encoding; +use encoding::{duration_ns_to_freq_hz, freq_hz_to_duration_ns, Pulses, Timing}; +mod builder; +pub use builder::ClocklessSpiBuilder; + +pub const fn clockless_spi_buffer_size( + pixel_count: usize, + freq_hz: u32, +) -> usize +where + Spi: SpiBus, + Word: Copy + 'static, +{ + let clock_period_ns = freq_hz_to_duration_ns(freq_hz); + let timing = Timing::::new(clock_period_ns); + + // TODO: Check that resulting timings are within spec for the LED and error if not + let spi_word_bits = size_of::() * 8; + let total_bits = spi_word_bits + * pixel_count + * Led::LED_CHANNELS.channel_count() + * timing.duty_cycle_bits() as usize + + timing.t_reset as usize; + total_bits.div_ceil(spi_word_bits) +} + +pub const fn clockless_spi_pulse_size(freq_hz: u32) -> usize +where + Spi: SpiBus, + Word: Copy + 'static, +{ + let clock_period_ns = freq_hz_to_duration_ns(freq_hz); + let timing = Timing::::new(clock_period_ns); + let spi_word_bits = size_of::() * 8; + + let total_bits = timing.duty_cycle_bits() as usize; + total_bits.div_ceil(spi_word_bits) +} + +// Brute-force an "ideal" clock frequency to run the SPI bus at +// Lower frequencies are better because they mean that we need fewer spi bits to +// encode a single LED bit which means less processing and memory usage. However +// lower frequencies increase the timing errors. +// Each LED has a tolerance for timing variations. We take advantage of this to pick +// the lowest clock frequency that gives us errors within our chosen target tolerance. +pub const fn clockless_spi_ideal_frequency_hz(target_tolerance_ns: u32) -> u32 { + // There's going to be some smart ways of doing this but for the time being + // let's just do the simplest possible thing and explore a whole range of timings + // and see what works best. + let t_cycle_ns = t_cycle::().to_nanos(); + let mut max_clock_period_ns = 0; + let mut clock_period_ns = 1; + loop { + let error_ns = Timing::::new(clock_period_ns).max_error_ns(); + if error_ns < target_tolerance_ns && clock_period_ns > max_clock_period_ns { + max_clock_period_ns = clock_period_ns; + } + clock_period_ns += 1; + if clock_period_ns >= t_cycle_ns { + break; + } + } + duration_ns_to_freq_hz(max_clock_period_ns) +} + +fn encode_spi_buffer< + Led: ClocklessLed, + const FRAME_BUFFER_SIZE: usize, + const SPI_BUFFER_SIZE: usize, + const N: usize, + SpiWord, +>( + frame: heapless::Vec, + buffer: &mut BitArray<[SpiWord; SPI_BUFFER_SIZE], Msb0>, + pulses: &Pulses, +) where + [SpiWord; SPI_BUFFER_SIZE]: BitViewSized, + Led::Word: BitView, +{ + let mut dest = buffer.as_mut_bitslice(); + for v in frame { + for bit in v.view_bits::() { + let pattern = pulses.get(*bit); + dest[..pattern.len()].clone_from_bitslice(pattern.bits()); + dest = &mut dest[pattern.len()..] + } + } +} + +pub struct ClocklessSpi +where + Led: ClocklessLed, + Spi: SpiBus, + SpiWord: Copy + 'static, +{ + spi: Spi, + pub timing: Timing, + pulses: Pulses, + _spi_word: PhantomData, +} + +#[cfg(feature = "async")] +pub struct ClocklessSpiAsync +where + Led: ClocklessLed, + Spi: SpiBusAsync, + SpiWord: Copy + 'static, +{ + spi: Spi, + pub timing: Timing, + pulses: Pulses, + _spi_word: PhantomData, +} + +impl + ClocklessSpi +where + Led: ClocklessLed, + Spi: SpiBus, + SpiWord: Copy + 'static, +{ + pub fn new(spi: Spi, freq_hz: u32) -> Self { + let timing = Timing::new(freq_hz_to_duration_ns(freq_hz)); + Self { + spi, + pulses: Pulses::new(&timing), + timing, + _spi_word: PhantomData, + } + } +} + +#[cfg(feature = "async")] +impl + ClocklessSpiAsync +where + Led: ClocklessLed, + SpiWord: Copy + 'static, + Spi: SpiBusAsync, +{ + pub fn new(spi: Spi, freq_hz: u32) -> Self { + let timing = Timing::new(freq_hz_to_duration_ns(freq_hz)); + Self { + spi, + pulses: Pulses::new::(&timing), + timing, + _spi_word: PhantomData, + } + } +} + +impl ClocklessWriter + for ClocklessSpi +where + Led: ClocklessLed, + Led::Word: BitView, + Spi: SpiBus, + SpiWord: Copy + 'static, + [SpiWord; BUFFER_SIZE]: BitViewSized, +{ + type Error = Spi::Error; + + fn write( + &mut self, + frame: heapless::Vec, + ) -> Result<(), Self::Error> { + let mut buffer = BitArray::<[SpiWord; BUFFER_SIZE], Msb0>::ZERO; + encode_spi_buffer::(frame, &mut buffer, &self.pulses); + self.spi.write(&buffer.into_inner()) + } +} + +#[cfg(feature = "async")] +impl ClocklessWriterAsync + for ClocklessSpiAsync +where + Led: ClocklessLed, + Led::Word: BitView, + Spi: SpiBusAsync, + SpiWord: Copy + 'static, + [SpiWord; BUFFER_SIZE]: BitViewSized, +{ + type Error = Spi::Error; + + async fn write( + &mut self, + frame: heapless::Vec, + ) -> Result<(), Self::Error> { + let mut buffer = BitArray::<[SpiWord; BUFFER_SIZE], Msb0>::ZERO; + encode_spi_buffer::(frame, &mut buffer, &self.pulses); + self.spi.write(&buffer.into_inner()).await + } +} diff --git a/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs b/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs new file mode 100644 index 0000000..ad47500 --- /dev/null +++ b/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs @@ -0,0 +1,114 @@ +#![no_std] +#![no_main] +#![deny( + clippy::mem_forget, + reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ + holding buffers for the duration of a data transfer." +)] +#![deny(clippy::large_stack_frames)] + +use blinksy::driver::ClocklessSpiBuilder; +use blinksy::{ + driver::{ + clockless_spi_buffer_size, clockless_spi_ideal_frequency_hz, clockless_spi_pulse_size, + ClocklessDriver, + }, + layout::{Layout2d, Shape2d, Vec2}, + layout2d, + leds::Ws2812, + patterns::rainbow::{Rainbow, RainbowParams}, + ControlBuilder, +}; +use blinksy_esp::time::elapsed; +use embassy_executor::Spawner; +use esp_alloc as _; +use esp_hal::{ + dma_rx_buffer, dma_tx_buffer, + spi::master::{Config, Spi, SpiDma}, + time::Rate, + timer::timg::TimerGroup, + Async, +}; +use panic_rtt_target as _; + +extern crate alloc; + +esp_bootloader_esp_idf::esp_app_desc!(); + +#[allow( + clippy::large_stack_frames, + reason = "it's not unusual to allocate larger buffers etc. in main" +)] +#[esp_rtos::main] +async fn main(_spawner: Spawner) -> ! { + rtt_target::rtt_init_defmt!(); + + let cpu_clock = esp_hal::clock::CpuClock::max(); + let config = esp_hal::Config::default().with_cpu_clock(cpu_clock); + let p = esp_hal::init(config); + + let timg0 = TimerGroup::new(p.TIMG0); + esp_rtos::start(timg0.timer0, p.FROM_CPU_INTR0); + + layout2d!( + Layout, + [Shape2d::Grid { + start: Vec2::new(1., -1.), + horizontal_end: Vec2::new(-1., -1.), + vertical_end: Vec2::new(1., 1.), + horizontal_pixel_count: 8, + vertical_pixel_count: 8, + serpentine: false + }] + ); + + let dma_rx_buf = dma_rx_buffer!(1024).unwrap(); + let dma_tx_buf = dma_tx_buffer!(1024).unwrap(); + + // let's see what an ideal SPI frequency would be + const SPI_FREQ_HZ: u32 = clockless_spi_ideal_frequency_hz::(150); + + type Led = Ws2812; + let spi = Spi::new( + p.SPI2, + Config::default().with_frequency(Rate::from_hz(SPI_FREQ_HZ)), + ) + .unwrap() + .with_mosi(p.GPIO17) + .with_dma(p.DMA_CH0) + .with_buffers(dma_rx_buf, dma_tx_buf) + .into_async(); + let writer = ClocklessSpiBuilder::default() + .with_spi(spi) + .with_freq_hz(SPI_FREQ_HZ) + .with_buffer_size::<{ clockless_spi_buffer_size::, _>(Layout::PIXEL_COUNT, SPI_FREQ_HZ) }>() + .with_pulse_size::<{ clockless_spi_pulse_size::, _>(SPI_FREQ_HZ) }>() + .with_async() + .build(); + + defmt::info!( + "ideal freq: {} Hz, error: {} ns, duty cycle bits: {}", + SPI_FREQ_HZ, + writer.timing.max_error_ns(), + writer.timing.duty_cycle_bits() + ); + + let driver = ClocklessDriver::default() + .with_led::() + .with_writer(writer); + + let mut control = ControlBuilder::new_2d_async() + .with_layout::() + .with_pattern::(RainbowParams { + ..Default::default() + }) + .with_driver(driver) + .with_frame_buffer_size::<{ Led::frame_buffer_size(Layout::PIXEL_COUNT) }>() + .build(); + control.set_brightness(0.1); // Set initial brightness (0.0 to 1.0) + + loop { + let elapsed_in_ms = elapsed().as_millis(); + control.tick(elapsed_in_ms).await.unwrap(); + } +} From b7cf66236bd01b6543518c6e6dd621d95d8078d5 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 03:50:46 +1100 Subject: [PATCH 02/36] Update Cargo.lock in blinksy-esp after blinksy update --- esp/Cargo.lock | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/esp/Cargo.lock b/esp/Cargo.lock index 3029285..07cd636 100644 --- a/esp/Cargo.lock +++ b/esp/Cargo.lock @@ -73,10 +73,22 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.1.0" +source = "git+https://github.com/ferrilab/ferrilab.git?rev=6af4ab0a33233573326f4b27f0193a2c956aada5#6af4ab0a33233573326f4b27f0193a2c956aada5" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blinksy" version = "0.11.0" dependencies = [ + "bitvec", "defmt 0.3.100", "embedded-hal 1.0.0", "embedded-hal-async", @@ -1209,6 +1221,12 @@ dependencies = [ "gcd", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-core" version = "0.3.31" @@ -1602,6 +1620,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1775bc532a9bfde46e26eba441ca1171b91608d14a3bae71fea371f18a00cffe" +dependencies = [ + "cfg-if", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1978,6 +2005,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.4.1" @@ -2274,6 +2307,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xtensa-lx" version = "0.13.0" From 5b5f405b507954ac9936b6a87d7da6c182960928 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 04:00:46 +1100 Subject: [PATCH 03/36] Rename PhantomData fields to match convention used in the crate --- blinksy/src/driver/clockless/spi/builder.rs | 42 ++++++++++---------- blinksy/src/driver/clockless/spi/encoding.rs | 4 +- blinksy/src/driver/clockless/spi/mod.rs | 8 ++-- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/builder.rs b/blinksy/src/driver/clockless/spi/builder.rs index f9fb4ff..b71356a 100644 --- a/blinksy/src/driver/clockless/spi/builder.rs +++ b/blinksy/src/driver/clockless/spi/builder.rs @@ -21,9 +21,9 @@ pub struct ClocklessSpiBuilder< > { spi: Spi, freq_hz: Freq, - _led: PhantomData, - _spi_word: PhantomData, - _dm: PhantomData, + led: PhantomData, + spi_word: PhantomData, + dm: PhantomData, } impl @@ -35,9 +35,9 @@ impl ClocklessSpiBuilder { spi: self.spi, freq_hz: self.freq_hz, - _led: PhantomData, - _spi_word: PhantomData, - _dm: PhantomData, + led: PhantomData, + spi_word: PhantomData, + dm: PhantomData, } } } @@ -52,9 +52,9 @@ impl ClocklessSpiBuilder { spi: self.spi, freq_hz, - _led: PhantomData, - _spi_word: PhantomData, - _dm: PhantomData, + led: PhantomData, + spi_word: PhantomData, + dm: PhantomData, } } } @@ -68,9 +68,9 @@ impl ClocklessSpiBuilder { spi: self.spi, freq_hz: self.freq_hz, - _led: PhantomData, - _spi_word: PhantomData, - _dm: PhantomData, + led: PhantomData, + spi_word: PhantomData, + dm: PhantomData, } } } @@ -84,9 +84,9 @@ impl ClocklessSpiBuilder { spi: self.spi, freq_hz: self.freq_hz, - _led: PhantomData, - _spi_word: PhantomData, - _dm: PhantomData, + led: PhantomData, + spi_word: PhantomData, + dm: PhantomData, } } } @@ -101,9 +101,9 @@ impl ClocklessSpiBuilder { spi, freq_hz: self.freq_hz, - _led: PhantomData, - _spi_word: PhantomData, - _dm: PhantomData, + led: PhantomData, + spi_word: PhantomData, + dm: PhantomData, } } } @@ -113,9 +113,9 @@ impl Default for ClocklessSpiBuilder<0, 0, (), Led, SpiWord, (), B Self { spi: (), freq_hz: (), - _led: PhantomData, - _spi_word: PhantomData, - _dm: PhantomData, + led: PhantomData, + spi_word: PhantomData, + dm: PhantomData, } } } diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index 381efe1..c129d74 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -17,7 +17,7 @@ pub struct Timing { pub t_1h: u32, pub t_1l: u32, pub t_reset: u32, - _led: PhantomData, + led: PhantomData, } impl Timing { @@ -29,7 +29,7 @@ impl Timing { t_1h: Led::T_1H.to_nanos() / clock_period_ns, t_1l: Led::T_1L.to_nanos() / clock_period_ns, t_reset: Led::T_RESET.to_nanos() / clock_period_ns, - _led: PhantomData, + led: PhantomData, } } diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index a4e52b1..9ac473b 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -110,7 +110,7 @@ where spi: Spi, pub timing: Timing, pulses: Pulses, - _spi_word: PhantomData, + spi_word: PhantomData, } #[cfg(feature = "async")] @@ -123,7 +123,7 @@ where spi: Spi, pub timing: Timing, pulses: Pulses, - _spi_word: PhantomData, + spi_word: PhantomData, } impl @@ -139,7 +139,7 @@ where spi, pulses: Pulses::new(&timing), timing, - _spi_word: PhantomData, + spi_word: PhantomData, } } } @@ -158,7 +158,7 @@ where spi, pulses: Pulses::new::(&timing), timing, - _spi_word: PhantomData, + spi_word: PhantomData, } } } From 612d46bed999e379e1a30523106699ee49309acc Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 04:05:07 +1100 Subject: [PATCH 04/36] Rename for consistency with https://github.com/ahdinosaur/blinksy/blob/main/blinksy/src/control.rs#L249 --- blinksy/src/driver/clockless/spi/builder.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/builder.rs b/blinksy/src/driver/clockless/spi/builder.rs index b71356a..1ba28f0 100644 --- a/blinksy/src/driver/clockless/spi/builder.rs +++ b/blinksy/src/driver/clockless/spi/builder.rs @@ -17,13 +17,13 @@ pub struct ClocklessSpiBuilder< Led, SpiWord, Freq, - Dm, + Exec, > { spi: Spi, freq_hz: Freq, led: PhantomData, spi_word: PhantomData, - dm: PhantomData, + exec: PhantomData, } impl @@ -37,7 +37,7 @@ impl freq_hz: self.freq_hz, led: PhantomData, spi_word: PhantomData, - dm: PhantomData, + exec: PhantomData, } } } @@ -54,7 +54,7 @@ impl freq_hz, led: PhantomData, spi_word: PhantomData, - dm: PhantomData, + exec: PhantomData, } } } @@ -70,7 +70,7 @@ impl freq_hz: self.freq_hz, led: PhantomData, spi_word: PhantomData, - dm: PhantomData, + exec: PhantomData, } } } @@ -86,7 +86,7 @@ impl freq_hz: self.freq_hz, led: PhantomData, spi_word: PhantomData, - dm: PhantomData, + exec: PhantomData, } } } @@ -103,7 +103,7 @@ impl freq_hz: self.freq_hz, led: PhantomData, spi_word: PhantomData, - dm: PhantomData, + exec: PhantomData, } } } @@ -115,7 +115,7 @@ impl Default for ClocklessSpiBuilder<0, 0, (), Led, SpiWord, (), B freq_hz: (), led: PhantomData, spi_word: PhantomData, - dm: PhantomData, + exec: PhantomData, } } } From 246e6d47288e659cd6b3235e4c3fbb45c7dec676 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 04:58:02 +1100 Subject: [PATCH 05/36] Make it easier to build examples in blinksy-esp --- esp/Cargo.lock | 5 +++ esp/blinksy-esp/Cargo.toml | 32 ++++++++++++++----- .../examples/ws2812-spi-matrix-8x8.rs | 9 ++++-- esp/blinksy-esp/examples/ws2812.rs | 1 + 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/esp/Cargo.lock b/esp/Cargo.lock index 07cd636..c4380d6 100644 --- a/esp/Cargo.lock +++ b/esp/Cargo.lock @@ -107,7 +107,12 @@ version = "0.11.0" dependencies = [ "blinksy", "defmt 1.1.1", + "embassy-executor", + "esp-alloc 0.9.0", + "esp-bootloader-esp-idf", "esp-hal", + "esp-println", + "esp-rtos", "heapless 0.9.1", "num-traits", ] diff --git a/esp/blinksy-esp/Cargo.toml b/esp/blinksy-esp/Cargo.toml index cccde02..0cd5a14 100644 --- a/esp/blinksy-esp/Cargo.toml +++ b/esp/blinksy-esp/Cargo.toml @@ -19,41 +19,57 @@ defmt = { version = "1.0.1", optional = true } heapless = "0.9.1" num-traits = { version = "0.2", default-features = false, features = ["libm"] } +[dev-dependencies] +esp-hal = { version = "1.2.0", features = ["unstable"] } +esp-alloc = "0.9.0" +esp-bootloader-esp-idf = "0.5.0" +esp-rtos = { version = "0.4.0", features = ["embassy"] } +embassy-executor = "0.10.0" +esp-println = "0.16.0" + +[[example]] +name = "ws2812-spi-matrix-8x8" +required-features = ["defmt", "async"] + +[[example]] +name = "ws2812" +required-features = ["defmt"] + [features] default = [] async = [ "blinksy/async", ] -defmt = ["dep:defmt", "blinksy/defmt", "esp-hal/defmt"] +defmt = ["dep:defmt", "blinksy/defmt", "esp-hal/defmt", "esp-println/defmt-espflash"] # Chip Support Feature Flags # Target the ESP32-C2. esp32c2 = [ - "esp-hal/esp32c2", + "esp-hal/esp32c2", "esp-alloc/esp32c2", "esp-bootloader-esp-idf/esp32c2", "esp-rtos/esp32c2", "esp-println/esp32c2" ] # Target the ESP32-C3. esp32c3 = [ - "esp-hal/esp32c3", + "esp-hal/esp32c3", "esp-alloc/esp32c3", "esp-bootloader-esp-idf/esp32c3", "esp-rtos/esp32c3", "esp-println/esp32c3" ] # Target the ESP32-C6. esp32c6 = [ - "esp-hal/esp32c6", + "esp-hal/esp32c6", "esp-alloc/esp32c6", "esp-bootloader-esp-idf/esp32c6", "esp-rtos/esp32c6", "esp-println/esp32c6" ] # Target the ESP32-H2. esp32h2 = [ - "esp-hal/esp32h2", + "esp-hal/esp32h2", "esp-alloc/esp32h2", "esp-bootloader-esp-idf/esp32h2", "esp-rtos/esp32h2", "esp-println/esp32h2" ] # Target the ESP32. esp32 = [ - "esp-hal/esp32", + "esp-hal/esp32", "esp-alloc/esp32", "esp-bootloader-esp-idf/esp32", "esp-rtos/esp32", "esp-println/esp32" ] # Target the ESP32-S2. esp32s2 = [ - "esp-hal/esp32s2", + "esp-hal/esp32s2", "esp-alloc/esp32s2", "esp-bootloader-esp-idf/esp32s2", "esp-rtos/esp32s2", "esp-println/esp32s2" ] # Target the ESP32-S3. esp32s3 = [ - "esp-hal/esp32s3", + "esp-hal/esp32s3", "esp-alloc/esp32s3", "esp-bootloader-esp-idf/esp32s3", "esp-rtos/esp32s3", "esp-println/esp32s3" ] [package.metadata.docs.rs] diff --git a/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs b/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs index ad47500..a314415 100644 --- a/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs +++ b/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs @@ -29,10 +29,15 @@ use esp_hal::{ timer::timg::TimerGroup, Async, }; -use panic_rtt_target as _; +use esp_println as _; extern crate alloc; +#[panic_handler] +fn panic(_: &core::panic::PanicInfo) -> ! { + loop {} +} + esp_bootloader_esp_idf::esp_app_desc!(); #[allow( @@ -41,8 +46,6 @@ esp_bootloader_esp_idf::esp_app_desc!(); )] #[esp_rtos::main] async fn main(_spawner: Spawner) -> ! { - rtt_target::rtt_init_defmt!(); - let cpu_clock = esp_hal::clock::CpuClock::max(); let config = esp_hal::Config::default().with_cpu_clock(cpu_clock); let p = esp_hal::init(config); diff --git a/esp/blinksy-esp/examples/ws2812.rs b/esp/blinksy-esp/examples/ws2812.rs index 203e0b8..069a284 100644 --- a/esp/blinksy-esp/examples/ws2812.rs +++ b/esp/blinksy-esp/examples/ws2812.rs @@ -20,6 +20,7 @@ use blinksy::{ ControlBuilder, }; use blinksy_esp::{rmt::ClocklessRmtBuilder, time::elapsed}; +use esp_println as _; #[panic_handler] fn panic(_: &core::panic::PanicInfo) -> ! { From 0239768c4bd165d45e71e6f896df0172fb6b17c3 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 05:14:19 +1100 Subject: [PATCH 06/36] Don't make things public that don't need to be Also put more public stuff at the top of the file --- blinksy/src/driver/clockless/spi/encoding.rs | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index c129d74..632fc61 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -2,14 +2,6 @@ use crate::driver::ClocklessLed; use bitvec::{array::BitArray, order::Msb0, slice::BitSlice}; use core::marker::PhantomData; -pub const fn freq_hz_to_duration_ns(freq_hz: u32) -> u32 { - 1_000_000_000 / freq_hz -} - -pub const fn duration_ns_to_freq_hz(duration_ns: u32) -> u32 { - 1_000_000_000 / duration_ns -} - pub struct Timing { clock_period_ns: u32, pub t_0h: u32, @@ -21,7 +13,7 @@ pub struct Timing { } impl Timing { - pub const fn new(clock_period_ns: u32) -> Self { + pub(crate) const fn new(clock_period_ns: u32) -> Self { Self { clock_period_ns, t_0h: Led::T_0H.to_nanos() / clock_period_ns, @@ -75,13 +67,13 @@ impl Timing { } } -pub struct PulseCode { +pub(crate) struct PulseCode { buffer: BitArray<[u8; N], Msb0>, len: usize, } impl PulseCode { - pub fn new(high: usize, low: usize) -> Self { + fn new(high: usize, low: usize) -> Self { let mut buffer = BitArray::new([0u8; N]); for mut v in &mut buffer[..high] { v.set(true); @@ -92,29 +84,29 @@ impl PulseCode { } } - pub fn bits(&self) -> &BitSlice { + pub(crate) fn bits(&self) -> &BitSlice { &self.buffer[..self.len] } - pub fn len(&self) -> usize { + pub(crate) fn len(&self) -> usize { self.len } } -pub struct Pulses { +pub(crate) struct Pulses { zero: PulseCode, one: PulseCode, } impl Pulses { - pub fn new(timing: &Timing) -> Pulses { + pub(crate) fn new(timing: &Timing) -> Pulses { Self { zero: PulseCode::new(timing.t_0h as usize, timing.t_0l as usize), one: PulseCode::new(timing.t_1h as usize, timing.t_1l as usize), } } - pub fn get(&self, value: bool) -> &PulseCode { + pub(crate) fn get(&self, value: bool) -> &PulseCode { if value { &self.one } else { @@ -122,3 +114,11 @@ impl Pulses { } } } + +pub(crate) const fn freq_hz_to_duration_ns(freq_hz: u32) -> u32 { + 1_000_000_000 / freq_hz +} + +pub(crate) const fn duration_ns_to_freq_hz(duration_ns: u32) -> u32 { + 1_000_000_000 / duration_ns +} From eaa94373bd3efb092a5c3d78293fcaa6dc0c4eff Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 05:15:49 +1100 Subject: [PATCH 07/36] Move helper function to the bottom of the file --- blinksy/src/driver/clockless/spi/mod.rs | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 9ac473b..44941cc 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -77,30 +77,6 @@ pub const fn clockless_spi_ideal_frequency_hz(target_toleranc duration_ns_to_freq_hz(max_clock_period_ns) } -fn encode_spi_buffer< - Led: ClocklessLed, - const FRAME_BUFFER_SIZE: usize, - const SPI_BUFFER_SIZE: usize, - const N: usize, - SpiWord, ->( - frame: heapless::Vec, - buffer: &mut BitArray<[SpiWord; SPI_BUFFER_SIZE], Msb0>, - pulses: &Pulses, -) where - [SpiWord; SPI_BUFFER_SIZE]: BitViewSized, - Led::Word: BitView, -{ - let mut dest = buffer.as_mut_bitslice(); - for v in frame { - for bit in v.view_bits::() { - let pattern = pulses.get(*bit); - dest[..pattern.len()].clone_from_bitslice(pattern.bits()); - dest = &mut dest[pattern.len()..] - } - } -} - pub struct ClocklessSpi where Led: ClocklessLed, @@ -205,3 +181,27 @@ where self.spi.write(&buffer.into_inner()).await } } + +fn encode_spi_buffer< + Led: ClocklessLed, + const FRAME_BUFFER_SIZE: usize, + const SPI_BUFFER_SIZE: usize, + const N: usize, + SpiWord, +>( + frame: heapless::Vec, + buffer: &mut BitArray<[SpiWord; SPI_BUFFER_SIZE], Msb0>, + pulses: &Pulses, +) where + [SpiWord; SPI_BUFFER_SIZE]: BitViewSized, + Led::Word: BitView, +{ + let mut dest = buffer.as_mut_bitslice(); + for v in frame { + for bit in v.view_bits::() { + let pattern = pulses.get(*bit); + dest[..pattern.len()].clone_from_bitslice(pattern.bits()); + dest = &mut dest[pattern.len()..] + } + } +} From 606bf086aa7a38f15ddea1a5672b0397afd8dec2 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 06:36:34 +1100 Subject: [PATCH 08/36] Add docs for clockless_spi_buffer_size --- blinksy/src/driver/clockless/spi/mod.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 44941cc..1679696 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -17,6 +17,28 @@ use encoding::{duration_ns_to_freq_hz, freq_hz_to_duration_ns, Pulses, Timing}; mod builder; pub use builder::ClocklessSpiBuilder; +/// Calculates the buffer size required for encoding one frame into bits that are sent to SPI +/// +/// # Usage +/// +/// ``` +/// clockless_spi_buffer_size::, _>(Layout::PIXEL_COUNT, SPI_FREQ_HZ) +/// ``` +/// +/// # Type Arguments +/// +/// - `Led` - The LED protocol implementation (must implement ClocklessLed) +/// - `Spi` - The SPI driver you're using (must implement SpiBus) +/// - `Word` - The word type of the Spi driver (should be able to be inferred from Spi) +/// +/// # Arguments +/// +/// - `pixel_count` - Number of pixels +/// - `freq_hz` - Clock frequency (in Hz) that SPI is running at +/// +/// # Returns +/// +/// Buffer size is in units of the SPI word size pub const fn clockless_spi_buffer_size( pixel_count: usize, freq_hz: u32, From 36bd5cf1e6feaee9f9ff1426281aaa721707e215 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 06:47:27 +1100 Subject: [PATCH 09/36] Add docs for clockless_spi_pulse_size --- blinksy/src/driver/clockless/spi/mod.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 1679696..2efe9f6 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] use core::marker::PhantomData; use bitvec::prelude::*; @@ -22,7 +23,7 @@ pub use builder::ClocklessSpiBuilder; /// # Usage /// /// ``` -/// clockless_spi_buffer_size::, _>(Layout::PIXEL_COUNT, SPI_FREQ_HZ) +/// clockless_spi_buffer_size::, _>(Layout::PIXEL_COUNT, SPI_FREQ_HZ) /// ``` /// /// # Type Arguments @@ -60,6 +61,27 @@ where total_bits.div_ceil(spi_word_bits) } +/// Calculates the size required for encoding one bit into the SPI buffer. +/// +/// # Usage +/// +/// ``` +/// clockless_spi_pulse_size::, _>(SPI_FREQ_HZ) +/// ``` +/// +/// # Type Arguments +/// +/// - `Led` - The LED protocol implementation (must implement ClocklessLed) +/// - `Spi` - The SPI driver you're using (must implement SpiBus) +/// - `Word` - The word type of the Spi driver (should be able to be inferred from Spi) +/// +/// # Arguments +/// +/// - `freq_hz` - Clock frequency (in Hz) that SPI is running at +/// +/// # Returns +/// +/// Size is in units of the SPI word size pub const fn clockless_spi_pulse_size(freq_hz: u32) -> usize where Spi: SpiBus, From a752fe682ad30229a7d1ecfa5a2f571f44cdfe90 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 07:01:44 +1100 Subject: [PATCH 10/36] Add docs for builder --- blinksy/src/driver/clockless/spi/builder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blinksy/src/driver/clockless/spi/builder.rs b/blinksy/src/driver/clockless/spi/builder.rs index 1ba28f0..74f993f 100644 --- a/blinksy/src/driver/clockless/spi/builder.rs +++ b/blinksy/src/driver/clockless/spi/builder.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use core::marker::PhantomData; use embedded_hal::spi::SpiBus; @@ -10,6 +11,7 @@ use crate::driver::ClocklessSpi; use crate::driver::ClocklessSpiAsync; use crate::markers::{Async, Blocking}; +/// Builder for [`ClocklessSpi`] and [`ClocklessSpiAsync`] pub struct ClocklessSpiBuilder< const BUFFER_SIZE: usize, const PULSE_SIZE: usize, From aea4ce43da9b28627dae1e99969474a91fcec77b Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 07:10:31 +1100 Subject: [PATCH 11/36] Add link to the spi driver in the clockless docs --- blinksy/src/driver/clockless/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blinksy/src/driver/clockless/mod.rs b/blinksy/src/driver/clockless/mod.rs index f9fafb5..812ac49 100644 --- a/blinksy/src/driver/clockless/mod.rs +++ b/blinksy/src/driver/clockless/mod.rs @@ -27,6 +27,8 @@ //! ## Writers //! //! - ~~[`ClocklessDelay`]: Writer using GPIO bit-banging with a delay timer~~ +//! - [`ClocklessSpi`]: Writer using any SPI that implements the [`embedded_hal::spi::SpiBus`] trait +//! - [`ClocklessSpiAsync`]: Writer using any SPI that implements the [`embedded_hal_async::spi::SpiBus`] trait //! - [`blinksy-esp::ClocklessRmt`]: Writer using RMT on the ESP32 //! //! [`blinksy-esp::ClocklessRmt`]: https://docs.rs/blinksy-esp/0.10/blinksy_esp/type.ClocklessRmt.html From 2868cc3fc415b6e1b52e84f7df3b86259fc291f7 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 07:51:20 +1100 Subject: [PATCH 12/36] First rough pass at documentation for ClocklessSpi --- blinksy/src/driver/clockless/spi/mod.rs | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 2efe9f6..4ebb51f 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -121,6 +121,42 @@ pub const fn clockless_spi_ideal_frequency_hz(target_toleranc duration_ns_to_freq_hz(max_clock_period_ns) } +/// Writer for clockless LEDs using [SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface) +/// +/// This works for any SPI driver that implements the [`embedded_hal::spi::SpiBus`] or [`embedded_hal_async::spi::SpiBus`] traits. +/// +/// # How this works +/// +/// Given an SPI that you have set to a particular frequency we figure out how to send the data out to best match the timings required by the LED. +/// In general, it's not possible to *exactly* match the timings. However, there is a timing tolerance (in the datasheets) that allows for a certain +/// amount of error. With [`clockless_spi_ideal_frequency_hz`] you can figure out the ideal frequency to run your SPI at given a certain +/// acceptable timing tolerance. A good starting point is the tolerance from the datasheet. For example in the case of WS2812B that is 150 ns. +/// The higher the tolerance that you and your hardware allows, the smaller number of bits in the SPI buffer that each bit can be encoded in and the +/// less memory and processing you will use. +/// +/// # Usage +/// +/// ``` +/// const SPI_FREQ_HZ: u32 = clockless_spi_ideal_frequency_hz::(150); +/// +/// // Create your spi driver (platform dependent) that implements the embedded-hal SpiBus trait. +/// // Set its frequency to SPI_FREQ_HZ and configure the MOSI (master out slave in) pin. +/// // The other SPI pins are not needed. +/// // +/// // let spi = ... +/// +/// let writer = ClocklessSpiBuilder::default() +/// .with_spi(spi) +/// .with_freq_hz(SPI_FREQ_HZ) +/// // Note that Spi should be the type of spi above +/// .with_buffer_size::<{ clockless_spi_buffer_size::(Layout::PIXEL_COUNT, SPI_FREQ_HZ) }>() +/// .with_pulse_size::<{ clockless_spi_pulse_size::(SPI_FREQ_HZ) }>() +/// .build(); +/// +/// let driver = ClocklessDriver::default() +/// .with_led::() +/// .with_writer(writer); +/// ``` pub struct ClocklessSpi where Led: ClocklessLed, From 3ebf906603a7ed852ed39d9c8b8c0dded1894645 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:04:15 +1100 Subject: [PATCH 13/36] document Timing --- blinksy/src/driver/clockless/spi/encoding.rs | 12 ++++++++++++ blinksy/src/driver/clockless/spi/mod.rs | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index 632fc61..f21e6a4 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -2,12 +2,21 @@ use crate::driver::ClocklessLed; use bitvec::{array::BitArray, order::Msb0, slice::BitSlice}; use core::marker::PhantomData; +/// Represents the timing (in number of SPI bits) to encode zero, one and reset LED signals +/// +/// These timings are equivalent to those in [`ClocklessLed`] but converted to number of SPI bits for +/// a particular SPI frequency. pub struct Timing { clock_period_ns: u32, + /// Number of SPI bits for the zero signal high pub t_0h: u32, + /// Number of SPI bits for the zero signal low pub t_0l: u32, + /// Number of SPI bits for the one signal high pub t_1h: u32, + /// Number of SPI bits for the one signal low pub t_1l: u32, + /// Number of SPI bits for the reset signal pub t_reset: u32, led: PhantomData, } @@ -33,6 +42,7 @@ impl Timing { self.t_1h + self.t_1l } + /// Returns total number of SPI bits needed to send a zero or a one signal pub const fn duty_cycle_bits(&self) -> u32 { let t0 = self.t0(); let t1 = self.t1(); @@ -44,6 +54,8 @@ impl Timing { } } + /// Returns the maximum error in nanoseconds that this encoding + /// has compared to the ideal timings in [`ClocklessLed`] pub const fn max_error_ns(&self) -> u32 { let error_0h_ns = Led::T_0H.to_nanos() - self.t_0h * self.clock_period_ns; let error_0l_ns = Led::T_0L.to_nanos() - self.t_0l * self.clock_period_ns; diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 4ebb51f..be21806 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -14,7 +14,8 @@ use crate::driver::ClocklessWriterAsync; use crate::driver::{ClocklessLed, ClocklessWriter}; mod encoding; -use encoding::{duration_ns_to_freq_hz, freq_hz_to_duration_ns, Pulses, Timing}; +pub use encoding::Timing; +use encoding::{duration_ns_to_freq_hz, freq_hz_to_duration_ns, Pulses}; mod builder; pub use builder::ClocklessSpiBuilder; @@ -164,6 +165,7 @@ where SpiWord: Copy + 'static, { spi: Spi, + /// Details of the timing used for sending the SPI signal pub timing: Timing, pulses: Pulses, spi_word: PhantomData, @@ -177,6 +179,7 @@ where SpiWord: Copy + 'static, { spi: Spi, + /// Details of the timing used for sending the SPI signal pub timing: Timing, pulses: Pulses, spi_word: PhantomData, From d07323e9874618feba54e0161fad03d346d68e17 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:12:48 +1100 Subject: [PATCH 14/36] Add rough docs for ClocklessSpiAsync --- blinksy/src/driver/clockless/spi/mod.rs | 39 ++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index be21806..799095a 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -124,7 +124,7 @@ pub const fn clockless_spi_ideal_frequency_hz(target_toleranc /// Writer for clockless LEDs using [SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface) /// -/// This works for any SPI driver that implements the [`embedded_hal::spi::SpiBus`] or [`embedded_hal_async::spi::SpiBus`] traits. +/// This works for any SPI driver that implements the [`embedded_hal::spi::SpiBus`] trait. /// /// # How this works /// @@ -171,6 +171,43 @@ where spi_word: PhantomData, } +/// Async writer for clockless LEDs using [SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface) +/// +/// This works for any SPI driver that implements the [`embedded_hal_async::spi::SpiBus`] trait. +/// +/// # How this works +/// +/// Given an SPI that you have set to a particular frequency we figure out how to send the data out to best match the timings required by the LED. +/// In general, it's not possible to *exactly* match the timings. However, there is a timing tolerance (in the datasheets) that allows for a certain +/// amount of error. With [`clockless_spi_ideal_frequency_hz`] you can figure out the ideal frequency to run your SPI at given a certain +/// acceptable timing tolerance. A good starting point is the tolerance from the datasheet. For example in the case of WS2812B that is 150 ns. +/// The higher the tolerance that you and your hardware allows, the smaller number of bits in the SPI buffer that each bit can be encoded in and the +/// less memory and processing you will use. +/// +/// # Usage +/// +/// ``` +/// const SPI_FREQ_HZ: u32 = clockless_spi_ideal_frequency_hz::(150); +/// +/// // Create your spi driver (platform dependent) that implements the embedded-hal-async SpiBus trait. +/// // Set its frequency to SPI_FREQ_HZ and configure the MOSI (master out slave in) pin. +/// // The other SPI pins are not needed. +/// // +/// // let spi = ... +/// +/// let writer = ClocklessSpiBuilder::default() +/// .with_spi(spi) +/// .with_freq_hz(SPI_FREQ_HZ) +/// // Note that Spi should be the type of spi above +/// .with_buffer_size::<{ clockless_spi_buffer_size::(Layout::PIXEL_COUNT, SPI_FREQ_HZ) }>() +/// .with_pulse_size::<{ clockless_spi_pulse_size::(SPI_FREQ_HZ) }>() +/// .with_async() +/// .build(); +/// +/// let driver = ClocklessDriver::default() +/// .with_led::() +/// .with_writer(writer); +/// ``` #[cfg(feature = "async")] pub struct ClocklessSpiAsync where From 96c68d7b3a9a37fc9ef91c5f80131c252b1420e3 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:23:37 +1100 Subject: [PATCH 15/36] Add documentation for frequency calculation --- blinksy/src/driver/clockless/spi/mod.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 799095a..5c157af 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -96,12 +96,24 @@ where total_bits.div_ceil(spi_word_bits) } -// Brute-force an "ideal" clock frequency to run the SPI bus at -// Lower frequencies are better because they mean that we need fewer spi bits to -// encode a single LED bit which means less processing and memory usage. However -// lower frequencies increase the timing errors. -// Each LED has a tolerance for timing variations. We take advantage of this to pick -// the lowest clock frequency that gives us errors within our chosen target tolerance. +/// Calculates the "ideal" clock frequency the the SPI bus at given a particular target LED timing tolerance in nanoseconds. +/// +/// The higher the target tolerance, the smaller the number of bits required for encoding and the lower the SPI frequency needs to +/// be with lower memory and CPU usage. +/// +/// You probably want to use the tolerance from the LED datasheet though your hardware may still work with a higher tolerance. +/// +/// # Type Arguments +/// +/// - `Led` - The LED protocol implementation (must implement ClocklessLed) +/// +/// # Arguments +/// +/// - `target_tolerance_ns` - The acceptable timing error in nanoseconds +/// +/// # Returns +/// +/// Frequency in Hz pub const fn clockless_spi_ideal_frequency_hz(target_tolerance_ns: u32) -> u32 { // There's going to be some smart ways of doing this but for the time being // let's just do the simplest possible thing and explore a whole range of timings From 17f1fd5b9fb82accda560590a4f9ed1e40ee4f7c Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:27:37 +1100 Subject: [PATCH 16/36] Remove warn(missing_docs) --- blinksy/src/driver/clockless/spi/builder.rs | 1 - blinksy/src/driver/clockless/spi/mod.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/builder.rs b/blinksy/src/driver/clockless/spi/builder.rs index 74f993f..ae019b7 100644 --- a/blinksy/src/driver/clockless/spi/builder.rs +++ b/blinksy/src/driver/clockless/spi/builder.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use core::marker::PhantomData; use embedded_hal::spi::SpiBus; diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 5c157af..21d112b 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -1,4 +1,3 @@ -#![warn(missing_docs)] use core::marker::PhantomData; use bitvec::prelude::*; From fb003f277e5cc8824054938272c6c5bad208685a Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:29:37 +1100 Subject: [PATCH 17/36] Rename struct --- blinksy/src/driver/clockless/spi/encoding.rs | 6 +++--- blinksy/src/driver/clockless/spi/mod.rs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index f21e6a4..8b3ac42 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -6,7 +6,7 @@ use core::marker::PhantomData; /// /// These timings are equivalent to those in [`ClocklessLed`] but converted to number of SPI bits for /// a particular SPI frequency. -pub struct Timing { +pub struct ClocklessSpiTiming { clock_period_ns: u32, /// Number of SPI bits for the zero signal high pub t_0h: u32, @@ -21,7 +21,7 @@ pub struct Timing { led: PhantomData, } -impl Timing { +impl ClocklessSpiTiming { pub(crate) const fn new(clock_period_ns: u32) -> Self { Self { clock_period_ns, @@ -111,7 +111,7 @@ pub(crate) struct Pulses { } impl Pulses { - pub(crate) fn new(timing: &Timing) -> Pulses { + pub(crate) fn new(timing: &ClocklessSpiTiming) -> Pulses { Self { zero: PulseCode::new(timing.t_0h as usize, timing.t_0l as usize), one: PulseCode::new(timing.t_1h as usize, timing.t_1l as usize), diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 21d112b..3008385 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -13,7 +13,7 @@ use crate::driver::ClocklessWriterAsync; use crate::driver::{ClocklessLed, ClocklessWriter}; mod encoding; -pub use encoding::Timing; +pub use encoding::ClocklessSpiTiming; use encoding::{duration_ns_to_freq_hz, freq_hz_to_duration_ns, Pulses}; mod builder; pub use builder::ClocklessSpiBuilder; @@ -49,7 +49,7 @@ where Word: Copy + 'static, { let clock_period_ns = freq_hz_to_duration_ns(freq_hz); - let timing = Timing::::new(clock_period_ns); + let timing = ClocklessSpiTiming::::new(clock_period_ns); // TODO: Check that resulting timings are within spec for the LED and error if not let spi_word_bits = size_of::() * 8; @@ -88,7 +88,7 @@ where Word: Copy + 'static, { let clock_period_ns = freq_hz_to_duration_ns(freq_hz); - let timing = Timing::::new(clock_period_ns); + let timing = ClocklessSpiTiming::::new(clock_period_ns); let spi_word_bits = size_of::() * 8; let total_bits = timing.duty_cycle_bits() as usize; @@ -121,7 +121,7 @@ pub const fn clockless_spi_ideal_frequency_hz(target_toleranc let mut max_clock_period_ns = 0; let mut clock_period_ns = 1; loop { - let error_ns = Timing::::new(clock_period_ns).max_error_ns(); + let error_ns = ClocklessSpiTiming::::new(clock_period_ns).max_error_ns(); if error_ns < target_tolerance_ns && clock_period_ns > max_clock_period_ns { max_clock_period_ns = clock_period_ns; } @@ -177,7 +177,7 @@ where { spi: Spi, /// Details of the timing used for sending the SPI signal - pub timing: Timing, + pub timing: ClocklessSpiTiming, pulses: Pulses, spi_word: PhantomData, } @@ -228,7 +228,7 @@ where { spi: Spi, /// Details of the timing used for sending the SPI signal - pub timing: Timing, + pub timing: ClocklessSpiTiming, pulses: Pulses, spi_word: PhantomData, } @@ -241,7 +241,7 @@ where SpiWord: Copy + 'static, { pub fn new(spi: Spi, freq_hz: u32) -> Self { - let timing = Timing::new(freq_hz_to_duration_ns(freq_hz)); + let timing = ClocklessSpiTiming::new(freq_hz_to_duration_ns(freq_hz)); Self { spi, pulses: Pulses::new(&timing), @@ -260,7 +260,7 @@ where Spi: SpiBusAsync, { pub fn new(spi: Spi, freq_hz: u32) -> Self { - let timing = Timing::new(freq_hz_to_duration_ns(freq_hz)); + let timing = ClocklessSpiTiming::new(freq_hz_to_duration_ns(freq_hz)); Self { spi, pulses: Pulses::new::(&timing), From 8412e6f158e40e8fa3381a86521e64b92b9de791 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:39:35 +1100 Subject: [PATCH 18/36] Improve "how this works" section --- blinksy/src/driver/clockless/spi/mod.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 3008385..d33561b 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -139,12 +139,12 @@ pub const fn clockless_spi_ideal_frequency_hz(target_toleranc /// /// # How this works /// -/// Given an SPI that you have set to a particular frequency we figure out how to send the data out to best match the timings required by the LED. -/// In general, it's not possible to *exactly* match the timings. However, there is a timing tolerance (in the datasheets) that allows for a certain -/// amount of error. With [`clockless_spi_ideal_frequency_hz`] you can figure out the ideal frequency to run your SPI at given a certain -/// acceptable timing tolerance. A good starting point is the tolerance from the datasheet. For example in the case of WS2812B that is 150 ns. -/// The higher the tolerance that you and your hardware allows, the smaller number of bits in the SPI buffer that each bit can be encoded in and the -/// less memory and processing you will use. +/// Given an SPI running at particular frequency the driver figures out how to send the data out that best matches the timings required by the LED. +/// +/// In general, it's not possible to *exactly* match the timings. However, clockless LEDs have some designed tolerance built in. +/// +/// Given an acceptable timing error (which we can look up in the LED datasheet) we can calculate an "ideal" SPI frequency. +/// Use [`clockless_spi_ideal_frequency_hz`] to do this first. /// /// # Usage /// @@ -188,12 +188,12 @@ where /// /// # How this works /// -/// Given an SPI that you have set to a particular frequency we figure out how to send the data out to best match the timings required by the LED. -/// In general, it's not possible to *exactly* match the timings. However, there is a timing tolerance (in the datasheets) that allows for a certain -/// amount of error. With [`clockless_spi_ideal_frequency_hz`] you can figure out the ideal frequency to run your SPI at given a certain -/// acceptable timing tolerance. A good starting point is the tolerance from the datasheet. For example in the case of WS2812B that is 150 ns. -/// The higher the tolerance that you and your hardware allows, the smaller number of bits in the SPI buffer that each bit can be encoded in and the -/// less memory and processing you will use. +/// Given an SPI running at particular frequency the driver figures out how to send the data out that best matches the timings required by the LED. +/// +/// In general, it's not possible to *exactly* match the timings. However, clockless LEDs have some designed tolerance built in. +/// +/// Given an acceptable timing error (which we can look up in the LED datasheet) we can calculate an "ideal" SPI frequency. +/// Use [`clockless_spi_ideal_frequency_hz`] to do this first. /// /// # Usage /// From c919176b3c37528fcefcee4c4d64ced74678c637 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:48:11 +1100 Subject: [PATCH 19/36] Code in added documentation can't be run unfortunately --- blinksy/src/driver/clockless/spi/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index d33561b..a9f047e 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -22,7 +22,7 @@ pub use builder::ClocklessSpiBuilder; /// /// # Usage /// -/// ``` +/// ```rust,ignore /// clockless_spi_buffer_size::, _>(Layout::PIXEL_COUNT, SPI_FREQ_HZ) /// ``` /// @@ -65,7 +65,7 @@ where /// /// # Usage /// -/// ``` +/// ```rust,ignore /// clockless_spi_pulse_size::, _>(SPI_FREQ_HZ) /// ``` /// @@ -148,7 +148,7 @@ pub const fn clockless_spi_ideal_frequency_hz(target_toleranc /// /// # Usage /// -/// ``` +/// ```rust,ignore /// const SPI_FREQ_HZ: u32 = clockless_spi_ideal_frequency_hz::(150); /// /// // Create your spi driver (platform dependent) that implements the embedded-hal SpiBus trait. @@ -197,7 +197,7 @@ where /// /// # Usage /// -/// ``` +/// ```rust,ignore /// const SPI_FREQ_HZ: u32 = clockless_spi_ideal_frequency_hz::(150); /// /// // Create your spi driver (platform dependent) that implements the embedded-hal-async SpiBus trait. From 45534ddd88f174d43c2ef4fe0449c9bb83b1cfd2 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Fri, 9 Oct 2026 08:59:47 +1100 Subject: [PATCH 20/36] Add link to new spi driver on crate level documentation --- blinksy/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blinksy/src/lib.rs b/blinksy/src/lib.rs index 9a1994b..fc4daaa 100644 --- a/blinksy/src/lib.rs +++ b/blinksy/src/lib.rs @@ -65,7 +65,7 @@ //! //! | Micro | HAL | Blinksy | Recommended Driver | Backup Driver | //! |----------------|-------------|---------------|------------------------|---------------| -//! | ALL | [embedded-hal] | [blinksy] | - | TODO [Spi #12][clockless-spi] | +//! | ALL | [embedded-hal] | [blinksy] | - | [Spi][clockless-spi] | //! | ESP32 | [esp-hal] | [blinksy-esp] | [Rmt][rmt] | - | //! | RP (2040/2350) | [rp-hal] | TODO | TODO [#36][rp-issue] | - | //! | STM32 | [stm32-hal] | TODO | TODO [#78][stm32-issue] | - | @@ -75,7 +75,7 @@ //! | CH32 | [ch32-hal] | TODO | TODO [#80][ch32-issue] | - | //! | ??? | - | - | - | - | //! -//! [clockless-spi]: https://github.com/ahdinosaur/blinksy/issues/12 +//! [clockless-spi]: crate::driver::clockless::ClocklessSpi //! [esp-hal]: https://docs.espressif.com/projects/rust/esp-hal/latest/ //! [blinksy-esp]: https://docs.rs/blinksy-esp/0.11/ //! [rmt]: https://docs.espressif.com/projects/rust/esp-hal/latest/ From 03433bea9ad0ba2b871d2a5177555081355b82c4 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 07:53:06 +1100 Subject: [PATCH 21/36] Small documentation fixes --- blinksy/src/driver/clockless/spi/encoding.rs | 2 +- blinksy/src/driver/clockless/spi/mod.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index 8b3ac42..208c586 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -2,7 +2,7 @@ use crate::driver::ClocklessLed; use bitvec::{array::BitArray, order::Msb0, slice::BitSlice}; use core::marker::PhantomData; -/// Represents the timing (in number of SPI bits) to encode zero, one and reset LED signals +/// Represents the timing (in number of SPI bits) to encode zero, one, and reset LED signals /// /// These timings are equivalent to those in [`ClocklessLed`] but converted to number of SPI bits for /// a particular SPI frequency. diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index a9f047e..2660b9e 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -28,9 +28,9 @@ pub use builder::ClocklessSpiBuilder; /// /// # Type Arguments /// -/// - `Led` - The LED protocol implementation (must implement ClocklessLed) -/// - `Spi` - The SPI driver you're using (must implement SpiBus) -/// - `Word` - The word type of the Spi driver (should be able to be inferred from Spi) +/// - `Led` - The LED protocol implementation (must implement [`ClocklessLed`]) +/// - `Spi` - The SPI driver you're using (must implement [`SpiBus`]) +/// - `Word` - The word type of the SPI driver (should be able to be inferred from `Spi`) /// /// # Arguments /// @@ -71,9 +71,9 @@ where /// /// # Type Arguments /// -/// - `Led` - The LED protocol implementation (must implement ClocklessLed) -/// - `Spi` - The SPI driver you're using (must implement SpiBus) -/// - `Word` - The word type of the Spi driver (should be able to be inferred from Spi) +/// - `Led` - The LED protocol implementation (must implement [`ClocklessLed`]) +/// - `Spi` - The SPI driver you're using (must implement [`SpiBus`]) +/// - `Word` - The word type of the SPI driver (should be able to be inferred from `Spi`) /// /// # Arguments /// @@ -104,7 +104,7 @@ where /// /// # Type Arguments /// -/// - `Led` - The LED protocol implementation (must implement ClocklessLed) +/// - `Led` - The LED protocol implementation (must implement [`ClocklessLed`]) /// /// # Arguments /// From 479d6527b6c8fe47fb757530bc11df00c16764de Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 08:32:40 +1100 Subject: [PATCH 22/36] More concise way of expressing the bitvec trait bound --- blinksy/src/driver/clockless/spi/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 2660b9e..be8a804 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -2,7 +2,6 @@ use core::marker::PhantomData; use bitvec::prelude::*; use bitvec::view::BitView; -use bitvec::view::BitViewSized; use embedded_hal::spi::SpiBus; #[cfg(feature = "async")] use embedded_hal_async::spi::SpiBus as SpiBusAsync; @@ -276,8 +275,7 @@ where Led: ClocklessLed, Led::Word: BitView, Spi: SpiBus, - SpiWord: Copy + 'static, - [SpiWord; BUFFER_SIZE]: BitViewSized, + SpiWord: Copy + 'static + BitStore, { type Error = Spi::Error; @@ -298,8 +296,7 @@ where Led: ClocklessLed, Led::Word: BitView, Spi: SpiBusAsync, - SpiWord: Copy + 'static, - [SpiWord; BUFFER_SIZE]: BitViewSized, + SpiWord: Copy + 'static + BitStore, { type Error = Spi::Error; @@ -324,7 +321,7 @@ fn encode_spi_buffer< buffer: &mut BitArray<[SpiWord; SPI_BUFFER_SIZE], Msb0>, pulses: &Pulses, ) where - [SpiWord; SPI_BUFFER_SIZE]: BitViewSized, + SpiWord: BitStore, Led::Word: BitView, { let mut dest = buffer.as_mut_bitslice(); From 2951d912e74704325675eadf60c9fd4b2bf2a2d1 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 08:58:13 +1100 Subject: [PATCH 23/36] Simplify slightly by passing BitSlice rather than BitArray to encode function --- blinksy/src/driver/clockless/spi/mod.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index be8a804..5394673 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -284,7 +284,7 @@ where frame: heapless::Vec, ) -> Result<(), Self::Error> { let mut buffer = BitArray::<[SpiWord; BUFFER_SIZE], Msb0>::ZERO; - encode_spi_buffer::(frame, &mut buffer, &self.pulses); + encode_spi_buffer::(frame, &self.pulses, buffer.as_mut_bitslice()); self.spi.write(&buffer.into_inner()) } } @@ -305,26 +305,20 @@ where frame: heapless::Vec, ) -> Result<(), Self::Error> { let mut buffer = BitArray::<[SpiWord; BUFFER_SIZE], Msb0>::ZERO; - encode_spi_buffer::(frame, &mut buffer, &self.pulses); + encode_spi_buffer::(frame, &self.pulses, buffer.as_mut_bitslice()); self.spi.write(&buffer.into_inner()).await } } -fn encode_spi_buffer< - Led: ClocklessLed, - const FRAME_BUFFER_SIZE: usize, - const SPI_BUFFER_SIZE: usize, - const N: usize, - SpiWord, ->( +fn encode_spi_buffer( frame: heapless::Vec, - buffer: &mut BitArray<[SpiWord; SPI_BUFFER_SIZE], Msb0>, pulses: &Pulses, + mut dest: &mut BitSlice, ) where SpiWord: BitStore, Led::Word: BitView, + Led: ClocklessLed, { - let mut dest = buffer.as_mut_bitslice(); for v in frame { for bit in v.view_bits::() { let pattern = pulses.get(*bit); From e658231ddb83098d5646a2844395b754e34b7b6d Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 09:17:56 +1100 Subject: [PATCH 24/36] Confine the bit mangling inside encode_spi_buffer --- blinksy/src/driver/clockless/spi/mod.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 5394673..59675f8 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -283,9 +283,9 @@ where &mut self, frame: heapless::Vec, ) -> Result<(), Self::Error> { - let mut buffer = BitArray::<[SpiWord; BUFFER_SIZE], Msb0>::ZERO; - encode_spi_buffer::(frame, &self.pulses, buffer.as_mut_bitslice()); - self.spi.write(&buffer.into_inner()) + let mut buffer = [SpiWord::ZERO; BUFFER_SIZE]; + encode_spi_buffer::(&frame, &mut buffer, &self.pulses); + self.spi.write(&buffer) } } @@ -304,21 +304,22 @@ where &mut self, frame: heapless::Vec, ) -> Result<(), Self::Error> { - let mut buffer = BitArray::<[SpiWord; BUFFER_SIZE], Msb0>::ZERO; - encode_spi_buffer::(frame, &self.pulses, buffer.as_mut_bitslice()); - self.spi.write(&buffer.into_inner()).await + let mut buffer = [SpiWord::ZERO; BUFFER_SIZE]; + encode_spi_buffer::(&frame, &mut buffer, &self.pulses); + self.spi.write(&buffer).await } } -fn encode_spi_buffer( - frame: heapless::Vec, +fn encode_spi_buffer( + frame: &[Led::Word], + buffer: &mut [SpiWord], pulses: &Pulses, - mut dest: &mut BitSlice, ) where - SpiWord: BitStore, Led::Word: BitView, Led: ClocklessLed, + SpiWord: Copy + 'static + BitStore, { + let mut dest = buffer.view_bits_mut::(); for v in frame { for bit in v.view_bits::() { let pattern = pulses.get(*bit); From 8facc5e943e208d014e3558dcb4aebfe1579e4f9 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 10:14:22 +1100 Subject: [PATCH 25/36] Fix buffer size calculation if led or spi word is not u8 --- blinksy/src/driver/clockless/mod.rs | 4 ++-- blinksy/src/driver/clockless/spi/mod.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/blinksy/src/driver/clockless/mod.rs b/blinksy/src/driver/clockless/mod.rs index 812ac49..eb558b3 100644 --- a/blinksy/src/driver/clockless/mod.rs +++ b/blinksy/src/driver/clockless/mod.rs @@ -71,7 +71,7 @@ use crate::{ color::{ColorCorrection, FromColor, LedChannels, LedColor, LinearSrgb}, driver::Driver, time::Nanoseconds, - util::component::Component, + util::{bits::Word, component::Component}, }; mod delay; @@ -105,7 +105,7 @@ pub use self::spi::*; /// ``` pub trait ClocklessLed { /// The word type (typically u8). - type Word: Component; + type Word: Component + Word; /// Duration of high signal for transmitting a '0' bit. const T_0H: Nanoseconds; diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 59675f8..3a5ece7 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -10,6 +10,7 @@ use crate::driver::t_cycle; #[cfg(feature = "async")] use crate::driver::ClocklessWriterAsync; use crate::driver::{ClocklessLed, ClocklessWriter}; +use crate::util::bits::Word; mod encoding; pub use encoding::ClocklessSpiTiming; @@ -51,13 +52,12 @@ where let timing = ClocklessSpiTiming::::new(clock_period_ns); // TODO: Check that resulting timings are within spec for the LED and error if not - let spi_word_bits = size_of::() * 8; - let total_bits = spi_word_bits - * pixel_count + let total_bits = Led::Word::BITS as usize * Led::LED_CHANNELS.channel_count() + * pixel_count * timing.duty_cycle_bits() as usize + timing.t_reset as usize; - total_bits.div_ceil(spi_word_bits) + total_bits.div_ceil(size_of::() * 8) } /// Calculates the size required for encoding one bit into the SPI buffer. From 7e8cb6aef5172307af70d3ba0f392472edf7f93c Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 11:37:28 +1100 Subject: [PATCH 26/36] First rough implementation of copying bits around --- Cargo.lock | 42 -------------- blinksy/Cargo.toml | 3 - blinksy/src/driver/clockless/spi/builder.rs | 5 +- blinksy/src/driver/clockless/spi/encoding.rs | 30 +++++----- blinksy/src/driver/clockless/spi/mod.rs | 37 ++++++------ blinksy/src/util/bits.rs | 60 ++++++++++++++++++++ esp/Cargo.lock | 42 -------------- 7 files changed, 94 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2f1fee..3347126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,22 +48,10 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -[[package]] -name = "bitvec" -version = "1.1.0" -source = "git+https://github.com/ferrilab/ferrilab.git?rev=6af4ab0a33233573326f4b27f0193a2c956aada5#6af4ab0a33233573326f4b27f0193a2c956aada5" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blinksy" version = "0.11.0" dependencies = [ - "bitvec", "defmt 0.3.100", "embedded-hal 1.0.0", "embedded-hal-async", @@ -470,12 +458,6 @@ dependencies = [ "gcd", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-core" version = "0.3.33" @@ -947,15 +929,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radium" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1775bc532a9bfde46e26eba441ca1171b91608d14a3bae71fea371f18a00cffe" -dependencies = [ - "cfg-if", -] - [[package]] name = "rand" version = "0.10.2" @@ -1105,12 +1078,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" version = "1.0.69" @@ -1593,15 +1560,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x11-clipboard" version = "0.9.3" diff --git a/blinksy/Cargo.toml b/blinksy/Cargo.toml index e411fa2..e978178 100644 --- a/blinksy/Cargo.toml +++ b/blinksy/Cargo.toml @@ -12,9 +12,6 @@ keywords.workspace = true categories.workspace = true [dependencies] -# radium 0.7 is a dependency of the latest released version of bitvec which breaks on esp32. So using an unreleased -# version for now. See https://github.com/ferrilab/ferrilab/issues/1#issuecomment-4243321551 -bitvec = { git = "https://github.com/ferrilab/ferrilab.git", rev = "6af4ab0a33233573326f4b27f0193a2c956aada5", default-features = false } defmt = { version = "0.3.10", optional = true } embedded-hal = "1.0.0" embedded-hal-async = { version = "1.0.0", optional = true } diff --git a/blinksy/src/driver/clockless/spi/builder.rs b/blinksy/src/driver/clockless/spi/builder.rs index ae019b7..44b956d 100644 --- a/blinksy/src/driver/clockless/spi/builder.rs +++ b/blinksy/src/driver/clockless/spi/builder.rs @@ -9,6 +9,7 @@ use crate::driver::ClocklessSpi; #[cfg(feature = "async")] use crate::driver::ClocklessSpiAsync; use crate::markers::{Async, Blocking}; +use crate::util::bits::Word; /// Builder for [`ClocklessSpi`] and [`ClocklessSpiAsync`] pub struct ClocklessSpiBuilder< @@ -125,7 +126,7 @@ impl ClocklessSpiBuilder where Spi: SpiBus, - SpiWord: Copy + 'static, + SpiWord: Word + 'static, Led: ClocklessLed, { pub fn build(self) -> ClocklessSpi { @@ -140,7 +141,7 @@ impl ClocklessSpiBuilder where Spi: SpiBusAsync, - SpiWord: Copy + 'static, + SpiWord: Word + 'static, Led: ClocklessLed, { pub fn build(self) -> ClocklessSpiAsync { diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index 208c586..2c90cc0 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -1,5 +1,5 @@ use crate::driver::ClocklessLed; -use bitvec::{array::BitArray, order::Msb0, slice::BitSlice}; +use crate::util::bits::{copy_bits_msb, Word}; use core::marker::PhantomData; /// Represents the timing (in number of SPI bits) to encode zero, one, and reset LED signals @@ -79,25 +79,23 @@ impl ClocklessSpiTiming { } } -pub(crate) struct PulseCode { - buffer: BitArray<[u8; N], Msb0>, +pub(crate) struct PulseCode { + buffer: [W; N], len: usize, } -impl PulseCode { +impl PulseCode { fn new(high: usize, low: usize) -> Self { - let mut buffer = BitArray::new([0u8; N]); - for mut v in &mut buffer[..high] { - v.set(true); - } + let mut buffer = [W::ZERO; N]; + copy_bits_msb(&[!W::ZERO; N], high, &mut buffer, 0); Self { buffer, len: high + low, } } - pub(crate) fn bits(&self) -> &BitSlice { - &self.buffer[..self.len] + pub(crate) fn bits(&self) -> &[W] { + &self.buffer } pub(crate) fn len(&self) -> usize { @@ -105,20 +103,20 @@ impl PulseCode { } } -pub(crate) struct Pulses { - zero: PulseCode, - one: PulseCode, +pub(crate) struct Pulses { + zero: PulseCode, + one: PulseCode, } -impl Pulses { - pub(crate) fn new(timing: &ClocklessSpiTiming) -> Pulses { +impl Pulses { + pub(crate) fn new(timing: &ClocklessSpiTiming) -> Self { Self { zero: PulseCode::new(timing.t_0h as usize, timing.t_0l as usize), one: PulseCode::new(timing.t_1h as usize, timing.t_1l as usize), } } - pub(crate) fn get(&self, value: bool) -> &PulseCode { + pub(crate) fn get(&self, value: bool) -> &PulseCode { if value { &self.one } else { diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 3a5ece7..0afdf32 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -1,7 +1,5 @@ use core::marker::PhantomData; -use bitvec::prelude::*; -use bitvec::view::BitView; use embedded_hal::spi::SpiBus; #[cfg(feature = "async")] use embedded_hal_async::spi::SpiBus as SpiBusAsync; @@ -10,7 +8,7 @@ use crate::driver::t_cycle; #[cfg(feature = "async")] use crate::driver::ClocklessWriterAsync; use crate::driver::{ClocklessLed, ClocklessWriter}; -use crate::util::bits::Word; +use crate::util::bits::{word_to_bits_msb, BitWriterMsb, Word}; mod encoding; pub use encoding::ClocklessSpiTiming; @@ -177,7 +175,7 @@ where spi: Spi, /// Details of the timing used for sending the SPI signal pub timing: ClocklessSpiTiming, - pulses: Pulses, + pulses: Pulses, spi_word: PhantomData, } @@ -228,7 +226,7 @@ where spi: Spi, /// Details of the timing used for sending the SPI signal pub timing: ClocklessSpiTiming, - pulses: Pulses, + pulses: Pulses, spi_word: PhantomData, } @@ -237,7 +235,7 @@ impl where Led: ClocklessLed, Spi: SpiBus, - SpiWord: Copy + 'static, + SpiWord: Word + 'static, { pub fn new(spi: Spi, freq_hz: u32) -> Self { let timing = ClocklessSpiTiming::new(freq_hz_to_duration_ns(freq_hz)); @@ -255,7 +253,7 @@ impl ClocklessSpiAsync where Led: ClocklessLed, - SpiWord: Copy + 'static, + SpiWord: Word + 'static, Spi: SpiBusAsync, { pub fn new(spi: Spi, freq_hz: u32) -> Self { @@ -273,9 +271,9 @@ impl Clock for ClocklessSpi where Led: ClocklessLed, - Led::Word: BitView, + Led::Word: Word, Spi: SpiBus, - SpiWord: Copy + 'static + BitStore, + SpiWord: Word + 'static, { type Error = Spi::Error; @@ -294,9 +292,9 @@ impl Clock for ClocklessSpiAsync where Led: ClocklessLed, - Led::Word: BitView, + Led::Word: Word, Spi: SpiBusAsync, - SpiWord: Copy + 'static + BitStore, + SpiWord: Word + 'static, { type Error = Spi::Error; @@ -313,18 +311,17 @@ where fn encode_spi_buffer( frame: &[Led::Word], buffer: &mut [SpiWord], - pulses: &Pulses, + pulses: &Pulses, ) where - Led::Word: BitView, + Led::Word: Word, Led: ClocklessLed, - SpiWord: Copy + 'static + BitStore, + SpiWord: Word + 'static, { - let mut dest = buffer.view_bits_mut::(); - for v in frame { - for bit in v.view_bits::() { - let pattern = pulses.get(*bit); - dest[..pattern.len()].clone_from_bitslice(pattern.bits()); - dest = &mut dest[pattern.len()..] + let mut writer = BitWriterMsb::new(buffer); + for word in frame { + for bit in word_to_bits_msb(*word) { + let pulse = pulses.get(bit); + writer.write_bits(pulse.bits(), pulse.len()); } } } diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index 555bd68..43aa54e 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -9,6 +9,8 @@ pub trait Word: Copy + PartialEq + core::ops::BitAnd + + core::ops::BitOr + + core::ops::Not + core::ops::Shl + core::ops::Shr { @@ -74,6 +76,48 @@ pub fn word_to_bits_msb(word: W) -> BitsMsb { BitsMsb::new(word) } +/// Copies the first `len` bits of `src` into `dst`, starting at bit `dst_start` +pub fn copy_bits_msb(src: &[W], len: usize, dst: &mut [W], dst_start: usize) { + let word_bits = W::BITS as usize; + let dst_first_index = dst_start / word_bits; + let dst_shift = dst_start % word_bits; + + for (i, &src_word) in src[..len.div_ceil(word_bits)].iter().enumerate() { + let dst_index = dst_first_index + i; + // Number of bits to copy in this word + let count = (len - i * word_bits).min(word_bits); + // Selects the top `count` bits, which are the ones being copied + let mask = !W::ZERO << (word_bits - count); + let src_masked_word = src_word & mask; + + // The bits that fit into the current destination word + dst[dst_index] = (dst[dst_index] & !(mask >> dst_shift)) | (src_masked_word >> dst_shift); + // The bits that overflow into the next destination word + if dst_shift + count > word_bits { + let back = word_bits - dst_shift; + dst[dst_index + 1] = (dst[dst_index + 1] & !(mask << back)) | (src_masked_word << back); + } + } +} + +/// Appends bits to a slice of words, filling each word from its most significant bit +pub struct BitWriterMsb<'a, W: Word> { + words: &'a mut [W], + position: usize, +} + +impl<'a, W: Word> BitWriterMsb<'a, W> { + pub fn new(words: &'a mut [W]) -> Self { + Self { words, position: 0 } + } + + /// Appends the first `len` bits of `src` + pub fn write_bits(&mut self, src: &[W], len: usize) { + copy_bits_msb(src, len, self.words, self.position); + self.position += len; + } +} + #[cfg(test)] mod tests { use super::*; @@ -101,4 +145,20 @@ mod tests { ]) ); } + + #[test] + fn test_copy_bits_msb_simple() { + let mut dst: [u8; 2] = [0b1111_111, 0b1111_1111]; + copy_bits_msb(&[0b0000_0000, 0b0000_0000], 11, &mut dst, 0); + + assert_eq!(dst, [0b0000_0000, 0b0001_1111]); + } + + #[test] + fn test_copy_bits_msb_across_word_boundary() { + let mut dst: [u8; 3] = [0b1111_1111, 0b1111_1111, 0b1111_1111]; + copy_bits_msb(&[0b0000_0000, 0b0000_0000], 11, &mut dst, 5); + + assert_eq!(dst, [0b1111_1000, 0b0000_0000, 0b1111_1111]); + } } diff --git a/esp/Cargo.lock b/esp/Cargo.lock index c4380d6..5000db1 100644 --- a/esp/Cargo.lock +++ b/esp/Cargo.lock @@ -73,22 +73,10 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "1.1.0" -source = "git+https://github.com/ferrilab/ferrilab.git?rev=6af4ab0a33233573326f4b27f0193a2c956aada5#6af4ab0a33233573326f4b27f0193a2c956aada5" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blinksy" version = "0.11.0" dependencies = [ - "bitvec", "defmt 0.3.100", "embedded-hal 1.0.0", "embedded-hal-async", @@ -1226,12 +1214,6 @@ dependencies = [ "gcd", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-core" version = "0.3.31" @@ -1625,15 +1607,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1775bc532a9bfde46e26eba441ca1171b91608d14a3bae71fea371f18a00cffe" -dependencies = [ - "cfg-if", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -2010,12 +1983,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "termcolor" version = "1.4.1" @@ -2312,15 +2279,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "xtensa-lx" version = "0.13.0" From 8607d57df63d73bff25ca2fe7d0180ec22a37aa1 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 11:53:59 +1100 Subject: [PATCH 27/36] Add function to just copy a bit of a word --- blinksy/src/util/bits.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index 43aa54e..cb047d0 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -100,6 +100,22 @@ pub fn copy_bits_msb(src: &[W], len: usize, dst: &mut [W], dst_start: u } } +fn copy_bits_in_word( + src: W, + dst: &mut W, + src_offset: usize, + dst_offset: usize, + len: usize, +) { + let word_bits = W::BITS as usize; + + let len_mask = !W::ZERO << (word_bits - len); + let src_mask = len_mask >> src_offset; + let dst_mask = len_mask >> dst_offset; + let masked_src_in_dst_position = (src & src_mask) >> (dst_offset - src_offset); + *dst = (*dst & !dst_mask) | masked_src_in_dst_position; +} + /// Appends bits to a slice of words, filling each word from its most significant bit pub struct BitWriterMsb<'a, W: Word> { words: &'a mut [W], From a6509a14ebbfb5ee90cc4fc699b3ac58db11f771 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 12:06:52 +1100 Subject: [PATCH 28/36] Add some simple tests and fix bug --- blinksy/src/util/bits.rs | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index cb047d0..f923252 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -109,11 +109,13 @@ fn copy_bits_in_word( ) { let word_bits = W::BITS as usize; - let len_mask = !W::ZERO << (word_bits - len); - let src_mask = len_mask >> src_offset; - let dst_mask = len_mask >> dst_offset; - let masked_src_in_dst_position = (src & src_mask) >> (dst_offset - src_offset); - *dst = (*dst & !dst_mask) | masked_src_in_dst_position; + let dst_mask = !W::ZERO << (word_bits - len) >> dst_offset; + let src_in_dst_position = if dst_offset > src_offset { + src >> (dst_offset - src_offset) + } else { + src << (src_offset - dst_offset) + }; + *dst = (*dst & !dst_mask) | (src_in_dst_position & dst_mask); } /// Appends bits to a slice of words, filling each word from its most significant bit @@ -177,4 +179,28 @@ mod tests { assert_eq!(dst, [0b1111_1000, 0b0000_0000, 0b1111_1111]); } + + #[test] + fn test_copy_bits_in_words_simple() { + let mut dst: u8 = 0b1111_1111; + copy_bits_in_word(0b0000_0000, &mut dst, 0, 0, 3); + + assert_eq!(dst, 0b0001_1111); + } + + #[test] + fn test_copy_bits_in_words_with_src_offset() { + let mut dst: u8 = 0b1111_1111; + copy_bits_in_word(0b0000_0000, &mut dst, 4, 0, 3); + + assert_eq!(dst, 0b0001_1111); + } + + #[test] + fn test_copy_bits_in_words_with_dst_offset() { + let mut dst: u8 = 0b1111_1111; + copy_bits_in_word(0b0000_0000, &mut dst, 0, 4, 3); + + assert_eq!(dst, 0b1111_0001); + } } From 9a6987233a8f1479911b6a8768beed8df12d6c99 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 12:26:31 +1100 Subject: [PATCH 29/36] Use the new function to simplify --- blinksy/src/util/bits.rs | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index f923252..30e2ac5 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -82,40 +82,40 @@ pub fn copy_bits_msb(src: &[W], len: usize, dst: &mut [W], dst_start: u let dst_first_index = dst_start / word_bits; let dst_shift = dst_start % word_bits; - for (i, &src_word) in src[..len.div_ceil(word_bits)].iter().enumerate() { - let dst_index = dst_first_index + i; + for (i, &input) in src[..len.div_ceil(word_bits)].iter().enumerate() { // Number of bits to copy in this word let count = (len - i * word_bits).min(word_bits); - // Selects the top `count` bits, which are the ones being copied - let mask = !W::ZERO << (word_bits - count); - let src_masked_word = src_word & mask; - // The bits that fit into the current destination word - dst[dst_index] = (dst[dst_index] & !(mask >> dst_shift)) | (src_masked_word >> dst_shift); - // The bits that overflow into the next destination word - if dst_shift + count > word_bits { - let back = word_bits - dst_shift; - dst[dst_index + 1] = (dst[dst_index + 1] & !(mask << back)) | (src_masked_word << back); + copy_bits_in_word(input, &mut dst[dst_first_index + i], 0, dst_shift, count); + // If there wasn't enough room in the first word we overflow into the next + if word_bits - dst_shift < count { + copy_bits_in_word( + input, + &mut dst[dst_first_index + i + 1], + word_bits - dst_shift, + 0, + count - (word_bits - dst_shift), + ); } } } fn copy_bits_in_word( - src: W, - dst: &mut W, - src_offset: usize, - dst_offset: usize, - len: usize, + input: W, + output: &mut W, + input_offset: usize, + output_offset: usize, + length: usize, ) { let word_bits = W::BITS as usize; - let dst_mask = !W::ZERO << (word_bits - len) >> dst_offset; - let src_in_dst_position = if dst_offset > src_offset { - src >> (dst_offset - src_offset) + let output_mask = !W::ZERO << (word_bits - length) >> output_offset; + let input_in_output_position = if output_offset > input_offset { + input >> (output_offset - input_offset) } else { - src << (src_offset - dst_offset) + input << (input_offset - output_offset) }; - *dst = (*dst & !dst_mask) | (src_in_dst_position & dst_mask); + *output = (*output & !output_mask) | (input_in_output_position & output_mask); } /// Appends bits to a slice of words, filling each word from its most significant bit From fef2a4d39bb66c9ddbfdfb4e812709b987b5a9b7 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 13:27:12 +1100 Subject: [PATCH 30/36] Start abstracting a slice of bits --- blinksy/src/driver/clockless/spi/mod.rs | 8 ++++++-- blinksy/src/util/bits.rs | 15 +++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 0afdf32..492b83b 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -8,7 +8,7 @@ use crate::driver::t_cycle; #[cfg(feature = "async")] use crate::driver::ClocklessWriterAsync; use crate::driver::{ClocklessLed, ClocklessWriter}; -use crate::util::bits::{word_to_bits_msb, BitWriterMsb, Word}; +use crate::util::bits::{word_to_bits_msb, BitSlice, BitWriterMsb, Word}; mod encoding; pub use encoding::ClocklessSpiTiming; @@ -321,7 +321,11 @@ fn encode_spi_buffer( for word in frame { for bit in word_to_bits_msb(*word) { let pulse = pulses.get(bit); - writer.write_bits(pulse.bits(), pulse.len()); + writer.write_bits(BitSlice { + buffer: pulse.bits(), + offset: 0, + length: pulse.len(), + }); } } } diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index 30e2ac5..88a17c6 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -118,6 +118,13 @@ fn copy_bits_in_word( *output = (*output & !output_mask) | (input_in_output_position & output_mask); } +// Represents a view of the bits in a buffer +pub struct BitSlice<'a, W: Word> { + pub buffer: &'a [W], + pub offset: usize, + pub length: usize, +} + /// Appends bits to a slice of words, filling each word from its most significant bit pub struct BitWriterMsb<'a, W: Word> { words: &'a mut [W], @@ -129,10 +136,10 @@ impl<'a, W: Word> BitWriterMsb<'a, W> { Self { words, position: 0 } } - /// Appends the first `len` bits of `src` - pub fn write_bits(&mut self, src: &[W], len: usize) { - copy_bits_msb(src, len, self.words, self.position); - self.position += len; + pub fn write_bits(&mut self, slice: BitSlice) { + assert_eq!(slice.offset, 0); + copy_bits_msb(slice.buffer, slice.length, self.words, self.position); + self.position += slice.length; } } From 6e8e21d912dc2df7765a19a77456e78c4748fac0 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 13:38:01 +1100 Subject: [PATCH 31/36] Make it easier getting the saved pulse code bits --- blinksy/src/driver/clockless/spi/encoding.rs | 14 +++++++------- blinksy/src/driver/clockless/spi/mod.rs | 7 +------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index 2c90cc0..f9054af 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -1,5 +1,5 @@ use crate::driver::ClocklessLed; -use crate::util::bits::{copy_bits_msb, Word}; +use crate::util::bits::{copy_bits_msb, BitSlice, Word}; use core::marker::PhantomData; /// Represents the timing (in number of SPI bits) to encode zero, one, and reset LED signals @@ -94,12 +94,12 @@ impl PulseCode { } } - pub(crate) fn bits(&self) -> &[W] { - &self.buffer - } - - pub(crate) fn len(&self) -> usize { - self.len + pub(crate) fn bits<'a>(&'a self) -> BitSlice<'a, W> { + BitSlice { + buffer: &self.buffer, + offset: 0, + length: self.len, + } } } diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 492b83b..d6a6aae 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -320,12 +320,7 @@ fn encode_spi_buffer( let mut writer = BitWriterMsb::new(buffer); for word in frame { for bit in word_to_bits_msb(*word) { - let pulse = pulses.get(bit); - writer.write_bits(BitSlice { - buffer: pulse.bits(), - offset: 0, - length: pulse.len(), - }); + writer.write_bits(pulses.get(bit).bits()); } } } From e05114f56ec6adba2e65752a09ca0be0f5ff596d Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 13:49:39 +1100 Subject: [PATCH 32/36] Tiny refactor on the way to generalising copy_bits_msb --- blinksy/src/driver/clockless/spi/encoding.rs | 2 +- blinksy/src/driver/clockless/spi/mod.rs | 2 +- blinksy/src/util/bits.rs | 34 +++++++++++++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index f9054af..3623c22 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -87,7 +87,7 @@ pub(crate) struct PulseCode { impl PulseCode { fn new(high: usize, low: usize) -> Self { let mut buffer = [W::ZERO; N]; - copy_bits_msb(&[!W::ZERO; N], high, &mut buffer, 0); + copy_bits_msb(&[!W::ZERO; N], 0, high, &mut buffer, 0, high); Self { buffer, len: high + low, diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index d6a6aae..2406c75 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -8,7 +8,7 @@ use crate::driver::t_cycle; #[cfg(feature = "async")] use crate::driver::ClocklessWriterAsync; use crate::driver::{ClocklessLed, ClocklessWriter}; -use crate::util::bits::{word_to_bits_msb, BitSlice, BitWriterMsb, Word}; +use crate::util::bits::{word_to_bits_msb, BitWriterMsb, Word}; mod encoding; pub use encoding::ClocklessSpiTiming; diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index 88a17c6..720c5db 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -76,15 +76,27 @@ pub fn word_to_bits_msb(word: W) -> BitsMsb { BitsMsb::new(word) } -/// Copies the first `len` bits of `src` into `dst`, starting at bit `dst_start` -pub fn copy_bits_msb(src: &[W], len: usize, dst: &mut [W], dst_start: usize) { +/// Copies the `len` bits of `src` starting at bit `src_start` into `dst`, starting at bit `dst_start` +pub fn copy_bits_msb( + src: &[W], + src_start: usize, + src_len: usize, + dst: &mut [W], + dst_start: usize, + dst_len: usize, +) { + assert_eq!(src_start, 0); + assert_eq!( + src_len, dst_len, + "source and destination need to be the same size when copying" + ); let word_bits = W::BITS as usize; let dst_first_index = dst_start / word_bits; let dst_shift = dst_start % word_bits; - for (i, &input) in src[..len.div_ceil(word_bits)].iter().enumerate() { + for (i, &input) in src[..src_len.div_ceil(word_bits)].iter().enumerate() { // Number of bits to copy in this word - let count = (len - i * word_bits).min(word_bits); + let count = (src_len - i * word_bits).min(word_bits); // The bits that fit into the current destination word copy_bits_in_word(input, &mut dst[dst_first_index + i], 0, dst_shift, count); // If there wasn't enough room in the first word we overflow into the next @@ -137,8 +149,14 @@ impl<'a, W: Word> BitWriterMsb<'a, W> { } pub fn write_bits(&mut self, slice: BitSlice) { - assert_eq!(slice.offset, 0); - copy_bits_msb(slice.buffer, slice.length, self.words, self.position); + copy_bits_msb( + slice.buffer, + slice.offset, + slice.length, + self.words, + self.position, + slice.length, + ); self.position += slice.length; } } @@ -174,7 +192,7 @@ mod tests { #[test] fn test_copy_bits_msb_simple() { let mut dst: [u8; 2] = [0b1111_111, 0b1111_1111]; - copy_bits_msb(&[0b0000_0000, 0b0000_0000], 11, &mut dst, 0); + copy_bits_msb(&[0b0000_0000, 0b0000_0000], 0, 11, &mut dst, 0, 11); assert_eq!(dst, [0b0000_0000, 0b0001_1111]); } @@ -182,7 +200,7 @@ mod tests { #[test] fn test_copy_bits_msb_across_word_boundary() { let mut dst: [u8; 3] = [0b1111_1111, 0b1111_1111, 0b1111_1111]; - copy_bits_msb(&[0b0000_0000, 0b0000_0000], 11, &mut dst, 5); + copy_bits_msb(&[0b0000_0000, 0b0000_0000], 0, 11, &mut dst, 5, 11); assert_eq!(dst, [0b1111_1000, 0b0000_0000, 0b1111_1111]); } From bdecf97ea9e5bd8571d1c465252a2b428ea5a867 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 14:29:27 +1100 Subject: [PATCH 33/36] Refactor interface of copy_bits_msb so there's just a source and a destination --- blinksy/src/driver/clockless/spi/encoding.rs | 16 ++- blinksy/src/util/bits.rs | 105 ++++++++++++------- 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/blinksy/src/driver/clockless/spi/encoding.rs b/blinksy/src/driver/clockless/spi/encoding.rs index 3623c22..b1cca58 100644 --- a/blinksy/src/driver/clockless/spi/encoding.rs +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -1,5 +1,5 @@ use crate::driver::ClocklessLed; -use crate::util::bits::{copy_bits_msb, BitSlice, Word}; +use crate::util::bits::{copy_bits_msb, BitSlice, BitSliceMut, Word}; use core::marker::PhantomData; /// Represents the timing (in number of SPI bits) to encode zero, one, and reset LED signals @@ -87,7 +87,19 @@ pub(crate) struct PulseCode { impl PulseCode { fn new(high: usize, low: usize) -> Self { let mut buffer = [W::ZERO; N]; - copy_bits_msb(&[!W::ZERO; N], 0, high, &mut buffer, 0, high); + let high_bits = BitSlice { + buffer: &[!W::ZERO; N], + offset: 0, + length: high, + }; + copy_bits_msb( + &high_bits, + &mut BitSliceMut { + buffer: &mut buffer, + offset: 0, + length: high_bits.length, + }, + ); Self { buffer, len: high + low, diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index 720c5db..9ad572d 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -76,40 +76,43 @@ pub fn word_to_bits_msb(word: W) -> BitsMsb { BitsMsb::new(word) } -/// Copies the `len` bits of `src` starting at bit `src_start` into `dst`, starting at bit `dst_start` -pub fn copy_bits_msb( - src: &[W], - src_start: usize, - src_len: usize, - dst: &mut [W], - dst_start: usize, - dst_len: usize, -) { - assert_eq!(src_start, 0); - assert_eq!( - src_len, dst_len, - "source and destination need to be the same size when copying" - ); - let word_bits = W::BITS as usize; - let dst_first_index = dst_start / word_bits; - let dst_shift = dst_start % word_bits; - - for (i, &input) in src[..src_len.div_ceil(word_bits)].iter().enumerate() { - // Number of bits to copy in this word - let count = (src_len - i * word_bits).min(word_bits); - // The bits that fit into the current destination word - copy_bits_in_word(input, &mut dst[dst_first_index + i], 0, dst_shift, count); - // If there wasn't enough room in the first word we overflow into the next - if word_bits - dst_shift < count { +/// Copies the bits from `src` to `dst` +pub fn copy_bits_msb(src: &BitSlice, dst: &mut BitSliceMut) { + { + assert_eq!(src.offset, 0); + assert_eq!( + src.length, dst.length, + "source and destination need to be the same size when copying" + ); + let word_bits = W::BITS as usize; + let dst_first_index = dst.offset / word_bits; + let dst_shift = dst.offset % word_bits; + for (i, &input) in src.buffer[..src.length.div_ceil(word_bits)] + .iter() + .enumerate() + { + // Number of bits to copy in this word + let count = (src.length - i * word_bits).min(word_bits); + // The bits that fit into the current destination word copy_bits_in_word( input, - &mut dst[dst_first_index + i + 1], - word_bits - dst_shift, + &mut dst.buffer[dst_first_index + i], 0, - count - (word_bits - dst_shift), + dst_shift, + count, ); + // If there wasn't enough room in the first word we overflow into the next + if word_bits - dst_shift < count { + copy_bits_in_word( + input, + &mut dst.buffer[dst_first_index + i + 1], + word_bits - dst_shift, + 0, + count - (word_bits - dst_shift), + ); + } } - } + }; } fn copy_bits_in_word( @@ -137,6 +140,12 @@ pub struct BitSlice<'a, W: Word> { pub length: usize, } +pub struct BitSliceMut<'a, W: Word> { + pub buffer: &'a mut [W], + pub offset: usize, + pub length: usize, +} + /// Appends bits to a slice of words, filling each word from its most significant bit pub struct BitWriterMsb<'a, W: Word> { words: &'a mut [W], @@ -150,12 +159,12 @@ impl<'a, W: Word> BitWriterMsb<'a, W> { pub fn write_bits(&mut self, slice: BitSlice) { copy_bits_msb( - slice.buffer, - slice.offset, - slice.length, - self.words, - self.position, - slice.length, + &slice, + &mut BitSliceMut { + buffer: self.words, + offset: self.position, + length: slice.length, + }, ); self.position += slice.length; } @@ -192,7 +201,18 @@ mod tests { #[test] fn test_copy_bits_msb_simple() { let mut dst: [u8; 2] = [0b1111_111, 0b1111_1111]; - copy_bits_msb(&[0b0000_0000, 0b0000_0000], 0, 11, &mut dst, 0, 11); + copy_bits_msb( + &BitSlice { + buffer: &[0b0000_0000, 0b0000_0000], + offset: 0, + length: 11, + }, + &mut BitSliceMut { + buffer: &mut dst, + offset: 0, + length: 11, + }, + ); assert_eq!(dst, [0b0000_0000, 0b0001_1111]); } @@ -200,7 +220,18 @@ mod tests { #[test] fn test_copy_bits_msb_across_word_boundary() { let mut dst: [u8; 3] = [0b1111_1111, 0b1111_1111, 0b1111_1111]; - copy_bits_msb(&[0b0000_0000, 0b0000_0000], 0, 11, &mut dst, 5, 11); + copy_bits_msb( + &BitSlice { + buffer: &[0b0000_0000, 0b0000_0000], + offset: 0, + length: 11, + }, + &mut BitSliceMut { + buffer: &mut dst, + offset: 5, + length: 11, + }, + ); assert_eq!(dst, [0b1111_1000, 0b0000_0000, 0b1111_1111]); } From eb256d305ecd5b7fef69aa91a749b744e114570a Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 14:51:14 +1100 Subject: [PATCH 34/36] Add a little note about alignment for the copy function --- blinksy/src/util/bits.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index 9ad572d..fd5ba53 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -77,6 +77,7 @@ pub fn word_to_bits_msb(word: W) -> BitsMsb { } /// Copies the bits from `src` to `dst` +/// Note that this is currently restricted to the source have zero offset pub fn copy_bits_msb(src: &BitSlice, dst: &mut BitSliceMut) { { assert_eq!(src.offset, 0); From d9d3ca996641ba7e9daa362abe7eaad21c9bf879 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 14:55:02 +1100 Subject: [PATCH 35/36] Reorder only --- blinksy/src/util/bits.rs | 76 ++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index fd5ba53..bb5140a 100644 --- a/blinksy/src/util/bits.rs +++ b/blinksy/src/util/bits.rs @@ -76,8 +76,45 @@ pub fn word_to_bits_msb(word: W) -> BitsMsb { BitsMsb::new(word) } +/// Appends bits to a slice of words, filling each word from its most significant bit +pub struct BitWriterMsb<'a, W: Word> { + words: &'a mut [W], + position: usize, +} + +impl<'a, W: Word> BitWriterMsb<'a, W> { + pub fn new(words: &'a mut [W]) -> Self { + Self { words, position: 0 } + } + + pub fn write_bits(&mut self, slice: BitSlice) { + copy_bits_msb( + &slice, + &mut BitSliceMut { + buffer: self.words, + offset: self.position, + length: slice.length, + }, + ); + self.position += slice.length; + } +} + +// Represents a view of the bits in a buffer +pub struct BitSlice<'a, W: Word> { + pub buffer: &'a [W], + pub offset: usize, + pub length: usize, +} + +pub struct BitSliceMut<'a, W: Word> { + pub buffer: &'a mut [W], + pub offset: usize, + pub length: usize, +} + /// Copies the bits from `src` to `dst` -/// Note that this is currently restricted to the source have zero offset +/// Note that this is currently restricted to the source having zero offset pub fn copy_bits_msb(src: &BitSlice, dst: &mut BitSliceMut) { { assert_eq!(src.offset, 0); @@ -134,43 +171,6 @@ fn copy_bits_in_word( *output = (*output & !output_mask) | (input_in_output_position & output_mask); } -// Represents a view of the bits in a buffer -pub struct BitSlice<'a, W: Word> { - pub buffer: &'a [W], - pub offset: usize, - pub length: usize, -} - -pub struct BitSliceMut<'a, W: Word> { - pub buffer: &'a mut [W], - pub offset: usize, - pub length: usize, -} - -/// Appends bits to a slice of words, filling each word from its most significant bit -pub struct BitWriterMsb<'a, W: Word> { - words: &'a mut [W], - position: usize, -} - -impl<'a, W: Word> BitWriterMsb<'a, W> { - pub fn new(words: &'a mut [W]) -> Self { - Self { words, position: 0 } - } - - pub fn write_bits(&mut self, slice: BitSlice) { - copy_bits_msb( - &slice, - &mut BitSliceMut { - buffer: self.words, - offset: self.position, - length: slice.length, - }, - ); - self.position += slice.length; - } -} - #[cfg(test)] mod tests { use super::*; From 96c13f11de4782d8c825c3570870fc5f8c058b3d Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Sun, 11 Oct 2026 16:50:26 +1100 Subject: [PATCH 36/36] Send a reset to the LED before the first write to ensure it's in a known state --- blinksy/src/driver/clockless/spi/mod.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs index 2406c75..c0d49cc 100644 --- a/blinksy/src/driver/clockless/spi/mod.rs +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -175,6 +175,7 @@ where spi: Spi, /// Details of the timing used for sending the SPI signal pub timing: ClocklessSpiTiming, + reset_required: bool, pulses: Pulses, spi_word: PhantomData, } @@ -227,6 +228,7 @@ where /// Details of the timing used for sending the SPI signal pub timing: ClocklessSpiTiming, pulses: Pulses, + reset_required: bool, spi_word: PhantomData, } @@ -243,6 +245,8 @@ where spi, pulses: Pulses::new(&timing), timing, + // We need to send a reset to the LED before we do anything else + reset_required: true, spi_word: PhantomData, } } @@ -262,6 +266,8 @@ where spi, pulses: Pulses::new::(&timing), timing, + // We need to send a reset to the LED before we do anything else + reset_required: true, spi_word: PhantomData, } } @@ -282,6 +288,12 @@ where frame: heapless::Vec, ) -> Result<(), Self::Error> { let mut buffer = [SpiWord::ZERO; BUFFER_SIZE]; + // Send a reset signal to the LEDs before we send the first data to make sure they are in a good state + if self.reset_required { + let reset_size = (self.timing.t_reset as usize).div_ceil(size_of::() * 8); + self.spi.write(&buffer[..reset_size])?; + self.reset_required = false; + } encode_spi_buffer::(&frame, &mut buffer, &self.pulses); self.spi.write(&buffer) } @@ -303,6 +315,12 @@ where frame: heapless::Vec, ) -> Result<(), Self::Error> { let mut buffer = [SpiWord::ZERO; BUFFER_SIZE]; + // Send a reset signal to the LEDs before we send the first data to make sure they are in a good state + if self.reset_required { + let reset_size = (self.timing.t_reset as usize).div_ceil(size_of::() * 8); + self.spi.write(&buffer[..reset_size]).await?; + self.reset_required = false; + } encode_spi_buffer::(&frame, &mut buffer, &self.pulses); self.spi.write(&buffer).await }