Writing an I²C Driver - Hard Version

We're not going to write an entire driver, merely the first step: the hello world of driver writing: reading the device ID of the sensor. This version is labelled hard, because you have to come up with the content of the methods and research information in the embedded-hal and datasheets yourself. You can work in the same file with either version.

i2c-driver/src/icm42670p.rs is a gap text of a very basic I²C IMU sensor driver. The task is to complete the file, so that running main.rs will log the device ID of the driver.

i2c-driver/src/icm42670p_solution.rs provides the solution to this exercise. If you want to run it, the imports need to be changed in main.rs and lib.rs. The imports are already there, you only need to comment the current imports out and uncomment the solutions as marked in the line comments.

Driver API

Instance of the Sensor

✅ Create a struct that represents the sensor. It has two fields, one that represents the sensor's device address and one that represents the I²C bus itself. This is done using traits defined in the embedded-hal crate. The struct is public as it needs to be accessible from outside this crate, but its fields are private.

✅ Implement an instantiating method in the impl block. This method needs to be accessible from outside, so it's labelled pub. The method takes ownership of the I²C bus and creates an instance of the struct you defined earlier.

Device Address

✅ This I²C device has two possible addresses, find them in the datasheet, section 9.3.

🔎 We tell the device which one we want it to use by applying either 0V or 3.3V to the AP_AD0 pin on the device. If we apply 0V, it listens to address 0x68. If we apply 3.3V it listens to address 0x69. You can therefore think of pin AD_AD0 as being a one-bit input which sets the least-significant bit of the device address.

✅ Create an enum that represents both address variants. The values of the variants need to be in binary representation.

Representation of Registers

✅ Create an enum that represents the sensor's registers. Each variant has the register's address as value. For now, you only need the WhoAmI register. Find its address in the datasheet.

✅ Implement a method that exposes the variant's address as u8.

read_register() and write_register()

✅ Check out the write and write_read function in the embedded-hal. Why is it write_read and not just read?

Answer The reason for this lies in the characteristics of the I²C protocol. We first need to write a command over the I²C bus to specify which register we want to read from.

✅ Define a read_register and a write_register method for the sensor instance. Use methods provided by the embedded-hal crate. They serve as helpers for more specific methods and as an abstraction that is adapted to a sensor with 8-bit registers. This means that the data that is written, as well as the data that is read is an unsigned 8-bit integer. Helper methods can remain private as they don't need to be accessible from outside this crate.

✅ Implement a public method that reads the WhoAmI register with the address 0x75. Make use of the above read_register() method.

✅ Optional: Implement further methods that add features to the driver. Check the documentation for the respective registers and their addresses. 💡 Some ideas:

  • Switching the gyroscope sensor or the accelerometer on
  • Starting measurements
  • Reading measurements

🔎 General Info About Peripheral Registers

  • Registers are small amounts of storage, immediately accessible by the processor. The registers on the sensor are 8 bits.
  • They can be accessed by their address
  • You can find register maps in the section 14.
  • Returning a value with MSB and LSB (most significant byte and least significant byte) is done by shifting MSB values, and OR LSB values.
#![allow(unused)]
fn main() {
let GYRO_DATA_X: i16 = ((GYRO_DATA_X1 as i16) << 8) | GYRO_DATA_X0 as i16;
}