Writing an OS Kernel from Scratch | I2C Bus — The Blood Vessels of Embedded Systems
Have you ever wondered: How does a gyroscope chip or touchscreen communicate with the main controller? They are not as complex as USB, nor do they need the massive bandwidth of PCIe — they use I2C.
I2C (Inter-Integrated Circuit) is the most common low-speed bus in embedded systems. It needs only two wires (SCL + SDA) to connect multiple devices. This article covers everything from protocol principles to Linux hands-on exercises, building your complete I2C debugging skills.
I. I2C Protocol: How Two Wires Transmit Data
I2C is a synchronous serial protocol using only two wires:
- SCL (Serial Clock): Clock line, provided by the master
- SDA (Serial Data): Data line, bidirectional half-duplex
Master Slave 1 Slave 2
│ │ │
─────────┼────────────────────────────────┼─────────────────┼───── SCL
│ │ │
─────────┼────────────────────────────────┼─────────────────┼───── SDA
│ │ │
Start condition │ │
Write addr 0x68 (7-bit) + W(0) │ │
←─── ACK ──── │ │
Write register addr 0x43 │ │
←─── ACK ──── │ │
Write data 0x00 │ │
←─── ACK ──── │ │
Stop condition │ │Timing details:
- Start (S): SDA goes low while SCL is high
- Address + R/W: 7-bit address + 1-bit read/write (8 bits total)
- ACK: Slave pulls SDA low on the 9th clock to acknowledge
- Data: Each byte followed by ACK
- Stop (P): SDA goes high while SCL is high
I2C address is 7 bits, but shifted left 1 bit + R/W bit during transmission, so 8 bits are actually sent:
# MPU6050 gyroscope I2C address 0x68
# Write: addr << 1 + 0 = 0xD0
# Read: addr << 1 + 1 = 0xD1II. Master-Slave Architecture and Address Conflicts
I2C is a single-master, multi-slave bus. One master can drive multiple slave devices, each with a unique 7-bit address.
Common address rules:
- Slave device addresses are fixed by the manufacturer or configured via pin strapping (high/low)
- Some chips have two address options (different AD0 pin)
- I2C addresses must not have duplicates (conflict)
# Scan all devices on the bus using i2cdetect on Linux
$ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- 68 -- -- -- --
70: -- -- -- -- -- -- -- --III. Linux I2C Debugging
# Common i2c-tools commands
$ i2cdetect -l # List all I2C buses
$ i2cdump -y 1 0x68 # Dump all registers of device at 0x68
$ i2cget -y 1 0x68 0x75 # Read a single register (0x75 = WHO_AM_I)
$ i2cset -y 1 0x68 0x6B 0x00 # Write a value to a register
# Kernel I2C debug
$ cat /sys/bus/i2c/devices/i2c-1/name # Bus name
$ ls /sys/bus/i2c/drivers/ # All registered I2C drivers
# Read MPU6050 WHO_AM_I (should return 0x68)
$ i2cget -y 1 0x68 0x75
0x68
Comments