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; }