LSM9DS1 is a 9 axis motion sensor module that comes with accelerometer, gyroscope, magnetometer, and temperature sensor. A cool thing about this device is that it has I2C and SPI interfaces. In this post, I will show how I supported both interfaces with trait.

Supporting SPI and I2C interfaces

My first idea was to have two different constructors (one for SPI and the other for I2C) and implement the corresponding read and write methods like spi_write(), i2c_write() at the highest level. But, it didn’t sound right. There would be too many duplicates… So, I decided to solve the problem with a generic trait object.

Generic interface

First, I defined a trait named Interface.

/// Interface Trait. `SpiInterface` and `I2cInterface` implement this.
pub trait Interface {
    type Error;

    /// Writes a byte to a sensor's specified register address.
    /// # Arguments
    /// * `sensor` - `Sensor` to talk to
    /// * `addr` - register address
    /// * `value` - value to write
    fn write(&mut self, sensor: Sensor, addr: u8, value: u8) -> Result<(), Self::Error>;
    /// Reads multiple bytes from a sensor's specified register address.
    /// # Arguments
    /// * `sensor` - `Sensor` to talk to
    /// * `addr` - register address
    /// * `buffer` - buffer to store read data
    fn read(&mut self, sensor: Sensor, addr: u8, buffer: &mut [u8]) -> Result<(), Self::Error>;
}

As you can see, there is no real implementation here. This only defines shared behaviors that are meant to be implemented by SPI and I2C types.

Implementing a trait on a type

Now let’s look at SPI and I2C structs. SPI uses chip select pins to address a device to talk to while I2C communicates with a device by specifying its address. So, the structs for SPI and I2C are very different. They take completely different arguments.

Here is how I defined SpiInterface type and its init method.

/// This combines the SPI Interface and chip select pins
pub struct SpiInterface<SPI, AG, M> {
    spi: SPI,
    ag_cs: AG,
    m_cs: M,
}

impl<SPI, AG, M, CommE, PinE> SpiInterface<SPI, AG, M>
where
    SPI: Transfer<u8, Error = CommE> + Write<u8, Error = CommE>,
    AG: OutputPin<Error = PinE>,
    M: OutputPin<Error = PinE>,
{
    /// Initializes an Interface with `SPI` instance and AG and M chip select `OutputPin`s
    /// # Arguments
    /// * `spi` - SPI instance
    /// * `ag_cs` - Chip Select pin for Accelerometer/Gyroscope
    /// * `m_cs` - Chip Select pin for Magnetometer
    pub fn init(spi: SPI, ag_cs: AG, m_cs: M) -> Self {
        Self { spi, ag_cs, m_cs }
    }
}

And here is I2cInterface. It takes I2C addresses.

/// This holds `I2C` and AG and Mag addresses
pub struct I2cInterface<I2C> {
    i2c: I2C,
    ag_addr: u8,
    mag_addr: u8,
}

impl<I2C> I2cInterface<I2C> {
    /// Initializes an Interface with `I2C` instance and AG and Mag addresses
    /// # Arguments
    /// * `i2C` - I2C instance
    /// * `ag_addr` - `AgAddress`: register address for Accelerometer/Gyroscope
    /// * `mag_addr` - `MagAddress`: register address for Magnetometer
    pub fn init(i2c: I2C, ag_addr: AgAddress, mag_addr: MagAddress) -> Self {
        Self {
            i2c,
            ag_addr: ag_addr.addr(),
            mag_addr: mag_addr.addr(),
        }
    }
}

Although the two interfaces are very different, I can make them both Interface by implementing Interface’s write and read methods.

Here is the implementation of Interface  on SpiInterface. You can see how it controls the chip select pins and communicates.

impl<SPI, AG, M, CommE, PinE> Interface for SpiInterface<SPI, AG, M>
where
    SPI: Transfer<u8, Error = CommE> + Write<u8, Error = CommE>,
    AG: OutputPin<Error = PinE>,
    M: OutputPin<Error = PinE>,
{
    type Error = Error<CommE, PinE>;

    fn write(&mut self, sensor: Sensor, addr: u8, value: u8) -> Result<(), Self::Error> {
        let bytes = [addr, value];
        match sensor {
            Accelerometer | Gyro | Temperature => {
                self.ag_cs.set_low().map_err(Error::Pin)?;
                self.spi.write(&bytes).map_err(Error::Comm)?;
                self.ag_cs.set_high().map_err(Error::Pin)?;
            }
            Magnetometer => {
                self.m_cs.set_low().map_err(Error::Pin)?;
                self.spi.write(&bytes).map_err(Error::Comm)?;
                self.m_cs.set_high().map_err(Error::Pin)?;
            }
        }
        Ok(())
    }

    fn read(&mut self, sensor: Sensor, addr: u8, buffer: &mut [u8]) -> Result<(), Self::Error> {
        match sensor {
            Accelerometer | Gyro | Temperature => {
                self.ag_cs.set_low().map_err(Error::Pin)?;
                self.spi.write(&[SPI_READ | addr]).map_err(Error::Comm)?;
                self.spi.transfer(buffer).map_err(Error::Comm)?;
                self.ag_cs.set_high().map_err(Error::Pin)?;
            }
            Magnetometer => {
                self.m_cs.set_low().map_err(Error::Pin)?;
                self.spi
                    .write(&[SPI_READ | MS_BIT | addr])
                    .map_err(Error::Comm)?;
                self.spi.transfer(buffer).map_err(Error::Comm)?;
                self.m_cs.set_high().map_err(Error::Pin)?;
            }
        }
        Ok(())
    }
}

