Skip to content

Commit

Permalink
feat: add async version of the driver (wip)
Browse files Browse the repository at this point in the history
guarded by `nb` feature flag. Not complete yet
  • Loading branch information
markus-k committed Jan 28, 2025
1 parent abc4d00 commit b6bc8f7
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 1 deletion.
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ license = "MIT OR Apache-2.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["nb"]
nb = ["dep:embedded-hal-async"]

[dependencies]
embedded-hal = "1.0"
defmt = "^0.3"
embedded-hal-async = { version = "1.0.0", optional = true }

[dev-dependencies]
embedded-hal-mock = "0.10"
embedded-hal-mock = { version = "0.11", features = ["embedded-hal-async"] }
110 changes: 110 additions & 0 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,116 @@ where
}
}

#[cfg(feature = "nb")]
pub mod nb {
use super::{spi_transmission_header, Error};
use embedded_hal_async::spi;

/// Trait for giving read and write access to registers
#[allow(async_fn_in_trait)]
pub trait RegisterAccess {
type Error;

/// Reads `N` values from multiple registers, starting from `start_register` and incrementing
/// the register for every elements.
async fn read_registers(
&mut self,
start_register: u16,
data: &mut [u8],
) -> Result<(), Self::Error>;

/// Writes to multiple registers, starting from `start_register` and incrementing
/// the register by one for every element in `data`.
async fn write_registers(
&mut self,
start_register: u16,
data: &[u8],
) -> Result<(), Self::Error>;

/// Reads a single value from `register`.
async fn read_register(&mut self, register: u16) -> Result<u8, Self::Error> {
let mut buffer: [u8; 1] = [0; 1];
self.read_registers(register, &mut buffer).await?;

Ok(buffer[0])
}

async fn read_register_wide(&mut self, register: u16) -> Result<u16, Self::Error> {
let mut bytes = [0; 2];
self.read_registers(register, &mut bytes).await?;
Ok(u16::from_le_bytes(bytes))
}

/// Writes a single value to `register`.
async fn write_register(&mut self, register: u16, value: u8) -> Result<(), Self::Error> {
self.write_registers(register, &[value]).await
}

async fn write_register_wide(
&mut self,
register: u16,
value: u16,
) -> Result<(), Self::Error> {
self.write_registers(register, &value.to_le_bytes()).await
}
}

pub struct SpiDeviceInterface<SPID> {
pub(crate) spi_device: SPID,
}

impl<SPID: spi::SpiDevice> SpiDeviceInterface<SPID> {
pub fn new(spi_device: SPID) -> Self {
Self { spi_device }
}

pub fn release(self) -> SPID {
self.spi_device
}
}

impl<SPID, IE> RegisterAccess for SpiDeviceInterface<SPID>
where
SPID: spi::SpiDevice<Error = IE>,
{
type Error = Error<IE>;

async fn read_registers(
&mut self,
start_register: u16,
data: &mut [u8],
) -> Result<(), Self::Error> {
let header = spi_transmission_header(start_register, false);

let mut operations = [spi::Operation::Write(&header), spi::Operation::Read(data)];

self.spi_device
.transaction(&mut operations)
.await
.map_err(Error::Interface)?;

Ok(())
}

async fn write_registers(
&mut self,
start_register: u16,
data: &[u8],
) -> Result<(), Self::Error> {
let header = spi_transmission_header(start_register, true);

let mut operations = [spi::Operation::Write(&header), spi::Operation::Write(data)];

self.spi_device
.transaction(&mut operations)
.await
.map_err(Error::Interface)?;

Ok(())
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use embedded_hal::delay::DelayNs;
use interface::RegisterAccess;
use register::{BitFlags, Register};

#[cfg(feature = "nb")]
pub mod nb;

/// Error enum for the LP586x driver
#[derive(Debug)]
pub enum Error<IE> {
Expand Down
246 changes: 246 additions & 0 deletions src/nb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use embedded_hal_async::delay::DelayNs;

use crate::{
configuration::{ConfigBuilderDeviceSpecific, Configuration},
interface,
register::Register,
DataMode16Bit, DataMode8Bit, DataModeMarker, DataRefMode, DeviceVariant, Error, Group,
Variant0, Variant0T, Variant1, Variant1T, Variant2, Variant4, Variant6, Variant6T, Variant8,
Variant8T, T_CHIP_EN_US,
};

pub struct Lp586x<DV, I, DM> {
interface: I,
_phantom_data: core::marker::PhantomData<(DV, DM)>,
}

impl<DV: DeviceVariant, DM: DataModeMarker, IE, SPI>
Lp586x<DV, interface::nb::SpiDeviceInterface<SPI>, DM>
where
SPI: embedded_hal_async::spi::SpiDevice<Error = IE>,
{
pub async fn init_with_spi_device<D: DelayNs>(
config: &ConfigBuilderDeviceSpecific<DV, DM>,
spi_device: SPI,
delay: &mut D,
) -> Result<Lp586x<DV, interface::nb::SpiDeviceInterface<SPI>, DM>, Error<IE>> {
Lp586x::<DV, _, DM>::init(
config,
interface::nb::SpiDeviceInterface::new(spi_device),
delay,
)
.await
}
}

impl<DV: DeviceVariant, I, DM: DataModeMarker, IE> Lp586x<DV, I, DM>
where
I: interface::nb::RegisterAccess<Error = Error<IE>>,
{
/// Number of current sinks of the LP586x
pub const NUM_CURRENT_SINKS: usize = DV::NUM_CURRENT_SINKS as usize;

/// Total number of LEDs supported by this driver
pub const NUM_DOTS: usize = DV::NUM_DOTS as usize;

/// Number of lines (switches) supported by this driver
pub const fn num_lines(&self) -> u8 {
DV::NUM_LINES
}

/// Total number of dots supported by this driver
pub const fn num_dots(&self) -> u16 {
DV::NUM_DOTS
}

pub async fn init<D: DelayNs>(
config: &ConfigBuilderDeviceSpecific<DV, DM>,
interface: I,
delay: &mut D,
) -> Result<Self, Error<IE>> {
let mut this = Self {
interface,
_phantom_data: Default::default(),
};

this.reset().await?;
this.enable(delay).await?;
this.configure(&config.configuration).await?;

match config.configuration.data_ref_mode {
DataRefMode::Mode2 | DataRefMode::Mode3 => {
// chip needs to be toggled on/off after setting max current in Mode 2 and 3
this.disable().await?;
this.enable(delay).await?;
}
DataRefMode::Mode1 => {}
};

Ok(this)
}

pub(crate) async fn configure(
&mut self,
configuration: &Configuration<DV>,
) -> Result<(), Error<IE>> {
self.interface
.write_registers(
Register::DEV_INITIAL,
&[
configuration.dev_initial_reg_value(),
configuration.dev_config1_reg_value(),
configuration.dev_config2_reg_value(),
configuration.dev_config3_reg_value(),
],
)
.await?;

Ok(())
}

/// Resets the chip.
pub async fn reset(&mut self) -> Result<(), Error<IE>> {
self.interface.write_register(Register::RESET, 0xff).await?;

Ok(())
}

/// Enables the chip and waits the required delay t_chip_en.
pub async fn enable<D: DelayNs>(&mut self, delay: &mut D) -> Result<(), Error<IE>> {
self.chip_enable(true).await?;
delay.delay_us(T_CHIP_EN_US).await;

Ok(())
}

/// Disables the chip. All data is retained.
pub async fn disable(&mut self) -> Result<(), Error<IE>> {
self.chip_enable(false).await?;

Ok(())
}

pub async fn chip_enable(&mut self, enable: bool) -> Result<(), Error<IE>> {
self.interface
.write_register(Register::CHIP_EN, if enable { 0xff } else { 0x00 })
.await?;

Ok(())
}

/// Set dot current, starting from `start_dot`.
pub async fn set_dot_current(
&mut self,
start_dot: u16,
current: &[u8],
) -> Result<(), Error<IE>> {
assert!(current.len() <= self.num_dots() as usize);
assert!(!current.is_empty());

self.interface
.write_registers(Register::DOT_CURRENT_START + start_dot, current)
.await?;

Ok(())
}

/// Sets the brightness across all LEDs in the given [`Group`].
/// Note that individual LEDS/dots need to be assigned to a `LED_DOT_GROUP`
/// for this setting to have effect. By default dots ar not assigned to any group.
pub async fn set_group_brightness(
&mut self,
group: Group,
brightness: u8,
) -> Result<(), Error<IE>> {
self.interface
.write_register(group.brightness_reg_addr(), brightness)
.await?;

Ok(())
}

/// Set color group current scaling (0..127).
pub async fn set_color_group_current(
&mut self,
group: Group,
current: u8,
) -> Result<(), Error<IE>> {
self.interface
.write_register(group.current_reg_addr(), current.min(0x7f))
.await?;

Ok(())
}

/// Sets the global brightness across all LEDs.
pub async fn set_global_brightness(&mut self, brightness: u8) -> Result<(), Error<IE>> {
self.interface
.write_register(Register::GLOBAL_BRIGHTNESS, brightness)
.await?;

Ok(())
}
}
impl<DV, I, IE> Lp586x<DV, I, DataMode8Bit>
where
DV: DeviceVariant,
I: interface::nb::RegisterAccess<Error = Error<IE>>,
{
pub async fn set_brightness(&mut self, start_dot: u16, values: &[u8]) -> Result<(), Error<IE>> {
assert!(start_dot as usize + values.len() <= self.num_dots() as usize);

self.interface
.write_registers(Register::PWM_BRIGHTNESS_START + start_dot, values)
.await?;

Ok(())
}
}

macro_rules! brightness_impl {
($variant:ident) => {
impl<I, IE> Lp586x<$variant, I, DataMode16Bit>
where
I: interface::nb::RegisterAccess<Error = Error<IE>>,
{
pub async fn set_brightness(
&mut self,
start_dot: u16,
values: &[u16],
) -> Result<(), Error<IE>> {
assert!(start_dot as usize + values.len() <= self.num_dots() as usize);

// we can't do something like [u8; N*2] with stable rust here, so we
// have to use macro to create a specialized impl for every variant...
let mut buffer = [0; $variant::NUM_DOTS as usize * 2];

buffer
.chunks_exact_mut(2)
.zip(values.iter())
.for_each(|(dest, src)| {
dest.copy_from_slice(&src.to_le_bytes());
});

self.interface
.write_registers(Register::PWM_BRIGHTNESS_START + start_dot, &buffer)
.await?;

Ok(())
}
}
};
}

// this is kind of stupid/crazy, but otherwise this can only be implemented
// by wasting tons of stack memory in stable rust at the moment
brightness_impl!(Variant0);
brightness_impl!(Variant1);
brightness_impl!(Variant2);
brightness_impl!(Variant4);
brightness_impl!(Variant6);
brightness_impl!(Variant8);

brightness_impl!(Variant0T);
brightness_impl!(Variant1T);
brightness_impl!(Variant6T);
brightness_impl!(Variant8T);

0 comments on commit b6bc8f7

Please sign in to comment.