Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fe9eca0
Add generalised spi driver for clockless LEDs
mlandauer Oct 7, 2026
b7cf662
Update Cargo.lock in blinksy-esp after blinksy update
mlandauer Oct 8, 2026
5b5f405
Rename PhantomData fields to match convention used in the crate
mlandauer Oct 8, 2026
612d46b
Rename for consistency with
mlandauer Oct 8, 2026
246e6d4
Make it easier to build examples in blinksy-esp
mlandauer Oct 8, 2026
0239768
Don't make things public that don't need to be
mlandauer Oct 8, 2026
eaa9437
Move helper function to the bottom of the file
mlandauer Oct 8, 2026
606bf08
Add docs for clockless_spi_buffer_size
mlandauer Oct 8, 2026
36bd5cf
Add docs for clockless_spi_pulse_size
mlandauer Oct 8, 2026
a752fe6
Add docs for builder
mlandauer Oct 8, 2026
aea4ce4
Add link to the spi driver in the clockless docs
mlandauer Oct 8, 2026
2868cc3
First rough pass at documentation for ClocklessSpi
mlandauer Oct 8, 2026
3ebf906
document Timing
mlandauer Oct 8, 2026
d07323e
Add rough docs for ClocklessSpiAsync
mlandauer Oct 8, 2026
96c68d7
Add documentation for frequency calculation
mlandauer Oct 8, 2026
17f1fd5
Remove warn(missing_docs)
mlandauer Oct 8, 2026
fb003f2
Rename struct
mlandauer Oct 8, 2026
8412e6f
Improve "how this works" section
mlandauer Oct 8, 2026
c919176
Code in added documentation can't be run unfortunately
mlandauer Oct 8, 2026
45534dd
Add link to new spi driver on crate level documentation
mlandauer Oct 8, 2026
03433be
Small documentation fixes
mlandauer Oct 10, 2026
479d652
More concise way of expressing the bitvec trait bound
mlandauer Oct 10, 2026
2951d91
Simplify slightly by passing BitSlice rather than BitArray to encode
mlandauer Oct 10, 2026
e658231
Confine the bit mangling inside encode_spi_buffer
mlandauer Oct 10, 2026
8facc5e
Fix buffer size calculation if led or spi word is not u8
mlandauer Oct 10, 2026
7e8cb6a
First rough implementation of copying bits around
mlandauer Oct 11, 2026
8607d57
Add function to just copy a bit of a word
mlandauer Oct 11, 2026
a6509a1
Add some simple tests and fix bug
mlandauer Oct 11, 2026
9a69872
Use the new function to simplify
mlandauer Oct 11, 2026
fef2a4d
Start abstracting a slice of bits
mlandauer Oct 11, 2026
6e8e21d
Make it easier getting the saved pulse code bits
mlandauer Oct 11, 2026
e05114f
Tiny refactor on the way to generalising copy_bits_msb
mlandauer Oct 11, 2026
bdecf97
Refactor interface of copy_bits_msb so there's just a source and a
mlandauer Oct 11, 2026
eb256d3
Add a little note about alignment for the copy function
mlandauer Oct 11, 2026
d9d3ca9
Reorder only
mlandauer Oct 11, 2026
96c13f1
Send a reset to the LED before the first write to ensure it's in a known
mlandauer Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions blinksy/src/driver/clockless/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,12 +71,14 @@ use crate::{
color::{ColorCorrection, FromColor, LedChannels, LedColor, LinearSrgb},
driver::Driver,
time::Nanoseconds,
util::component::Component,
util::{bits::Word, component::Component},
};

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.
///
Expand All @@ -101,7 +105,7 @@ pub use self::delay::*;
/// ```
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;
Expand All @@ -126,14 +130,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:
Expand Down Expand Up @@ -174,6 +170,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<Led: ClocklessLed>() -> 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<Led: ClocklessLed> {
type Error;
Expand Down
152 changes: 152 additions & 0 deletions blinksy/src/driver/clockless/spi/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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};
use crate::util::bits::Word;

/// Builder for [`ClocklessSpi`] and [`ClocklessSpiAsync`]
pub struct ClocklessSpiBuilder<

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to see that you understood my builder pattern! 👍

It might be a lot of code, but it's mostly just boilerplate so I don't mind.

const BUFFER_SIZE: usize,
const PULSE_SIZE: usize,
Spi,
Led,
SpiWord,
Freq,
Exec,
> {
spi: Spi,
freq_hz: Freq,
led: PhantomData<Led>,
spi_word: PhantomData<SpiWord>,
exec: PhantomData<Exec>,
}

impl<const BUFFER_SIZE: usize, const PULSE_SIZE: usize, Spi, Led, SpiWord, Freq>
ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, Freq, Blocking>
{
pub fn with_async(
self,
) -> ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, Freq, Async> {
ClocklessSpiBuilder {
spi: self.spi,
freq_hz: self.freq_hz,
led: PhantomData,
spi_word: PhantomData,
exec: PhantomData,
}
}
}

