从零写OS内核 | I2C 总线——嵌入式系统的血管
你有没有想过:一块陀螺仪芯片或触摸屏,是怎么和主控芯片通信的? 它们不像 USB 那样复杂,也没有 PCIe 那么庞大的带宽——它们用的是 I2C。
I2C(Inter-Integrated Circuit)是嵌入式系统中最常见的低速总线,只需要两根线(SCL + SDA)就能挂多个设备。本文从协议原理讲到 Linux 实战,帮你建立完整的 I2C 调试能力。
一、I2C 协议:两根线怎么传数据
I2C 是同步串行协议,只有两根线:
- SCL(Serial Clock):时钟线,主设备提供
- SDA(Serial Data):数据线,双向半双工
Master Slave 1 Slave 2
│ │ │
─────────┼────────────────────────────────┼─────────────────┼───── SCL
│ │ │
─────────┼────────────────────────────────┼─────────────────┼───── SDA
│ │ │
开始条件 │ │
写地址 0x68 (7位) + W(0) │ │
←─── ACK ──── │ │
写寄存器地址 0x43 │ │
←─── ACK ──── │ │
写数据 0x00 │ │
←─── ACK ──── │ │
停止条件 │ │时序细节:
- Start(S):SCL 高时 SDA 拉低
- Address + R/W:7 位地址 + 1 位读写位(共 8 位)
- ACK:从设备在第 9 个时钟拉低 SDA 表示应答
- Data:每字节后跟 ACK
- Stop(P):SCL 高时 SDA 拉高
I2C 地址是 7 位的,但传输时左移 1 位 + R/W 位,所以实际传输 8 位:
# MPU6050 陀螺仪 I2C 地址 0x68
# 写操作:地址左移 + 0 = 0xD0
# 读操作:地址左移 + 1 = 0xD1二、主从架构和地址冲突
I2C 是单主多从总线,一个主设备可以驱动多个从设备,每个从设备有唯一的 7 位地址。
常见地址规则:
- 从设备地址由厂商固定,或通过引脚接地/接高电平配置
- 有些芯片有两组地址(AD0 引脚不同)
- I2C 地址不允许重复(冲突)
# Linux 上用 i2cdetect 扫描总线上的所有设备
$ 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: -- -- -- -- -- -- -- 50 -- -- -- -- -- -- -- -- -- ← EEPROM
60: -- -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- ← MPU6050
70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 77 ← BMP280三、SMBus vs I2C:它们的区别
SMBus(System Management Bus)是 I2C 的一个子集,广泛用于笔记本电脑的硬件监控:
| 特性 | I2C | SMBus |
|---|---|---|
| 地址格式 | 7 位 + 10 位扩展 | 7 位 |
| 最大频率 | 100kHz/400kHz/3.4MHz | 100kHz |
| 传输超时 | 无定义 | 有(35ms) |
| 典型应用 | 传感器、EEPROM | 电池、EC、温感 |
Linux 的 i2c-dev 同时支持 I2C 和 SMBus 协议。
四、Linux i2c-dev 驱动架构
Linux 为用户空间提供了 /dev/i2c-N 设备文件,通过 ioctl 可以直接操作 I2C 总线:
用户程序(i2c-tools / 自定义程序)
│
▼ ioctl(I2C_SLAVE/I2C_SMBUS)
/dev/i2c-1
│
▼
┌─────────────────────┐
│ i2c-dev.ko │ ← 字符设备层
└──────────┬──────────┘
│ i2c_client / i2c_adapter
┌─────────┴─────────────┐
│ i2c-core.ko │ ← 总线抽象层
└──────────┬───────────┘
│ i2c_algorithm
┌──────────┴───────────────┐
│ i2c-pxa / i2c-i801 │ ← 控制器驱动
└───────────────────────────┘实战命令:
# 1. 查看系统有几条 I2C 总线
$ ls /sys/bus/i2c/devices/
i2c-0 i2c-1 i2c-2
# 2. 扫描总线上的设备(I2C-1)
$ i2cdetect -y 1
# 3. 读取设备寄存器(MPU6050 示例,读取 WHO_AM_I = 0x75)
$ i2cget -y 1 0x68 0x75
0x68 # 返回 0x68 = MPU6050 ✓
# 4. 写设备寄存器
$ i2cset -y 1 0x68 0x6B 0x00 # PWR_MGMT_1 = 0(唤醒 MPU6050)
# 5. 读取多字节(读取 MPU6050 加速度计数据)
$ i2cdump -y 1 0x68
# 会 dump 出所有寄存器的值
# 6. 查看总线上已加载的驱动
$ ls /sys/bus/i2c/devices/i2c-1/
device driver name new_device uevent
$ cat /sys/bus/i2c/devices/i2c-1/name
AST2600 i2c五、wandos 的 I2C 实现
wandos 目前在 arch/x86/core/i2c.cpp 实现了模拟 I2C bitbang(GPIO 模拟 I2C 时序):
// wandos i2c bitbang(简化)
void i2c_start() {
SDA_HIGH; SCL_HIGH;
udelay(4);
SDA_LOW; // SCL 高时 SDA 拉低 = START
SCL_LOW;
}
void i2c_stop() {
SCL_LOW; SDA_LOW;
SCL_HIGH;
udelay(4);
SDA_HIGH; // SCL 高时 SDA 拉高 = STOP
}
uint8_t i2c_write_byte(uint8_t data) {
for (int i = 7; i >= 0; i--) {
SCL_LOW;
if (data & (1 << i)) SDA_HIGH; else SDA_LOW;
SCL_HIGH; // SCL 上升沿数据采样
}
SCL_LOW;
SDA_HIGH; // 释放 SDA 准备收 ACK
SCL_HIGH;
uint8_t ack = SDA_READ; // 读 ACK
SCL_LOW;
return ack;
}wandos I2C 的局限:
- 纯软件模拟,性能差
- 只支持主设备,不支持多主
- 尚未对接真实的 I2C 控制器硬件
公众号 footer
相关阅读:
- 《从零写OS内核 | SPI 总线——高速串行通信》(349)
- 《从零写OS内核 | 总线与地址空间——CPU 是怎么找到每个设备的》(343)
动手环节:
在你的嵌入式设备上运行 i2cdetect -y N(N=总线编号),看看能扫到哪些 I2C 设备。如果有树莓派,可以接一个 MPU6050 陀螺仪模块验证。
仓库地址:
github.com/golang12306/os-kernel-from-scratch
关注公众号「AI不着急」,回复”资料”获取内核学习路线图。
评论