Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Implementing RegisterInterface and AsyncRegisterInterface

At this point we have generated code, but it still has no idea how to reach real hardware. That is our job.

The generated Inner type wants a tiny transport object that knows how to read and write a register by address. For TMP102 that transport is just I2C plus the selected device address.

TMP102 does not live at an arbitrary address. Datasheet table 6-4 gives us four legal values, so we should model exactly those four values.

/// Logic level wired onto the TMP102 A0 pin.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum A0 {
    /// A0 tied to ground (`0x48`).
    #[default]
    Gnd,
    /// A0 tied to V+ (`0x49`).
    Vplus,
    /// A0 tied to SDA (`0x4a`).
    Sda,
    /// A0 tied to SCL (`0x4b`).
    Scl,
}

impl From<A0> for u8 {
    fn from(a0: A0) -> Self {
        match a0 {
            A0::Gnd => 0x48,
            A0::Vplus => 0x49,
            A0::Sda => 0x4a,
            A0::Scl => 0x4b,
        }
    }
}

That alone eliminates a whole class of mistakes. The caller can no longer accidentally pass 0x52 and then wonder why the driver never sees an ACK.

Wrap the bus

Now create the transport type that the generated layer will sit on top of:

use embedded_hal::i2c::I2c;
use embedded_hal_async::i2c::I2c as AsyncI2c;

struct Interface<I2C> {
    i2c: I2C,
    addr: u8,
}

impl<I2C> Interface<I2C> {
    fn new(i2c: I2C, a0: A0) -> Self {
        Self {
            i2c,
            addr: a0.into(),
        }
    }
}

The job of Interface is deliberately boring: take register operations from generated code and translate them into real I2C transactions.

Blocking register access

For blocking embedded-hal, that translation looks like this:

use device_driver::RegisterInterface;
use embedded_hal::i2c::{Error, ErrorKind, I2c};

impl<I2C: I2c> RegisterInterface for Interface<I2C> {
    type Error = ErrorKind;
    type AddressType = u8;

    fn write_register(
        &mut self,
        address: Self::AddressType,
        _size_bits: u32,
        data: &[u8],
    ) -> Result<(), Self::Error> {
        let mut buf = [0u8; 3];
        buf[0] = address;
        buf[1..].copy_from_slice(data);

        self.i2c.write(self.addr, &buf).map_err(|e| e.kind())
    }

    fn read_register(
        &mut self,
        address: Self::AddressType,
        _size_bits: u32,
        data: &mut [u8],
    ) -> Result<(), Self::Error> {
        self.i2c
            .write_read(self.addr, &[address], data)
            .map_err(|e| e.kind())
    }
}

TMP102 keeps this pleasantly simple: one pointer byte, then two bytes of payload.

Note

The fixed [u8; 3] buffer is TMP102-specific. For a bigger device with wider registers, size the stack buffer to your largest write or switch to a small growable buffer type.

Async register access

The async version is the same idea with .await in the obvious places:

use device_driver::AsyncRegisterInterface;
use embedded_hal_async::i2c::{Error, ErrorKind, I2c as AsyncI2c};

impl<I2C: AsyncI2c> AsyncRegisterInterface for Interface<I2C> {
    type Error = ErrorKind;
    type AddressType = u8;

    async fn write_register(
        &mut self,
        address: Self::AddressType,
        _size_bits: u32,
        data: &[u8],
    ) -> Result<(), Self::Error> {
        let mut buf = [0u8; 3];
        buf[0] = address;
        buf[1..].copy_from_slice(data);

        self.i2c.write(self.addr, &buf).await.map_err(|e| e.kind())
    }

    async fn read_register(
        &mut self,
        address: Self::AddressType,
        _size_bits: u32,
        data: &mut [u8],
    ) -> Result<(), Self::Error> {
        self.i2c
            .write_read(self.addr, &[address], data)
            .await
            .map_err(|e| e.kind())
    }
}

So far, so good. The generated code can finally talk to the sensor.

Put the generated layer behind a real driver type

If you stop here and run cargo build, you will see the same warnings as in the original draft: fields and methods in Inner are “never used”. That is the compiler telling us something true: we generated a low-level API, but we still have not wrapped it in a driver humans will actually call.

A minimal wrapper is enough to make those warnings go away and give the chapter a clean place to keep growing:

mod inner;

use inner::Inner;

pub struct Tmp102<I2C> {
    inner: Inner<Interface<I2C>>,
    extended_mode: bool,
}

impl<I2C> Tmp102<I2C> {
    pub fn new(i2c: I2C, a0: A0) -> Self {
        Self {
            inner: Inner::new(Interface::new(i2c, a0)),
            extended_mode: false,
        }
    }
}

impl<I2C: I2c> Tmp102<I2C> {
    pub fn raw_temperature_register(&mut self) -> Result<[u8; 2], ErrorKind> {
        Ok(self.inner.temperature().read()?.into())
    }

    pub fn configure_shutdown_bit(&mut self, shutdown: bool) -> Result<(), ErrorKind> {
        self.inner.configuration().modify(|reg| {
            reg.set_sd(if shutdown {
                ShutdownMode::PowerOff
            } else {
                ShutdownMode::Running
            })
        })
    }

    pub fn set_low_limit_raw(&mut self, raw: [u8; 2]) -> Result<(), ErrorKind> {
        self.inner.tlow().write(|reg| *reg = raw.into())
    }

    pub fn set_high_limit_raw(&mut self, raw: [u8; 2]) -> Result<(), ErrorKind> {
        self.inner.thigh().write(|reg| *reg = raw.into())
    }
}

impl<I2C: AsyncI2c> Tmp102<I2C> {
    pub async fn raw_temperature_register_async(&mut self) -> Result<[u8; 2], ErrorKind> {
        Ok(self.inner.temperature().read_async().await?.into())
    }
}

Now inner.temperature(), inner.configuration(), inner.tlow(), and inner.thigh() are all exercised through Tmp102, so those dead-code warnings disappear for the right reason: the generated layer is no longer orphaned.

This wrapper is still too raw for real use, but that is fine. The next step is where the driver starts feeling like a crate you would actually publish.