3. I2C驱动框架:Linux I2C子系统架构、I2C设备驱动注册流程、i2c_transfer与SMBus接口
好,我们进入第三章。这一章是Sensor驱动开发的核心基础——I2C驱动框架。说实话,我见过不少工程师,写Sensor驱动时直接照搬别人的模板,一旦遇到I2C通信异常就抓瞎了。说白了,就是没搞懂Linux I2C子系统到底是怎么工作的。
这一章,我会带你从架构到实战,把I2C驱动框架彻底讲透。
3.1 Linux I2C子系统架构
先看整体架构。Linux I2C子系统分为三层:
- I2C核心层:提供总线注册、设备注册、驱动注册等通用接口
- I2C总线驱动层:也就是适配器驱动,直接操作硬件I2C控制器
- I2C设备驱动层:我们写Sensor驱动主要待的地方
我刚开始接触这个框架时,总觉得分层太多,有点绕。后来在项目中调试一个温度传感器,发现只要把这三层的职责搞清楚,定位问题就快多了。
核心思想:I2C核心层负责"牵线搭桥",总线驱动负责"怎么发",设备驱动负责"发什么"。
下面这张图,我建议你多看几遍,它把整个I2C子系统的数据流和依赖关系都画出来了。
3.2 I2C设备驱动注册流程
写一个I2C设备驱动,核心就是注册一个i2c_driver结构体。我习惯把这个流程拆成四步:
- 定义i2c_driver结构体:填充probe、remove、id_table等回调
- 实现probe函数:在这里初始化设备、注册字符设备或输入设备
- 实现remove函数:释放资源、注销设备
- 注册/注销驱动:module_i2c_driver宏一键搞定
我的习惯:probe函数里一定要先检查i2c_check_functionality,确认适配器支持你需要的功能。我曾经在一个项目里没做这个检查,结果适配器不支持SMBus块读,调试了整整两天。
来看一个典型的Sensor驱动框架代码:
#include <linux/i2c.h>
#include <linux/module.h>
static int sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct sensor_data *data;
int ret;
dev_info(&client->dev, "Sensor probe start\n");
// 1. 检查适配器功能
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA |
I2C_FUNC_SMBUS_I2C_BLOCK)) {
dev_err(&client->dev, "Adapter不支持所需功能\n");
return -ENODEV;
}
// 2. 分配私有数据结构
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 3. 保存client指针
data->client = client;
i2c_set_clientdata(client, data);
// 4. 初始化设备(读ID、配置寄存器等)
ret = sensor_init_hw(client);
if (ret)
return ret;
// 5. 注册其他子系统(如IIO、输入子系统等)
// ...
dev_info(&client->dev, "Sensor probe success\n");
return 0;
}
static int sensor_remove(struct i2c_client *client)
{
struct sensor_data *data = i2c_get_clientdata(client);
// 1. 注销其他子系统
// 2. 关闭设备
sensor_power_down(client);
dev_info(&client->dev, "Sensor removed\n");
return 0;
}
static const struct i2c_device_id sensor_id[] = {
{ "sensor_xxx", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, sensor_id);
#ifdef CONFIG_OF
static const struct of_device_id sensor_of_match[] = {
{ .compatible = "vendor,sensor-xxx" },
{ }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);
#endif
static struct i2c_driver sensor_driver = {
.driver = {
.name = "sensor_xxx",
.of_match_table = of_match_ptr(sensor_of_match),
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id,
};
module_i2c_driver(sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("I2C Sensor Driver");
注意:of_match_table和id_table至少要提供一个。设备树匹配优先于id_table匹配。如果两个都提供了,内核会先查设备树。
3.3 i2c_transfer与SMBus接口
驱动注册好了,接下来就是怎么和Sensor通信了。Linux I2C子系统提供了两套API:
| 接口类型 | 核心函数 | 适用场景 | 特点 |
|---|---|---|---|
| i2c_transfer | i2c_transfer(adapter, msgs, num) |
任意I2C通信,支持复合消息 | 灵活,可组合读写,需要构造i2c_msg |
| SMBus接口 | i2c_smbus_read_byte_data()等 |
寄存器读写、简单命令 | 封装好,使用简单,但功能受限 |
我个人更倾向于在Sensor驱动里用SMBus接口,因为它封装了大部分细节。但遇到一些特殊时序要求时,就得用i2c_transfer了。
3.3.1 SMBus接口实战
SMBus接口其实就是对i2c_transfer的封装。最常用的几个:
i2c_smbus_read_byte_data(client, reg):读一个8位寄存器i2c_smbus_write_byte_data(client, reg, value):写一个8位寄存器i2c_smbus_read_word_data(client, reg):读一个16位寄存器i2c_smbus_read_i2c_block_data(client, reg, len, buf):块读i2c_smbus_write_i2c_block_data(client, reg, len, buf):块写
经验之谈:很多Sensor的数据手册会写"支持I2C快速模式",但实际用SMBus接口完全没问题。SMBus是I2C的子集,时序更严格。如果你的Sensor能跑SMBus,那I2C肯定也能跑。
看一个实际读Sensor ID的例子:
static int sensor_read_id(struct i2c_client *client)
{
int ret;
u8 chip_id;
ret = i2c_smbus_read_byte_data(client, SENSOR_REG_CHIP_ID);
if (ret < 0) {
dev_err(&client->dev, "读取芯片ID失败: %d\n", ret);
return ret;
}
chip_id = ret & 0xFF;
dev_info(&client->dev, "芯片ID: 0x%02x\n", chip_id);
if (chip_id != EXPECTED_CHIP_ID) {
dev_err(&client->dev, "芯片ID不匹配: 期望0x%02x, 实际0x%02x\n",
EXPECTED_CHIP_ID, chip_id);
return -ENODEV;
}
return 0;
}
3.3.2 i2c_transfer高级用法
什么时候必须用i2c_transfer?我遇到过两种情况:
- 需要组合消息:比如先发命令再读数据,中间不产生STOP信号
- 需要自定义时序:比如某些Sensor要求先写寄存器地址,再写数据,但地址和数据之间要有特殊延迟
来看一个组合消息的例子:
static int sensor_read_multiple_regs(struct i2c_client *client,
u8 start_reg, u8 *buf, int len)
{
struct i2c_msg msgs[2];
int ret;
// 消息0:写寄存器起始地址
msgs[0].addr = client->addr;
msgs[0].flags = 0; // 写
msgs[0].len = 1;
msgs[0].buf = &start_reg;
// 消息1:读数据
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD; // 读
msgs[1].len = len;
msgs[1].buf = buf;
// 发送组合消息
ret = i2c_transfer(client->adapter, msgs, 2);
if (ret != 2) {
dev_err(&client->dev, "i2c_transfer失败: %d\n", ret);
return ret < 0 ? ret : -EIO;
}
return 0;
}
避坑指南:i2c_transfer返回的是成功传输的消息数量,不是字节数。如果返回1,说明第一条写成功了,但第二条读失败了。我曾经在这里踩过坑,一直以为返回值是字节数,结果数据全是错的。
3.4 调试技巧与常见问题
最后分享几个我在项目中积累的调试经验:
- 先确认硬件地址:用
i2cdetect工具扫描总线,确认Sensor的I2C地址是否正确。我遇到过好几次,原理图上写的地址和实际焊的地址不一样。 - 检查适配器功能:
i2c_check_functionality一定要调用,否则后面调用SMBus接口可能返回-EOPNOTSUPP。 - 善用dev_dbg:在probe和读写函数里加
dev_dbg打印,动态调试时打开,能快速定位问题。 - 注意时序:有些Sensor对I2C时序很敏感,尤其是上电后需要等待一段时间才能通信。我习惯在probe里加一个
msleep。
我的调试三板斧:1) i2cdetect看设备在不在;2) i2cget读寄存器看返回值;3) 示波器抓波形。这三步走完,90%的问题都能定位。
好了,这一章的内容就到这里。I2C驱动框架是Sensor驱动开发的基础,把这三层架构和两种通信接口搞明白,后面写具体Sensor驱动时就会顺手很多。