Repository navigation
Add generalised spi driver for clockless LEDs #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ahdinosaur
merged 36 commits into
ahdinosaur:main
from
mlandauer:clockless-spi-driver-rebased
Oct 12, 2026
Merged
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 b7cf662
Update Cargo.lock in blinksy-esp after blinksy update
mlandauer 5b5f405
Rename PhantomData fields to match convention used in the crate
mlandauer 612d46b
Rename for consistency with
mlandauer 246e6d4
Make it easier to build examples in blinksy-esp
mlandauer 0239768
Don't make things public that don't need to be
mlandauer eaa9437
Move helper function to the bottom of the file
mlandauer 606bf08
Add docs for clockless_spi_buffer_size
mlandauer 36bd5cf
Add docs for clockless_spi_pulse_size
mlandauer a752fe6
Add docs for builder
mlandauer aea4ce4
Add link to the spi driver in the clockless docs
mlandauer 2868cc3
First rough pass at documentation for ClocklessSpi
mlandauer 3ebf906
document Timing
mlandauer d07323e
Add rough docs for ClocklessSpiAsync
mlandauer 96c68d7
Add documentation for frequency calculation
mlandauer 17f1fd5
Remove warn(missing_docs)
mlandauer fb003f2
Rename struct
mlandauer 8412e6f
Improve "how this works" section
mlandauer c919176
Code in added documentation can't be run unfortunately
mlandauer 45534dd
Add link to new spi driver on crate level documentation
mlandauer 03433be
Small documentation fixes
mlandauer 479d652
More concise way of expressing the bitvec trait bound
mlandauer 2951d91
Simplify slightly by passing BitSlice rather than BitArray to encode
mlandauer e658231
Confine the bit mangling inside encode_spi_buffer
mlandauer 8facc5e
Fix buffer size calculation if led or spi word is not u8
mlandauer 7e8cb6a
First rough implementation of copying bits around
mlandauer 8607d57
Add function to just copy a bit of a word
mlandauer a6509a1
Add some simple tests and fix bug
mlandauer 9a69872
Use the new function to simplify
mlandauer fef2a4d
Start abstracting a slice of bits
mlandauer 6e8e21d
Make it easier getting the saved pulse code bits
mlandauer e05114f
Tiny refactor on the way to generalising copy_bits_msb
mlandauer bdecf97
Refactor interface of copy_bits_msb so there's just a source and a
mlandauer eb256d3
Add a little note about alignment for the copy function
mlandauer d9d3ca9
Reorder only
mlandauer 96c13f1
Send a reset to the LED before the first write to ensure it's in a known
mlandauer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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< | ||
| 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"); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.