diff --git a/blinksy/src/driver/clockless/mod.rs b/blinksy/src/driver/clockless/mod.rs index 7bfd500..eb558b3 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 @@ -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. /// @@ -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; @@ -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: @@ -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() -> 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..44b956d --- /dev/null +++ b/blinksy/src/driver/clockless/spi/builder.rs @@ -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, + spi_word: PhantomData, + exec: PhantomData, +} + +impl + ClocklessSpiBuilder +{ + pub fn with_async( + self, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz: self.freq_hz, + led: PhantomData, + spi_word: PhantomData, + exec: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_freq_hz( + self, + freq_hz: u32, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz, + led: PhantomData, + spi_word: PhantomData, + exec: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_pulse_size( + self, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz: self.freq_hz, + led: PhantomData, + spi_word: PhantomData, + exec: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_buffer_size( + self, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi: self.spi, + freq_hz: self.freq_hz, + led: PhantomData, + spi_word: PhantomData, + exec: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +{ + pub fn with_spi( + self, + spi: Spi, + ) -> ClocklessSpiBuilder { + ClocklessSpiBuilder { + spi, + freq_hz: self.freq_hz, + led: PhantomData, + spi_word: PhantomData, + exec: PhantomData, + } + } +} + +impl Default for ClocklessSpiBuilder<0, 0, (), Led, SpiWord, (), Blocking> { + fn default() -> Self { + Self { + spi: (), + freq_hz: (), + led: PhantomData, + spi_word: PhantomData, + exec: PhantomData, + } + } +} + +impl + ClocklessSpiBuilder +where + Spi: SpiBus, + SpiWord: Word + '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: Word + '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..b1cca58 --- /dev/null +++ b/blinksy/src/driver/clockless/spi/encoding.rs @@ -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 { + 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, +} + +impl ClocklessSpiTiming { + 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 { + buffer: [W; N], + len: usize, +} + +impl PulseCode { + 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 { + zero: PulseCode, + one: PulseCode, +} + +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 { + 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 +} diff --git a/blinksy/src/driver/clockless/spi/mod.rs b/blinksy/src/driver/clockless/spi/mod.rs new file mode 100644 index 0000000..c0d49cc --- /dev/null +++ b/blinksy/src/driver/clockless/spi/mod.rs @@ -0,0 +1,344 @@ +use core::marker::PhantomData; + +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}; +use crate::util::bits::{word_to_bits_msb, BitWriterMsb, Word}; + +mod encoding; +pub use encoding::ClocklessSpiTiming; +use encoding::{duration_ns_to_freq_hz, freq_hz_to_duration_ns, Pulses}; +mod builder; +pub use builder::ClocklessSpiBuilder; + +/// Calculates the buffer size required for encoding one frame into bits that are sent to SPI +/// +/// # Usage +/// +/// ```rust,ignore +/// 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, +) -> usize +where + Spi: SpiBus, + Word: Copy + 'static, +{ + let clock_period_ns = freq_hz_to_duration_ns(freq_hz); + let timing = ClocklessSpiTiming::::new(clock_period_ns); + + // TODO: Check that resulting timings are within spec for the LED and error if not + 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(size_of::() * 8) +} + +/// Calculates the size required for encoding one bit into the SPI buffer. +/// +/// # Usage +/// +/// ```rust,ignore +/// 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, + Word: Copy + 'static, +{ + let clock_period_ns = freq_hz_to_duration_ns(freq_hz); + let timing = ClocklessSpiTiming::::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) +} + +/// 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 + // 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 = 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; + } + clock_period_ns += 1; + if clock_period_ns >= t_cycle_ns { + break; + } + } + 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`] trait. +/// +/// # How this works +/// +/// 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 +/// +/// ```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. +/// // 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, + Spi: SpiBus, + SpiWord: Copy + 'static, +{ + spi: Spi, + /// Details of the timing used for sending the SPI signal + pub timing: ClocklessSpiTiming, + reset_required: bool, + pulses: Pulses, + 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 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 +/// +/// ```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. +/// // 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 + Led: ClocklessLed, + Spi: SpiBusAsync, + SpiWord: Copy + 'static, +{ + spi: Spi, + /// Details of the timing used for sending the SPI signal + pub timing: ClocklessSpiTiming, + pulses: Pulses, + reset_required: bool, + spi_word: PhantomData, +} + +impl + ClocklessSpi +where + Led: ClocklessLed, + Spi: SpiBus, + SpiWord: Word + 'static, +{ + pub fn new(spi: Spi, freq_hz: u32) -> Self { + let timing = ClocklessSpiTiming::new(freq_hz_to_duration_ns(freq_hz)); + Self { + 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, + } + } +} + +#[cfg(feature = "async")] +impl + ClocklessSpiAsync +where + Led: ClocklessLed, + SpiWord: Word + 'static, + Spi: SpiBusAsync, +{ + pub fn new(spi: Spi, freq_hz: u32) -> Self { + let timing = ClocklessSpiTiming::new(freq_hz_to_duration_ns(freq_hz)); + Self { + 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, + } + } +} + +impl ClocklessWriter + for ClocklessSpi +where + Led: ClocklessLed, + Led::Word: Word, + Spi: SpiBus, + SpiWord: Word + 'static, +{ + type Error = Spi::Error; + + fn write( + &mut self, + 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) + } +} + +#[cfg(feature = "async")] +impl ClocklessWriterAsync + for ClocklessSpiAsync +where + Led: ClocklessLed, + Led::Word: Word, + Spi: SpiBusAsync, + SpiWord: Word + 'static, +{ + type Error = Spi::Error; + + async fn write( + &mut self, + 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 + } +} + +fn encode_spi_buffer( + frame: &[Led::Word], + buffer: &mut [SpiWord], + pulses: &Pulses, +) where + Led::Word: Word, + Led: ClocklessLed, + SpiWord: Word + 'static, +{ + let mut writer = BitWriterMsb::new(buffer); + for word in frame { + for bit in word_to_bits_msb(*word) { + writer.write_bits(pulses.get(bit).bits()); + } + } +} 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/ diff --git a/blinksy/src/util/bits.rs b/blinksy/src/util/bits.rs index 555bd68..bb5140a 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,101 @@ 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 having zero offset +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.buffer[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.buffer[dst_first_index + i + 1], + word_bits - dst_shift, + 0, + count - (word_bits - dst_shift), + ); + } + } + }; +} + +fn copy_bits_in_word( + input: W, + output: &mut W, + input_offset: usize, + output_offset: usize, + length: usize, +) { + let word_bits = W::BITS as usize; + + 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 { + input << (input_offset - output_offset) + }; + *output = (*output & !output_mask) | (input_in_output_position & output_mask); +} + #[cfg(test)] mod tests { use super::*; @@ -101,4 +198,66 @@ mod tests { ]) ); } + + #[test] + fn test_copy_bits_msb_simple() { + let mut dst: [u8; 2] = [0b1111_111, 0b1111_1111]; + 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]); + } + + #[test] + fn test_copy_bits_msb_across_word_boundary() { + let mut dst: [u8; 3] = [0b1111_1111, 0b1111_1111, 0b1111_1111]; + 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]); + } + + #[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); + } } diff --git a/esp/Cargo.lock b/esp/Cargo.lock index 3029285..5000db1 100644 --- a/esp/Cargo.lock +++ b/esp/Cargo.lock @@ -95,7 +95,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 new file mode 100644 index 0000000..a314415 --- /dev/null +++ b/esp/blinksy-esp/examples/ws2812-spi-matrix-8x8.rs @@ -0,0 +1,117 @@ +#![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 esp_println as _; + +extern crate alloc; + +#[panic_handler] +fn panic(_: &core::panic::PanicInfo) -> ! { + loop {} +} + +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) -> ! { + 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(); + } +} 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) -> ! {