diff --git a/Cargo.lock b/Cargo.lock index 729aa13..0f1acc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + [[package]] name = "defmt" version = "1.0.1" @@ -82,21 +91,64 @@ dependencies = [ "thiserror", ] +[[package]] +name = "embedded-crc-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1c75747a43b086df1a87fb2a889590bc0725e0abf54bba6d0c4bf7bd9e762c" + [[package]] name = "embedded-hal" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" +dependencies = [ + "defmt 0.3.100", +] + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "defmt 0.3.100", + "embedded-hal", +] + +[[package]] +name = "embedded-hal-mock" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a0f04f8886106faf281c47b6a0e4054a369baedaf63591fdb8da9761f3f379" +dependencies = [ + "embedded-hal", + "embedded-hal-async", + "embedded-hal-nb", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal", + "nb", +] [[package]] name = "embedded-mcu-hal" version = "0.2.0" dependencies = [ "chrono", - "defmt", + "defmt 1.0.1", "embedded-hal", + "embedded-hal-async", + "embedded-hal-mock", "num_enum", "proptest", + "smbus-pec", "tokio", ] @@ -146,6 +198,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + [[package]] name = "num-traits" version = "0.2.19" @@ -343,6 +401,15 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "smbus-pec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0763a680cd5d72b28f7bfc8a054c117d8841380a6ad4f72f05bd2a34217d3e" +dependencies = [ + "embedded-crc-macros", +] + [[package]] name = "storage_bus" version = "0.1.0" diff --git a/embedded-mcu-hal/Cargo.toml b/embedded-mcu-hal/Cargo.toml index be43739..8a6127c 100644 --- a/embedded-mcu-hal/Cargo.toml +++ b/embedded-mcu-hal/Cargo.toml @@ -17,16 +17,22 @@ chrono = { version = "^0.4", default-features = false, optional = true } defmt = { version = "1.0", optional = true } embedded-hal = { workspace = true } num_enum = { version = "0.7.5", default-features = false } +embedded-hal-async = "1.0.0" [dev-dependencies] proptest = "1.0.0" tokio = { version = "1", features = ["macros", "rt", "time"] } +embedded-hal-mock = { version = "0.11", default-features = false, features = [ + "eh1", + "embedded-hal-async", +] } +smbus-pec = { version = "1.0", default-features = false } [features] default = [] chrono = ["dep:chrono"] -defmt = ["dep:defmt"] +defmt = ["dep:defmt", "embedded-hal-async/defmt-03"] [lints] workspace = true diff --git a/embedded-mcu-hal/src/lib.rs b/embedded-mcu-hal/src/lib.rs index fa11d5b..659f5ec 100644 --- a/embedded-mcu-hal/src/lib.rs +++ b/embedded-mcu-hal/src/lib.rs @@ -17,9 +17,14 @@ //! * **Device-agnostic** — no register addresses, magic values, or //! MCU-specific types appear in any public API. //! -//! * **Trait-only** — the crate ships no runtime code beyond the -//! [`time::Datetime`] value type and its helpers. Keeping implementations -//! out-of-crate means zero overhead: you only pay for what you use. +//! * **Mostly trait-only** — the crate is primarily a contract surface, +//! not a runtime. The only concrete runtime code shipped today is the +//! [`time::Datetime`] value type and its helpers, and a portable software +//! SMBus controller ([`smbus::bus::asynch::SwSmbusI2c`]) that layers the +//! SMBus protocol on top of any [`embedded_hal_async::i2c::I2c`] +//! implementation. Hardware-specific implementations of every trait +//! still belong in board or chip support crates, so generic drivers pay +//! only for what they use. //! //! * **`no_std` first** — every public item is usable in bare-metal firmware //! with no heap allocator. The standard library is only linked during @@ -45,6 +50,7 @@ //! |--------|----------------| //! | [`time`] | Wall-clock date/time types and real-time clock (RTC) traits | //! | [`nvram`] | Non-Volatile RAM storage traits | +//! | [`smbus`] | SMBus controller traits and a portable software implementation atop `embedded-hal-async` I²C | //! | [`watchdog`] | Watchdog timer trait | //! //! # Optional Cargo features @@ -70,5 +76,6 @@ pub mod i2c; pub mod nvram; +pub mod smbus; pub mod time; pub mod watchdog; diff --git a/embedded-mcu-hal/src/smbus/bus/asynch/mod.rs b/embedded-mcu-hal/src/smbus/bus/asynch/mod.rs new file mode 100644 index 0000000..d4a14ba --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/asynch/mod.rs @@ -0,0 +1,932 @@ +//! Async SMBus controller trait and software implementation. +//! +//! This module defines the [`Smbus`] async controller trait describing the +//! SMBus protocol surface. The trait declares the protocol-level +//! operations as required methods plus two associated items — +//! [`Smbus::PecCalc`] and [`Smbus::get_pec_calc`] — and a +//! default-implemented helper [`Smbus::check_pec`]. +//! +//! Concrete bit-banging of the protocol on top of an +//! [`embedded_hal_async::i2c::I2c`] bus is provided by [`SwSmbusI2c`]. +//! HAL authors with a hardware SMBus peripheral may instead implement +//! [`Smbus`] directly. +//! +//! See the [parent module](super) for the protocol overview, PEC handling, +//! and driver/HAL guidance. + +use core::hash::Hasher; +use core::marker::PhantomData; + +use crate::smbus::bus::Error as SMBusError; +use embedded_hal_async::i2c::{Error as I2cError, I2c, Operation}; + +/// PEC calculator factory for [`SwSmbusI2c`]. +/// +/// Decouples [`SwSmbusI2c`] from a particular PEC implementation. Provide +/// a type implementing this trait as the `P` type parameter of +/// [`SwSmbusI2c`] to describe what PEC calculator (if any) the bus should +/// use. +pub trait PecProvider { + /// PEC calculator type. + type Calc: Hasher; + + /// Construct a fresh PEC calculator, or return `None` when PEC is + /// unsupported on this bus. When `None` is returned, any operation + /// invoked with `use_pec = true` fails with + /// [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec). + fn new_calc() -> Option; +} + +/// Async SMBus controller trait. +/// +/// Declares the SMBus protocol surface. Implementations may either be a +/// software protocol-basher over a generic I²C bus (see [`SwSmbusI2c`]) +/// or a HAL-level wrapper around a hardware SMBus peripheral. +#[allow(async_fn_in_trait)] +pub trait Smbus: crate::smbus::bus::ErrorType { + /// PEC (Packet Error Code) calculator type. + /// + /// When a SMBus operation requests PEC verification (`use_pec = true`), + /// implementations should return a `PecCalc` instance from `get_pec_calc()` + /// that is then fed the transmitted/received bytes in bus order. The calculator + /// should expose the checksum through `finish()`; this crate treats the + /// resulting value as a single-byte PEC. + /// + /// The type must implement `core::hash::Hasher`. PEC calculators are obtained + /// via the `get_pec_calc()` method, which returns `Option`. If + /// `get_pec_calc()` returns `None`, any operation with `use_pec = true` will + /// return an error of kind `ErrorKind::Pec`. + type PecCalc: core::hash::Hasher; + + /// Obtain a PEC calculator instance if PEC support is available. + /// + /// Returns `Some(calculator)` if PEC support is available, or `None` if + /// not. When `None` is returned, any operation with `use_pec = true` + /// fails with an error of kind + /// [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec). + fn get_pec_calc() -> Option; + + /// Check PEC (Packet Error Code) validity. + /// + /// Compares a received PEC byte against a computed PEC value. Only the + /// low byte of `computed_pec` is used. + fn check_pec(received_pec: u8, computed_pec: u64) -> Result<(), ::Error> { + computed_pec + .eq(&received_pec.into()) + .then_some(()) + .ok_or_else(|| ::Error::from_kind(crate::smbus::bus::ErrorKind::Pec)) + } + + /// Quick Command. + async fn quick_command( + &mut self, + address: u8, + read: bool, + ) -> Result<(), ::Error>; + + /// Send Byte. + async fn send_byte(&mut self, address: u8, byte: u8) -> Result<(), ::Error>; + + /// Send Byte with PEC. + async fn send_byte_with_pec( + &mut self, + address: u8, + byte: u8, + ) -> Result<(), ::Error>; + + /// Receive Byte. + async fn receive_byte(&mut self, address: u8) -> Result::Error>; + + /// Receive Byte with PEC. + async fn receive_byte_with_pec(&mut self, address: u8) + -> Result::Error>; + + /// Write Byte. + async fn write_byte( + &mut self, + address: u8, + register: u8, + byte: u8, + ) -> Result<(), ::Error>; + + /// Write Byte with PEC. + async fn write_byte_with_pec( + &mut self, + address: u8, + register: u8, + byte: u8, + ) -> Result<(), ::Error>; + + /// Write Word (little-endian on the wire). + async fn write_word( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result<(), ::Error>; + + /// Write Word with PEC (little-endian on the wire). + async fn write_word_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result<(), ::Error>; + + /// Read Byte. + async fn read_byte( + &mut self, + address: u8, + register: u8, + ) -> Result::Error>; + + /// Read Byte with PEC. + async fn read_byte_with_pec( + &mut self, + address: u8, + register: u8, + ) -> Result::Error>; + + /// Read Word (little-endian on the wire). + async fn read_word( + &mut self, + address: u8, + register: u8, + ) -> Result::Error>; + + /// Read Word with PEC (little-endian on the wire). + async fn read_word_with_pec( + &mut self, + address: u8, + register: u8, + ) -> Result::Error>; + + /// Process Call. + async fn process_call( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result::Error>; + + /// Process Call with PEC. + async fn process_call_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result::Error>; + + /// Block Write. + async fn block_write( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), ::Error>; + + /// Block Write with PEC. + async fn block_write_with_pec( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), ::Error>; + + /// Block Read. + async fn block_read( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), ::Error>; + + /// Block Read with PEC. + async fn block_read_with_pec( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), ::Error>; + + /// Block Write / Block Read / Process Call. + async fn block_write_block_read_process_call( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), ::Error>; + + /// Block Write / Block Read / Process Call with PEC. + async fn block_write_block_read_process_call_with_pec( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), ::Error>; +} + +impl Smbus for &mut T { + type PecCalc = T::PecCalc; + + #[inline] + fn get_pec_calc() -> Option { + T::get_pec_calc() + } + + #[inline] + async fn quick_command( + &mut self, + address: u8, + read: bool, + ) -> Result<(), ::Error> { + T::quick_command(*self, address, read).await + } + + #[inline] + async fn send_byte(&mut self, address: u8, byte: u8) -> Result<(), ::Error> { + T::send_byte(*self, address, byte).await + } + + #[inline] + async fn send_byte_with_pec( + &mut self, + address: u8, + byte: u8, + ) -> Result<(), ::Error> { + T::send_byte_with_pec(*self, address, byte).await + } + + #[inline] + async fn receive_byte(&mut self, address: u8) -> Result::Error> { + T::receive_byte(*self, address).await + } + + #[inline] + async fn receive_byte_with_pec( + &mut self, + address: u8, + ) -> Result::Error> { + T::receive_byte_with_pec(*self, address).await + } + + #[inline] + async fn write_byte( + &mut self, + address: u8, + register: u8, + byte: u8, + ) -> Result<(), ::Error> { + T::write_byte(*self, address, register, byte).await + } + + #[inline] + async fn write_byte_with_pec( + &mut self, + address: u8, + register: u8, + byte: u8, + ) -> Result<(), ::Error> { + T::write_byte_with_pec(*self, address, register, byte).await + } + + #[inline] + async fn write_word( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result<(), ::Error> { + T::write_word(*self, address, register, word).await + } + + #[inline] + async fn write_word_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result<(), ::Error> { + T::write_word_with_pec(*self, address, register, word).await + } + + #[inline] + async fn read_byte( + &mut self, + address: u8, + register: u8, + ) -> Result::Error> { + T::read_byte(*self, address, register).await + } + + #[inline] + async fn read_byte_with_pec( + &mut self, + address: u8, + register: u8, + ) -> Result::Error> { + T::read_byte_with_pec(*self, address, register).await + } + + #[inline] + async fn read_word( + &mut self, + address: u8, + register: u8, + ) -> Result::Error> { + T::read_word(*self, address, register).await + } + + #[inline] + async fn read_word_with_pec( + &mut self, + address: u8, + register: u8, + ) -> Result::Error> { + T::read_word_with_pec(*self, address, register).await + } + + #[inline] + async fn process_call( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result::Error> { + T::process_call(*self, address, register, word).await + } + + #[inline] + async fn process_call_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result::Error> { + T::process_call_with_pec(*self, address, register, word).await + } + + #[inline] + async fn block_write( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), ::Error> { + T::block_write(*self, address, register, data).await + } + + #[inline] + async fn block_write_with_pec( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), ::Error> { + T::block_write_with_pec(*self, address, register, data).await + } + + #[inline] + async fn block_read( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), ::Error> { + T::block_read(*self, address, register, data).await + } + + #[inline] + async fn block_read_with_pec( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), ::Error> { + T::block_read_with_pec(*self, address, register, data).await + } + + #[inline] + async fn block_write_block_read_process_call( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), ::Error> { + T::block_write_block_read_process_call(*self, address, register, write_data, read_data).await + } + + #[inline] + async fn block_write_block_read_process_call_with_pec( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), ::Error> { + T::block_write_block_read_process_call_with_pec(*self, address, register, write_data, read_data).await + } +} + +/// Software SMBus controller built on top of an async I²C bus. +/// +/// `SwSmbusI2c` implements [`Smbus`] by bit-banging the SMBus +/// protocol on top of an [`embedded_hal_async::i2c::I2c`] bus `I`. PEC +/// support is delegated to the [`PecProvider`] `P`. +pub struct SwSmbusI2c { + i2c: I, + _pec: PhantomData

, +} + +impl SwSmbusI2c { + /// Wrap an I²C bus to form a software SMBus controller. + #[inline] + pub const fn new(i2c: I) -> Self { + Self { i2c, _pec: PhantomData } + } + + /// Consume the wrapper and return the underlying I²C bus. + #[inline] + pub fn into_inner(self) -> I { + self.i2c + } + + /// Borrow the underlying I²C bus. + #[inline] + pub fn inner(&self) -> &I { + &self.i2c + } + + /// Mutably borrow the underlying I²C bus. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + &mut self.i2c + } +} + +impl crate::smbus::bus::ErrorType for SwSmbusI2c { + type Error = crate::smbus::bus::ErrorKind; +} + +impl SwSmbusI2c +where + P: PecProvider, +{ + /// Obtain a fresh PEC calculator pre-fed with the write-address byte, + /// or [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) if the + /// provider returns `None`. + fn pec_calc_with_write_addr(address: u8) -> Result { + let mut pec = P::new_calc().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write_u8(crate::smbus::bus::write_address_byte(address)); + Ok(pec) + } + + /// Obtain a fresh PEC calculator pre-fed with the read-address byte, + /// or [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) if the + /// provider returns `None`. Used by pure-read transactions (e.g. + /// Receive Byte) whose first wire byte is the read-direction address. + fn pec_calc_with_read_addr(address: u8) -> Result { + let mut pec = P::new_calc().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + Ok(pec) + } + + /// Truncate a finished PEC value to its low byte. + fn finalize_pec_byte(pec: u64) -> Result { + pec.try_into().map_err(|_| crate::smbus::bus::ErrorKind::Pec) + } +} + +impl SwSmbusI2c +where + I: I2c, + P: PecProvider, +{ + /// Write a buffer of data with optional PEC computation. + /// + /// When `use_pec` is true, the caller must size `operations` to include + /// one extra trailing byte for the PEC; that byte is filled in with the + /// computed PEC before the I²C write. + async fn write_buf( + &mut self, + address: u8, + use_pec: bool, + operations: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if use_pec { + let mut pec = Self::pec_calc_with_write_addr(address)?; + let (pec_elem, rest) = operations.split_last_mut().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write(rest); + *pec_elem = Self::finalize_pec_byte(pec.finish())?; + } + self.i2c + .write(address, operations) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + Ok(()) + } + + /// Read a buffer of data with optional PEC verification. + /// + /// When `use_pec` is true, the caller must size `read` to include one + /// extra trailing byte for the PEC byte; it is verified after the read. + /// The PEC is seeded with the read-direction address byte because this + /// helper drives a pure-read SMBus transaction (e.g. Receive Byte). + async fn read_buf( + &mut self, + address: u8, + use_pec: bool, + read: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if use_pec { + let mut pec = Self::pec_calc_with_read_addr(address)?; + self.i2c + .read(address, read) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + let (pec_byte, rest) = read.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write(rest); + ::check_pec(*pec_byte, pec.finish())?; + } else { + self.i2c + .read(address, read) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + } + Ok(()) + } + + /// Write a buffer and then read a buffer, with optional PEC verification. + /// + /// When `use_pec` is true, the caller must size `read` to include one + /// extra trailing byte for the PEC byte; it is verified against a + /// locally computed PEC. + async fn write_read_buf( + &mut self, + address: u8, + use_pec: bool, + write: &[u8], + read: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + // When PEC is requested, fail fast without touching the bus if no + // PEC calculator is available. + let mut pec = if use_pec { + Some(Self::pec_calc_with_write_addr(address)?) + } else { + None + }; + self.i2c + .transaction(address, &mut [Operation::Write(write), Operation::Read(read)]) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if let Some(pec) = pec.as_mut() { + pec.write(write); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + let (pec_byte, rest) = read.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write(rest); + ::check_pec(*pec_byte, pec.finish())?; + } + Ok(()) + } +} + +impl Smbus for SwSmbusI2c +where + I: I2c, + P: PecProvider, +{ + type PecCalc = P::Calc; + + #[inline] + fn get_pec_calc() -> Option { + P::new_calc() + } + + #[inline] + async fn quick_command(&mut self, address: u8, read: bool) -> Result<(), crate::smbus::bus::ErrorKind> { + self.i2c + .transaction( + address, + &mut if read { + [Operation::Read(&mut [])] + } else { + [Operation::Write(&[])] + }, + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + Ok(()) + } + + async fn send_byte(&mut self, address: u8, byte: u8) -> Result<(), crate::smbus::bus::ErrorKind> { + self.write_buf(address, false, &mut [byte]).await + } + + async fn send_byte_with_pec(&mut self, address: u8, byte: u8) -> Result<(), crate::smbus::bus::ErrorKind> { + self.write_buf(address, true, &mut [byte, 0]).await + } + + async fn receive_byte(&mut self, address: u8) -> Result { + let mut buf = [0u8; 1]; + self.read_buf(address, false, &mut buf).await?; + Ok(buf[0]) + } + + async fn receive_byte_with_pec(&mut self, address: u8) -> Result { + let mut buf = [0u8; 2]; + self.read_buf(address, true, &mut buf).await?; + Ok(buf[0]) + } + + async fn write_byte(&mut self, address: u8, register: u8, byte: u8) -> Result<(), crate::smbus::bus::ErrorKind> { + self.write_buf(address, false, &mut [register, byte]).await + } + + async fn write_byte_with_pec( + &mut self, + address: u8, + register: u8, + byte: u8, + ) -> Result<(), crate::smbus::bus::ErrorKind> { + self.write_buf(address, true, &mut [register, byte, 0]).await + } + + async fn write_word(&mut self, address: u8, register: u8, word: u16) -> Result<(), crate::smbus::bus::ErrorKind> { + let b = u16::to_le_bytes(word); + self.write_buf(address, false, &mut [register, b[0], b[1]]).await + } + + async fn write_word_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result<(), crate::smbus::bus::ErrorKind> { + let b = u16::to_le_bytes(word); + self.write_buf(address, true, &mut [register, b[0], b[1], 0]).await + } + + async fn read_byte(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 1]; + self.write_read_buf(address, false, &[register], &mut buf).await?; + Ok(buf[0]) + } + + async fn read_byte_with_pec(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 2]; + self.write_read_buf(address, true, &[register], &mut buf).await?; + Ok(buf[0]) + } + + async fn read_word(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 2]; + self.write_read_buf(address, false, &[register], &mut buf).await?; + Ok(u16::from_le_bytes(buf)) + } + + async fn read_word_with_pec(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 3]; + self.write_read_buf(address, true, &[register], &mut buf).await?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } + + async fn process_call( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result { + let mut buf = [0u8; 2]; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + Ok(u16::from_le_bytes(buf)) + } + + async fn process_call_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result { + let mut buf = [0u8; 3]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write(&word.to_le_bytes()); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + let (recvd_pec, data) = buf.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write(data); + Self::check_pec(*recvd_pec, pec.finish())?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } + + async fn block_write( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + Ok(()) + } + + async fn block_write_with_pec( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(data.len() as u8); + pec.write(data); + let pec: u8 = Self::finalize_pec_byte(pec.finish())?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + Operation::Write(&[pec]), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + Ok(()) + } + + async fn block_read( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + let mut msg_size = [0u8]; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(msg_size[0]) != data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); + } + Ok(()) + } + + async fn block_read_with_pec( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + let mut msg_size = [0u8]; + let mut pec_buf = [0u8]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(msg_size[0]) != data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); + } + pec.write(&msg_size); + pec.write(data); + Self::check_pec(pec_buf[0], pec.finish())?; + Ok(()) + } + + async fn block_write_block_read_process_call( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if write_data.len() + read_data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + let mut read_msg_size = [0u8]; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(read_msg_size[0]) != read_data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); + } + Ok(()) + } + + async fn block_write_block_read_process_call_with_pec( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if write_data.len() + read_data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + let mut read_msg_size = [0u8]; + let mut pec_buf = [0u8]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(write_data.len() as u8); + pec.write(write_data); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(read_msg_size[0]) != read_data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); + } + pec.write(&read_msg_size); + pec.write(read_data); + Self::check_pec(pec_buf[0], pec.finish())?; + Ok(()) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::cast_possible_truncation)] +mod tests; diff --git a/embedded-mcu-hal/src/smbus/bus/asynch/tests.rs b/embedded-mcu-hal/src/smbus/bus/asynch/tests.rs new file mode 100644 index 0000000..001a8a1 --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/asynch/tests.rs @@ -0,0 +1,704 @@ +use super::Smbus; +use crate::smbus::bus::{ + read_address_byte, write_address_byte, Error as SmbusError, ErrorKind, MAX_BLOCK_SIZE, READ_BIT, +}; +use core::hash::Hasher; +use embedded_hal_async::i2c::ErrorKind as I2cErrorKind; +use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as Tx}; +use smbus_pec::{pec, Pec}; + +const ADDR: u8 = 0x42; +const REG: u8 = 0x07; + +/// Compute the expected SMBus PEC byte over a flat concatenation of byte +/// slices, using the `smbus-pec` crate as the reference implementation. +fn expected_pec(parts: &[&[u8]]) -> u8 { + let mut buf: std::vec::Vec = std::vec::Vec::new(); + for p in parts { + buf.extend_from_slice(p); + } + pec(&buf) +} + +/// PEC provider that exposes the `smbus-pec` calculator. +struct TestPec; +impl super::PecProvider for TestPec { + type Calc = Pec; + fn new_calc() -> Option { + Some(Pec::new()) + } +} + +/// PEC provider that reports PEC as unavailable. +struct NoPec; +impl super::PecProvider for NoPec { + type Calc = Pec; + fn new_calc() -> Option { + None + } +} + +type TestBus = super::SwSmbusI2c; +type NoPecBus = super::SwSmbusI2c; + +fn new_bus(expectations: &[Tx]) -> TestBus { + super::SwSmbusI2c::new(I2cMock::new(expectations)) +} + +fn done(mut bus: TestBus) { + bus.inner_mut().done(); +} + +// ---------- constants / helpers ---------- + +#[test] +fn constants() { + assert_eq!(MAX_BLOCK_SIZE, 255); + assert_eq!(READ_BIT, 0x01); + assert_eq!(write_address_byte(0x42), 0x84); + assert_eq!(read_address_byte(0x42), 0x85); +} + +#[test] +fn error_kind_display_and_kind() { + let k = ErrorKind::Timeout; + assert_eq!(k.kind(), ErrorKind::Timeout); + // Display impls cover all branches. + for k in [ + ErrorKind::I2c(I2cErrorKind::Bus), + ErrorKind::Timeout, + ErrorKind::Pec, + ErrorKind::TooLargeBlockTransaction, + ErrorKind::BlockSizeMismatch, + ErrorKind::Other, + ] { + let s = std::format!("{}", k); + assert!(!s.is_empty()); + } +} + +#[test] +fn error_kind_from_i2c_error_kind() { + let k: ErrorKind = I2cErrorKind::Bus.into(); + assert_eq!(k, ErrorKind::I2c(I2cErrorKind::Bus)); +} + +#[test] +fn infallible_error_to_kind_round_trip() { + // Infallible cannot be constructed; we only check the trait wires up. + fn _accepts(_e: &E) {} + let k = ErrorKind::Pec; + _accepts(&k); +} + +#[tokio::test] +async fn check_pec_match() { + let bus = new_bus(&[]); + TestBus::check_pec(0x42, 0x42).unwrap(); + TestBus::check_pec(0x00, 0x00).unwrap(); + done(bus); +} + +#[tokio::test] +async fn check_pec_mismatch() { + let bus = new_bus(&[]); + let err = TestBus::check_pec(0x42, 0x43).unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +// ---------- write_buf / read_buf (low-level) ---------- + +#[tokio::test] +async fn write_buf_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0xAB, 0xCD])]); + bus.write_buf(ADDR, false, &mut [0xAB, 0xCD]).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_buf_pec() { + let payload = [0xAB, 0xCD]; + let pec = expected_pec(&[&[write_address_byte(ADDR)], &payload]); + let mut buf = [0xAB, 0xCD, 0x00]; + let mut wire = std::vec![0xAB, 0xCD, pec]; + let mut bus = new_bus(&[Tx::write(ADDR, wire.clone())]); + bus.write_buf(ADDR, true, &mut buf).await.unwrap(); + // Last byte should now be the PEC. + assert_eq!(buf[2], pec); + wire.clear(); + done(bus); +} + +#[tokio::test] +async fn read_buf_no_pec() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0x22])]); + let mut buf = [0u8; 2]; + bus.read_buf(ADDR, false, &mut buf).await.unwrap(); + assert_eq!(buf, [0x11, 0x22]); + done(bus); +} + +#[tokio::test] +async fn read_buf_pec() { + let data = 0x11u8; + let pec = expected_pec(&[&[read_address_byte(ADDR)], &[data]]); + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); + let mut buf = [0u8; 2]; + bus.read_buf(ADDR, true, &mut buf).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn read_buf_pec_mismatch() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0xFF])]); // wrong PEC + let mut buf = [0u8; 2]; + let err = bus.read_buf(ADDR, true, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +// ---------- quick_command ---------- + +#[tokio::test] +async fn quick_command_write() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![]), + Tx::transaction_end(ADDR), + ]); + bus.quick_command(ADDR, false).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn quick_command_read() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::read(ADDR, std::vec![]), + Tx::transaction_end(ADDR), + ]); + bus.quick_command(ADDR, true).await.unwrap(); + done(bus); +} + +// ---------- send_byte / receive_byte ---------- + +#[tokio::test] +async fn send_byte_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); + bus.send_byte(ADDR, 0x55).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn send_byte_pec() { + let pec = expected_pec(&[&[write_address_byte(ADDR), 0x55]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55, pec])]); + bus.send_byte_with_pec(ADDR, 0x55).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn receive_byte_no_pec() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99])]); + let b = bus.receive_byte(ADDR).await.unwrap(); + assert_eq!(b, 0x99); + done(bus); +} + +#[tokio::test] +async fn receive_byte_pec() { + let data = 0x99u8; + let pec = expected_pec(&[&[read_address_byte(ADDR), data]]); + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); + let b = bus.receive_byte_with_pec(ADDR).await.unwrap(); + assert_eq!(b, data); + done(bus); +} + +#[tokio::test] +async fn receive_byte_pec_mismatch() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99, 0xFF])]); + let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +// ---------- write_byte / write_word ---------- + +#[tokio::test] +async fn write_byte_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33])]); + bus.write_byte(ADDR, REG, 0x33).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_byte_pec() { + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, 0x33]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33, pec])]); + bus.write_byte_with_pec(ADDR, REG, 0x33).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_word_no_pec() { + let word: u16 = 0xBEEF; + let bytes = word.to_le_bytes(); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1]])]); + bus.write_word(ADDR, REG, word).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_word_pec() { + let word: u16 = 0xBEEF; + let bytes = word.to_le_bytes(); + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, bytes[0], bytes[1]]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1], pec])]); + bus.write_word_with_pec(ADDR, REG, word).await.unwrap(); + done(bus); +} + +// ---------- read_byte / read_word ---------- + +#[tokio::test] +async fn read_byte_no_pec() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77]), + Tx::transaction_end(ADDR), + ]); + let b = bus.read_byte(ADDR, REG).await.unwrap(); + assert_eq!(b, 0x77); + done(bus); +} + +#[tokio::test] +async fn read_byte_pec() { + let data = 0x77u8; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), data]]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![data, pec]), + Tx::transaction_end(ADDR), + ]); + let b = bus.read_byte_with_pec(ADDR, REG).await.unwrap(); + assert_eq!(b, data); + done(bus); +} + +#[tokio::test] +async fn read_byte_pec_mismatch() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77, 0xFF]), + Tx::transaction_end(ADDR), + ]); + let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +#[tokio::test] +async fn read_word_no_pec() { + let lo = 0x12u8; + let hi = 0x34u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![lo, hi]), + Tx::transaction_end(ADDR), + ]); + let w = bus.read_word(ADDR, REG).await.unwrap(); + assert_eq!(w, u16::from_le_bytes([lo, hi])); + done(bus); +} + +#[tokio::test] +async fn read_word_pec() { + let lo = 0x12u8; + let hi = 0x34u8; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), lo, hi]]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![lo, hi, pec]), + Tx::transaction_end(ADDR), + ]); + let w = bus.read_word_with_pec(ADDR, REG).await.unwrap(); + assert_eq!(w, u16::from_le_bytes([lo, hi])); + done(bus); +} + +// ---------- process_call ---------- + +#[tokio::test] +async fn process_call_no_pec() { + let word: u16 = 0x0102; + let resp_lo = 0xAAu8; + let resp_hi = 0xBBu8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, word.to_le_bytes().to_vec()), + Tx::read(ADDR, std::vec![resp_lo, resp_hi]), + Tx::transaction_end(ADDR), + ]); + let r = bus.process_call(ADDR, REG, word).await.unwrap(); + assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); + done(bus); +} + +#[tokio::test] +async fn process_call_pec() { + let word: u16 = 0x0102; + let resp_lo = 0xAAu8; + let resp_hi = 0xBBu8; + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write(&word.to_le_bytes()); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[resp_lo, resp_hi]); + let pec = hasher.finish() as u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, word.to_le_bytes().to_vec()), + Tx::read(ADDR, std::vec![resp_lo, resp_hi, pec]), + Tx::transaction_end(ADDR), + ]); + let r = bus.process_call_with_pec(ADDR, REG, word).await.unwrap(); + assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); + done(bus); +} + +// ---------- block_write ---------- + +#[tokio::test] +async fn block_write_no_pec() { + let data = [0xDE, 0xAD, 0xBE, 0xEF]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![data.len() as u8]), + Tx::write(ADDR, data.to_vec()), + Tx::transaction_end(ADDR), + ]); + bus.block_write(ADDR, REG, &data).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn block_write_pec() { + let data = [0xDE, 0xAD, 0xBE, 0xEF]; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, data.len() as u8], &data]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![data.len() as u8]), + Tx::write(ADDR, data.to_vec()), + Tx::write(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_write_with_pec(ADDR, REG, &data).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn block_write_too_large() { + let mut bus = new_bus(&[]); + let data = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_write(ADDR, REG, &data).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +// ---------- block_read ---------- + +#[tokio::test] +async fn block_read_no_pec() { + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![3]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::transaction_end(ADDR), + ]); + bus.block_read(ADDR, REG, &mut buf).await.unwrap(); + assert_eq!(buf, [0x10, 0x20, 0x30]); + done(bus); +} + +#[tokio::test] +async fn block_read_pec() { + let payload = [0x10u8, 0x20, 0x30]; + let len = payload.len() as u8; + // PEC source matches the implementation: addr+W, reg, addr+R, then msg_size, then data. + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[len]); + hasher.write(&payload); + let pec = hasher.finish() as u8; + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![len]), + Tx::read(ADDR, payload.to_vec()), + Tx::read(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap(); + assert_eq!(buf, payload); + done(bus); +} + +#[tokio::test] +async fn block_read_too_large() { + let mut bus = new_bus(&[]); + let mut buf = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +#[tokio::test] +async fn block_read_size_mismatch_no_pec() { + // Device reports `2` but the caller expected `3`. + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![2]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::transaction_end(ADDR), + ]); + let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +#[tokio::test] +async fn block_read_size_mismatch_pec() { + // Device reports `2` but the caller expected `3`. The mismatch must be + // reported as `BlockSizeMismatch` rather than `Pec`, even though the + // received PEC byte would not match either. + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![2]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::read(ADDR, std::vec![0x00]), + Tx::transaction_end(ADDR), + ]); + let err = bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +// ---------- block_write_block_read_process_call ---------- + +#[tokio::test] +async fn bwbr_no_pec() { + let write_data = [0x01u8, 0x02]; + let read_payload = [0xAAu8, 0xBB]; + let mut read_buf = [0u8; 2]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![read_payload.len() as u8]), + Tx::read(ADDR, read_payload.to_vec()), + Tx::transaction_end(ADDR), + ]); + bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap(); + assert_eq!(read_buf, read_payload); + done(bus); +} + +#[tokio::test] +async fn bwbr_pec() { + let write_data = [0x01u8, 0x02]; + let read_payload = [0xAAu8, 0xBB]; + let mut read_buf = [0u8; 2]; + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write_u8(write_data.len() as u8); + hasher.write(&write_data); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[read_payload.len() as u8]); + hasher.write(&read_payload); + let pec = hasher.finish() as u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![read_payload.len() as u8]), + Tx::read(ADDR, read_payload.to_vec()), + Tx::read(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap(); + assert_eq!(read_buf, read_payload); + done(bus); +} + +#[tokio::test] +async fn bwbr_too_large() { + let mut bus = new_bus(&[]); + let write_data = std::vec![0u8; 200]; + let mut read_buf = std::vec![0u8; 60]; // 200 + 60 > 255 + let err = bus + .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +#[tokio::test] +async fn bwbr_size_mismatch_no_pec() { + let write_data = [0x01u8, 0x02]; + let mut read_buf = [0u8; 2]; + // Device returns count `1` but caller expected `2`. + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![1]), + Tx::read(ADDR, std::vec![0xAA, 0xBB]), + Tx::transaction_end(ADDR), + ]); + let err = bus + .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +#[tokio::test] +async fn bwbr_size_mismatch_pec() { + let write_data = [0x01u8, 0x02]; + let mut read_buf = [0u8; 2]; + // Device returns count `1` but caller expected `2`. Must be reported + // as `BlockSizeMismatch` rather than `Pec`. + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![1]), + Tx::read(ADDR, std::vec![0xAA, 0xBB]), + Tx::read(ADDR, std::vec![0x00]), + Tx::transaction_end(ADDR), + ]); + let err = bus + .block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +// ---------- PEC unavailable ---------- + +fn new_no_pec_bus(expectations: &[Tx]) -> NoPecBus { + super::SwSmbusI2c::new(I2cMock::new(expectations)) +} + +fn done_no_pec(mut bus: NoPecBus) { + bus.inner_mut().done(); +} + +#[test] +fn no_pec_bus_get_pec_calc_returns_none() { + assert!(NoPecBus::get_pec_calc().is_none()); +} + +#[tokio::test] +async fn no_pec_bus_non_pec_ops_still_work() { + // All `use_pec = false` paths must succeed even though `get_pec_calc` + // returns `None`: the trait must not consult the PEC calculator unless + // PEC was actually requested. + let mut bus = new_no_pec_bus(&[ + Tx::write(ADDR, std::vec![0x55]), + Tx::read(ADDR, std::vec![0x99]), + Tx::write(ADDR, std::vec![REG, 0x33]), + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77]), + Tx::transaction_end(ADDR), + ]); + bus.send_byte(ADDR, 0x55).await.unwrap(); + assert_eq!(bus.receive_byte(ADDR).await.unwrap(), 0x99); + bus.write_byte(ADDR, REG, 0x33).await.unwrap(); + assert_eq!(bus.read_byte(ADDR, REG).await.unwrap(), 0x77); + done_no_pec(bus); +} + +#[tokio::test] +async fn pec_unavailable_returns_pec_error() { + let mut bus = super::SwSmbusI2c::::new(I2cMock::new(&[])); + // Any PEC-requiring path should fail without touching the bus. + let err = bus.send_byte_with_pec(ADDR, 0x55).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.read_word_with_pec(ADDR, REG).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.process_call_with_pec(ADDR, REG, 0).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.block_write_with_pec(ADDR, REG, &[1, 2]).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let mut rb = [0u8; 2]; + let err = bus.block_read_with_pec(ADDR, REG, &mut rb).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus + .block_write_block_read_process_call_with_pec(ADDR, REG, &[1], &mut rb) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + bus.inner_mut().done(); +} + +// ---------- &mut T forwarding ---------- + +#[tokio::test] +async fn mut_ref_smbus_forwards() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); + let r: &mut TestBus = &mut bus; + r.send_byte(ADDR, 0x55).await.unwrap(); + assert!(<&mut TestBus as Smbus>::get_pec_calc().is_some()); + done(bus); +} + +// ---------- error propagation from underlying I2C ---------- + +#[tokio::test] +async fn i2c_error_propagates() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55]).with_error(I2cErrorKind::Bus)]); + let err = bus.send_byte(ADDR, 0x55).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::I2c(I2cErrorKind::Bus)); + done(bus); +} diff --git a/embedded-mcu-hal/src/smbus/bus/mod.rs b/embedded-mcu-hal/src/smbus/bus/mod.rs new file mode 100644 index 0000000..6af5ec7 --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/mod.rs @@ -0,0 +1,175 @@ +//! SMBus controller API. +//! +//! This module hosts SMBus controller-side traits built on top of the +//! controller traits from [`embedded_hal_async::i2c`]. Where the underlying +//! I²C controller traits move arbitrary byte streams across the bus, the +//! SMBus traits encode the higher-level SMBus protocol transactions +//! (quick command, send/receive byte, byte/word/block read/write, process +//! calls) together with optional Packet Error Code (PEC) computation and +//! verification. +//! +//! # PEC handling +//! +//! When an SMBus operation is invoked with `use_pec = true`, the +//! implementation obtains a fresh PEC calculator from +//! [`asynch::Smbus::get_pec_calc`] and feeds it the bytes that appear on +//! the wire (address, register, payload, …) in bus order. The truncated +//! low byte of [`core::hash::Hasher::finish`] is treated as the PEC. +//! +//! Implementations that do not support PEC return `None` from +//! `get_pec_calc()`; any operation with `use_pec = true` then fails with +//! [`ErrorKind::Pec`]. +//! +//! # For driver authors +//! +//! Drivers should take an `Smbus` instance by value, not by `&mut`. The +//! blanket impl for `&mut T` lets the user pass either, but owning the +//! instance keeps the driver's API symmetric with the controller-side +//! traits in [`embedded_hal_async::i2c`]. +//! +//! # For HAL authors +//! +//! - Bus configuration (clocking, addressing, SMBus role) is a peripheral +//! concern handled at construction time. These traits deliberately +//! expose none of that — they only describe the protocol-level +//! transactions. +//! +//! - Block transfers are capped at 255 bytes per the SMBus specification; +//! exceeding this returns [`ErrorKind::TooLargeBlockTransaction`]. +//! +//! - The SMBus slave timeout (35 ms) is reported as [`ErrorKind::Timeout`]. +//! +//! [`embedded_hal_async::i2c`]: +//! https://docs.rs/embedded-hal-async/1.0.0/embedded_hal_async/i2c/index.html + +pub mod asynch; + +/// Maximum payload size, in bytes, of a single SMBus block transfer. +/// +/// The SMBus specification caps the `length` field of a block read or +/// block write at one byte, so a single block transaction can carry at +/// most 255 data bytes. +pub(crate) const MAX_BLOCK_SIZE: usize = 255; + +/// Read-bit value OR-ed into the shifted address byte to mark a read. +/// +/// The 8-bit address byte placed on the wire is `(address << 1) | rw`, +/// where `rw` is `0` for a write and [`READ_BIT`] (`1`) for a read. +pub(crate) const READ_BIT: u8 = 0x01; + +/// Compute the 8-bit write-address byte (`address << 1`) used on the wire. +#[inline] +pub(crate) const fn write_address_byte(address: u8) -> u8 { + address << 1 +} + +/// Compute the 8-bit read-address byte (`(address << 1) | READ_BIT`) used +/// on the wire. +#[inline] +pub(crate) const fn read_address_byte(address: u8) -> u8 { + (address << 1) | READ_BIT +} + +/// SMBus error. +pub trait Error: core::fmt::Debug { + /// Convert error to a generic SMBus error kind. + /// + /// By using this method, SMBus errors freely defined by HAL implementations + /// can be converted to a common set of SMBus errors upon which generic + /// code can act. + fn kind(&self) -> ErrorKind; + /// Construct an error from a generic SMBus error kind. + fn from_kind(kind: ErrorKind) -> Self; +} + +impl Error for core::convert::Infallible { + #[inline] + fn kind(&self) -> ErrorKind { + match *self {} + } + #[inline] + fn from_kind(_kind: ErrorKind) -> Self { + // `Infallible` is uninhabited, so this function can never actually + // be called + #[allow(clippy::unreachable)] + { + unreachable!() + } + } +} + +/// SMBus error kind. +/// +/// This represents a common set of SMBus operation errors. HAL implementations are +/// free to define more specific or additional error types. However, by providing +/// a mapping to these common SMBus errors, generic code can still react to them. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum ErrorKind { + /// Error shared with I2C. + I2c(embedded_hal_async::i2c::ErrorKind), + /// Bus timeout, SMBus defines slave timeout as 35ms. + Timeout, + /// Packet Error Checking (PEC) byte incorrect. + Pec, + /// Block read/write too large transfer, at most 255 bytes can be read/written at once. + TooLargeBlockTransaction, + /// Block read returned a byte count that did not match the caller's + /// expected buffer length. + BlockSizeMismatch, + /// A different error occurred. The original error may contain more information. + Other, +} + +impl From for ErrorKind { + fn from(value: embedded_hal_async::i2c::ErrorKind) -> Self { + Self::I2c(value) + } +} + +impl Error for ErrorKind { + #[inline] + fn kind(&self) -> ErrorKind { + *self + } + #[inline] + fn from_kind(kind: ErrorKind) -> Self { + kind + } +} + +impl core::fmt::Display for ErrorKind { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::I2c(e) => e.fmt(f), + Self::Timeout => write!(f, "Bus timeout, SMBus defines slave timeout as 35ms"), + Self::Pec => write!(f, "Packet Error Checking (PEC) byte incorrect."), + Self::TooLargeBlockTransaction => write!( + f, + "Block read/write transfer size too large, at most 255 bytes can be read/written at once." + ), + Self::BlockSizeMismatch => write!( + f, + "Block read returned a byte count that did not match the caller's expected buffer length." + ), + Self::Other => write!( + f, + "A different error occurred. The original error may contain more information" + ), + } + } +} + +/// SMBus error type trait. +/// +/// This just defines the error type, to be used by the other traits. +pub trait ErrorType { + /// Error type + type Error: Error + From; +} + +impl ErrorType for &mut T { + type Error = T::Error; +} diff --git a/embedded-mcu-hal/src/smbus/mod.rs b/embedded-mcu-hal/src/smbus/mod.rs new file mode 100644 index 0000000..c734dcd --- /dev/null +++ b/embedded-mcu-hal/src/smbus/mod.rs @@ -0,0 +1,3 @@ +//! Traits for interacting with SMBus controllers and targets. + +pub mod bus; diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 8bd31a2..cdeed2a 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -11,6 +11,17 @@ who = "Felipe Balbi " criteria = "safe-to-run" delta = "1.0.0 -> 1.0.4" +[[audits.defmt]] +who = "matteotullo " +criteria = "safe-to-deploy" +version = "0.3.100" +notes = "defmt-rtt is used for all our logging purposes." + +[[audits.embedded-hal-mock]] +who = "matteotullo " +criteria = "safe-to-run" +delta = "0.8.0 -> 0.11.1" + [[audits.embedded-mcu-hal]] who = "Felipe Balbi " criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index a04850a..04fa752 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -19,6 +19,12 @@ criteria = "safe-to-deploy" version = "1.0.0" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embedded-crc-macros]] +who = "Matteo Tullo " +criteria = "safe-to-deploy" +version = "1.0.0" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.embedded-hal]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -32,6 +38,20 @@ delta = "0.2.7 -> 1.0.0" notes = "Pure no_std trait crate. Complete API redesign for 1.0: removed nb-based traits, CAN module, all unsafe code. Only defines traits/enums/types for digital, I2C, SPI, PWM, delay. No build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embedded-hal-async]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "no_std async HAL trait definitions. No unsafe in library. Build script only runs rustc --version. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-hal-nb]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "no_std trait-only crate. No unsafe, no build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.proc-macro-error-attr2]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -44,6 +64,12 @@ criteria = "safe-to-deploy" version = "2.0.1" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.smbus-pec]] +who = "Matteo Tullo " +criteria = "safe-to-deploy" +version = "1.0.1" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.tokio]] who = "Robert Zieba " criteria = "safe-to-run" @@ -117,6 +143,12 @@ criteria = "safe-to-deploy" version = "1.0.0" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.embedded-hal-mock]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.8.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.errno]] who = "Ying Hsu " criteria = "safe-to-run" @@ -199,6 +231,18 @@ criteria = "safe-to-run" version = "0.6.5" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.nb]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.nb]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.1.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.num-traits]] who = "Manish Goregaokar " criteria = "safe-to-deploy"