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..5bf7fe1 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" +smbus-pec = { version = "1.0", default-features = false } [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", +] } [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..7b1178d --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/asynch/mod.rs @@ -0,0 +1,910 @@ +//! Async SMBus controller trait and software implementation. +//! +//! This module defines the [`Smbus`] async controller trait describing the +//! SMBus protocol surface. The trait declares each SMBus protocol +//! transaction as a required method; PEC-capable transactions appear +//! twice, once as a plain variant and once as a `*_with_pec` variant +//! that handles PEC computation or verification. +//! +//! Concrete bit-banging of the protocol on top of an +//! [`embedded_hal_async::i2c::I2c`] bus is provided by [`SwSmbusI2c`], +//! which uses the [`smbus_pec`] crate to compute and verify PEC bytes. +//! HAL authors with a hardware SMBus peripheral may instead implement +//! [`Smbus`] directly and handle PEC however the peripheral supports it. +//! +//! See the [parent module](super) for the protocol overview, PEC handling, +//! and driver/HAL guidance. + +use core::hash::Hasher; + +use embedded_hal_async::i2c::{Error as I2cError, I2c, Operation}; + +/// Async SMBus controller trait. +/// +/// Declares the SMBus protocol surface. Each PEC-capable transaction is +/// exposed twice: a plain method that issues the transaction with no PEC +/// byte on the wire, and a `*_with_pec` method that appends a PEC byte on +/// writes or verifies the trailing PEC byte on reads. Implementations +/// that do not support PEC should return +/// [`ErrorKind::PecNotAvailable`](crate::smbus::bus::ErrorKind::PecNotAvailable) +/// from every `*_with_pec` method while still servicing the plain +/// variants. +/// +/// 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 { + /// 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. + /// + /// The `data` slice must be sized to exactly match the number of bytes + /// the peripheral is expected to send back. The implementation reads + /// the device-reported byte count and rejects the transfer with + /// [`ErrorKind::BlockSizeMismatch`](crate::smbus::bus::ErrorKind::BlockSizeMismatch) + /// if it does not equal `data.len()`. + async fn block_read( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), ::Error>; + + /// Block Read with PEC. + /// + /// The `data` slice must be sized to exactly match the number of bytes + /// the peripheral is expected to send back. The implementation reads + /// the device-reported byte count and rejects the transfer with + /// [`ErrorKind::BlockSizeMismatch`](crate::smbus::bus::ErrorKind::BlockSizeMismatch) + /// if it does not equal `data.len()`. The size check runs before PEC + /// verification. + async fn block_read_with_pec( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), ::Error>; + + /// Block Write / Block Read / Process Call. + /// + /// The `read_data` slice must be sized to exactly match the number of + /// bytes the peripheral is expected to send back. The implementation + /// reads the device-reported byte count and rejects the transfer with + /// [`ErrorKind::BlockSizeMismatch`](crate::smbus::bus::ErrorKind::BlockSizeMismatch) + /// if it does not equal `read_data.len()`. + 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. + /// + /// The `read_data` slice must be sized to exactly match the number of + /// bytes the peripheral is expected to send back. The implementation + /// reads the device-reported byte count and rejects the transfer with + /// [`ErrorKind::BlockSizeMismatch`](crate::smbus::bus::ErrorKind::BlockSizeMismatch) + /// if it does not equal `read_data.len()`. The size check runs before + /// PEC verification. + 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 { + #[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 computation +/// and verification are provided by the [`smbus_pec`] crate. +pub struct SwSmbusI2c { + i2c: I, +} + +impl SwSmbusI2c { + /// Wrap an I²C bus to form a software SMBus controller. + #[inline] + pub const fn new(i2c: I) -> Self { + Self { i2c } + } + + /// 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 { + /// Obtain a fresh PEC calculator pre-fed with the write-address byte. + fn pec_calc_with_write_addr(address: u8) -> smbus_pec::Pec { + let mut pec = smbus_pec::Pec::new(); + pec.write_u8(crate::smbus::bus::write_address_byte(address)); + pec + } + + /// Obtain a fresh PEC calculator pre-fed with the read-address byte. + /// 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) -> smbus_pec::Pec { + let mut pec = smbus_pec::Pec::new(); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + pec + } + + /// Compare 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<(), crate::smbus::bus::ErrorKind> { + if computed_pec as u8 == received_pec { + Ok(()) + } else { + Err(crate::smbus::bus::ErrorKind::Pec) + } + } +} + +impl SwSmbusI2c +where + I: I2c, +{ + /// 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 = pec.finish() as u8; + } + 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); + Self::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, prepare the calculator before touching + // the bus. + 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); + Self::check_pec(*pec_byte, pec.finish())?; + } + Ok(()) + } +} + +impl Smbus for SwSmbusI2c +where + I: I2c, +{ + #[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 = pec.finish() as u8; + 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( + usize::from(msg_size[0]), + data.len(), + )); + } + 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( + usize::from(msg_size[0]), + data.len(), + )); + } + 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( + usize::from(read_msg_size[0]), + read_data.len(), + )); + } + 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( + usize::from(read_msg_size[0]), + read_data.len(), + )); + } + 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..18364bb --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/asynch/tests.rs @@ -0,0 +1,676 @@ +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) +} + +type TestBus = 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::PecNotAvailable, + ErrorKind::TooLargeBlockTransaction, + ErrorKind::BlockSizeMismatch(2, 3), + ErrorKind::Other, + ] { + let s = std::format!("{}", k); + assert!(!s.is_empty()); + } + // The Display impl for `BlockSizeMismatch` must surface both the + // received byte count and the caller's expected buffer length. + let s = std::format!("{}", ErrorKind::BlockSizeMismatch(2, 5)); + assert!(s.contains('2')); + assert!(s.contains('5')); +} + +#[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); +} + +// ---------- 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(2, 3)); + 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(2, 3)); + 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(1, 2)); + 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(1, 2)); + done(bus); +} + +// ---------- &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(); + done(bus); +} + +// ---------- inner / into_inner accessors ---------- + +#[tokio::test] +async fn inner_accessors() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x01])]); + // `inner` returns a shared reference; just touch it. + let _: &I2cMock = bus.inner(); + bus.send_byte(ADDR, 0x01).await.unwrap(); + // `into_inner` returns ownership of the wrapped bus; `done()` proves + // the same I2c mock is returned. + let mut inner = bus.into_inner(); + inner.done(); +} + +// ---------- `*_with_pec` too-large guards ---------- + +#[tokio::test] +async fn block_write_with_pec_too_large() { + let mut bus = new_bus(&[]); + let data = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_write_with_pec(ADDR, REG, &data).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +#[tokio::test] +async fn block_read_with_pec_too_large() { + let mut bus = new_bus(&[]); + let mut buf = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +#[tokio::test] +async fn bwbr_with_pec_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_with_pec(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +// ---------- ErrorKind::from_kind round trip ---------- + +#[test] +fn error_kind_from_kind_round_trip() { + for k in [ + ErrorKind::I2c(I2cErrorKind::Bus), + ErrorKind::Timeout, + ErrorKind::Pec, + ErrorKind::PecNotAvailable, + ErrorKind::TooLargeBlockTransaction, + ErrorKind::BlockSizeMismatch(1, 2), + ErrorKind::Other, + ] { + assert_eq!(::from_kind(k), k); + assert_eq!(::kind(&k), k); + } +} + +// ---------- 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..17af26f --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/mod.rs @@ -0,0 +1,195 @@ +//! 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 +//! +//! Every PEC-capable SMBus protocol transaction is exposed as two +//! methods: a plain variant (e.g. [`asynch::Smbus::write_byte`]) that +//! issues the transaction with no PEC byte on the wire, and a +//! `*_with_pec` variant (e.g. [`asynch::Smbus::write_byte_with_pec`]) +//! that appends a PEC byte on writes and verifies the trailing PEC byte +//! on reads. +//! +//! The PEC is computed over the bytes that appear on the wire (address, +//! register, payload, …) in bus order. Implementations are free to +//! produce the PEC however they wish — by software computation, a +//! hardware peripheral, or a third-party crate. The software +//! implementation [`asynch::SwSmbusI2c`] uses the [`smbus_pec`] crate. +//! +//! Implementations that cannot support PEC should return +//! [`ErrorKind::PecNotAvailable`] from every `*_with_pec` method while +//! still servicing the non-PEC variants normally. +//! +//! # 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`]. +//! +//! - Block reads and the read leg of the block write/read process call +//! verify that the device-reported byte count matches the caller's +//! expected length and return [`ErrorKind::BlockSizeMismatch`] +//! otherwise. The size check runs before any PEC verification. +//! +//! - 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 +//! [`smbus_pec`]: https://docs.rs/smbus-pec + +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, + /// A PEC-requiring operation was invoked on an implementation that + /// does not support PEC. + PecNotAvailable, + /// 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. Format is (recvd byte count, buffer length). + BlockSizeMismatch(usize, usize), + /// 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::PecNotAvailable => write!( + f, + "PEC was requested but this implementation does not support PEC." + ), + Self::TooLargeBlockTransaction => write!( + f, + "Block read/write transfer size too large, at most 255 bytes can be read/written at once." + ), + Self::BlockSizeMismatch(byte_count, buf_len) => write!( + f, + "Block read returned a byte count ({byte_count}) that did not match the caller's expected buffer length ({buf_len})." + ), + 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"