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:

  1. 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)

  2. 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)
  3. 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?

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