Skip to content

Add SMBus HAL, dependent on embedded-hal-async::I2c#7

Open
tullom wants to merge 5 commits into
OpenDevicePartnership:mainfrom
tullom:smbus-hal
Open

Add SMBus HAL, dependent on embedded-hal-async::I2c#7
tullom wants to merge 5 commits into
OpenDevicePartnership:mainfrom
tullom:smbus-hal

Conversation

@tullom
Copy link
Copy Markdown

@tullom tullom commented Nov 24, 2025

Add SMBus support in the form of a helper trait built on top of an embedded-hal-async I2C implementation.

Based on the SMBus v3,3 spec.

@tullom tullom self-assigned this Nov 24, 2025
@tullom tullom requested a review from a team as a code owner November 24, 2025 10:05
@tullom tullom added the enhancement New feature or request label Nov 24, 2025
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Nov 24, 2025

Cargo Vet Audit Passed

cargo vet has passed in this PR. No new unvetted dependencies were found.

@jeffglaum jeffglaum moved this to In review in ODP Backlog Nov 24, 2025
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
@tullom tullom requested a review from RobertZ2011 November 24, 2025 19:32
@tullom tullom requested a review from a team as a code owner November 24, 2025 19:37
@tullom tullom requested review from felipebalbi and kurtjd November 25, 2025 05:09
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
kurtjd
kurtjd previously approved these changes Nov 25, 2025
Copy link
Copy Markdown

@kurtjd kurtjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Robert's comments are a good idea but otherwise looks good.

Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
@jerrysxie
Copy link
Copy Markdown
Contributor

@tullom This looks fine to me as long as some of Robert's comments are addressed. I would like to see a demonstration of this on one of the charger/fuel gage driver. Put it in action is good way to iron out the kinks.

Non-blocking: Another consideration would be to use embedded-hal i2c mock to unit test some of the default implementation. Please create an issue for this.

@tullom tullom moved this from In review to Backlog in ODP Backlog Apr 7, 2026
Copilot AI review requested due to automatic review settings May 12, 2026 18:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 8 changed files in this pull request and generated 9 comments.

Comments suppressed due to low confidence (4)

embedded-mcu-hal/src/smbus/bus/asynch.rs:354

  • finalize_pec_byte uses try_into() which fails if the hasher's finish() value doesn't fit into u8. Earlier docs state that the PEC is the truncated low byte of finish(), so this should explicitly truncate/mask instead of erroring on larger u64 values.
    /// Truncate a finished PEC value to its low byte.
    fn finalize_pec_byte(pec: u64) -> Result<u8, crate::smbus::bus::ErrorKind> {
        pec.try_into().map_err(|_| crate::smbus::bus::ErrorKind::Pec)
    }

embedded-mcu-hal/src/smbus/bus/asynch.rs:629

  • block_write is implemented as a transaction with multiple Operation::Write steps (register, length, payload, PEC). This will generally insert repeated STARTs/address bytes between chunks, which is not equivalent to an SMBus block write frame and will also break PEC calculation. Build a single contiguous write buffer for the whole write phase (register + len + data [+ pec]) and send it as one write op.
            self.i2c
                .transaction(
                    address,
                    &mut [
                        Operation::Write(&[register]),
                        Operation::Write(&[data.len() as u8]),
                        Operation::Write(data),
                        Operation::Write(&[pec]),
                    ],
                )

embedded-mcu-hal/src/smbus/bus/asynch.rs:725

  • block_write_block_read_process_call is executed as many separate write/read operations inside a single transaction. This will generally introduce extra repeated STARTs/address bytes between operations, changing the SMBus wire sequence and PEC inputs (similar to process_call and block_read). Restructure to a single contiguous write op for the write phase and a single contiguous read op for the read phase (then parse length/data/pec).
            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),
                    ],
                )

embedded-mcu-hal/src/smbus/bus/mod.rs:160

  • The ErrorType trait is documented as "I2C error type trait" even though it is defining the error type for the SMBus traits in this module. Updating the wording will avoid confusion for HAL/driver authors implementing this API.
/// I2C 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<embedded_hal_async::i2c::ErrorKind>;
}

Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/asynch/mod.rs
Comment thread embedded-mcu-hal/src/smbus/bus/asynch/mod.rs
Comment thread embedded-mcu-hal/src/smbus/bus/asynch/mod.rs
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/asynch/mod.rs
Comment thread embedded-mcu-hal/src/smbus/bus/mod.rs
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
RobertZ2011
RobertZ2011 previously approved these changes May 14, 2026
Copy link
Copy Markdown

@RobertZ2011 RobertZ2011 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to add on top of the existing comments.

kurtjd
kurtjd previously approved these changes May 14, 2026
Copy link
Copy Markdown

@kurtjd kurtjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good (though Copilot suggestions seem like they might be valid).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 8 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (7)

