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:

WirePurpose
SCLSerial Clock — driven by the master
SDASerial 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

ModeSpeedUse Case
Standard100 kHzDefault, works with everything
Fast400 kHzMost modern sensors
Fast Plus1 MHzSome newer devices
High Speed3.4 MHzRarely 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:

  1. Start condition: Master pulls SDA LOW while SCL is HIGH — this wakes everyone up
  2. Address byte: Master sends the 7-bit slave address plus a read/write bit
  3. ACK: The addressed slave pulls SDA LOW for one clock cycle — "I'm here!"
  4. Data bytes: One or more bytes are transferred, each followed by an ACK
  5. 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:

DeviceDescriptionAddress(es)
BMP280 / BME280Pressure + temperature (+ humidity)0x76 or 0x77 (SDO pin selects)
MPU60506-axis IMU (accel + gyro)0x68 or 0x69 (AD0 pin selects)
AT24Cxx EEPROMNon-volatile storage0x50 - 0x57 (A0-A2 pins select)
SSD1306128x64 OLED display0x3C or 0x3D
AHT20Temperature + humidity0x38
INA219Current/power monitor0x40 - 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:

FeatureI2CSPI
Wires2 (SCL, SDA) + GND4+ (SCK, MOSI, MISO, CS) + GND
Speed100 kHz - 1 MHz typical1 MHz - 50+ MHz
DuplexHalf duplexFull duplex
Multiple devicesUp to 128 on same 2 wiresOne CS pin per device
Pull-up resistorsRequired (4.7k typical)Not needed
Pin count for 5 sensors2 pins total7 pins (SCK + MOSI + MISO + 5 CS)
Best forTemperature, pressure, EEPROM, slow sensorsIMUs, flash, displays, fast ADCs
Protocol overheadAddress + ACK per transactionJust 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.