Writing an OS Kernel from Scratch | 348: I2C Bus — The Blood Vessels of Embedded Systems
Abstract: I2C is the most common low-speed bus in embedded systems. With just two wires, it can connect a large number of devices — sensors, EEPROMs, RTCs, touchscreen controllers… This article covers I2C protocol principles, master-slave architecture, and Linux i2c-tools hands-on exercises, giving you mastery over this “system blood vessel.”
1. I2C Protocol: Why Two Wires Can Connect So Many Devices
1.1 Physical Layer: Open-Drain + Pull-Up
I2C has only two signal lines:
- SCL (Serial Clock): Clock line
- SDA (Serial Data): Data line
Both lines use open-drain output + pull-up resistors, meaning:
VCC ──────[Pull-up 4.7KΩ] ──── SDA/SCL
│
┌──────────────────┘
↓
┌────┴────┐
│ Driver │ (Open-drain: can only pull low, or Hi-Z)
└─────────┘The advantage of open-drain is: any device can pull the line low, but no one can force it high. This avoids high-level conflicts during multi-master contention. Even if the number of masters is uncertain, the bus can be shared safely.
1.2 Address: 7-bit or 10-bit?
I2C slave device addresses are 7 bits (or 10-bit extended mode). 7-bit addresses theoretically allow up to 128 devices, but address 0000 000 is reserved, so the practical maximum is 112.
7-bit address format:
[A6][A5][A4][A3][A2][A1][A0][R/W]
Example: MPU6050 address = 0x68 (110 1000)
Write: 0xD0 (0x68 << 1 | 0)
Read: 0xD1 (0x68 << 1 | 1)Note that the address is shifted left 1 bit then the R/W bit is added, so many chip datasheets say “0x68” but the actual read/write addresses are 0xD0/0xD1.
1.3 Start/Stop Conditions
SCL=high + SDA=falling edge → START
SCL=high + SDA=rising edge → STOP
START
↓
┌──────┐ ┌──────────┐
│ │ │ │
SDA ── ─────── ────────── ← STOP
└─────────────────────┘1.4 Transfer Format: Each Byte Followed by ACK
Master → Slave Master ← Slave
[START][ADDR+W][ACK][DATA][ACK][DATA][ACK]...[STOP]
↑
Slave pulls SDA low (ACK)
If NACK, Master stops transmittingAfter each byte (8 bits), the receiver must pull SDA low to indicate ACK (unless it’s the last byte or a NACK situation).
1.5 I2C Read/Write Timing Diagram
Writing a byte:
Master SCL SDA
─── ───
START: ──┐ ──┐
│ └──┘ (SDA fall while SCL high)
ADDR+W: │ D7 D6 D5 D4 D3 D2 D1 D0
│ ────────────────────────────
ACK: ─┘ (slave pulls SDA low)
DATA: │ D7 D6 D5 D4 D3 D2 D1 D0
│ ────────────────────────────
ACK: ─┘ (slave pulls SDA low)
STOP: │ (SDA rises while SCL high)2. Linux I2C Driver Writing
// Minimal I2C client driver
#include <linux/i2c.h>
#include <linux/module.h>
static int my_probe(struct i2c_client *client) {
dev_info(&client->dev, "I2C device detected at addr 0x%02xn",
client->addr);
return 0;
}
static void my_remove(struct i2c_client *client) {
dev_info(&client->dev, "I2C device removedn");
}
static const struct i2c_device_id my_id_table[] = {
{ "my_i2c_dev", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_id_table);
static struct i2c_driver my_driver = {
.driver = {
.name = "my_i2c_driver",
.owner = THIS_MODULE,
},
.probe = my_probe,
.remove = my_remove,
.id_table = my_id_table,
};
module_i2c_driver(my_driver);
MODULE_LICENSE("GPL");
Comments