embedded-mcu-hal/src/smbus/bus/asynch.rs:22

  • SMBusError and I2cError are imported but not referenced anywhere in this module (search only finds them in the use lines). Please remove these unused imports to avoid warnings and keep the module tidy.
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};

embedded-mcu-hal/src/smbus/bus/asynch.rs:78

  • check_pec claims to compare only the low byte of computed_pec, but the current implementation compares the full u64 against received_pec. This will reject valid PECs whenever the hasher's finish() contains non-zero high bytes; compare against (computed_pec as u8) (or computed_pec & 0xFF) instead.
    /// 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<(), <Self as crate::smbus::bus::ErrorType>::Error> {
        computed_pec
            .eq(&received_pec.into())
            .then_some(())
            .ok_or_else(|| <Self as crate::smbus::bus::ErrorType>::Error::to_kind(crate::smbus::bus::ErrorKind::Pec))
    }

embedded-mcu-hal/src/smbus/bus/asynch.rs:354

  • finalize_pec_byte currently uses try_into() from u64 to u8, which errors if the hasher’s finish() isn’t already <= 255. The module/docs describe PEC as the truncated low byte of finish(), so this should mask/cast to the low byte rather than failing.
    /// Truncate a finished PEC value to its low byte.
    fn finalize_pec_byte(pec: u64) -> Result<u8, crate::smbus::bus::ErrorKind> {
        pec.try_into().map_err(|_| crate::smbus::bus::ErrorKind::Pec)
    }

embedded-mcu-hal/src/smbus/bus/asynch.rs:404

  • When use_pec is true, read_buf seeds the PEC with the write address byte (addr<<1). For pure-read SMBus operations (e.g., Receive Byte), the on-the-wire address byte has the read bit set, so PEC should be seeded with read_address_byte(address) (and, if applicable, include the write address only when there was an actual write phase). As written, this will compute a different PEC than targets expect.
    /// 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.
    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_write_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 as Smbus>::check_pec(*pec_byte, pec.finish())?;

embedded-mcu-hal/src/smbus/bus/asynch.rs:692

  • block_read reads the reported msg_size byte but then unconditionally reads data.len() bytes and computes PEC over the entire data buffer. This ignores the SMBus length field and can over-read/under-read the bus (and validate PEC over the wrong byte sequence). Please validate msg_size[0] against the provided buffer and only read/hash the indicated number of bytes (or return the actual length).
        let mut msg_size = [0u8];
        if use_pec {
            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()))?;
            pec.write(&msg_size);
            pec.write(data);
            Self::check_pec(pec_buf[0], pec.finish())?;
        } else {
            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()))?;
        }
        Ok(())

embedded-mcu-hal/src/smbus/bus/asynch.rs:745

  • block_write_block_read_process_call reads the target-provided read_msg_size but then always reads read_data.len() bytes and includes the full buffer in PEC. This can desynchronize the transaction if the device reports a different length, and it makes PEC validation incorrect. Please validate and only read/hash the reported number of bytes (or change the API to return the actual length).
        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];
        if use_pec {
            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()))?;
            pec.write(&read_msg_size);
            pec.write(read_data);
            Self::check_pec(pec_buf[0], pec.finish())?;
        } else {
            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()))?;
        }

embedded-mcu-hal/src/smbus/bus/mod.rs:160

  • This section is labeled “I2C error type trait”, but it defines the associated error type for SMBus traits (and even depends on crate::smbus::bus::ErrorKind). Please rename/reword the doc comments to SMBus here to match the module and avoid confusion.
/// I2C 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<embedded_hal_async::i2c::ErrorKind>;
}

Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/mod.rs
Comment thread embedded-mcu-hal/src/lib.rs
@tullom tullom dismissed stale reviews from kurtjd and RobertZ2011 via 70603ae May 14, 2026 23:38
tullom added 4 commits May 14, 2026 16:41
Split the async SMBus surface into an abstract protocol trait and a
concrete software implementation.

The `Smbus` trait now declares only the SMBus protocol operations plus
the PEC associated items (`PecCalc`, `get_pec_calc`, `check_pec`). All
default implementations that bit-bang the protocol on top of an I²C bus have moved into a new `SwSmbusI2c<I, P>` wrapper struct that owns an `embedded_hal_async::i2c::I2c` bus and delegates PEC calculator
construction to a new `PecProvider` trait via the `P` parameter. HALs
with a hardware SMBus peripheral can implement `Smbus` directly without inheriting any I²C-specific machinery.

`write_buf`, `read_buf`, and `write_read_buf` are no longer trait
methods; they are inherent helpers on `SwSmbusI2c` that the other
protocol methods reuse, eliminating duplicated PEC setup and I²C error
mapping across the byte/word/block operations. PEC calculator priming
with the write-address byte and the truncation of the finished hash to
a single byte are likewise factored into private helpers
(`pec_calc_with_write_addr`, `finalize_pec_byte`).