And here is the implementation on I2cInterface. No chip select pins here. This uses an I2C address to specify a sensor.

impl<I2C, CommE> Interface for I2cInterface<I2C>
where
    I2C: WriteRead<Error = CommE> + Write<Error = CommE>,
{
    type Error = Error<CommE>;

    fn write(&mut self, sensor: Sensor, addr: u8, value: u8) -> Result<(), Self::Error> {
        let sensor_addr = match sensor {
            Accelerometer | Gyro | Temperature => self.ag_addr,
            Magnetometer => self.mag_addr,
        };
        core::prelude::v1::Ok(
            self.i2c
                .write(sensor_addr, &[addr, value])
                .map_err(Error::Comm)?,
        )
    }

    fn read(&mut self, sensor: Sensor, addr: u8, buffer: &mut [u8]) -> Result<(), Self::Error> {
        let sensor_addr = match sensor {
            Accelerometer | Gyro | Temperature => self.ag_addr,
            Magnetometer => self.mag_addr,
        };
        core::prelude::v1::Ok(
            self.i2c
                .write_read(sensor_addr, &[addr], buffer)
                .map_err(Error::Comm)?,
        )
    }
}

Init LSM9DS1 driver with generic

Because both SpiInterface and I2cinterface are now Interface, I can init my driver with generic T: Interface instead of a concrete SPI/I2C type.

pub struct LSM9DS1<T>
where
    T: Interface,
{
    interface: T,
    accel: AccelSettings,
    gyro: GyroSettings,
    mag: MagSettings,
}

The fact that it takes Interface means that I can pass either SpiInterface or I2cInterface to instantiate the driver. This way, when I talk to a sensor, I can simply call self.interface.write() or self.interface.read().

For example, here is how to get a temperature measurement. It uses a chosen interface to communicate.

pub fn read_temp(&mut self) -> Result<f32, T::Error> {
    let mut bytes = [0u8; 2];
    self.interface.read(
        Sensor::Accelerometer,
        register::AG::OUT_TEMP_L.addr(),
        &mut bytes,
    )?;
    let result: i16 = (bytes[1] as i16) << 8 | bytes[0] as i16;
    Ok((result as f32) / TEMP_SCALE + TEMP_BIAS)
}

Once I pass the interface to LSM9DS1 driver, I don’t need to worry about if uses SPI or I2C. There is no need to implement separate methods for SPI and I2C. Pretty cool.

Here is the GitHub repo if you are interested.

One more thing…

I found this trait technique really powerful. As an experiment, I also made a fake interface that could be used for testing. This struct implements Interface and has arrays that mimic registers. I can read and write these fake registers through the APIs.

In the end, I did testing at lower levels and didn’t really need this interface. But, I learned a lot in the process. I feel much more comfortable with trait now.

//! Fake Interface for unit tests
use super::Interface;
use super::Sensor;
use Sensor::*;

/// Errors in this crate
#[derive(Debug)]
pub enum Error {
    /// Communication error
    Invalid,
}

/// This holds fake registers
pub struct FakeInterface {
    ag_registers: [u8; 256],
    mag_registers: [u8; 256],
}

impl Default for FakeInterface {
    fn default() -> Self {
        FakeInterface {
            ag_registers: [0u8; 256],
            mag_registers: [0u8; 256],
        }
    }
}

impl FakeInterface {
    /// create a fake interface
    pub fn new() -> Self {
        Default::default()
    }
}

/// Implementation of `Interface`
impl Interface for FakeInterface {
    type Error = Error;

    fn write(&mut self, sensor: Sensor, addr: u8, value: u8) -> Result<(), Self::Error> {
        match sensor {
            Accelerometer | Gyro | Temperature => self.ag_registers[addr as usize] = value,
            Magnetometer => self.mag_registers[addr as usize] = value,
        }
        Ok(())
    }

    fn read(&mut self, sensor: Sensor, addr: u8, buffer: &mut [u8]) -> Result<(), Self::Error> {
        let registers = match sensor {
            Accelerometer | Gyro | Temperature => self.ag_registers,
            Magnetometer => self.mag_registers,
        };
        for i in 0..buffer.len() {
            buffer[i] = registers[(addr as usize) + i];
        }
        Ok(())
    }
}