impl<const BUFFER_SIZE: usize, const PULSE_SIZE: usize, Spi, Led, SpiWord, Dm>
ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, (), Dm>
{
pub fn with_freq_hz(
self,
freq_hz: u32,
) -> ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, u32, Dm> {
ClocklessSpiBuilder {
spi: self.spi,
freq_hz,
led: PhantomData,
spi_word: PhantomData,
exec: PhantomData,
}
}
}

impl<const BUFFER_SIZE: usize, Spi, Led, SpiWord, Freq, Dm>
ClocklessSpiBuilder<BUFFER_SIZE, 0, Spi, Led, SpiWord, Freq, Dm>
{
pub fn with_pulse_size<const PULSE_SIZE: usize>(
self,
) -> ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, Freq, Dm> {
ClocklessSpiBuilder {
spi: self.spi,
freq_hz: self.freq_hz,
led: PhantomData,
spi_word: PhantomData,
exec: PhantomData,
}
}
}

impl<const N: usize, const PULSE_SIZE: usize, Spi, Led, SpiWord, Freq, Dm>
ClocklessSpiBuilder<N, PULSE_SIZE, Spi, Led, SpiWord, Freq, Dm>
{
pub fn with_buffer_size<const BUFFER_SIZE: usize>(
self,
) -> ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, Freq, Dm> {
ClocklessSpiBuilder {
spi: self.spi,
freq_hz: self.freq_hz,
led: PhantomData,
spi_word: PhantomData,
exec: PhantomData,
}
}
}

impl<const BUFFER_SIZE: usize, const PULSE_SIZE: usize, Led, SpiWord, Freq, Dm>
ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, (), Led, SpiWord, Freq, Dm>
{
pub fn with_spi<Spi>(
self,
spi: Spi,
) -> ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, Freq, Dm> {
ClocklessSpiBuilder {
spi,
freq_hz: self.freq_hz,
led: PhantomData,
spi_word: PhantomData,
exec: PhantomData,
}
}
}

impl<Led, SpiWord> Default for ClocklessSpiBuilder<0, 0, (), Led, SpiWord, (), Blocking> {
fn default() -> Self {
Self {
spi: (),
freq_hz: (),
led: PhantomData,
spi_word: PhantomData,
exec: PhantomData,
}
}
}

impl<const BUFFER_SIZE: usize, const PULSE_SIZE: usize, Spi, Led, SpiWord>
ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, u32, Blocking>
where
Spi: SpiBus<SpiWord>,
SpiWord: Word + 'static,
Led: ClocklessLed,
{
pub fn build(self) -> ClocklessSpi<BUFFER_SIZE, PULSE_SIZE, Led, Spi, SpiWord> {
assert!(BUFFER_SIZE > 0, "set buffer_size before calling build");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

assert!(PULSE_SIZE > 0, "set pulse_size before calling build");
ClocklessSpi::new(self.spi, self.freq_hz)
}
}

#[cfg(feature = "async")]
impl<const BUFFER_SIZE: usize, const PULSE_SIZE: usize, Spi, Led, SpiWord>
ClocklessSpiBuilder<BUFFER_SIZE, PULSE_SIZE, Spi, Led, SpiWord, u32, Async>
where
Spi: SpiBusAsync<SpiWord>,
SpiWord: Word + 'static,
Led: ClocklessLed,
{
pub fn build(self) -> ClocklessSpiAsync<BUFFER_SIZE, PULSE_SIZE, Led, Spi, SpiWord> {
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)
}
}
146 changes: 146 additions & 0 deletions blinksy/src/driver/clockless/spi/encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use crate::driver::ClocklessLed;
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
///
/// These timings are equivalent to those in [`ClocklessLed`] but converted to number of SPI bits for
/// a particular SPI frequency.
pub struct ClocklessSpiTiming<Led: ClocklessLed> {
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<Led>,
}

impl<Led: ClocklessLed> ClocklessSpiTiming<Led> {
pub(crate) 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
}

/// 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();
// We can't yet use max in const function
if t0 > t1 {
t0
} else {
t1
}
}

/// 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;
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(crate) struct PulseCode<W, const N: usize> {
buffer: [W; N],
len: usize,
}

impl<W: Word, const N: usize> PulseCode<W, N> {
fn new(high: usize, low: usize) -> Self {
let mut buffer = [W::ZERO; N];
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,
}
}

pub(crate) fn bits<'a>(&'a self) -> BitSlice<'a, W> {
BitSlice {
buffer: &self.buffer,
offset: 0,
length: self.len,
}
}
}

pub(crate) struct Pulses<W, const N: usize> {
zero: PulseCode<W, N>,
one: PulseCode<W, N>,
}

impl<W: Word, const N: usize> Pulses<W, N> {
pub(crate) fn new<Led: ClocklessLed>(timing: &ClocklessSpiTiming<Led>) -> 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<W, N> {
if value {
&self.one
} else {
&self.zero
}
}
}

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
}
Loading
Loading