Table of Contents
KEY TAKEAWAYS
- I2C uses just two wires (SDA and SCL) with open-drain outputs and pull-up resistors for multi-device communication.
- Every I2C transaction starts with a START condition and ends with a STOP condition, with each byte followed by an ACK/NACK bit.
- 7-bit addressing supports up to 112 usable device addresses on a single bus (16 reserved addresses).
- Clock stretching allows slow slave devices to hold SCL low, pausing the master until they are ready.
- Multi-master arbitration uses SDA line monitoring — the master that drives SDA high while another drives it low loses arbitration.
What Is I2C?
I2C (Inter-Integrated Circuit), pronounced “I-squared-C” or “I-two-C,” is a synchronous, half-duplex serial communication protocol invented by Philips Semiconductor (now NXP) in 1982. It was designed to allow multiple integrated circuits on a circuit board to communicate using just two wires — a remarkable achievement that made it the go-to protocol for sensor networks, EEPROMs, RTCs, and display controllers in embedded systems.
Unlike SPI which needs a separate chip select for each device, or UART which is point-to-point, I2C uses an addressing scheme that lets you connect dozens of devices on the same two-wire bus. This makes I2C ideal for board-level communication where pin count matters.
The Two-Wire Bus: SDA and SCL
I2C uses exactly two signal lines:
- SDA (Serial Data) — Carries the actual data bits, bidirectional
- SCL (Serial Clock) — Clock signal generated by the master
Both lines use open-drain (or open-collector) outputs. This means a device can only pull the line LOW — it cannot drive it HIGH. External pull-up resistors (typically 4.7kΩ for 100kHz, 2.2kΩ for 400kHz) pull the lines HIGH when no device is pulling them down. This open-drain design is what makes I2C so elegant: multiple devices can safely share the same bus without bus contention destroying signals.
VCC (3.3V or 5V)
| |
[Rp] [Rp] Rp = Pull-up resistor
| |
─────┤─────────┤───── SDA
| |
─────┤─────────┤───── SCL
| |
┌───┴───┐ ┌───┴───┐
│Master │ │ Slave │
│(MCU) │ │(Sensor)│
└───────┘ └───────┘When a device releases the bus (sets its output to high-impedance), the pull-up resistor pulls the line back to VCC. When a device wants to signal a ‘0’, it actively pulls the line low. This “wired-AND” behavior is fundamental to how I2C handles arbitration and clock stretching.
I2C Addressing
Each I2C device has a unique address on the bus. The standard 7-bit addressing scheme provides 128 possible addresses (0x00–0x7F), but 16 are reserved for special purposes, leaving 112 usable addresses. Some devices also support 10-bit addressing (1024 addresses), though this is less common.
The address byte sent on the bus is structured as follows:
/* I2C Address Byte Structure (7-bit addressing) * * Bit: 7 6 5 4 3 2 1 0 * [A6][A5][A4][A3][A2][A1][A0][R/W] * |_______ 7-bit address ______| | * | * 0 = Write * 1 = Read * * Example: BMP280 sensor at address 0x76 * Write: (0x76 << 1) | 0 = 0xEC * Read: (0x76 << 1) | 1 = 0xED */ #define I2C_WRITE 0 #define I2C_READ 1 #define I2C_ADDR_BYTE(addr, rw) (((addr) << 1) | (rw))
Reserved addresses include 0x00 (General Call), 0x01 (CBUS), 0x02 (different bus format), 0x03 (future use), 0x04–0x07 (Hs-mode master code), and 0x78–0x7F (10-bit addressing and future reserved).
Many I2C devices have configurable address pins (A0, A1, A2) that let you change their base address. For example, the PCF8574 I/O expander has base address 0x20 with three address pins, allowing up to 8 devices (0x20–0x27) on the same bus.
START and STOP Conditions
I2C uses two special signal conditions to frame transactions, both defined by SDA transitions while SCL is HIGH:
- START condition (S): SDA transitions from HIGH to LOW while SCL is HIGH
- STOP condition (P): SDA transitions from LOW to HIGH while SCL is HIGH
During normal data transfer, SDA changes only when SCL is LOW. The START and STOP conditions are the only exceptions to this rule, which is how devices distinguish them from regular data bits.
/* I2C START and STOP Conditions — Bit-banged Implementation
*
* START: SDA goes LOW while SCL is HIGH
* STOP: SDA goes HIGH while SCL is HIGH
*/
#include <stdint.h>
/* GPIO register definitions (ARM Cortex-M style) */
#define GPIOB_ODR (*(volatile uint32_t *)0x40010C0C)
#define GPIOB_IDR (*(volatile uint32_t *)0x40010C08)
#define SDA_PIN (1 << 7)
#define SCL_PIN (1 << 6)
static void i2c_delay(void) {
/* ~5µs delay for 100kHz I2C
* Adjust for your clock speed */
volatile int i = 50;
while (i--);
}
static void sda_high(void) { GPIOB_ODR |= SDA_PIN; } /* Release SDA (pull-up pulls high) */
static void sda_low(void) { GPIOB_ODR &= ~SDA_PIN; } /* Pull SDA low */
static void scl_high(void) { GPIOB_ODR |= SCL_PIN; }
static void scl_low(void) { GPIOB_ODR &= ~SCL_PIN; }
void i2c_start(void) {
sda_high();
i2c_delay();
scl_high();
i2c_delay();
sda_low(); /* SDA goes LOW while SCL is HIGH = START */
i2c_delay();
scl_low(); /* Prepare for data transfer */
i2c_delay();
}
void i2c_stop(void) {
sda_low();
i2c_delay();
scl_high();
i2c_delay();
sda_high(); /* SDA goes HIGH while SCL is HIGH = STOP */
i2c_delay();
}There is also a Repeated START (Sr) condition, which is a START issued without a preceding STOP. This is used to change the direction of data transfer (from write to read) without releasing the bus — critical for reading register values from I2C devices.
Data Transfer: Byte-by-Byte with ACK/NACK
Data on I2C is transferred one byte at a time, MSB first. After each byte, the receiving device must send an acknowledgment:
- ACK (Acknowledge): Receiver pulls SDA LOW during the 9th clock pulse — means “byte received, send more”
- NACK (Not Acknowledge): Receiver leaves SDA HIGH during the 9th clock pulse — means “done” or “error”
/* I2C Byte Transmission and ACK/NACK Handling */
/* Returns 0 if ACK received, 1 if NACK */
uint8_t i2c_write_byte(uint8_t data) {
uint8_t bit;
/* Send 8 bits, MSB first */
for (bit = 0; bit < 8; bit++) {
if (data & 0x80) {
sda_high();
} else {
sda_low();
}
data <<= 1;
i2c_delay();
scl_high(); /* Clock the bit out */
i2c_delay();
scl_low();
i2c_delay();
}
/* Release SDA for ACK/NACK from slave */
sda_high();
i2c_delay();
scl_high();
i2c_delay();
/* Read ACK/NACK: LOW = ACK, HIGH = NACK */
uint8_t nack = (GPIOB_IDR & SDA_PIN) ? 1 : 0;
scl_low();
i2c_delay();
return nack;
}
/* Read a byte from slave, send ACK or NACK */
uint8_t i2c_read_byte(uint8_t send_ack) {
uint8_t data = 0;
uint8_t bit;
sda_high(); /* Release SDA so slave can drive it */
for (bit = 0; bit < 8; bit++) {
data <<= 1;
scl_high();
i2c_delay();
if (GPIOB_IDR & SDA_PIN) {
data |= 1;
}
scl_low();
i2c_delay();
}
/* Send ACK (pull SDA low) or NACK (leave SDA high) */
if (send_ack) {
sda_low();
} else {
sda_high();
}
i2c_delay();
scl_high();
i2c_delay();
scl_low();
sda_high(); /* Release SDA */
i2c_delay();
return data;
}Complete I2C Transaction Anatomy
Let’s trace through two common I2C transactions — writing to a device register and reading from a device register. These are the building blocks for communicating with any I2C sensor, EEPROM, or peripheral.
Writing to a Register
Writing to a slave’s register is the most common I2C operation — it’s how you configure sensors, set operating modes, and control actuators. The sequence is: send START, transmit the slave address with the write bit, send the register address, send the data byte(s), then send STOP. The slave ACKs each byte to confirm reception. If the slave NACKs at any point, the operation has failed and you should send a STOP and retry or report an error.
/* Write a value to a specific register of an I2C device
*
* Transaction on the bus:
* [S] [ADDR+W] [ACK] [REG] [ACK] [DATA] [ACK] [P]
*
* S = START condition
* ADDR+W = 7-bit address + Write bit (0)
* REG = Register address to write to
* DATA = Value to write
* ACK = Acknowledge from slave
* P = STOP condition
*/
int i2c_write_register(uint8_t dev_addr, uint8_t reg, uint8_t value) {
i2c_start();
/* Send device address with write bit */
if (i2c_write_byte(I2C_ADDR_BYTE(dev_addr, I2C_WRITE))) {
i2c_stop();
return -1; /* NACK — device not responding */
}
/* Send register address */
if (i2c_write_byte(reg)) {
i2c_stop();
return -2; /* NACK on register address */
}
/* Send data value */
if (i2c_write_byte(value)) {
i2c_stop();
return -3; /* NACK on data */
}
i2c_stop();
return 0; /* Success */
}
/* Example: Configure BMP280 pressure sensor
* Address: 0x76
* Control register: 0xF4
* Value: 0x27 (normal mode, 1x oversampling)
*/
void bmp280_init(void) {
int result = i2c_write_register(0x76, 0xF4, 0x27);
if (result < 0) {
/* Handle error: device not found or communication failure */
}
}Reading from a Register
Reading a register from an I2C slave uses a combined write-then-read sequence. First, you write the register address you want to read (just like a write operation, but without the data byte). Then, without sending a STOP, you issue a repeated START and send the slave address again with the read bit set. The slave now drives the SDA line with the register’s contents. The master ACKs each byte it wants to continue reading, and NACKs the last byte to signal it’s done, followed by STOP.
/* Read a register from an I2C device
*
* This requires a "Repeated START" because we need to:
* 1. WRITE the register address to the device
* 2. READ the register value back
*
* Transaction on the bus:
* [S] [ADDR+W] [ACK] [REG] [ACK] [Sr] [ADDR+R] [ACK] [DATA] [NACK] [P]
*
* Sr = Repeated START (START without preceding STOP)
* The NACK before STOP tells the slave we're done reading
*/
int i2c_read_register(uint8_t dev_addr, uint8_t reg, uint8_t *value) {
/* Phase 1: Write the register address */
i2c_start();
if (i2c_write_byte(I2C_ADDR_BYTE(dev_addr, I2C_WRITE))) {
i2c_stop();
return -1; /* Device not responding */
}
if (i2c_write_byte(reg)) {
i2c_stop();
return -2;
}
/* Phase 2: Repeated START, then read */
i2c_start(); /* Repeated START — no STOP before this */
if (i2c_write_byte(I2C_ADDR_BYTE(dev_addr, I2C_READ))) {
i2c_stop();
return -3;
}
/* Read one byte, send NACK (we only want one byte) */
*value = i2c_read_byte(0); /* 0 = NACK = last byte */
i2c_stop();
return 0;
}
/* Read multiple bytes (burst read) */
int i2c_read_registers(uint8_t dev_addr, uint8_t start_reg,
uint8_t *buffer, uint8_t length) {
uint8_t i;
i2c_start();
if (i2c_write_byte(I2C_ADDR_BYTE(dev_addr, I2C_WRITE))) {
i2c_stop();
return -1;
}
if (i2c_write_byte(start_reg)) {
i2c_stop();
return -2;
}
i2c_start(); /* Repeated START */
if (i2c_write_byte(I2C_ADDR_BYTE(dev_addr, I2C_READ))) {
i2c_stop();
return -3;
}
/* Read all bytes: ACK for all except the last one */
for (i = 0; i < length; i++) {
uint8_t is_last = (i == length - 1);
buffer[i] = i2c_read_byte(!is_last); /* ACK all except last */
}
i2c_stop();
return 0;
}
/* Example: Read BMP280 chip ID (register 0xD0, expected value 0x58) */
void bmp280_check_id(void) {
uint8_t chip_id;
if (i2c_read_register(0x76, 0xD0, &chip_id) == 0) {
if (chip_id == 0x58) {
/* BMP280 detected */
}
}
}Clock Stretching
Clock stretching is a feature that allows a slave device to slow down the master. When a slave needs more time to process data (for example, an ADC performing a conversion), it holds the SCL line LOW after the master releases it. The master must check that SCL actually went HIGH before proceeding.
/* Clock stretching — master must wait for slave to release SCL
*
* After the master releases SCL (sets it high), a slow slave
* can hold SCL low to buy time. The master must poll SCL
* and wait until it actually goes high.
*/
#define I2C_TIMEOUT 10000 /* Maximum wait cycles */
static int scl_wait_high(void) {
uint32_t timeout = I2C_TIMEOUT;
scl_high(); /* Release SCL (open-drain, pull-up should pull it high) */
/* Wait for SCL to actually go high (slave might hold it low) */
while (!(GPIOB_IDR & SCL_PIN)) {
if (--timeout == 0) {
return -1; /* Timeout — slave stuck or bus fault */
}
}
return 0;
}
/* Updated write_byte with clock stretching support */
uint8_t i2c_write_byte_safe(uint8_t data) {
uint8_t bit;
for (bit = 0; bit < 8; bit++) {
if (data & 0x80)
sda_high();
else
sda_low();
data <<= 1;
i2c_delay();
if (scl_wait_high() < 0) /* Wait for clock stretching */
return 2; /* Timeout error */
i2c_delay();
scl_low();
i2c_delay();
}
sda_high();
i2c_delay();
if (scl_wait_high() < 0)
return 2;
i2c_delay();
uint8_t nack = (GPIOB_IDR & SDA_PIN) ? 1 : 0;
scl_low();
i2c_delay();
return nack;
}Multi-Master Arbitration
I2C supports multiple masters on the same bus. When two masters start a transaction simultaneously, arbitration determines which one gets to continue. The process is elegant and non-destructive:
- Both masters monitor the SDA line as they transmit
- If a master sends a ‘1’ (releases SDA) but sees SDA is actually ‘0’ (another master pulling it low), it has lost arbitration
- The losing master immediately stops driving the bus and becomes a listener
- The winning master continues, unaware that arbitration even occurred
/* Multi-master arbitration check during bit transmission
*
* After placing a bit on SDA and raising SCL, the master
* reads SDA back. If it doesn't match what was sent,
* another master won arbitration.
*/
typedef enum {
I2C_ARB_OK = 0,
I2C_ARB_LOST = 1
} i2c_arb_status_t;
i2c_arb_status_t i2c_check_arbitration(uint8_t sent_bit) {
uint8_t actual = (GPIOB_IDR & SDA_PIN) ? 1 : 0;
if (sent_bit == 1 && actual == 0) {
/* We sent HIGH but SDA is LOW — another master is driving
* We lost arbitration, must back off immediately */
return I2C_ARB_LOST;
}
return I2C_ARB_OK;
}
/* Write byte with arbitration detection */
int i2c_write_byte_multimaster(uint8_t data) {
uint8_t bit;
for (bit = 0; bit < 8; bit++) {
uint8_t sent = (data & 0x80) ? 1 : 0;
if (sent)
sda_high();
else
sda_low();
data <<= 1;
i2c_delay();
scl_high();
i2c_delay();
/* Check arbitration */
if (i2c_check_arbitration(sent) == I2C_ARB_LOST) {
/* Release bus — another master has priority */
sda_high();
scl_high();
return -1; /* Arbitration lost */
}
scl_low();
i2c_delay();
}
/* Check ACK */
sda_high();
i2c_delay();
scl_high();
i2c_delay();
uint8_t nack = (GPIOB_IDR & SDA_PIN) ? 1 : 0;
scl_low();
i2c_delay();
return nack; /* 0 = ACK, 1 = NACK */
}I2C Speed Modes
The I2C specification defines several speed modes:
- Standard Mode: up to 100 kbit/s
- Fast Mode: up to 400 kbit/s
- Fast Mode Plus: up to 1 Mbit/s
- High-Speed Mode (Hs): up to 3.4 Mbit/s
- Ultra-Fast Mode (UFm): up to 5 Mbit/s (push-pull, unidirectional)
The pull-up resistor value affects the maximum achievable speed. Lower resistance means faster rise times but higher power consumption. For standard mode, 4.7kΩ works well. For fast mode, 2.2kΩ or 1kΩ is typical. The formula for maximum pull-up resistance is:
/* Pull-up resistor calculation
*
* Maximum Rp = rise_time / (0.8473 × bus_capacitance)
*
* Standard mode: rise time = 1000ns, Cb_max = 400pF
* Rp_max = 1000ns / (0.8473 × 400pF) ≈ 2.95kΩ
* Typical: 4.7kΩ (works with lower capacitance)
*
* Fast mode: rise time = 300ns, Cb_max = 400pF
* Rp_max = 300ns / (0.8473 × 400pF) ≈ 886Ω
* Typical: 2.2kΩ or 1kΩ
*
* Minimum Rp = (VCC - VOL_max) / IOL
* VOL_max = 0.4V, IOL = 3mA (standard)
* Rp_min = (3.3 - 0.4) / 3mA ≈ 967Ω
*/
/* Delay calibration for different speeds */
void i2c_delay_100khz(void) {
/* 100kHz: period = 10µs, half-period = 5µs */
volatile int i = 50; /* Calibrate for your MCU clock */
while (i--);
}
void i2c_delay_400khz(void) {
/* 400kHz: period = 2.5µs, half-period = 1.25µs */
volatile int i = 12;
while (i--);
}I2C Bus Scanner
A bus scanner is an invaluable debugging tool. It tries every possible 7-bit address and reports which devices respond with an ACK. This is the embedded equivalent of “ping” for I2C devices.
/* I2C Bus Scanner — Detect all devices on the bus
*
* Scans addresses 0x08 through 0x77 (skipping reserved ranges)
* A device is present if it ACKs its address byte
*/
#include <stdint.h>
void i2c_scan_bus(void) {
uint8_t addr;
uint8_t found = 0;
printf("I2C Bus Scanne\r\n");
printf(" 0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
for (addr = 0; addr < 128; addr++) {
/* Print row header */
if (addr % 16 == 0) {
printf("%02X: ", addr);
}
/* Skip reserved addresses */
if (addr 0x77) {
printf(" ");
} else {
/* Try to communicate with this address */
i2c_start();
uint8_t nack = i2c_write_byte(I2C_ADDR_BYTE(addr, I2C_WRITE));
i2c_stop();
if (!nack) {
printf("%02X ", addr);
found++;
} else {
printf("-- ");
}
}
if (addr % 16 == 15) {
printf("\n");
}
}
printf("\nFound %d device(s)\n", found);
}
/* Example output:
* 0 1 2 3 4 5 6 7 8 9 A B C D E F
* 00: -- -- -- -- -- -- -- --
* 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
* 20: -- -- -- -- -- -- -- 27 -- -- -- -- -- -- -- --
* 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
* 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
* 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
* 60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
* 70: -- -- -- -- -- -- 76 --
*
* Found 3 device(s)
* 0x27 = PCF8574 LCD backpack
* 0x68 = MPU6050 accelerometer/gyroscope
* 0x76 = BMP280 pressure/temperature sensor
*/Bus Error Recovery
Sometimes the I2C bus gets stuck — typically when a slave holds SDA low (perhaps due to an interrupted transaction or a slave that crashed mid-byte). The standard recovery procedure is to toggle SCL until the slave releases SDA.
/* I2C Bus Recovery — Unstick a locked bus
*
* If SDA is stuck LOW, a slave is likely in the middle of
* sending a byte. Toggle SCL up to 9 times to clock out
* the remaining bits, then issue a STOP condition.
*/
int i2c_bus_recover(void) {
uint8_t i;
/* Release both lines first */
sda_high();
scl_high();
i2c_delay();
/* Check if SDA is stuck low */
if (GPIOB_IDR & SDA_PIN) {
return 0; /* Bus is fine, SDA is high */
}
/* Clock up to 9 times to free the slave */
for (i = 0; i < 9; i++) {
scl_low();
i2c_delay();
scl_high();
i2c_delay();
/* Check if slave released SDA */
if (GPIOB_IDR & SDA_PIN) {
break; /* SDA is free! */
}
}
if (!(GPIOB_IDR & SDA_PIN)) {
return -1; /* Bus still stuck — hardware fault */
}
/* Generate a STOP condition to reset all slaves */
sda_low();
i2c_delay();
scl_high();
i2c_delay();
sda_high();
i2c_delay();
return 0; /* Bus recovered */
}I2C vs SPI vs UART Comparison
Choosing between I2C, SPI, and UART depends on your requirements:
I2C is best when: You need to connect many slow-to-medium-speed devices (sensors, EEPROMs, RTCs) and pin count is limited. Typical use: reading temperature from 5 sensors on a single two-wire bus.
SPI is best when: You need high-speed data transfer (displays, SD cards, flash memory) and have enough pins. SPI is simpler at the protocol level and much faster than I2C.
UART is best when: You need point-to-point communication between two devices (MCU to PC, MCU to GPS module) without requiring a clock signal. UART is asynchronous — no shared clock needed.
Common I2C Pitfalls
- Missing pull-up resistors: Without pull-ups, the bus stays at an undefined level. This is the #1 I2C debugging issue.
- Wrong pull-up values: Too high = slow rise times and communication errors. Too low = excessive current and inability to pull SDA low.
- Address conflicts: Two devices with the same address on one bus. Use address pins or an I2C multiplexer (TCA9548A).
- Long bus wires: I2C is designed for board-level communication. Long wires add capacitance, slowing rise times. Keep I2C traces under 30cm for standard mode.
- Level mismatch: Mixing 3.3V and 5V devices without a level shifter can damage the 3.3V device or cause unreliable communication.
- No timeout on clock stretching: A faulty slave can hold SCL low forever, hanging the master. Always implement a timeout.
Related Articles
- Writing an I2C Driver in Embedded C
- SPI Protocol Deep Dive for Embedded Engineers
- Writing an SPI Driver in Embedded C
- UART Protocol Deep Dive for Embedded Engineers
- Writing a UART Driver in Embedded C
- Debugging Communication Protocols with Logic Analyzers
- Communication Interfaces: UART, SPI, and I2C
- Interfacing Sensors with Microcontrollers
- Complete Guide to Sensors for Embedded Systems
- 5v-3.3v Bi-Directional Logic Level Converter
📖 Related: Building an I2C Sensor Network in C: Complete Project Guide
Related on this site
- For the worked implementation in embedded C — register-level setup, state machine, error handling — see writing an I²C driver in embedded C.
- For a practical project using multiple I²C sensors on a shared bus, see building an I²C sensor network in C.
- When the I²C bus stops responding, see debugging communication protocols for the systematic diagnostic workflow.

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.




![Memory Layout of a C Structure: Padding, Alignment, and sizeof Memory layout diagram of a C struct room array with 3 elements r[0] r[1] r[2], showing how members temperature luminosity soundLevel and humidity are stored at consecutive memory addresses 700 through 744, with the struct definition containing int and float members](https://nerdyelectronics.com/wp-content/uploads/2020/06/structure_memory_layout_nerdyelectronics-1-e1591528678954.png)