3. I2C协议栈实现:从总线信号到驱动落地
I2C,搞嵌入式的没人不知道它。两根线,一主多从,简单得让人想笑。但真要在项目里把I2C驱动写得稳、写得快,光会调库可不够。今天我就带你从总线协议到底层适配器,再到从设备驱动,把整个I2C协议栈捋一遍。
3.1 I2C总线协议:两根线上的舞蹈
I2C只有两条线:SCL(时钟)和SDA(数据)。所有设备都挂在这两根线上,靠地址区分。你想想看,一个MCU挂十几个传感器,只用两根线,省GPIO省到极致。
但省线的代价是什么?协议时序必须精确。我早期有个项目,I2C挂了个温湿度传感器,死活读不到数据。示波器一抓,发现SCL高电平时间偏短,从机不认。嗯,这就是时序的坑。
3.1.1 基本时序单元
I2C的每个操作都由几个基本信号组成:
| 信号 | 描述 | 时序要求 |
|---|---|---|
| 起始条件 (S) | SCL高电平时,SDA从高变低 | tHD:STA ≥ 4μs (100kHz) |
| 停止条件 (P) | SCL高电平时,SDA从低变高 | tSU:STO ≥ 4μs |
| 数据采样 | SCL低电平时改变SDA,高电平时采样 | tSU:DAT ≥ 250ns |
| 应答 (ACK) | 第9个SCL周期,从机拉低SDA | 从机必须在SCL高前准备好 |
这里有个关键点:数据只能在SCL低电平时变化。SCL高电平时SDA必须稳定。我见过不少新手写的软件模拟I2C,在SCL高电平期间动了SDA,结果从机直接当成起始或停止条件——数据全乱套。
3.1.2 完整读写流程
一个典型的I2C写操作流程:
- 主机发送起始条件
- 主机发送7位从机地址 + 写位(0)
- 从机应答ACK
- 主机发送寄存器地址
- 从机应答ACK
- 主机发送数据字节,每字节后等待ACK
- 主机发送停止条件
读操作稍微复杂一点,需要先写寄存器地址,再重启起始条件,然后读数据:
// I2C读操作伪代码
start();
write(addr << 1 | 0); // 写地址+写位
wait_ack();
write(reg_addr); // 写寄存器地址
wait_ack();
repeated_start(); // 重启条件
write(addr << 1 | 1); // 写地址+读位
wait_ack();
data = read(); // 读数据
send_nack(); // 主机发送非应答
stop();
3.2 Linux I2C核心层:别重复造轮子
Linux内核已经把I2C协议栈分成了三层:核心层、适配器驱动、从设备驱动。你写驱动时,大部分时候只需要关心从设备驱动这一层。
3.2.1 核心层数据结构
核心层主要维护三个结构体:
struct i2c_adapter:代表一个I2C控制器,包含算法函数指针struct i2c_algorithm:master_xfer等底层传输函数struct i2c_client:代表一个从设备,挂载在适配器上
它们的关系是这样的:
3.2.2 适配器驱动:连接硬件与核心层
适配器驱动负责实现i2c_algorithm中的master_xfer函数。这个函数接收一个i2c_msg数组,里面包含读写标志、数据缓冲区和长度。
我曾经调试过一个I2C适配器驱动,发现master_xfer里每次只处理一条消息。但上层驱动有时会发多条消息(比如先写地址再读数据),适配器必须支持消息合并。如果适配器不支持,核心层会自动拆成多次传输,但效率会下降。
3.3 从设备驱动开发:实战出真知
好了,到了最核心的部分——写一个I2C从设备驱动。我以一个常见的温度传感器为例,带你走一遍完整流程。
3.3.1 驱动框架搭建
先定义设备结构体和设备ID表:
#include <linux/i2c.h>
#include <linux/module.h>
struct temp_sensor_data {
struct i2c_client *client;
int temperature;
};
static const struct i2c_device_id temp_sensor_id[] = {
{ "tmp117", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, temp_sensor_id);
然后实现probe和remove函数:
static int temp_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct temp_sensor_data *data;
int ret;
// 检查设备是否存在
ret = i2c_smbus_read_byte_data(client, 0x0F); // 读设备ID寄存器
if (ret < 0) {
dev_err(&client->dev, "device not found\n");
return -ENODEV;
}
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
dev_info(&client->dev, "temp sensor probed, id=0x%02x\n", ret);
return 0;
}
static void temp_sensor_remove(struct i2c_client *client)
{
dev_info(&client->dev, "temp sensor removed\n");
}
3.3.2 数据读写:避开常见的坑
读写数据时,我建议优先使用i2c_smbus_*系列函数。它们封装了重复起始条件,代码更简洁:
static int temp_sensor_read_temp(struct i2c_client *client)
{
int temp_raw;
// 读温度寄存器 (0x00),16位数据
temp_raw = i2c_smbus_read_word_swapped(client, 0x00);
if (temp_raw < 0)
return temp_raw;
// TMP117: 分辨率 0.0078125°C/bit
return temp_raw * 78125 / 10000;
}
这里有个细节:i2c_smbus_read_word_swapped会自动交换字节序。为什么?因为I2C设备通常是大端传输,而ARM/Little Endian处理器需要转换。我刚开始写驱动时没注意这个,读出来的温度值直接除以16,结果全是乱码。后来查了芯片手册才发现字节序问题。
3.3.3 注册驱动
最后,把驱动注册到内核:
static struct i2c_driver temp_sensor_driver = {
.driver = {
.name = "tmp117",
.of_match_table = of_match_ptr(temp_sensor_of_match),
},
.probe = temp_sensor_probe,
.remove = temp_sensor_remove,
.id_table = temp_sensor_id,
};
module_i2c_driver(temp_sensor_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("TMP117 Temperature Sensor Driver");
MODULE_LICENSE("GPL");
- probe函数里一定要做设备存在性检查,别假设设备一定在线
- 使用
devm_*系列函数管理内存,省去手动释放的麻烦 - 注意字节序,I2C设备大多是大端,ARM/Linux默认小端
- 调试时多用
dev_dbg和dev_err,别用printk
3.4 调试技巧:示波器是你的眼睛
写I2C驱动,光看代码不够。我调试I2C问题,第一件事就是抓波形。逻辑分析仪便宜又好用,几百块的就能抓100MHz采样率。
常见问题排查清单:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 无ACK响应 | 地址错误/设备未上电 | 检查地址7位/8位对齐,测VDD |
| 数据全为0xFF | 上拉电阻缺失 | 加4.7kΩ上拉到VDD |
| 偶尔通信失败 | 时序裕量不足 | 降低速率到50kHz测试 |
| 多设备冲突 | 地址重复 | 检查设备地址引脚配置 |
我记得有一次,客户反馈设备每100次通信就有1次失败。抓波形发现SCL上有毛刺,原来是PCB走线太长,信号反射导致的。加了个33pF电容到地,问题解决。嗯,硬件问题有时候比软件更难搞。
I2C协议栈说简单也简单,说复杂也复杂。核心就三点:理解时序、用好核心层、写好从设备驱动。把这三点吃透了,市面上99%的I2C设备你都能搞定。