4. I2C子系统详解:从协议到驱动分离模型
I2C总线,说实话,是嵌入式开发里最常用的低速总线之一。传感器、EEPROM、触摸屏、音频Codec……十有八九都挂在I2C上。我刚开始做驱动时,总觉得I2C很简单——不就两根线嘛。直到被一个时序问题折腾了两天,才老老实实把协议细节啃了一遍。
这一章,咱们把I2C子系统拆开揉碎。从硬件协议到Linux内核的软件架构,再到驱动怎么分层、怎么解耦,一条线讲清楚。
4.1 I2C总线协议基础
I2C只有两根线:SCL(时钟)和SDA(数据)。所有设备都挂在这两根线上,靠地址区分。主设备发起通信,从设备响应。
核心要点:I2C是同步、半双工、多主从的总线。标准模式100kHz,快速模式400kHz,高速模式3.4MHz。传感器驱动里,100kHz和400kHz最常见。
4.1.1 通信时序
一个完整的I2C传输包含以下阶段:
- 起始条件(START):SCL高电平时,SDA从高变低。
- 从设备地址 + 读写位:主设备发送7位地址(或10位),最后一位是R/W。
- 应答(ACK/NACK):从设备拉低SDA表示应答。
- 数据字节:每字节8位,MSB先发,后面跟应答位。
- 停止条件(STOP):SCL高电平时,SDA从低变高。
我遇到过一个问题:某个传感器在400kHz下偶尔丢数据。后来用示波器一看,发现从设备的ACK信号延迟超过了规范。降到100kHz就稳了。嗯,有时候不是代码的问题,是硬件时序余量不够。
经验之谈:调试I2C时,示波器或逻辑分析仪是必备的。别光靠printk,你看不到ACK的波形。
4.1.2 数据格式
I2C的数据传输格式很灵活。传感器驱动里最常用的是这种:
// 写寄存器:先发寄存器地址,再发数据
START + 设备地址(W) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP
// 读寄存器:先写寄存器地址,再重新发起START读数据
START + 设备地址(W) + ACK + 寄存器地址 + ACK +
RESTART + 设备地址(R) + ACK + 数据 + NACK + STOP
说白了,读操作需要一次“重复起始条件”(Repeated START)。很多新手在这里犯错——以为读之前要先发STOP再发START。其实不用,RESTART更高效,也避免了总线被其他主设备抢走。
4.2 Linux I2C核心层
Linux内核把I2C子系统分成了三层。我画了张图,你看一眼就明白了:
这张图你得记住。三层架构的核心思想就是分离——总线驱动只管怎么发数据,设备驱动只管发什么数据。两者通过核心层对接。
4.2.1 核心层关键数据结构
核心层有几个关键结构体,驱动开发时天天打交道:
| 结构体 | 作用 | 关键字段 |
|---|---|---|
i2c_adapter |
代表一个I2C控制器 | algo(算法指针)、nr(总线编号) |
i2c_algorithm |
定义收发数据的底层方法 | master_xfer、smbus_xfer |
i2c_driver |
代表一个设备驱动 | probe、remove、id_table |
i2c_client |
代表一个具体的I2C从设备 | addr(7位地址)、adapter(所属总线) |
注意:i2c_client的地址是7位,左对齐的。很多数据手册写的是8位地址(含读写位),你得右移1位再赋值。我见过有人直接拿8位地址填进去,结果驱动死活找不到设备。
4.3 I2C设备驱动与总线驱动分离模型
这是Linux设备驱动模型在I2C子系统上的具体体现。说白了,就是“谁负责控制硬件,谁负责解析协议”分开。
4.3.1 总线驱动(Adapter Driver)
总线驱动由SoC厂商提供,负责操作I2C控制器的寄存器。它要实现i2c_algorithm中的master_xfer函数。这个函数接收一个i2c_msg数组,然后按照I2C协议在硬件上发出START、地址、数据、STOP。
举个例子,i2c-imx驱动里,master_xfer最终会操作I2C_IADR、I2C_IFDR、I2C_I2DR等寄存器。这部分代码,你作为传感器驱动开发者,基本不用碰。除非你在移植内核到新板子。
4.3.2 设备驱动(Client Driver)
这才是咱们传感器驱动开发的主战场。设备驱动通过核心层提供的API与硬件通信,不关心底层是哪个I2C控制器。
核心API就这几个:
// 最常用的读写接口
int i2c_master_send(struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);
// 带寄存器地址的读写(SMBus风格)
int i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
int i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value);
// 更灵活的传输(支持多个msg)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
我的建议:传感器驱动里,优先用i2c_smbus_xxx系列。它封装了重复起始条件,代码更简洁。如果传感器有特殊时序要求(比如连续读取多个寄存器),再用i2c_transfer自己组装msg。
4.3.3 驱动匹配机制
设备驱动怎么找到对应的硬件设备?Linux用设备树(Device Tree)来完成匹配。在设备树里声明I2C从设备,驱动通过of_match_table匹配。
设备树片段示例:
&i2c1 {
status = "okay";
clock-frequency = <400000>;
bmp280@76 {
compatible = "bosch,bmp280";
reg = <0x76>;
};
};
驱动里对应的匹配表:
static const struct of_device_id bmp280_of_match[] = {
{ .compatible = "bosch,bmp280" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, bmp280_of_match);
static struct i2c_driver bmp280_driver = {
.driver = {
.name = "bmp280",
.of_match_table = bmp280_of_match,
},
.probe = bmp280_probe,
.remove = bmp280_remove,
.id_table = bmp280_id, // 传统ID匹配,用于非设备树平台
};
匹配成功后,核心层调用驱动的probe函数。你在probe里做初始化、注册输入设备或IIO设备、创建sysfs文件等。
4.4 实战:写一个完整的I2C传感器驱动骨架
光说不练假把式。咱们写一个虚拟温度传感器的驱动骨架,把上面讲的知识点串起来。
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#define TEMP_SENSOR_REG_TEMP 0x00
#define TEMP_SENSOR_REG_CONFIG 0x01
struct temp_sensor_data {
struct i2c_client *client;
int temperature;
};
static int temp_sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
int ret;
ret = i2c_smbus_read_byte_data(client, reg);
if (ret < 0)
return ret;
*val = ret & 0xff;
return 0;
}
static int temp_sensor_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
return i2c_smbus_write_byte_data(client, reg, val);
}
static int temp_sensor_probe(struct i2c_client *client)
{
struct temp_sensor_data *data;
u8 chip_id;
int ret;
// 1. 检测设备是否存在
ret = temp_sensor_read_reg(client, TEMP_SENSOR_REG_CONFIG, &chip_id);
if (ret < 0) {
dev_err(&client->dev, "failed to read config register\n");
return ret;
}
// 2. 分配私有数据
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 3. 初始化设备(比如设置连续转换模式)
temp_sensor_write_reg(client, TEMP_SENSOR_REG_CONFIG, 0x01);
dev_info(&client->dev, "temperature sensor probed, chip_id=0x%02x\n", chip_id);
return 0;
}
static void temp_sensor_remove(struct i2c_client *client)
{
// 关闭设备,释放资源
temp_sensor_write_reg(client, TEMP_SENSOR_REG_CONFIG, 0x00);
dev_info(&client->dev, "temperature sensor removed\n");
}
static const struct i2c_device_id temp_sensor_id[] = {
{ "temp-sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, temp_sensor_id);
static const struct of_device_id temp_sensor_of_match[] = {
{ .compatible = "virtual,temp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, temp_sensor_of_match);
static struct i2c_driver temp_sensor_driver = {
.driver = {
.name = "temp-sensor",
.of_match_table = temp_sensor_of_match,
},
.probe = temp_sensor_probe,
.remove = temp_sensor_remove,
.id_table = temp_sensor_id,
};
module_i2c_driver(temp_sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Virtual Temperature Sensor Driver");
避坑指南:我曾经在probe里忘了调用i2c_set_clientdata,结果remove时拿不到私有数据,导致空指针panic。这个小细节,记牢了。
4.5 调试技巧
I2C驱动调试,我总结了几条实用经验:
- 用i2c-tools验证硬件:在用户空间用
i2cdetect -r 1扫描总线,确认设备地址。再用i2cget和i2cset手动读写寄存器。硬件没问题了,再写驱动。 - 打开内核调试信息:
# echo 0x10 > /sys/module/i2c_core/parameters/debug,可以看到每次传输的细节。 - 检查ACK:如果i2c_transfer返回负值,大概率是从设备没应答。检查地址、电源、上拉电阻。
- 时序问题:用示波器抓SCL和SDA,看START条件、数据位、ACK位是否正常。我遇到过SCL频率偏差太大导致通信失败的案例。
重要提醒:I2C从设备的地址是7位,但很多数据手册写的是8位(含读写位)。比如手册写0xD0,实际7位地址是0x68。填错了,驱动永远probe失败。