Exercise B: Using a Driver Crate
~25 min
You've found devices on the I2C bus. Now let's read real sensor data using a driver crate — and see the embedded-hal abstraction in action.
Starting Point
Open the examples/i2c-driver/ project in the workshop repo. This example reads data from an I2C sensor using a third-party driver crate.
Read through src/main.rs carefully:
#![allow(unused)] fn main() { // I2C setup (same as before — Create → Configure) let i2c = I2c::new(peripherals.I2C0, Config::default()) .with_sda(peripherals.GPIO?) .with_scl(peripherals.GPIO?); // DRIVER CRATE: Create a sensor driver // Notice: it takes `i2c` — a generic I2c implementor, not an esp-hal specific type! let mut sensor = SensorDriver::new(i2c, address); // CONTROL: Read sensor data let reading = sensor.some_reading().unwrap(); }
Look at the driver crate's `new()` function signature. It takes `impl I2c` — not `esp_hal::i2c::master::I2c`. This driver was written without any knowledge of ESP32. It works because esp-hal implements the `embedded_hal::i2c::I2c` trait.
Your Task
Step 1: Adapt the Pins
Same as before — update the SDA/SCL pins for your uFerris board.
Step 2: Run It
cd examples/i2c-driver
cargo build --release
espflash flash target/riscv32imc-unknown-none-elf/release/i2c-driver --monitor
You should see sensor readings printing to the serial console.
Step 3: Explore the Driver Crate Documentation
This is the key exercise. The example reads one value from the sensor. But the sensor can do much more.
Find the driver crate on [docs.rs](https://docs.rs). Look at:
1. The **struct** page for the sensor driver
2. What **methods** are available beyond the one used in the example?
3. Does the driver have a **Config** struct? What can you configure?
Answer these questions by reading the docs:
-
What else can you read? The example reads one measurement. Can you read other values? (e.g., different axes, temperature, raw data vs. normalized data)
-
Configuration: Can you change the sensor's behavior?
- Sample rate / output data rate
- Measurement range (e.g., ±2g vs ±16g for an accelerometer)
- Power mode (low power vs. high performance)
-
The driver has its own Create → Configure → Control pattern. Can you map it?
- Create:
SensorDriver::new(i2c, address)— what parameters? - Configure: Is there a config struct or initialization method?
- Control: What readings can you take?
- Create:
Step 4: Try a Different Configuration
Change something in the driver's configuration and observe how the readings change:
#![allow(unused)] fn main() { // Example: if the driver supports changing the measurement range let mut sensor = SensorDriver::new(i2c, address); sensor.set_range(Range::High).unwrap(); // ← Will this method exist? Check the docs! }
You just navigated TWO levels of documentation:
1. **esp-hal I2C docs** — to set up the bus (HAL layer)
2. **Driver crate docs** — to read the sensor (Driver layer)
And the driver crate worked because of the `embedded-hal` trait in between. This is the layer cake from Part 1, in practice.
Based on the sensor address you found in Exercise A, search [crates.io](https://crates.io) for the sensor name. Most sensor driver crates follow the naming pattern: `sensor-name` (e.g., `icm42670`, `bmp180`, `shtcx`).
If you need to use the same I2C bus with multiple devices, you can't just pass `i2c` to two drivers (Rust's ownership won't let you). Use `embedded_hal_bus::i2c::RefCellDevice` to create shareable references:
```rust
use embedded_hal_bus::i2c::RefCellDevice;
use core::cell::RefCell;
let i2c_ref = RefCell::new(i2c);
let mut sensor1 = Driver1::new(RefCellDevice::new(&i2c_ref), addr1);
let mut sensor2 = Driver2::new(RefCellDevice::new(&i2c_ref), addr2);