Exercise A: I2C Bus Scan
~20 min
Before you can talk to an I2C device, you need to know its address. Let's scan the bus and see who's out there.
Starting Point
Open the examples/i2c-scan/ project in the workshop repo. This is a working I2C bus scanner.
Read through src/main.rs. Map it to Create → Configure → Control:
#![allow(unused)] fn main() { // CREATE: instantiate the I2C controller let i2c = I2c::new( peripherals.I2C0, Config::default(), // ← What does default give us? ) .with_sda(peripherals.GPIO?) // ← Which pin? .with_scl(peripherals.GPIO?); // ← Which pin? // CONTROL: scan all possible addresses for addr in 0x01..=0x7F { if i2c.write(addr, &[]).is_ok() { // Device found at this address! } } }
Your Task
Step 1: Adapt the Pins
Check your uFerris pinout card:
- Which GPIO is SDA?
- Which GPIO is SCL?
Update the example with the correct pins.
Step 2: Run the Scanner
cd examples/i2c-scan
cargo build --release
espflash flash target/riscv32imc-unknown-none-elf/release/i2c-scan --monitor
The scanner should print the addresses of all devices it finds. Write them down — you'll need them in the next exercise.
Common I2C addresses on embedded dev boards:
- `0x68` or `0x69` — IMU / accelerometer
- `0x44` or `0x70` — temperature/humidity sensor
- `0x3C` or `0x3D` — OLED display
Cross-reference with your uFerris board documentation to identify each device.
Step 3: Explore the Configuration
Now look at the docs:
Open [`esp_hal::i2c::master::Config`](https://docs.espressif.com/projects/rust/esp-hal/latest/esp32c3/esp_hal/i2c/master/struct.Config.html). What can you configure?
-
Clock frequency: The default is likely 100 kHz (standard mode). Try changing it:
#![allow(unused)] fn main() { let config = Config::default() .with_frequency(Rate::from_khz(400)); // Fast mode }Does the scan still work at 400 kHz? What about other speeds?
-
Timeout: Is there a timeout setting? What happens if you change it?
-
What does
Config::default()actually give you? Look it up in the docs.
esp-hal 1.0 uses a builder pattern for I2C:
```rust
let i2c = I2c::new(peripherals.I2C0, Config::default())
.with_sda(peripherals.GPIOXX)
.with_scl(peripherals.GPIOXX);
The .with_sda() and .with_scl() calls are chained after new().
```admonish hint title="Hint: Full scanner" collapsible=true
See `solutions/i2c-configs/src/main.rs` for the complete scanner with uFerris pin assignments and different configurations.