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 知识体系总览

为了让你更直观地理解整个流程,我画了一张图:

I2C设备驱动初始化与probe流程 I2C设备驱动初始化与probe流程 module_init i2c_add_driver() 注册到I2C总线 设备树/ACPI枚举 匹配id_table/of_match 匹配成功? 否 跳过 是 probe() 回调 probe内部步骤 ① 分配私有数据 (devm_kzalloc) ② 保存client指针 ③ 硬件初始化 → ④ 注册接口

从这张图你可以看到,整个流程其实就两条线:一条是模块加载时注册驱动,另一条是设备枚举时触发匹配。两条线在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,因为有时候需要在注册驱动前后做些额外操作。