I2C (Inter-Integrated Circuit) is a synchronous, multi-master, multi-slave serial communication bus. It uses two bidirectional open-drain lines: SDA (Serial Data) and SCL (Serial Clock). Unlike push-pull outputs that actively drive a line high or low, I2C devices can only pull the line to ground (logic 0) or release it to float high (logic 1) via external pull-up resistors. This wired-AND architecture allows multiple devices to share the same two wires without short-circuiting if one drives high while another drives low.
The Physical Layer: Wires, Pull-Ups, and Bus Limits
Before writing a single line of code, you must understand the physical constraints of the bus. The NXP I2C-bus specification (UM10204) defines strict limits on capacitance, speed, and voltage levels. Because the lines are open-drain, the rise time of the signal is entirely dependent on the RC time constant formed by your pull-up resistors and the parasitic capacitance of your wires and devices. If the capacitance is too high or the resistor value is too large, the voltage won't reach the logic-high threshold before the next clock edge, causing data corruption.
| Parameter | Standard Mode | Fast Mode | Fast Mode Plus | High Speed |
|---|---|---|---|---|
| Max Bit Rate | 100 kbit/s | 400 kbit/s | 1 Mbit/s | 3.4 Mbit/s |
| Max Bus Capacitance ($C_b$) | 400 pF | 400 pF | 550 pF | 550 pF |
| Address Space | 7-bit (128 addresses, 16 reserved) or 10-bit (1024 addresses) | |||
| Typical Max Distance | ~1 meter | ~30 cm | ~10 cm | ~10 cm |
| Required Pull-Up | 4.7 kΩ | 2.2 kΩ | 1 kΩ | Specialized active |
Do not rely on the ESP32 or Arduino internal pull-ups (typically 20kΩ to 45kΩ) for Fast Mode (400 kHz). They are too weak, resulting in slow rise times that violate the I2C timing spec. For a 3.3V bus at 400 kHz with standard jumper wires (~50pF capacitance), use 2.2 kΩ external resistors tied to 3.3V. If you are running long wires (>30cm) and hitting the 400pF capacitance limit, drop to 100 kHz Standard Mode and use 4.7 kΩ resistors to ensure clean logic highs.
Bus Mechanics: Addressing and the Minimal Exchange
An I2C transaction follows a rigid sequence. The master initiates communication with a START condition (pulling SDA low while SCL is high). It then clocks out 7 bits for the target slave address, followed by 1 bit for Read/Write direction. The slave responds with an ACK (pulling SDA low on the 9th clock pulse). Data bytes follow, each acknowledged, until the master issues a STOP condition (releasing SDA to go high while SCL is high).
Wiring the ESP32 to a BME280 Sensor
To demonstrate a minimal working exchange, we will wire an ESP32 DevKit V1 to a BME280 temperature/pressure sensor. The BME280 operates at 3.3V and supports up to 1 MHz, but we will run it at 400 kHz.
- ESP32 GPIO 21 to BME280 SDA
- ESP32 GPIO 22 to BME280 SCL
- ESP32 3V3 to BME280 VIN and CSB (CSB high forces I2C mode)
- ESP32 GND to BME280 GND
- 2.2 kΩ resistors from SDA to 3V3, and SCL to 3V3.
Below is a complete, copy-pasteable Arduino sketch using the native Wire library to scan the bus and verify the ACK.
#include <Wire.h>
// Explicitly define pins for ESP32 DevKit V1
const int SDA_PIN = 21;
const int SCL_PIN = 22;
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
// Initialize I2C at 400kHz (Fast Mode)
Wire.begin(SDA_PIN, SCL_PIN);
Wire.setClock(400000);
Serial.println("Scanning I2C bus...");
}
void loop() {
byte error, address;
int deviceCount = 0;
for (address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("Device found at address 0x");
if (address < 16) Serial.print("0");
Serial.println(address, HEX);
deviceCount++;
} else if (error == 4) {
Serial.print("Unknown error at address 0x");
if (address < 16) Serial.print("0");
Serial.println(address, HEX);
}
}
if (deviceCount == 0) {
Serial.println("No I2C devices found. Check wiring and pull-ups.");
}
Serial.println("Scan complete.\n");
delay(5000);
}
Protocol Selection: I2C vs. SPI vs. UART
Choosing the right protocol depends entirely on your constraints regarding distance, speed, and device count. I2C is the undisputed king of low-pin-count sensor networks on a single PCB, but it falls apart when you need high bandwidth or long cable runs.
| Feature | I2C | SPI | UART |
|---|---|---|---|
| Wires Required | 2 (shared by all) | 3 shared + 1 CS per device | 2 (TX/RX per pair) |
| Max Practical Speed | 400 kHz (typ) / 3.4 MHz | 10 MHz to 50+ MHz | 115.2 kbps to 921.6 kbps |
| Topology | Multi-master, Multi-slave bus | Single master, Multi-slave (star) | Point-to-Point |
| Max Distance | ~1 meter (at 100 kHz) | ~20 cm (on PCB) | ~15 meters (at 9600 baud) |
| Best Use Case | Low-speed sensors, EEPROMs, RTCs | Displays, SD cards, high-speed ADCs | GPS modules, PC serial consoles |
Choose I2C when: You are short on GPIO pins, connecting multiple low-speed environmental sensors (BME280, SHT31, BH1750) on the same breadboard, and speed is not critical.
Choose SPI when: You need to push pixels to a TFT display, read from an SD card, or sample high-frequency data where I2C's 400 kHz ceiling would bottleneck your throughput.
Choose UART when: You are communicating point-to-point with a peripheral that has its own internal clock and state machine, like a GPS receiver or a cellular modem.
Debugging the Bus: Classic Failures and How to Sniff
When an I2C bus fails, it rarely fails silently. It hangs, throws NACKs, or returns garbage data. Based on years of bench debugging, here are the most common failure modes and how to fix them.
1. The Missing or Weak Pull-Up
Symptom: The bus scanner finds nothing, or finds devices sporadically. An oscilloscope shows the SDA/SCL lines lingering at ~1.5V instead of snapping to 3.3V.
The Fix: You forgot the external pull-ups, or your jumper wires have too much capacitance for the 4.7kΩ resistors you used at 400 kHz. Add 2.2kΩ resistors or drop the clock to 100 kHz via Wire.setClock(100000);.
2. Address Clashes
Symptom: You wire two identical sensors (e.g., two BME280s) to the bus, but only one responds, or the bus locks up.
The Fix: Many I2C sensors have a hardcoded default address (0x76 for BME280). Check the datasheet for an address-select pin (often labeled SDO or ADDR). Tying this pin to GND sets one address (0x76), while tying it to VCC sets the alternate (0x77). If the chip lacks this pin, you must use an I2C multiplexer like the TCA9548A to route the signals.
3. Baud Mismatch and Clock Stretching
Symptom: The master sends data, but the slave holds SCL low indefinitely, freezing the microcontroller.
The Fix: This is clock stretching. The slave is busy processing (e.g., an ADC conversion) and is physically holding the clock line low to tell the master to wait. If your master (like some older bit-banged implementations) doesn't support clock stretching, it will interpret the low SCL as a glitch. Ensure you are using the hardware I2C peripheral via the Wire library, which handles stretching automatically in hardware.
How to Sniff the Bus
When the multimeter isn't enough, you need to see the digital timing. Connect a logic analyzer (like a Saleae Logic 8 or a budget DSLogic Plus) to SDA and SCL. Set your sample rate to at least 10x the bus speed (4 MS/s for a 400 kHz bus). Configure the trigger to catch the START condition (a falling edge on SDA while SCL is high). Decode the I2C protocol in your analyzer software to verify that the master is actually sending the correct 7-bit address and that the slave is pulling SDA low for the ACK bit. If the ACK bit stays high, the slave is either unpowered, at the wrong address, or physically disconnected.






