Module embedded_hal::i2c

source ·
Expand description

Blocking I2C API.

This API supports 7-bit and 10-bit addresses. Traits feature an AddressMode marker type parameter. Two implementation of the AddressMode exist: SevenBitAddress and TenBitAddress.

Through this marker types it is possible to implement each address mode for the traits independently in embedded-hal implementations and device drivers can depend only on the mode that they support.

Additionally, the I2C 10-bit address mode has been developed to be fully backwards compatible with the 7-bit address mode. This allows for a software-emulated 10-bit addressing implementation if the address mode is not supported by the hardware.

Since 7-bit addressing is the mode of the majority of I2C devices, SevenBitAddress has been set as default mode and thus can be omitted if desired.

Bus sharing

I2C allows sharing a single bus between many I2C devices. The SDA and SCL lines are wired in parallel to all devices. When starting a transfer an “address” is sent so that the addressed device can respond and all the others can ignore the transfer.

SDA
SDA
SCL
SCL
MCU
MCU
SDA
SDA
SCL
SCL
I2C DEVICE 1
I2C DEVICE 1
SDA
SDA
SCL
SCL
I2C DEVICE 2
I2C DEVICE 2
Text is not SVG - cannot display

This bus sharing is common when having multiple I2C devices in the same board, since it uses fewer MCU pins (2 instead of 2*n), and fewer MCU I2C peripherals (1 instead of n).

This API supports bus sharing natively. Types implementing I2c are allowed to represent either exclusive or shared access to an I2C bus. HALs typically provide exclusive access implementations. Drivers shouldn’t care which kind they receive, they just do transactions on it and let the underlying implementation share or not.

The embedded-hal-bus crate provides several implementations for sharing I2C buses. You can use them to take an exclusive instance you’ve received from a HAL and “split” it into multiple shared ones, to instantiate several drivers on the same bus.

Flushing

Implementations must flush the transfer, ensuring the bus has returned to an idle state before returning. No pipelining is allowed. Users must be able to shut down the I2C peripheral immediately after a transfer returns, without any risk of e.g. cutting short a stop condition.

(Implementations must wait until the last ACK bit to report it as an error anyway. Therefore pipelining would only yield very small time savings, not worth the complexity)

For driver authors

Drivers can select the adequate address length with I2c<SevenBitAddress> or I2c<TenBitAddress> depending on the target device. If it can use either, the driver can be generic over the address kind as well, though this is rare.

Drivers should take the I2c instance as an argument to new(), and store it in their struct. They should not take &mut I2c, the trait has a blanket impl for all &mut T so taking just I2c ensures the user can still pass a &mut, but is not forced to.

Drivers should not try to enable bus sharing by taking &mut I2c at every method. This is much less ergonomic than owning the I2c, which still allows the user to pass an implementation that does sharing behind the scenes (from embedded-hal-bus, or others).

Device driver compatible only with 7-bit addresses

For demonstration purposes the address mode parameter has been omitted in this example.

use embedded_hal::i2c::{I2c, Error};

const ADDR: u8 = 0x15;
pub struct TemperatureSensorDriver<I2C> {
    i2c: I2C,
}

impl<I2C: I2c> TemperatureSensorDriver<I2C> {
    pub fn new(i2c: I2C) -> Self {
        Self { i2c }
    }

    pub fn read_temperature(&mut self) -> Result<u8, I2C::Error> {
        let mut temp = [0];
        self.i2c.write_read(ADDR, &[TEMP_REGISTER], &mut temp)?;
        Ok(temp[0])
    }
}

Device driver compatible only with 10-bit addresses

use embedded_hal::i2c::{Error, TenBitAddress, I2c};

const ADDR: u16 = 0x158;
pub struct TemperatureSensorDriver<I2C> {
    i2c: I2C,
}

impl<I2C: I2c<TenBitAddress>> TemperatureSensorDriver<I2C> {
    pub fn new(i2c: I2C) -> Self {
        Self { i2c }
    }

    pub fn read_temperature(&mut self) -> Result<u8, I2C::Error> {
        let mut temp = [0];
        self.i2c.write_read(ADDR, &[TEMP_REGISTER], &mut temp)?;
        Ok(temp[0])
    }
}

For HAL authors

HALs should not include bus sharing mechanisms. They should expose a single type representing exclusive ownership over the bus, and let the user use embedded-hal-bus if they want to share it. (One exception is if the underlying platform already supports sharing, such as Linux or some RTOSs.)

Here is an example of an embedded-hal implementation of the I2C trait for both addressing modes. All trait methods have have default implementations in terms of transaction. As such, that is the only method that requires implementation in the HAL.

use embedded_hal::i2c::{self, SevenBitAddress, TenBitAddress, I2c, Operation};

/// I2C0 hardware peripheral which supports both 7-bit and 10-bit addressing.
pub struct I2c0;

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Error {
    // ...
}

impl i2c::Error for Error {
    fn kind(&self) -> i2c::ErrorKind {
        match *self {
            // ...
        }
    }
}

impl i2c::ErrorType for I2c0 {
    type Error = Error;
}

impl I2c<SevenBitAddress> for I2c0 {
    fn transaction(&mut self, address: u8, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
        // ...
    }
}

impl I2c<TenBitAddress> for I2c0 {
    fn transaction(&mut self, address: u16, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
        // ...
    }
}

Enums

Traits

Type Aliases