4、I2C设备驱动编写:设备注册与注销、probe与remove回调、设备树节点解析

好,咱们今天聊聊I2C设备驱动的核心骨架。说白了,就是怎么让你的驱动被内核“认领”,怎么在设备接入时干活,怎么在设备移除时善后。这部分内容我当年刚入门时也绕了不少弯路,今天一次性给你讲透。

4.1 I2C设备驱动的注册与注销

每个I2C设备驱动,本质上就是一个i2c_driver结构体。你把它注册到内核,内核才知道“哦,原来有个驱动能管这个设备”。

注册和注销的接口很简单:

// 注册
int i2c_add_driver(struct i2c_driver *driver);
// 或者用这个宏,更省事
module_i2c_driver(my_i2c_driver);

// 注销
void i2c_del_driver(struct i2c_driver *driver);

我个人习惯用module_i2c_driver这个宏。它把module_init和module_exit都封装好了,代码看起来干净。你想想看,少写两行样板代码,出错的概率也小一点。

来看一个完整的驱动骨架:

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_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C Sensor Driver");

关键点:id_table和of_match_table至少得有一个。前者用于传统板级代码匹配,后者用于设备树匹配。现在主流都是用设备树,所以of_match_table基本是标配。

4.2 probe回调函数——驱动的“入场券”

probe函数是驱动和设备的“相亲现场”。内核发现一个设备与你的驱动匹配时,就会调用你的probe。这时候你要做几件事:

  • 分配并初始化设备私有数据结构
  • 获取硬件资源(中断号、GPIO、时钟等)
  • 注册字符设备或输入设备等上层接口
  • 初始化硬件(比如设置寄存器、校准传感器)

我写一个典型的probe函数给你看:

static int my_sensor_probe(struct i2c_client *client,
                           const struct i2c_device_id *id)
{
    struct my_sensor_data *data;
    int ret;

    // 1. 分配私有数据
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    // 2. 保存client指针,后面读写要用
    data->client = client;
    i2c_set_clientdata(client, data);

    // 3. 读取设备树中的配置
    ret = device_property_read_u32(&client->dev, "measure-interval-ms",
                                   &data->interval);
    if (ret)
        data->interval = 100; // 默认100ms

    // 4. 初始化硬件
    ret = my_sensor_hw_init(client);
    if (ret) {
        dev_err(&client->dev, "硬件初始化失败\n");
        return ret;
    }

    // 5. 注册字符设备或输入设备
    ret = my_sensor_register_dev(data);
    if (ret)
        return ret;

    dev_info(&client->dev, "传感器驱动加载成功\n");
    return 0;
}

我的经验:probe函数里尽量用devm_系列API(比如devm_kzalloc)。这些资源会在设备移除时自动释放,省得你在remove里一个个手动清理。我曾经因为漏掉一个kfree导致内存泄漏,排查了两天才找到原因。

4.3 remove回调函数——体面地退场

remove函数是probe的逆操作。当设备被拔出或驱动被卸载时,内核会调用它。你要做的就是把probe里申请的资源都释放掉。

static int my_sensor_remove(struct i2c_client *client)
{
    struct my_sensor_data *data = i2c_get_clientdata(client);

    // 1. 注销上层设备(字符设备、输入设备等)
    my_sensor_unregister_dev(data);

    // 2. 关闭硬件(比如进入低功耗模式)
    my_sensor_power_down(client);

    // 3. 如果用了devm_系列API,这里不用手动释放内存
    //    但如果有手动申请的资源,记得释放

    dev_info(&client->dev, "传感器驱动已卸载\n");
    return 0;
}

注意:如果probe失败了,remove不会被调用。所以probe里如果中途出错,要自己把已经申请的资源释放干净。我建议用goto错误处理模式,清晰又安全。

4.4 I2C设备树节点解析

设备树是驱动和设备之间的“红娘”。你得在设备树里描述好设备的信息,驱动才能正确找到它。

