-
Notifications
You must be signed in to change notification settings - Fork 1
To make our implementations of sensors board-agnostic (i.e., they should work regardless of what board we use them on), we cannot use the embassy_stm32
crate since this requires us to enable exactly one board feature.
Instead, we create our own traits to represent different IO protocols. These traits can then be implemented for a particular board (typically a very short piece of code) when required.
This means that sensor implementations are defined for a struct that implements the trait for the protocol that they require, so have no dependency on embassy_stm32
.
This has the added benefit of making it easy to create mock IO implementations which allow us to write tests suites to check that the behaviour of our sensor implementation is correct for particular inputs.
So far, we have the following traits defined:
-
HypedI2c
- for I2C -
HypedGpioPin
- for GPIO, we also defineMockGpio
which can be used for testing
For example, the HypedI2c
trait looks like this at time of writing:
pub trait HypedI2c {
fn read_byte(&mut self, device_address: u8, register_address: u8) -> Option<u8>;
fn write_byte_to_register(
&mut self,
device_address: u8,
register_address: u8,
data: u8,
) -> Result<(), I2cError>;
}
As you can see, we are provided with two functions: read_byte
and write_byte_to_register
. These are probably the only two functions an I2C sensor implementation will require, but if it isn't then you can add a new function to it.
Note that by adding a new function to this trait, you will have to create a hardware implementation for every board that currently has an I2C struct which implements
HypedI2c
.
Once we have our traits defined, we can implement them for a particular board.
We have the following hardware implementations of HypedI2c
and HypedGpioPin
for the STM32L476 board (found in boards/stm32l476/src/io
):
-
Stm32l476rgGpio
- A GPIO implementation for the STM32L476 board, which implements theHypedGpioPin
trait. It works by taking in an input pin fromembassy_stm32
and mapping its API to the one defined by theHypedGpioPin
trait. -
Stm32l476rgI2c
- An I2C implementation for the STM32L476 board, which implements theHypedI2c
trait. It works by taking in anembassy_stm32::i2c:I2c
struct and mapping its API to the one defined by theHypedI2c
trait.
An example hardware implementation is Stm32l476rgI2c
, which looks like this at the time of writing:
use embassy_stm32::{i2c::I2c, mode::Blocking};
use hyped_io::i2c::{HypedI2c, I2cError};
pub struct Stm32l476rgI2c<'d> {
i2c: I2c<'d, Blocking>,
}
impl<'d> HypedI2c for Stm32l476rgI2c<'d> {
/// Read a byte from a register on a device
fn read_byte(&mut self, device_address: u8, register_address: u8) -> Option<u8> {
let mut read = [0];
let result =
self.i2c
.blocking_write_read(device_address, [register_address].as_ref(), &mut read);
match result {
Ok(_) => Some(read[0]),
Err(_) => None,
}
}
/// Write a byte to a register on a device
fn write_byte_to_register(
&mut self,
device_address: u8,
register_address: u8,
data: u8,
) -> Result<(), I2cError> {
let result = self
.i2c
.blocking_write(device_address, [register_address, data].as_ref());
match result {
Ok(_) => Ok(()),
Err(e) => Err(match e {
embassy_stm32::i2c::Error::Bus => I2cError::Bus,
embassy_stm32::i2c::Error::Arbitration => I2cError::Arbitration,
embassy_stm32::i2c::Error::Nack => I2cError::Nack,
embassy_stm32::i2c::Error::Timeout => I2cError::Timeout,
embassy_stm32::i2c::Error::Crc => I2cError::Crc,
embassy_stm32::i2c::Error::Overrun => I2cError::Overrun,
embassy_stm32::i2c::Error::ZeroLengthTransfer => I2cError::ZeroLengthTransfer,
}),
}
}
}
impl<'d> Stm32l476rgI2c<'d> {
/// Create a new instance of our I2C implementation for the STM32L476RG
pub fn new(i2c: I2c<'d, Blocking>) -> Self {
Self { i2c }
}
}
This can be broken up into the following:
We define a struct to hold anything required for our implementation, typically the embassy_stm32
struct for whatever sensor we are implementing:
pub struct Stm32l476rgI2c<'d> {
i2c: I2c<'d, Blocking>,
}
Here we are taking in a blocking version of embassy_stm32::i2c::I2c
.
We can then implement the IO trait by using impl _ for _
which tells Rust that our struct will have the functions from the IO trait implemented on it, which Rust will guarantee at compile time.
impl<'d> HypedI2c for Stm32l476rgI2c<'d> {
/// Read a byte from a register on a device
fn read_byte(&mut self, device_address: u8, register_address: u8) -> Option<u8> {
...
}
/// Write a byte to a register on a device
fn write_byte_to_register(
&mut self,
device_address: u8,
register_address: u8,
data: u8,
) -> Result<(), I2cError> {
...
}
}
The last step is to define a function for creating an instance of our struct.
impl<'d> Stm32l476rgI2c<'d> {
/// Create a new instance of our I2C implementation for the STM32L476RG
pub fn new(i2c: I2c<'d, Blocking>) -> Self {
Self { i2c }
}
}
In this example, our new
function takes in an embassy_stm32::i2c::I2c
struct.
Note that we are not passing in a reference, instead we are taking ownership of this IO