The blanket `impl<T: Smbus + ?Sized> Smbus for &mut T` forwarding impl
is preserved, and `SwSmbusI2c::Error` is `ErrorKind` directly so callers
do not need to define a wrapper error type. Tests are reworked to drive
`SwSmbusI2c<I2cMock, _>` through small `PecProvider` impls (`TestPec`,
`NoPec`).

Assisted-by: GitHub Copilot:claude-opus-4.7
- Renamed `to_kind` method to `from_kind` in the `Error` trait for clarity.
- Updated the `ErrorKind` enum to include a new variant `BlockSizeMismatch`.
- Made API more explicit by breaking out the pec and non pec SMBUS transactions into their own methods.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 8 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

embedded-mcu-hal/src/smbus/bus/asynch.rs:22

  • SMBusError and I2cError are imported but not used anywhere in this module (only appear in the use lines). Please remove these unused imports to avoid warnings and keep the module tidy.
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};

embedded-mcu-hal/src/smbus/bus/asynch.rs:78

  • check_pec's doc comment says only the low byte of computed_pec is used, but the implementation currently compares the full u64 against received_pec (after widening). Either update the docs to describe the strict comparison semantics, or change the implementation to explicitly truncate/mask to the low byte so the behavior matches the docs.
    /// 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<(), <Self as crate::smbus::bus::ErrorType>::Error> {
        computed_pec
            .eq(&received_pec.into())
            .then_some(())
            .ok_or_else(|| <Self as crate::smbus::bus::ErrorType>::Error::from_kind(crate::smbus::bus::ErrorKind::Pec))
    }

embedded-mcu-hal/src/smbus/bus/mod.rs:97

  • unreachable!() is used here to implement from_kind for Infallible. With workspace clippy lints set to deny(panic) and deny(unreachable), this is likely to fail CI (since unreachable!() is a form of panic). Consider using a non-panicking diverging implementation (e.g., an infinite loop with core::hint::unreachable_unchecked() only if acceptable) or explicitly allow the clippy::panic lint in this exact scope.
    fn from_kind(_kind: ErrorKind) -> Self {
        // `Infallible` is uninhabited, so this function can never actually
        // be called
        #[allow(clippy::unreachable)]
        {
            unreachable!()
        }

Comment thread embedded-mcu-hal/src/smbus/bus/asynch.rs Outdated
Comment thread embedded-mcu-hal/src/smbus/bus/mod.rs
@tullom tullom enabled auto-merge (squash) May 15, 2026 00:08
Copy link
Copy Markdown
Contributor

@jerrysxie jerrysxie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it has been a while since I last reviewed this. So this is basically a new review.

/// 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<(), <Self as crate::smbus::bus::ErrorType>::Error> {
computed_pec
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

computed_pec is u64 but received_pec is u8. We are converting received_pec into u64 to compare it with computed_pec. Is the hasher guaranteed to return zeros in for bit 63 to bit 8?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEC is CRC8, no? Why u64 at all?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its because I define that the CRC calculator must implement core::hash::Hasher, which outputs its final hash in the form of u64. Most hashing crates implement this trait. The hasher should be guaranteed to return zeros for bit 63 to bit 8, and i check for this in the implementation by promoting the u8 to a u64.

use core::hash::Hasher;
use core::marker::PhantomData;

use crate::smbus::bus::Error as SMBusError;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import seems to be not needed now.

Copy link
Copy Markdown
Author

@tullom tullom May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&mut self,
address: u8,
register: u8,
data: &mut [u8],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For SMBus varliable data length responses, is the assumption here that the buffer passed in is always big enough for the largest data payload? The client has to fulfill this guanrantee?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For block reads, the size of the response is actually encoded in the response itself as byte 1 (zero indexed). The spec also defines an upper size limit of 64 bytes. So yes, the client needs to fulfill this guarantee. I2C::ErrorKind, which is encoded in an SMBus error, has an Overrun error kind that the client can use to indicate this error.

&mut self,
address: u8,
use_pec: bool,
operations: &mut [u8],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider cases where a reference to an array of size 0 is passed in all of our API where it takes a reference to an array.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically for this method, write_buf(), it is not a public api method, but rather a convenience method, and every use case of it makes it impossible to pass in an array of size zero. As for the other methods that take in a reference to an array, the logic is correct according to the spec. A size 0 block write is valid in SMBus v3 and above

Comment on lines +90 to +95
/// Send Byte with PEC.
async fn send_byte_with_pec(
&mut self,
address: u8,
byte: u8,
) -> Result<(), <Self as crate::smbus::bus::ErrorType>::Error>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, my bad, I should have suggested before that the _with_pec() variants can easily get a default implementation built on top of the non-pec variants.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, should i revert to the way it was previously? im already using a convenience method to make the code repeatable and i dont think its needed to revert.

@tullom tullom requested review from felipebalbi and jerrysxie May 18, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cargo vet enhancement New feature or request

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

7 participants