从零写OS内核|348:I2C 总线——嵌入式系统的血管
摘要:I2C 是嵌入式系统中最常见的低速总线,只要两根线就能挂大量设备——传感器、EEPROM、RTC、触摸屏控制器……本篇文章从 I2C 协议原理、主从架构、到 Linux i2c-tools 实战,带你掌握这条”系统血管”的用法。
1. I2C 协议:为什么两根线能挂那么多设备
1.1 物理层:开漏 + 上拉
I2C 只有两根信号线:
- SCL(Serial Clock):时钟线
- SDA(Serial Data):数据线
这两根线都是开漏输出 + 上拉电阻,意味着:
VCC ──────[上拉 4.7KΩ] ──── SDA/SCL
│
┌──────────────────┘
↓
┌────┴────┐
│ Driver │ (开漏:只能拉低,或Hi-Z高阻态)
└─────────┘开漏的好处是:任何一个设备都可以把线拉低,但没有人能强制拉高。这样避免了多主设备竞争时的高电平冲突。即使主设备数量不确定,也能安全共享总线。
1.2 地址:7 位还是 10 位?
I2C 从设备地址是 7 位(或者 10 位扩展模式)。7 位地址意味着理论上最多挂 128 个设备,但地址 0000 000 保留,所以实际最多 112 个。
7位地址格式:
[A6][A5][A4][A3][A2][A1][A0][R/W]
示例:MPU6050 地址 = 0x68 (110 1000)
写操作:0xD0 (0x68 << 1 | 0)
读操作:0xD1 (0x68 << 1 | 1)注意地址是左移 1 位后加 R/W 位,所以很多芯片手册会写”0x68″但实际读写地址是 0xD0/0xD1。
1.3 开始/停止条件
SCL=高 + SDA=下降沿 → START
SCL=高 + SDA=上升沿 → STOP
START
↓
┌──────┐ ┌──────────┐
│ │ │ │
SDA ── ─────── ────────── ← STOP
└─────────────────────┘1.4 传输格式:每个字节后跟 ACK
Master → Slave Master ← Slave
[START][ADDR+W][ACK][DATA][ACK][DATA][ACK]...[STOP]
↑
Slave 可以拉低 SDA (ACK)
如果 NACK,Master 停止发送每个字节(8 bits)后,接收方必须 pull SDA low 表示 ACK(除非是最后一个字节或 NACK 情况)。
1.5 I2C 读写时序图
写一个字节:
Master SCL SDA
─── ───
START: ──┐ ──┐
│ └──┘ (SDA fall while SCL high)
ADDR+W: │ D7 D6 D5 D4 D3 D2 D1 D0
│ ────────────────────────────
ACK: ─┘ (slave pull Sda low)
DATA: │ D7 D6 D5 D4 D3 D2 D1 D0
│ ────────────────────────────
ACK: ─┘ (slave pull Sda low)
STOP: │ (SDA rise while SCL high)
└─────────────────────────────读一个字节:
Master SCL SDA
─── ───
START: ──┐ ──┐
ADDR+R: │ D7 D6 D5 D4 D3 D2 D1 D0
│ ────────────────────────────
ACK: ─┘ (master pull Sda low during ACK)
DATA: │ D7 D6 D5 D4 D3 D2 D1 D0
│◄───────────────────────────
ACK: ─┘ (master pull Sda low)
STOP: │ (SDA rise while SCL high)
└─────────────────────────────2. Linux i2c-tools:快速调试神器
2.1 安装与查看总线
$ sudo apt install i2c-tools -y
$ ls /dev/i2c-*
/dev/i2c-0 /dev/i2c-1 /dev/i2c-2
$ i2cdetect -l
i2c-1 i2c 808622C1:00 I2C adapter
i2c-2 i2c 808622C1:01 I2C adapter
i2c-0 i2c SMBus PIIX4 adapter at 9080 SMBus adapter2.2 扫描总线上的设备
$ sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 4b --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --地址 0x68(MPU6050)和 0x4b(光照传感器)在总线上。
2.3 读写寄存器
MPU6050 是 6 轴加速度计 + 陀螺仪,最常用的 I2C 传感器之一:
# 读取 MPU6050 WHO_AM_I 寄存器 (0x75)
$ sudo i2cget -y 1 0x68 0x75
0x68
# 返回 0x68,说明芯片正常(这就是它的默认 WHO_AM_I 值)
# 读取加速度原始值(ACCEL_XOUT_H = 0x3B, ACCEL_XOUT_L = 0x3C)
$ sudo i2cget -y 1 0x68 0x3B w
0x1e00 # 大端序,返回 0x001e(X轴加速度高字节)
# 写入:配置采样率分频器 (SMPLRT_DIV = 0x19)
$ sudo i2cset -y 1 0x68 0x19 0x07
# 读取多个寄存器(burst read)
$ sudo i2cdump -y 1 0x68 0x3B i
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: 1e 00 3d 00 00 00 80 3f 00 00 00 00 00 00 00 00
...2.4 用 python py-smbus 读写
import smbus2
bus = smbus2.SMBus(1)
addr = 0x68
# 读取 WHO_AM_I
who = bus.read_byte_data(addr, 0x75)
print(f"MPU6050 WHO_AM_I = 0x{who:02x}")
# 读取加速度 X/Y/Z
accel_x = bus.read_word_data(addr, 0x3B) # ACCEL_XOUT_H
print(f"Accel X = {accel_x}")
# 配置 MPU6050
bus.write_byte_data(addr, 0x19, 0x07) # SMPLRT_DIV = 7
bus.write_byte_data(addr, 0x1B, 0x00) # GYRO_CONFIG = 0 (250 deg/s)
bus.close()3. Linux 内核 I2C 子系统
3.1 架构分层
用户空间(i2c-tools, 应用程序)
↓ I2C ioctl / SMBUS
内核 I2C Core (i2c-core.c)
↓
I2C Adapter Driver (比如 i2c-piix4, i2c-i801, i2c-i2c-imx)
↓
硬件(南桥 PIIX4 / I2C 控制器 / SoC I2C)I2C Core 负责注册 I2CAdapter、I2CDriver,维护 device 列表和 driver 列表,实现 device-driver match。
3.2 注册 I2C 设备驱动
// 在内核中注册一个 I2C 驱动
static int mysensor_probe(struct i2c_client *client,
const struct i2c_device_id *id) {
dev_info(&client->dev, "MPU6050 probed at %sn", client->adapter->name);
return 0;
}
static int mysensor_remove(struct i2c_client *client) {
return 0;
}
static const struct i2c_device_id mpu6050_id[] = {
{ "mpu6050", 0 },
{ },
};
MODULE_DEVICE_TABLE(i2c, mpu5xxx_id);
static struct i2c_driver mpu6050_driver = {
.driver = {
.name = "mpu6050",
},
.probe = mysensor_probe,
.remove = mysensor_remove,
.id_table = mpu6050_id,
};
module_i2c_driver(mpu6050_driver);3.3 在 DTS 中声明 I2C 设备
// 设备树示例
&i2c1 {
status = "okay";
mpu6050@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
};
};4. I2C 常见问题与调试
4.1 总线拉死(SCL 或 SDA 一直低)
如果某个从设备发送了数据但没发送 ACK,主设备可能卡在等待 ACK 状态。解决方法:
# 尝试用 i2cdetect 会看到 "UU"(地址被占用)
$ sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- --
...
60: -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- -- --
↑
68 显示为 UU 表示设备正在被某个驱动使用强制复位 I2C 总线(手动 clock 9 次让从设备释放 SDA):
# 使用 i2cdetect 的 -f 强制扫描,跳过 ACK 检测
$ sudo i2cdetect -f -y 14.2 速率问题
# 查看 I2C 总线速率
$ cat /sys/bus/i2c/devices/i2c-1/speed
400000 # 400KHz (Fast Mode)
# 或者查看当前配置
$ sudo cat /sys/kernel/debug/i2c/i2c-1/state
i2c-1: polling 100 kHz可以在加载驱动时设置速率:i2c_bus=1,400000 或在设备树中配置 clock-frequency = <400000>;。
5. wandos 对比:简化的 I2C 模拟
wandos 目前的 I2C 实现非常简化:没有完整的中断驱动 I2C 控制器驱动,也没有设备树支持。wandos 的外设模型中,I2C 总线通过 GPIO bit-banging 实现(软件模拟 I2C 时序),主要用来读取 MPU6050 等传感器数据。wandos 未来的方向是实现标准的 Linux I2C 子系统接口,使传感器驱动可以在 wandos 和 Linux 之间共享代码。
6. I2C vs SPI:选哪个?
| 特性 | I2C | SPI |
|---|---|---|
| 线数 | 2 (SDA+SCL) | 4+ (MOSI/MISO/CLK/CS) |
| 速率 | 100K/400K/3.4M | 几十 MHz |
| 地址 | 7/10 位(总线共享) | 每个设备独立 CS |
| 协议 | 有 ACK/NACK | 无协议层(纯数据流) |
| 多主 | 支持 | 不支持 |
| 适合 | 传感器、EEPROM、RTC | 大容量 Flash、显示屏、高速率 |
公众号底部:
📢 关注公众号【内核栈】,获取更多 OS/内核干货
💡 本文属于「从零写OS内核」系列,欢迎回顾:
- 篇 347:《显示器——从 Framebuffer 到 GPU》
- 篇 346:《键盘——按键是怎么变成字符的》
- 篇 345:《USB 协议》
🔗 回复「I2C」获取 I2C 协议规范和 MPU6050 数据手册
作者:内核栈 | 原创出品
本文永久链接:https://kernelstack.dev/348-i2c
评论