LSM9DS1: Using a Trait to Support SPI and I2C Interfaces
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 struct
s. SPI uses chip select pins to address a device to talk to while I2C communicates with a device by specifying its address. So, the struct
s 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(())
}
}