diff --git a/esp-hal-smartled/CHANGELOG.md b/esp-hal-smartled/CHANGELOG.md index b56517e..e1a2bc7 100644 --- a/esp-hal-smartled/CHANGELOG.md +++ b/esp-hal-smartled/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## 0.15.0 + +### Added + +- New `SmartLedsAdapterAsync` which is an asynchronous, non-blocking version of the driver. + +## 0.14.0 + +## 0.13.1 + ### Added ### Changed diff --git a/esp-hal-smartled/Cargo.toml b/esp-hal-smartled/Cargo.toml index 08f9fbd..2a8fd2c 100644 --- a/esp-hal-smartled/Cargo.toml +++ b/esp-hal-smartled/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "esp-hal-smartled" -version = "0.14.0" +version = "0.15.0" edition = "2021" rust-version = "1.84.0" description = "RMT peripheral adapter for smart LEDs" @@ -15,7 +15,7 @@ targets = ["riscv32imac-unknown-none-elf"] defmt = { version = "0.3.10", optional = true } document-features = "0.2.10" esp-hal = { version = "1.0.0-beta.0", features = ["unstable"] } -smart-leds-trait = "0.3.0" +smart-leds-trait = "0.3.1" [dev-dependencies] cfg-if = "1.0.0" @@ -24,6 +24,9 @@ esp-backtrace = { version = "0.15.0", features = [ "panic-handler", "println", ] } +esp-hal-embassy = "0.7" +embassy-executor = "0.7.0" +embassy-time = "0.4.0" esp-println = "0.13.0" smart-leds = "0.4.0" @@ -33,14 +36,14 @@ defmt = ["dep:defmt", "esp-hal/defmt"] #! ### Chip Support Feature Flags ## Target the ESP32. -esp32 = ["esp-backtrace/esp32", "esp-hal/esp32", "esp-println/esp32"] +esp32 = ["esp-backtrace/esp32", "esp-hal/esp32", "esp-println/esp32", "esp-hal-embassy/esp32"] ## Target the ESP32-C3. -esp32c3 = ["esp-backtrace/esp32c3", "esp-hal/esp32c3", "esp-println/esp32c3"] +esp32c3 = ["esp-backtrace/esp32c3", "esp-hal/esp32c3", "esp-println/esp32c3", "esp-hal-embassy/esp32c3"] ## Target the ESP32-C6. -esp32c6 = ["esp-backtrace/esp32c6", "esp-hal/esp32c6", "esp-println/esp32c6"] +esp32c6 = ["esp-backtrace/esp32c6", "esp-hal/esp32c6", "esp-println/esp32c6", "esp-hal-embassy/esp32c6"] ## Target the ESP32-H2. -esp32h2 = ["esp-backtrace/esp32h2", "esp-hal/esp32h2", "esp-println/esp32h2"] +esp32h2 = ["esp-backtrace/esp32h2", "esp-hal/esp32h2", "esp-println/esp32h2", "esp-hal-embassy/esp32h2"] ## Target the ESP32-S2. -esp32s2 = ["esp-backtrace/esp32s2", "esp-hal/esp32s2", "esp-println/esp32s2"] +esp32s2 = ["esp-backtrace/esp32s2", "esp-hal/esp32s2", "esp-println/esp32s2", "esp-hal-embassy/esp32s2"] ## Target the ESP32-S3. -esp32s3 = ["esp-backtrace/esp32s3", "esp-hal/esp32s3", "esp-println/esp32s3"] +esp32s3 = ["esp-backtrace/esp32s3", "esp-hal/esp32s3", "esp-println/esp32s3", "esp-hal-embassy/esp32s3"] diff --git a/esp-hal-smartled/examples/hello_rgb.rs b/esp-hal-smartled/examples/hello_rgb.rs index aa89497..007c64b 100644 --- a/esp-hal-smartled/examples/hello_rgb.rs +++ b/esp-hal-smartled/examples/hello_rgb.rs @@ -24,7 +24,7 @@ use esp_backtrace as _; use esp_hal::{delay::Delay, main, rmt::Rmt, time::Rate}; -use esp_hal_smartled::{smartLedBuffer, SmartLedsAdapter}; +use esp_hal_smartled::{smart_led_buffer, SmartLedsAdapter}; use smart_leds::{ brightness, gamma, hsv::{hsv2rgb, Hsv}, @@ -64,7 +64,7 @@ fn main() -> ! { // We use one of the RMT channels to instantiate a `SmartLedsAdapter` which can // be used directly with all `smart_led` implementations - let rmt_buffer = smartLedBuffer!(1); + let rmt_buffer = smart_led_buffer!(1); let mut led = SmartLedsAdapter::new(rmt.channel0, led_pin, rmt_buffer); let delay = Delay::new(); diff --git a/esp-hal-smartled/examples/hello_rgb_async.rs b/esp-hal-smartled/examples/hello_rgb_async.rs new file mode 100644 index 0000000..65ca937 --- /dev/null +++ b/esp-hal-smartled/examples/hello_rgb_async.rs @@ -0,0 +1,94 @@ +//! Asynchronous RGB LED Demo +//! +//! This example drives an SK68XX RGB LED, which is connected to a pin on the +//! official DevKits. +//! +//! The demo will leverage the [`smart_leds`](https://crates.io/crates/smart-leds) +//! crate functionality to circle through the HSV hue color space (with +//! saturation and value both at 255). Additionally, we apply a gamma correction +//! and limit the brightness to 10 (out of 255). +//! +//! The following wiring is assumed for ESP32: +//! - LED => GPIO33 +//! The following wiring is assumed for ESP32C3: +//! - LED => GPIO8 +//! The following wiring is assumed for ESP32C6, ESP32H2: +//! - LED => GPIO8 +//! The following wiring is assumed for ESP32S2: +//! - LED => GPIO18 +//! The following wiring is assumed for ESP32S3: +//! - LED => GPIO48 + +#![no_std] +#![no_main] + +use esp_backtrace as _; +use esp_hal::{rmt::Rmt, time::Rate, timer::timg::TimerGroup, Config}; +use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync}; +use smart_leds::{ + brightness, gamma, + hsv::{hsv2rgb, Hsv}, + SmartLedsWriteAsync, +}; +use embassy_executor::Spawner; +use embassy_time::{Duration, Timer}; + +#[esp_hal_embassy::main] +async fn main(_spawner: Spawner) -> ! { + let peripherals = esp_hal::init(Config::default()); + let timg0 = TimerGroup::new(peripherals.TIMG0); + esp_hal_embassy::init(timg0.timer0); + + // Each devkit uses a unique GPIO for the RGB LED, so in order to support + // all chips we must unfortunately use `#[cfg]`s: + cfg_if::cfg_if! { + if #[cfg(feature = "esp32")] { + let led_pin = peripherals.GPIO33; + } else if #[cfg(feature = "esp32c3")] { + let led_pin = peripherals.GPIO8; + } else if #[cfg(any(feature = "esp32c6", feature = "esp32h2"))] { + let led_pin = peripherals.GPIO8; + } else if #[cfg(feature = "esp32s2")] { + let led_pin = peripherals.GPIO18; + } else if #[cfg(feature = "esp32s3")] { + let led_pin = peripherals.GPIO48; + } + } + + // Configure RMT peripheral globally + cfg_if::cfg_if! { + if #[cfg(feature = "esp32h2")] { + let freq = Rate::from_mhz(32); + } else { + let freq = Rate::from_mhz(80); + } + } + + let rmt = Rmt::new(peripherals.RMT, freq).unwrap().into_async(); + + // We use one of the RMT channels to instantiate a `SmartLedsAdapter` which can + // be used directly with all `smart_led` implementations + let rmt_buffer: [u32; buffer_size_async(1)] = [0; buffer_size_async(1)]; + let mut led = SmartLedsAdapterAsync::new(rmt.channel0, led_pin, rmt_buffer); + + let mut color = Hsv { + hue: 0, + sat: 255, + val: 255, + }; + let mut data; + + loop { + for hue in 0..=255 { + color.hue = hue; + // Convert from the HSV color space (where we can easily transition from one + // color to the other) to the RGB color space that we can then send to the LED + data = [hsv2rgb(color)]; + // When sending to the LED, we do a gamma correction first (see smart_leds + // documentation for details) and then limit the brightness to 10 out of 255 so + // that the output it's not too bright. + led.write(brightness(gamma(data.iter().cloned()), 20)).await.unwrap(); + Timer::after(Duration::from_millis(10)).await; + } + } +} diff --git a/esp-hal-smartled/src/lib.rs b/esp-hal-smartled/src/lib.rs index fb51b0d..f85c6fb 100644 --- a/esp-hal-smartled/src/lib.rs +++ b/esp-hal-smartled/src/lib.rs @@ -27,11 +27,18 @@ use core::{fmt::Debug, slice::IterMut}; use esp_hal::{ clock::Clocks, - gpio::{Level, OutputPin}, + gpio::{Level, interconnect::PeripheralOutput}, peripheral::Peripheral, - rmt::{Error as RmtError, PulseCode, TxChannel, TxChannelConfig, TxChannelCreator}, + rmt::{ + Error as RmtError, PulseCode, TxChannel, TxChannelAsync, TxChannelConfig, TxChannelCreator, + TxChannelCreatorAsync, + }, }; -use smart_leds_trait::{SmartLedsWrite, RGB8}; +use smart_leds_trait::{SmartLedsWrite, SmartLedsWriteAsync, RGB8}; + +// Required RMT RAM to drive one LED. +// number of channels (r,g,b -> 3) * pulses per channel 8) +const RMT_RAM_ONE_LED: usize = 3 * 8; const SK68XX_CODE_PERIOD: u32 = 1250; // 800kHz const SK68XX_T0H_NS: u32 = 400; // 300ns per SK6812 datasheet, 400 per WS2812. Some require >350ns for T0H. Others <500ns for T0H. @@ -56,21 +63,86 @@ impl From for LedAdapterError { } } +fn led_pulses_for_clock(src_clock: u32) -> (u32, u32) { + ( + PulseCode::new( + Level::High, + ((SK68XX_T0H_NS * src_clock) / 1000) as u16, + Level::Low, + ((SK68XX_T0L_NS * src_clock) / 1000) as u16, + ), + PulseCode::new( + Level::High, + ((SK68XX_T1H_NS * src_clock) / 1000) as u16, + Level::Low, + ((SK68XX_T1L_NS * src_clock) / 1000) as u16, + ), + ) +} + +fn led_config() -> TxChannelConfig { + TxChannelConfig::default() + .with_clk_divider(1) + .with_idle_output_level(Level::Low) + .with_carrier_modulation(false) + .with_idle_output(true) +} + +fn convert_rgb_to_pulses( + value: RGB8, + mut_iter: &mut IterMut, + pulses: (u32, u32), +) -> Result<(), LedAdapterError> { + convert_rgb_channel_to_pulses(value.g, mut_iter, pulses)?; + convert_rgb_channel_to_pulses(value.r, mut_iter, pulses)?; + convert_rgb_channel_to_pulses(value.b, mut_iter, pulses)?; + Ok(()) +} + +fn convert_rgb_channel_to_pulses( + channel_value: u8, + mut_iter: &mut IterMut, + pulses: (u32, u32), +) -> Result<(), LedAdapterError> { + for position in [128, 64, 32, 16, 8, 4, 2, 1] { + *mut_iter.next().ok_or(LedAdapterError::BufferSizeExceeded)? = + match channel_value & position { + 0 => pulses.0, + _ => pulses.1, + } + } + + Ok(()) +} + +/// Function to calculate the required RMT buffer size for a given number of LEDs when using +/// the blocking API. +/// +/// This buffer size is calculated for the synchronous API provided by the [SmartLedsAdapter]. +/// [buffer_size_async] should be used for the asynchronous API. +pub const fn buffer_size(num_leds: usize) -> usize { + // 1 additional pulse for the end delimiter + num_leds * RMT_RAM_ONE_LED + 1 +} + /// Macro to allocate a buffer sized for a specific number of LEDs to be /// addressed. /// /// Attempting to use more LEDs that the buffer is configured for will result in /// an `LedAdapterError:BufferSizeExceeded` error. #[macro_export] +macro_rules! smart_led_buffer { + ( $num_leds: expr ) => { + [0u32; $crate::buffer_size($num_leds)] + }; +} + +/// Deprecated alias for [smart_led_buffer] macro. +#[macro_export] +#[deprecated] macro_rules! smartLedBuffer { - ( $buffer_size: literal ) => { - // The size we're assigning here is calculated as following - // ( - // Nr. of LEDs - // * channels (r,g,b -> 3) - // * pulses per channel 8) - // ) + 1 additional pulse for the end delimiter - [0u32; $buffer_size * 24 + 1] + ( $num_leds: expr ) => { + smart_led_buffer!($num_leds); }; } @@ -92,71 +164,23 @@ where /// Create a new adapter object that drives the pin using the RMT channel. pub fn new( channel: C, - pin: impl Peripheral

