4. I2C驱动框架(Linux内核)
好,咱们今天来聊聊Linux内核里的I2C驱动框架。说实话,刚接触内核驱动的时候,我也被那一堆结构体搞得晕头转向。但后来我发现,只要抓住三个核心角色——适配器、设备、驱动——整个框架就清晰了。
你可以把I2C子系统想象成一个总线管理系统。适配器就是总线控制器,负责物理信号的收发;设备就是挂在总线上的那些传感器、EEPROM;驱动就是操作设备的软件逻辑。这三者通过内核的注册机制绑定在一起。
核心思想:Linux I2C子系统将硬件操作抽象为三层——i2c_adapter(适配器层)、i2c_client(设备层)、i2c_driver(驱动层)。驱动开发者主要关注后两者。
4.1 子系统架构概览
先看一张整体架构图。我习惯用这种图来理解代码的层次关系。
从这张图你能看到,数据流是从用户空间往下,经过驱动层、核心层、适配器层,最终到达硬件。反过来,硬件中断或数据到达时,也是沿着这条路往上走。
4.2 核心数据结构详解
这三个结构体是I2C驱动的基石。我当年第一次看源码时,花了整整一个下午才理清它们的关系。咱们一个一个来拆解。
4.2.1 i2c_adapter —— 总线控制器
i2c_adapter 代表一个I2C总线控制器。它负责物理层的时序生成、起始/停止信号、ACK/NACK检测等。说白了,它就是那个在硬件上“拉高拉低SDA/SCL”的家伙。
struct i2c_adapter {
struct module *owner;
unsigned int class; // 适配器支持的设备类型
const struct i2c_algorithm *algo; // 算法:收发消息的具体实现
void *algo_data;
struct device dev; // 内核设备模型
int nr; // 总线编号
struct list_head userspace_clients;
// ... 其他字段
};
关键字段是 algo,它指向 i2c_algorithm 结构体,里面定义了 master_xfer() 函数指针。这个函数就是实际发送和接收I2C数据的地方。
避坑指南:我曾经在一个项目里遇到适配器注册失败的问题,折腾了两天才发现是 class 字段没设置。如果你想让用户空间通过 /dev/i2c-N 访问设备,记得把 class 设为 I2C_CLASS_HWMON 或 I2C_CLASS_SPD。
4.2.2 i2c_client —— 从设备
i2c_client 代表挂在总线上的一个具体设备。每个I2C从设备都有一个唯一的7位或10位地址,这个地址就存在 addr 字段里。
struct i2c_client {
unsigned short flags; // 标志位,如 10 位地址
unsigned short addr; // 从设备地址(7位或10位)
char name[I2C_NAME_SIZE]; // 设备名称
struct i2c_adapter *adapter;// 所属适配器
struct device dev; // 内核设备模型
int irq; // 中断号(如果有)
struct list_head detected; // 用于设备检测链表
};
这个结构体通常在设备树或板级文件中静态定义,或者在驱动探测时动态创建。我个人习惯在设备树里声明,这样代码更干净。
4.2.3 i2c_driver —— 设备驱动
i2c_driver 是驱动开发者最常打交道的东西。它定义了驱动如何与设备匹配、如何初始化、如何卸载。
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
void (*shutdown)(struct i2c_client *client);
struct device_driver driver; // 内核驱动模型
const struct i2c_device_id *id_table; // 设备ID匹配表
const struct of_device_id *of_match_table; // 设备树匹配表
// ... 其他回调
};
你想想看,内核怎么知道你的驱动该驱动哪个设备?就是通过 id_table 或 of_match_table 来匹配的。匹配成功后,probe 函数被调用,你在里面做初始化、注册字符设备、创建sysfs文件等操作。
4.3 数据结构关系图
为了让你更直观地理解这三个结构体怎么协作,我画了一张关系图。
4.4 驱动注册与注销流程
理解了数据结构,接下来就是怎么用它们。驱动注册和注销的流程其实很固定,我总结了一个步骤表。
| 步骤 | 操作 | 关键函数/宏 | 说明 |
|---|---|---|---|
| 1 | 定义 i2c_driver 结构体 | struct i2c_driver |
填充 probe、remove、id_table |
| 2 | 定义设备ID表 | i2c_device_id |
用于传统匹配方式 |
| 3 | 定义设备树匹配表 | of_device_id |
用于设备树匹配(推荐) |
| 4 | 注册驱动 | i2c_add_driver() |
宏展开为 i2c_register_driver() |
| 5 | 注销驱动 | i2c_del_driver() |
在模块卸载时调用 |
嗯,这里要注意:i2c_add_driver() 是一个宏,它帮你做了模块所有权设置和注册两步操作。我建议你直接用这个宏,少写几行代码,也少犯错误。
4.5 一个完整的驱动骨架
光说不练假把式。我给你写一个最简单的I2C驱动骨架,你可以直接拿去改。
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
// 设备树匹配表
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "mycompany,my-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
// 传统ID匹配表
static const struct i2c_device_id my_sensor_id[] = {
{ "my-sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_sensor_id);
// probe 函数:设备匹配成功后调用
static int my_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
dev_info(&client->dev, "My sensor probed, addr=0x%02x\n", client->addr);
// 在这里做:分配私有数据、初始化硬件、注册设备等
return 0;
}
// remove 函数:设备移除时调用
static int my_sensor_remove(struct i2c_client *client)
{
dev_info(&client->dev, "My sensor removed\n");
// 在这里做:释放资源、注销设备等
return 0;
}
// 定义 i2c_driver
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = "my-sensor",
.of_match_table = my_sensor_of_match,
},
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.id_table = my_sensor_id,
};
// 模块入口/出口
module_i2c_driver(my_sensor_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C sensor driver");
MODULE_LICENSE("GPL");
注意:这个骨架里 module_i2c_driver() 宏同时完成了 module_init 和 module_exit 的定义。如果你需要额外的初始化逻辑(比如注册多个驱动),可以手动写 module_init 和 module_exit 函数。
4.6 匹配机制:驱动怎么找到设备?
内核通过两种方式匹配驱动和设备:
- 设备树匹配:内核遍历
of_match_table,与设备树节点中的compatible属性比较。这是目前最主流的方式。 - ID表匹配:内核遍历
id_table,与板级文件或ACPI中声明的设备名称比较。这种方式在旧内核或非设备树平台中常见。
匹配顺序是:先查设备树,再查ID表。如果都没匹配上,驱动就不会被调用。
个人经验:我曾经调试过一个驱动,死活不进入 probe。最后发现是设备树里 compatible 字符串写错了——多了一个空格。内核匹配是精确字符串比较,多一个空格都不行。所以,检查设备树时,建议用 cat /proc/device-tree/.../compatible 看看实际内容。
4.7 小结
这一章我们拆解了Linux I2C子系统的三个核心数据结构。记住:i2c_adapter 是总线控制器,i2c_client 是从设备,i2c_driver 是驱动逻辑。驱动开发的核心就是写好 i2c_driver,填好匹配表,实现 probe 和 remove。