I2C is probably the protocol I use most often after UART, mainly because so many sensors — accelerometers, temperature sensors, EEPROMs, real-time clocks, OLED displays — expose an I2C interface. What drew me to it initially was how few wires it needs even when you’re talking to a dozen different chips. In this article I’ll go through how I2C actually works electrically and at the protocol level, how to configure and use it in code, and where it tends to bite you in real projects.
What Is I2C?
I2C (Inter-Integrated Circuit, sometimes written I²C or pronounced “I-squared-C”) is a synchronous, multi-master, multi-slave serial communication protocol developed by Philips (now NXP) in the 1980s. Unlike UART, I2C uses a shared clock line, so both devices sample data based on that clock rather than a pre-agreed baud rate. Unlike SPI, I2C needs only two wires total, regardless of how many devices are on the bus.
graph TD
MCU[Microcontroller - Master] ---|SDA| Bus((I2C Bus))
MCU ---|SCL| Bus
Bus --- S1[Temperature Sensor - 0x48]
Bus --- S2[EEPROM - 0x50]
Bus --- S3[RTC - 0x68]
Bus --- S4[OLED Display - 0x3C]
The Two Wires
- SDA (Serial Data) — carries the actual data, bidirectionally.
- SCL (Serial Clock) — carries the clock signal, generated by the master, that all devices use to time their reads/writes of SDA.
Both lines are open-drain, meaning devices can only pull them low, never drive them high. Pull-up resistors (typically 2.2kΩ–10kΩ, depending on bus speed and capacitance) bring the lines back to a logic-high level when no device is pulling them low. This open-drain design is what allows multiple devices to share the same two wires without conflicts — if any device pulls the line low, it goes low, and only when everyone releases it does it return high.
Addressing
Every I2C peripheral (slave) on the bus has a 7-bit address (or, less commonly, a 10-bit address for extended addressing). The master initiates every transaction by sending the target device’s address, so multiple devices can share the exact same two wires without interfering with each other — only the addressed device responds.
Address byte: [A6 A5 A4 A3 A2 A1 A0 | R/W]
The 8th bit indicates whether the master wants to Read (1) or Write (0) from/to that address.
I2C Transaction Structure
sequenceDiagram
participant M as Master
participant S as Slave (0x48)
M->>S: START condition
M->>S: Address (0x48) + Write bit
S-->>M: ACK
M->>S: Register address (e.g. 0x00)
S-->>M: ACK
M->>S: REPEATED START
M->>S: Address (0x48) + Read bit
S-->>M: ACK
S-->>M: Data byte
M-->>S: ACK
S-->>M: Data byte
M-->>S: NACK (end of read)
M->>S: STOP condition
- START condition: SDA transitions from high to low while SCL is high — this signals the beginning of a transaction.
- STOP condition: SDA transitions from low to high while SCL is high — this signals the end.
- ACK/NACK: After every byte, the receiver pulls SDA low for one clock cycle to acknowledge receipt (ACK), or leaves it high to indicate it did not/could not receive the byte (NACK).
- Repeated START: A very common pattern is writing a register address, then issuing another START (without a STOP in between) to switch to reading — this is exactly how you typically read sensor registers.
Timing Diagram: START, Address, ACK
sequenceDiagram
participant SDA
participant SCL
Note over SDA,SCL: Idle - both high
SDA->>SDA: Falls while SCL high (START)
loop 8 bits (Address + R/W)
SCL->>SCL: Clock pulse
SDA->>SDA: Data bit valid while SCL high
end
SCL->>SCL: 9th clock pulse
SDA->>SDA: Slave pulls low (ACK)
I2C Bus Speeds
| Mode | Speed |
|---|---|
| Standard mode | 100 kHz |
| Fast mode | 400 kHz |
| Fast mode plus | 1 MHz |
| High-speed mode | 3.4 MHz |
Most sensors and displays used in hobbyist and mid-range commercial embedded projects run at Standard (100kHz) or Fast (400kHz) mode.
Configuring I2C on an STM32 (HAL Example)
#include "stm32f4xx_hal.h"
I2C_HandleTypeDef hi2c1;
void I2C1_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000; // 400 kHz Fast mode
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
uint8_t I2C_ReadRegister(uint8_t dev_addr, uint8_t reg_addr) {
uint8_t data;
HAL_I2C_Master_Transmit(&hi2c1, dev_addr << 1, ®_addr, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, (dev_addr << 1) | 0x01, &data, 1, 100);
return data;
}
void I2C_WriteRegister(uint8_t dev_addr, uint8_t reg_addr, uint8_t value) {
uint8_t buf[2] = { reg_addr, value };
HAL_I2C_Master_Transmit(&hi2c1, dev_addr << 1, buf, 2, 100);
}
Note the dev_addr << 1: HAL expects the 7-bit address pre-shifted into the upper 7 bits of the byte, leaving bit 0 free for the R/W bit that the HAL library manages internally.
Example: Reading Temperature from an LM75-Style Sensor
#define LM75_ADDR 0x48
#define TEMP_REG 0x00
float read_temperature(void) {
uint8_t raw[2];
uint8_t reg = TEMP_REG;
HAL_I2C_Master_Transmit(&hi2c1, LM75_ADDR << 1, ®, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, (LM75_ADDR << 1) | 1, raw, 2, 100);
int16_t temp_raw = (raw[0] << 8) | raw[1];
temp_raw >>= 5; // LM75 uses 11-bit resolution
return temp_raw * 0.125f; // 0.125 °C per LSB
}
Configuring I2C on Arduino (Wire Library)
#include <Wire.h>
void setup() {
Wire.begin(); // Join I2C bus as master
Serial.begin(9600);
}
void loop() {
Wire.beginTransmission(0x48);
Wire.write(0x00); // Point to temperature register
Wire.endTransmission(false); // Repeated START, no STOP
Wire.requestFrom(0x48, 2); // Read 2 bytes
if (Wire.available() == 2) {
int16_t raw = (Wire.read() << 8) | Wire.read();
raw >>= 5;
float tempC = raw * 0.125;
Serial.println(tempC);
}
delay(1000);
}
Clock Stretching
Some slower slave devices need extra time to process a request before responding. I2C supports clock stretching, where the slave holds SCL low even after the master releases it, effectively pausing the clock until the slave is ready. The master must monitor SCL and wait for it to actually go high before proceeding — most hardware I2C peripherals handle this automatically, but it’s worth knowing about when debugging unexplained slowdowns on the bus.
Multi-Master Arbitration
I2C technically supports multiple masters on the same bus. If two masters start a transaction at the same time, they monitor the bus while transmitting; the moment one master’s transmitted bit doesn’t match what’s actually on the bus (because another master pulled it low), that master immediately backs off and waits — this is called arbitration. In practice, multi-master I2C setups are uncommon in typical embedded projects, but the protocol was designed with this in mind from the start.
Common I2C Devices in Embedded Projects
| Device Type | Example Chips |
|---|---|
| Temperature sensors | LM75, TMP102, MCP9808 |
| Accelerometers/IMUs | MPU6050, ADXL345, BMI160 |
| Real-time clocks | DS3231, PCF8523 |
| EEPROMs | AT24C256, 24LC512 |
| OLED/LCD displays | SSD1306, SH1106 |
| Port expanders | PCF8574, MCP23017 |
| ADCs/DACs | ADS1115, MCP4725 |
I2C vs SPI vs UART
| Feature | I2C | SPI | UART |
|---|---|---|---|
| Wires | 2 (SDA, SCL) | 4 (MOSI, MISO, SCK, CS) | 2 (TX, RX) |
| Addressing | Built-in (7/10-bit) | Via separate CS lines | None (point-to-point) |
| Max devices | Many (address-limited) | Limited by CS pins | 2 |
| Speed | Moderate (up to 3.4 MHz) | High (tens of MHz) | Moderate |
| Complexity | Moderate | Moderate | Low |
Real-World and IoT Applications
In a home automation sensor node I worked on, a single I2C bus carried a BME280 (temperature/humidity/pressure), an MPU6050 (motion detection for tamper alerts), and a small SSD1306 OLED for local status display — all on the same two wires, differentiated purely by their I2C addresses. This is exactly the kind of design where I2C shines: lots of low-speed peripherals, minimal pin usage, simple wiring.
graph LR
ESP32[ESP32 - Master] ---|SDA/SCL| BME[BME280 - 0x76]
ESP32 ---|SDA/SCL| MPU[MPU6050 - 0x68]
ESP32 ---|SDA/SCL| OLED[SSD1306 OLED - 0x3C]
Debugging I2C Issues
The most common problems I’ve run into, roughly in order of frequency:
- Missing or wrong-value pull-up resistors — many dev boards include them, but breadboard setups with bare sensor modules often don’t, leading to unreliable or completely dead communication.
- Wrong 7-bit address, or confusing the 8-bit written address with the 7-bit address — datasheets sometimes list the 8-bit form (already including the R/W bit), which trips people up constantly.
- Bus lockups — if a slave gets stuck holding SDA low mid-transaction (e.g., after a reset during a transfer), the bus can hang. A common fix is toggling SCL manually a few times on startup to force the stuck device to release SDA.
- Address conflicts — two devices on the bus sharing the same address (common with multiple identical breakout boards) will corrupt communication; many sensor chips provide address-select pins for exactly this reason.
A logic analyzer with I2C decoding is, in my experience, the single most useful debugging tool here — you can see the address byte, ACK/NACK bits, and data bytes laid out clearly, which usually makes the root cause obvious within seconds.
Reliability and Power Considerations
I2C’s shared bus and moderate speed make it well-suited to sensor networks where update rates don’t need to be extremely fast. For lower-power designs, keep in mind that pull-up resistors constantly draw a small current whenever a line is held low; very low-power designs sometimes use higher-value pull-ups or only enable the I2C peripheral’s power domain when actively communicating.
Security Considerations
I2C has no built-in authentication or encryption, and because most I2C buses in a product are only accessible with physical access to internal test points, this is rarely a primary attack surface — but on devices where I2C peripherals are exposed on an external connector (like a debug or expansion header), an attacker could potentially spoof sensor readings or extract data from EEPROMs by tapping the bus.
Frequently Asked Questions
Can I connect I2C devices that use different voltage levels (3.3V vs 5V)? Not directly without a level shifter — since I2C lines are open-drain, mixing voltage domains without a bidirectional level-shifting circuit can damage a lower-voltage device or cause unreliable communication.
What happens if two I2C devices have the same address? Communication will be corrupted or unreliable, since both devices will respond to the same address. Many sensors provide address pins (like ADDR0/ADDR1) to let you choose between two or more possible addresses to avoid conflicts.
Do I always need pull-up resistors? Yes, unless your microcontroller’s internal pull-ups are strong enough for your specific bus length/speed/capacitance (they usually aren’t for anything beyond very short, low-speed buses) — external resistors in the 2.2kΩ–10k�. range are standard practice.
Is I2C faster than SPI? No, generally SPI is significantly faster than I2C, since I2C’s maximum standard speeds top out around 3.4 MHz (High-speed mode) versus SPI, which routinely runs at tens of MHz.
Summary
I2C is a two-wire, clocked, address-based serial protocol that lets a microcontroller talk to many peripherals — sensors, displays, EEPROMs, RTCs — over a shared bus with minimal pin usage. Its open-drain, pull-up-resistor design allows multiple devices to coexist safely on the same lines, addressed individually by their unique 7-bit addresses, with START/STOP conditions and ACK/NACK bits framing every transaction. Once you understand the address-then-register-then-data pattern most I2C devices follow, working with new sensors becomes largely a matter of reading the right datasheet register map rather than learning a new protocol each time.
References and Further Reading
- NXP I2C-Bus Specification and User Manual (UM10204) — nxp.com
- STMicroelectronics STM32 I2C Peripheral Reference Manual — st.com
- Arduino Wire Library Documentation — docs.arduino.cc/language-reference/en/functions/communication/wire
- Espressif ESP32 I2C Driver Documentation — docs.espressif.com
- Microchip AT24C Series EEPROM Datasheets — microchip.com
- Bosch Sensortec BME280/BMI160 Datasheets — bosch-sensortec.com