+ 'd, + pin: impl Peripheral + 'd, rmt_buffer: [u32; BUFFER_SIZE], ) -> SmartLedsAdapter where - O: OutputPin + 'd, + O: PeripheralOutput, C: TxChannelCreator<'d, TX, O>, { - let config = TxChannelConfig::default() - .with_clk_divider(1) - .with_idle_output_level(Level::Low) - .with_carrier_modulation(false) - .with_idle_output(true); - - let channel = channel.configure(pin, config).unwrap(); + let channel = channel.configure(pin, led_config()).unwrap(); // Assume the RMT peripheral is set up to use the APB clock - let clocks = Clocks::get(); - let src_clock = clocks.apb_clock.as_mhz(); + let src_clock = Clocks::get().apb_clock.as_mhz(); Self { channel: Some(channel), rmt_buffer, - pulses: ( - PulseCode::new( - Level::High, - ((SK68XX_T0H_NS * src_clock) / 1000) as u16, - Level::Low, - ((SK68XX_T0L_NS * src_clock) / 1000) as u16, - ), - PulseCode::new( - Level::High, - ((SK68XX_T1H_NS * src_clock) / 1000) as u16, - Level::Low, - ((SK68XX_T1L_NS * src_clock) / 1000) as u16, - ), - ), - } - } - - fn convert_rgb_to_pulse( - value: RGB8, - mut_iter: &mut IterMut, - pulses: (u32, u32), - ) -> Result<(), LedAdapterError> { - Self::convert_rgb_channel_to_pulses(value.g, mut_iter, pulses)?; - Self::convert_rgb_channel_to_pulses(value.r, mut_iter, pulses)?; - Self::convert_rgb_channel_to_pulses(value.b, mut_iter, pulses)?; - - Ok(()) - } - - fn convert_rgb_channel_to_pulses( - channel_value: u8, - mut_iter: &mut IterMut, - pulses: (u32, u32), - ) -> Result<(), LedAdapterError> { - for position in [128, 64, 32, 16, 8, 4, 2, 1] { - *mut_iter.next().ok_or(LedAdapterError::BufferSizeExceeded)? = - match channel_value & position { - 0 => pulses.0, - _ => pulses.1, - } + pulses: led_pulses_for_clock(src_clock), } - - Ok(()) } } @@ -182,7 +206,7 @@ where // This will result in an `BufferSizeExceeded` error in case // the iterator provides more elements than the buffer can take. for item in iterator { - Self::convert_rgb_to_pulse(item.into(), &mut seq_iter, self.pulses)?; + convert_rgb_to_pulses(item.into(), &mut seq_iter, self.pulses)?; } // Finally, add an end element. @@ -202,3 +226,98 @@ where } } } + +/// Support for asynchronous and non-blocking use of the RMT peripheral to drive smart LEDs. + +/// Function to calculate the required RMT buffer size for a given number of LEDs when using +/// the asynchronous API. This buffer size is calculated for the asynchronous API provided by the +/// [SmartLedsAdapterAsync]. [buffer_size] should be used for the synchronous API. +pub const fn buffer_size_async(num_leds: usize) -> usize { + // 1 byte end delimiter for each transfer. + num_leds * (RMT_RAM_ONE_LED + 1) +} + +/// Adapter taking an RMT channel and a specific pin and providing RGB LED +/// interaction functionality. +pub struct SmartLedsAdapterAsync { + channel: Tx, + rmt_buffer: [u32; BUFFER_SIZE], + pulses: (u32, u32), +} + +impl<'d, Tx: TxChannelAsync, const BUFFER_SIZE: usize> SmartLedsAdapterAsync { + /// Create a new adapter object that drives the pin using the RMT channel. + pub fn new( + channel: C, + pin: impl Peripheral + 'd, + rmt_buffer: [u32; BUFFER_SIZE], + ) -> SmartLedsAdapterAsync + where + O: PeripheralOutput, + C: TxChannelCreatorAsync<'d, Tx, O>, + { + let channel = channel.configure(pin, led_config()).unwrap(); + + // Assume the RMT peripheral is set up to use the APB clock + let src_clock = Clocks::get().apb_clock.as_mhz(); + + Self { + channel, + rmt_buffer, + pulses: led_pulses_for_clock(src_clock), + } + } + + fn prepare_rmt_buffer>( + &mut self, + iterator: impl IntoIterator, + ) -> Result<(), LedAdapterError> { + // We always start from the beginning of the buffer + let mut seq_iter = self.rmt_buffer.iter_mut(); + + // Add all converted iterator items to the buffer. + // This will result in an `BufferSizeExceeded` error in case + // the iterator provides more elements than the buffer can take. + for item in iterator { + Self::convert_rgb_to_pulse(item.into(), &mut seq_iter, self.pulses)?; + } + Ok(()) + } + + /// Converts a RGB value to the correspodnign pulse value. + fn convert_rgb_to_pulse( + value: RGB8, + mut_iter: &mut IterMut, + pulses: (u32, u32), + ) -> Result<(), LedAdapterError> { + convert_rgb_to_pulses(value, mut_iter, pulses)?; + *mut_iter.next().ok_or(LedAdapterError::BufferSizeExceeded)? = 0; + + Ok(()) + } +} + +impl SmartLedsWriteAsync + for SmartLedsAdapterAsync +{ + type Error = LedAdapterError; + type Color = RGB8; + + /// Convert all RGB8 items of the iterator to the RMT format and + /// add them to internal buffer, then start perform all asynchronous operations based on + /// that buffer. + async fn write(&mut self, iterator: T) -> Result<(), Self::Error> + where + T: IntoIterator, + I: Into, + { + self.prepare_rmt_buffer(iterator)?; + for chunk in self.rmt_buffer.chunks(RMT_RAM_ONE_LED + 1) { + self.channel + .transmit(chunk) + .await + .map_err(LedAdapterError::TransmissionError)?; + } + Ok(()) + } +}