一个典型的I2C设备节点长这样:

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;  // 400kHz快速模式

    my_sensor: sensor@48 {
        compatible = "mycompany,my-sensor";
        reg = <0x48>;           // I2C地址
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        measure-interval-ms = <200>;
    };
};

驱动里怎么解析这些属性呢?看代码:

static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "mycompany,my-sensor" },
    { /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

// 在probe中解析
static int my_sensor_parse_dt(struct i2c_client *client,
                              struct my_sensor_data *data)
{
    struct device *dev = &client->dev;
    u32 val;
    int ret;

    // 读取自定义属性
    ret = device_property_read_u32(dev, "measure-interval-ms", &val);
    if (ret == 0)
        data->interval = val;
    else
        data->interval = 100; // 默认值

    // 读取中断号(设备树中已描述)
    data->irq = client->irq;
    if (data->irq < 0) {
        dev_warn(dev, "未配置中断,将使用轮询模式\n");
    }

    return 0;
}

避坑指南:我曾经在设备树里把reg地址写成了0x48,但实际芯片地址是0x48左移一位(7位地址)。结果驱动死活匹配不上,查了两天发现是设备树地址格式问题。记住:reg里填的是7位I2C地址,不要左移。

4.5 知识体系总览

下面这张图帮你理清整个I2C设备驱动的核心逻辑:

I2C设备驱动核心流程 设备树节点 compatible + reg + 属性 内核匹配 of_match_table / id_table probe回调 分配资源 + 初始化硬件 probe内部工作 1. 分配私有数据 2. 解析设备树属性 3. 获取中断/GPIO 4. 硬件初始化 5. 注册上层设备 6. 注册中断处理 remove回调 释放资源 + 关闭硬件 i2c_add_driver module_i2c_driver宏 i2c_del_driver 模块卸载时自动调用

这张图把整个流程串起来了:设备树描述设备 → 内核匹配驱动 → probe初始化 → 正常工作 → remove清理。嗯,核心逻辑就这么简单。

4.6 完整示例:一个温度传感器驱动

最后给你一个完整的驱动示例,把上面所有知识点揉在一起:

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/delay.h>

struct temp_sensor_data {
    struct i2c_client *client;
    int irq;
    int interval_ms;
};

static int temp_sensor_probe(struct i2c_client *client,
                             const struct i2c_device_id *id)
{
    struct temp_sensor_data *data;
    int ret;

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->client = client;
    i2c_set_clientdata(client, data);

    // 解析设备树
    device_property_read_u32(&client->dev, "measure-interval-ms",
                             &data->interval_ms);
    data->irq = client->irq;

    // 初始化硬件(示例:读取设备ID)
    ret = i2c_smbus_read_byte_data(client, 0x0F);
    if (ret < 0) {
        dev_err(&client->dev, "读取设备ID失败\n");
        return ret;
    }
    dev_info(&client->dev, "设备ID: 0x%02x\n", ret);

    // 注册字符设备(略)
    dev_info(&client->dev, "温度传感器就绪\n");
    return 0;
}

static int temp_sensor_remove(struct i2c_client *client)
{
    // 关闭硬件(示例:进入休眠模式)
    i2c_smbus_write_byte_data(client, 0x01, 0x00);
    dev_info(&client->dev, "温度传感器已关闭\n");
    return 0;
}

static const struct of_device_id temp_sensor_of_match[] = {
    { .compatible = "mycompany,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,
};

module_i2c_driver(temp_sensor_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("I2C Temperature Sensor Driver");

我的建议:刚开始写I2C驱动时,先用i2c_smbus_*系列函数读写寄存器。这些函数封装了I2C协议细节,不容易出错。等熟悉了再考虑直接操作i2c_transfer。

好了,I2C设备驱动的核心骨架就这些。说白了就是:注册驱动 → 设备树匹配 → probe干活 → remove收尾。把这个流程刻在脑子里,写任何I2C驱动都不会跑偏。


可可精品资料库