I2C — Two-Wire Sensor Bus
If SPI is a four-lane highway, I2C is a two-lane country road — slower, narrower, but it goes everywhere. You can connect a temperature sensor, a pressure sensor, an OLED display, and an EEPROM all on the same two wires. No extra chip-select lines. No extra pins. Just two wires and some clever addressing.
How I2C Works
I2C (pronounced "I-squared-C" or "I-two-C") stands for Inter-Integrated Circuit. It was invented by Philips (now NXP) in 1982 and has been the go-to protocol for low-speed sensors ever since.
The bus uses just two wires:
| Wire | Purpose |
|---|---|
| SCL | Serial Clock — driven by the master |
| SDA | Serial Data — bidirectional, shared by everyone |
Plus GND of course. Both lines need pull-up resistors (typically 4.7 kohm to 3.3V). This is not optional — without pull-ups, nothing works.
3.3V 3.3V
│ │
[4.7k] [4.7k]
│ │
SCL ───┼───────────┼──── Slave A ──── Slave B ──── Slave C
SDA ───┼───────────┼──── Slave A ──── Slave B ──── Slave C
GND ───┴───────────┴──── Slave A ──── Slave B ──── Slave C
Why Pull-Up Resistors?
I2C uses an open-drain bus. Devices can only pull the line LOW — they cannot drive it HIGH. The pull-up resistors pull the lines back to HIGH when nobody is pulling them down. This is what allows multiple devices to share the same wires without electrical conflict.
🧠 Think About It: Why did the designers choose open-drain instead of push-pull? Think about what happens if two devices try to drive the same wire to different levels at the same time.
Addresses
Every device on an I2C bus has a 7-bit address (0x00 to 0x7F, though some are reserved). When the master wants to talk to a specific device, it sends that device's address first. Only the addressed device responds. Everyone else stays quiet.
This means you can have up to 128 devices on a single bus — though in practice, you rarely use more than a dozen.
Speed
| Mode | Speed | Use Case |
|---|---|---|
| Standard | 100 kHz | Default, works with everything |
| Fast | 400 kHz | Most modern sensors |
| Fast Plus | 1 MHz | Some newer devices |
| High Speed | 3.4 MHz | Rarely used with MCUs |
Most of the time, you will use 400 kHz (Fast mode). It works with nearly all modern sensors and gives a nice balance of speed and reliability.
Start, Stop, ACK, and NACK
An I2C transaction looks like this:
- Start condition: Master pulls SDA LOW while SCL is HIGH — this wakes everyone up
- Address byte: Master sends the 7-bit slave address plus a read/write bit
- ACK: The addressed slave pulls SDA LOW for one clock cycle — "I'm here!"
- Data bytes: One or more bytes are transferred, each followed by an ACK
- NACK or Stop: When the transfer is done, the master sends a NACK (no acknowledge) or a stop condition
If nobody ACKs the address byte, the master knows something is wrong — either the address is incorrect or the device is not on the bus.
💡 Fun Fact: The I2C patent expired in 2006, which is why you now find I2C on absolutely everything. Before that, manufacturers had to pay NXP a licensing fee, which is why some early microcontrollers used a compatible but differently-named protocol called "TWI" (Two-Wire Interface).
I2C in Embassy
Basic Setup
use embassy_stm32::i2c::{Config, I2c}; use embassy_stm32::time::Hertz; use embassy_stm32::bind_interrupts; bind_interrupts!(struct Irqs { I2C1_EV => embassy_stm32::i2c::EventInterruptHandler<embassy_stm32::peripherals::I2C1>; I2C1_ER => embassy_stm32::i2c::ErrorInterruptHandler<embassy_stm32::peripherals::I2C1>; }); #[embassy_executor::main] async fn main(_spawner: Spawner) { let p = embassy_stm32::init(Default::default()); let i2c = I2c::new( p.I2C1, p.PB6, // SCL p.PB7, // SDA Irqs, p.DMA1_CH6, // TX DMA p.DMA1_CH0, // RX DMA Hertz(400_000), // 400 kHz — Fast mode Config::default(), ); }
Reading a Sensor Register
The most common I2C pattern is write-then-read: you write the register address you want to read, then read back the data. Embassy provides write_read for exactly this:
#![allow(unused)] fn main() { const BMP280_ADDR: u8 = 0x76; const CHIP_ID_REG: u8 = 0xD0; let mut chip_id = [0u8; 1]; i2c.write_read(BMP280_ADDR, &[CHIP_ID_REG], &mut chip_id).await.unwrap(); defmt::info!("BMP280 chip ID: 0x{:02x}", chip_id[0]); // Should be 0x58 }
Writing to a Register
To write a register, you send the register address followed by the data — all in one write call:
#![allow(unused)] fn main() { const CTRL_MEAS_REG: u8 = 0xF4; const NORMAL_MODE: u8 = 0b0010_0111; // temp x1, press x1, normal mode i2c.write(BMP280_ADDR, &[CTRL_MEAS_REG, NORMAL_MODE]).await.unwrap(); }
Reading Multiple Bytes
Many sensors let you read a burst of consecutive registers in a single transaction:
#![allow(unused)] fn main() { const PRESS_MSB_REG: u8 = 0xF7; let mut raw_data = [0u8; 6]; // 3 bytes pressure + 3 bytes temperature i2c.write_read(BMP280_ADDR, &[PRESS_MSB_REG], &mut raw_data).await.unwrap(); let raw_pressure = ((raw_data[0] as u32) << 12) | ((raw_data[1] as u32) << 4) | ((raw_data[2] as u32) >> 4); let raw_temp = ((raw_data[3] as u32) << 12) | ((raw_data[4] as u32) << 4) | ((raw_data[5] as u32) >> 4); defmt::info!("Raw pressure: {}, Raw temp: {}", raw_pressure, raw_temp); }
Common I2C Devices
You will encounter these addresses over and over:
| Device | Description | Address(es) |
|---|---|---|
| BMP280 / BME280 | Pressure + temperature (+ humidity) | 0x76 or 0x77 (SDO pin selects) |
| MPU6050 | 6-axis IMU (accel + gyro) | 0x68 or 0x69 (AD0 pin selects) |
| AT24Cxx EEPROM | Non-volatile storage | 0x50 - 0x57 (A0-A2 pins select) |
| SSD1306 | 128x64 OLED display | 0x3C or 0x3D |
| AHT20 | Temperature + humidity | 0x38 |
| INA219 | Current/power monitor | 0x40 - 0x4F (A0-A1 pins select) |
Notice how some devices have configurable addresses. The BMP280, for example, has an SDO pin — tie it to GND and the address is 0x76, tie it to VCC and it is 0x77. This lets you put two BMP280s on the same bus.
💡 Fun Fact: If you do not know the address of a device, you can scan the entire bus. Send a write to every address from 0x08 to 0x77. Any address that ACKs has a device on it. This is called an "I2C scan" and it is the first thing to try when a sensor is not responding.
I2C vs SPI — When to Use Which
This is one of the most common questions in embedded development. Here is the honest comparison:
| Feature | I2C | SPI |
|---|---|---|
| Wires | 2 (SCL, SDA) + GND | 4+ (SCK, MOSI, MISO, CS) + GND |
| Speed | 100 kHz - 1 MHz typical | 1 MHz - 50+ MHz |
| Duplex | Half duplex | Full duplex |
| Multiple devices | Up to 128 on same 2 wires | One CS pin per device |
| Pull-up resistors | Required (4.7k typical) | Not needed |
| Pin count for 5 sensors | 2 pins total | 7 pins (SCK + MOSI + MISO + 5 CS) |
| Best for | Temperature, pressure, EEPROM, slow sensors | IMUs, flash, displays, fast ADCs |
| Protocol overhead | Address + ACK per transaction | Just CS toggle |
Rule of thumb: Use I2C when you have many slow sensors and few pins. Use SPI when you need speed or are talking to high-bandwidth devices.
Debugging I2C
I2C problems have characteristic symptoms:
No ACK (NACK on address byte):
- Wrong address. Double-check the datasheet — some list the 8-bit address (left-shifted by 1), not the 7-bit address.
- Missing pull-up resistors. Without them, nothing works. Measure the voltage on SCL and SDA — they should sit at 3.3V when idle.
- Device not powered. Check VCC.
SDA stuck LOW (bus lockup):
- A slave got confused mid-transaction and is holding SDA low. Fix: toggle SCL manually 9 times — this clocks out any stuck slave.
Data corruption or intermittent failures:
- Pull-up resistors too weak (too high a value). Try 2.2k instead of 4.7k, especially with long wires or many devices.
- Bus capacitance too high. Long wires or too many devices load the bus. Shorten wires or reduce speed.
🧠 Think About It: Why does toggling SCL 9 times fix a stuck bus? Think about the I2C protocol — a slave releases SDA after the 9th clock pulse (the ACK bit) of every byte. Clocking 9 times guarantees you hit that release point regardless of where the slave got stuck.
Summary
I2C is the protocol of choice when you want simplicity and need to connect multiple sensors. Two wires, pull-up resistors, and 7-bit addresses give you a clean, well-defined bus that just works — as long as you remember those pull-ups.
The pattern you will use most often is write_read: send a register address, read back data. Master that, and you can talk to any I2C sensor on the market.
Next up: ADC — turning analog voltages from the real world into numbers your code can work with.