4. I2C设备驱动编写入门:模块初始化与退出、i2c_driver结构体填充、probe与remove回调函数实现
好,咱们今天正式动手写I2C设备驱动。说实话,很多初学者一上来就被各种内核框架吓住了。其实I2C设备驱动的骨架就那么几块:模块的加载卸载、驱动结构体的填充、还有probe和remove这两个回调。把这几个搞明白,剩下的就是往里面填业务逻辑了。
我在全志T113上调试第一款I2C传感器时,就是照着这个路子一步步来的。今天我把这套方法论拆开揉碎了讲给你听。
4.1 模块初始化与退出:驱动的“开关”
每个内核模块都有两个入口点:module_init 和 module_exit。说白了,一个管“开机”,一个管“关机”。
核心要点: 在I2C设备驱动中,init函数通常只做一件事——注册I2C驱动。exit函数也只做一件事——注销I2C驱动。
来看一个最简模板:
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/platform_device.h>
static int __init my_i2c_driver_init(void)
{
int ret;
ret = i2c_add_driver(&my_i2c_driver);
if (ret < 0) {
pr_err("Failed to register I2C driver\n");
return ret;
}
pr_info("My I2C driver initialized\n");
return 0;
}
static void __exit my_i2c_driver_exit(void)
{
i2c_del_driver(&my_i2c_driver);
pr_info("My I2C driver exited\n");
}
module_init(my_i2c_driver_init);
module_exit(my_i2c_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple I2C device driver");
这里有个细节我提醒你注意:__init 和 __exit 这两个宏。加了 __init 的函数,在模块加载完成后,内核会把这段代码占用的内存释放掉。我刚开始写驱动时没加这个,结果被代码评审的同事怼了一顿——浪费内存嘛。
我的习惯: 所有只在初始化阶段运行的函数,都加上 __init。同理,只在卸载时运行的函数加 __exit。这不仅是规范,更是对嵌入式系统有限资源的尊重。
4.2 i2c_driver结构体填充:驱动的“身份证”
i2c_driver 是I2C设备驱动的核心结构体。它告诉内核:我这个驱动支持哪些设备、怎么探测、怎么移除。
结构体定义在 include/linux/i2c.h 中,我们只需要关注几个关键字段:
| 字段 | 类型 | 作用 |
|---|---|---|
driver.name |
const char * |
驱动名称,会在 /sys/bus/i2c/drivers/ 下显示 |
id_table |
struct i2c_device_id * |
该驱动支持的设备ID列表 |
probe |
函数指针 | 设备匹配成功时调用 |
remove |
函数指针 | 设备移除时调用 |
填充示例:
static const struct i2c_device_id my_i2c_id[] = {
{ "my_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_sensor_drv",
.of_match_table = of_match_ptr(my_of_match),
},
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.id_table = my_i2c_id,
};
你可能会问:id_table 和 of_match_table 有什么区别?
嗯,这个问题我当年也困惑过。简单说:id_table 是传统的I2C设备ID匹配方式,而 of_match_table 是设备树匹配方式。在全志T113这种使用设备树的平台上,我建议你两个都填上,兼容性最好。
我曾经踩过的坑: 只填了 id_table 没填 of_match_table,结果设备树里配了 compatible 属性,驱动死活不probe。查了两天才发现是匹配方式不对。所以,设备树平台一定要配 of_match_table。
4.3 probe与remove回调:驱动的“生命线”
probe 函数是驱动和设备“握手成功”后的第一个动作。它负责初始化硬件、分配资源、注册设备接口。
remove 函数则相反,负责清理资源、关闭硬件。
来看一个完整的probe实现:
static int my_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_device_data *data;
int ret;
dev_info(&client->dev, "Probe: %s at 0x%02x\n",
client->name, client->addr);
// 1. 分配私有数据结构
data = devm_kzalloc(&client->dev,
sizeof(struct my_device_data),
GFP_KERNEL);
if (!data)
return -ENOMEM;
// 2. 保存client指针,后续通信要用
data->client = client;
i2c_set_clientdata(client, data);
// 3. 初始化硬件(比如复位传感器)
ret = my_device_hw_init(client);
if (ret < 0) {
dev_err(&client->dev, "Hardware init failed\n");
return ret;
}
// 4. 注册字符设备或输入设备等
ret = my_device_create_sysfs(client);
if (ret < 0)
return ret;
dev_info(&client->dev, "Probe success\n");
return 0;
}
static int my_i2c_remove(struct i2c_client *client)
{
struct my_device_data *data = i2c_get_clientdata(client);
dev_info(&client->dev, "Removing device\n");
// 1. 清理sysfs接口
my_device_remove_sysfs(client);
// 2. 关闭硬件
my_device_hw_shutdown(client);
// 3. 释放资源(devm_* 会自动释放,但显式清理更安全)
// 注意:devm_kzalloc分配的内存不需要手动释放
return 0;
}
这里有个关键点:devm_kzalloc 和 i2c_set_clientdata 的配合使用。我个人的习惯是,能用 devm_ 系列函数就用它,因为内核会在设备移除时自动释放资源,省得你在remove里写一堆清理代码。
4.4 知识体系总览
为了让你更直观地理解整个流程,我画了一张图:
从这张图你可以看到,整个流程其实就两条线:一条是模块加载时注册驱动,另一条是设备枚举时触发匹配。两条线在probe处汇合,然后驱动开始真正工作。
4.5 完整代码骨架
最后,我把上面所有片段拼成一个完整的驱动骨架。你在全志T113上开发时,可以直接拿这个改:
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/slab.h>
struct my_device_data {
struct i2c_client *client;
/* 你的私有数据 */
};
static const struct of_device_id my_of_match[] = {
{ .compatible = "mycompany,my-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static const struct i2c_device_id my_i2c_id[] = {
{ "my_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);
static int my_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_device_data *data;
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, "Probed at 0x%02x\n", client->addr);
return 0;
}
static int my_i2c_remove(struct i2c_client *client)
{
dev_info(&client->dev, "Removed\n");
return 0;
}
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_sensor_drv",
.of_match_table = of_match_ptr(my_of_match),
},
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.id_table = my_i2c_id,
};
module_i2c_driver(my_i2c_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C Sensor Driver");
一个小技巧: 注意最后一行 module_i2c_driver 这个宏。它其实等价于 module_init + module_exit + i2c_add_driver + i2c_del_driver 的组合。用这个宏可以少写几行样板代码。不过我个人还是喜欢显式写init/exit,因为有时候需要在注册驱动前后做些额外操作。