4、编写第一个驱动:创建自定义传感器驱动、实现probe/init/read函数、注册到设备树

好,咱们终于到了动手写代码的环节了。说实话,很多初学者一上来就被设备树、probe、init这些概念吓住了。其实没那么玄乎——你想想看,驱动说白了就是让内核认识你的硬件,然后告诉它怎么干活。

我个人习惯把驱动开发分成三步走:先让系统找到你(probe),再让硬件准备好(init),最后让数据流起来(read)。今天我们就拿一个虚拟的温度传感器来练手,一步步把它跑通。

核心目标:写一个I2C温度传感器驱动,实现probe检测、init初始化、read读取三个核心函数,并挂载到设备树节点上。

4.1 驱动骨架:从module_init开始

每个驱动都有一个入口。我刚开始写驱动时,总喜欢先把module_init和module_exit写好,就像盖房子先搭脚手架一样。

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/hwmon.h>
#include <linux/hwmon-sysfs.h>

static int __init mytemp_init(void)
{
    pr_info("MyTemp: driver loaded\n");
    return i2c_add_driver(&mytemp_driver);
}

static void __exit mytemp_exit(void)
{
    i2c_del_driver(&mytemp_driver);
    pr_info("MyTemp: driver unloaded\n");
}

module_init(mytemp_init);
module_exit(mytemp_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My first temperature sensor driver");

嗯,这里要注意:i2c_add_driver 会自动去扫描I2C总线,匹配设备树里注册的设备。匹配上了,就会调用你的probe函数。

4.2 probe函数:让内核认识你的硬件

probe函数是驱动的灵魂。我在项目中遇到过好几次probe失败的情况,最后发现都是设备树节点写错了。所以咱们先看probe怎么写:

static int mytemp_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct mytemp_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. 检查设备是否存在(读设备ID寄存器)
    ret = i2c_smbus_read_byte_data(client, MYTEMP_REG_ID);
    if (ret < 0) {
        dev_err(&client->dev, "failed to read device ID\n");
        return ret;
    }
    if (ret != MYTEMP_EXPECTED_ID) {
        dev_err(&client->dev, "unexpected device ID: 0x%02x\n", ret);
        return -ENODEV;
    }

    // 4. 注册hwmon设备
    data->hwmon_dev = devm_hwmon_device_register_with_info(
                        &client->dev, "mytemp", data,
                        &mytemp_chip_info, NULL);

    dev_info(&client->dev, "MyTemp sensor detected\n");
    return 0;
}

为什么probe里要做这么多事?说白了,probe就是「验明正身」的过程。你得确认I2C总线上挂着的确实是你的设备,而不是别的什么东西。我曾经踩过一个坑:某次设备ID寄存器读回来全是0xFF,查了半天发现是I2C地址配错了。

小技巧:probe函数里尽量用devm_系列函数(managed device resource),这样出错时内核会自动帮你释放资源,省去很多手动清理的麻烦。

4.3 init函数:让硬件准备好干活

设备找到了,接下来要初始化。init函数通常在probe里调用,或者放在一个单独的函数里。我个人习惯把硬件配置抽出来,这样代码更清晰:

static int mytemp_init_hw(struct mytemp_data *data)
{
    int ret;

    // 1. 软复位传感器
    ret = i2c_smbus_write_byte_data(data->client,
                                    MYTEMP_REG_CTRL, 0x80);
    if (ret < 0)
        return ret;
    msleep(10);  // 等待复位完成

    // 2. 配置采样率:连续转换模式,1Hz
    ret = i2c_smbus_write_byte_data(data->client,
                                    MYTEMP_REG_CTRL, 0x10);
    if (ret < 0)
        return ret;

    // 3. 读取配置确认
    ret = i2c_smbus_read_byte_data(data->client,
                                   MYTEMP_REG_CTRL);
    if (ret < 0)
        return ret;

    dev_dbg(&data->client->dev,
            "init complete, CTRL=0x%02x\n", ret);
    return 0;
}

你可能会问:为什么init要单独写?嗯,因为有些传感器初始化步骤很复杂,比如要校准、要加载固件。单独写出来,probe函数就不会太臃肿,也方便以后复用。

4.4 read函数:把数据读回来

终于到了最激动人心的环节——读数据。对于温度传感器,通常就是读两个字节的寄存器值,然后换算成摄氏度:

static int mytemp_read(struct device *dev,
                       enum hwmon_sensor_types type,
                       u32 attr, int channel, long *val)
{
    struct mytemp_data *data = dev_get_drvdata(dev);
    int raw;

    if (type != hwmon_temp)
        return -EOPNOTSUPP;

    switch (attr) {
    case hwmon_temp_input:
        // 读取温度寄存器(16位)
        raw = i2c_smbus_read_word_swapped(data->client,
                                          MYTEMP_REG_TEMP);
        if (raw < 0)
            return raw;

        // 换算:假设传感器精度为0.0625°C/LSB
        *val = (raw >> 4) * 625;  // 单位:毫摄氏度
        return 0;

    case hwmon_temp_max:
        // 读取上限报警阈值
        raw = i2c_smbus_read_byte_data(data->client,
                                       MYTEMP_REG_THIGH);
        if (raw < 0)
            return raw;
        *val = raw * 1000;  // 单位:毫摄氏度
        return 0;

    default:
        return -EOPNOTSUPP;
    }
}

这里有个细节:i2c_smbus_read_word_swappedi2c_smbus_read_word_data 的区别在于字节序。我当年就被这个坑过——读回来的温度忽高忽低,最后发现是大小端搞反了。所以,一定要看数据手册里寄存器是高字节在前还是低字节在前

4.5 注册到设备树

驱动写好了,怎么让内核知道它对应哪个硬件?答案就是设备树。在dts文件里加一个节点:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;

    mytemp@48 {
        compatible = "mycompany,mytemp";
        reg = <0x48>;
    };
};

注意三点:

  • compatible:必须和驱动里 of_match_table 中的字符串完全一致
  • reg:I2C设备地址,7位地址左移1位(0x48对应7位地址0x24)
  • status:如果父节点是disabled,子节点默认也是disabled

然后在驱动里加上匹配表:

static const struct of_device_id mytemp_of_match[] = {
    { .compatible = "mycompany,mytemp" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mytemp_of_match);

static struct i2c_driver mytemp_driver = {
    .driver = {
        .name = "mytemp",
        .of_match_table = mytemp_of_match,
    },
    .probe = mytemp_probe,
    .id_table = mytemp_id_table,
};

4.6 完整流程梳理

为了让你看得更清楚,我画了一张流程图,把整个调用链串起来:

驱动加载与数据流完整流程 系统启动 / 模块加载 module_init → i2c_add_driver 内核扫描I2C总线 → 匹配设备树节点 probe() 被调用 分配内存 读ID验证 init_hw() 初始化传感器 read() 读取温度数据 用户空间通过sysfs或ioctl触发read

4.7 避坑指南

最后,分享几个我亲身踩过的坑:

我曾经犯过的错:

  • 设备树节点没加status = "okay"——结果probe死活不调用,查了两天才发现父节点是disabled
  • I2C地址写错了——7位地址和8位地址搞混,0x48写成了0x90,设备根本找不到
  • probe里忘了检查返回值——i2c_smbus_read失败后继续往下走,导致内核panic
  • 换算公式抄错了数据手册——读出来的温度比室温高了50度,后来发现是精度位搞反了

调试小技巧:在probe和read函数里多加点dev_dbg打印,然后用dmesg -w实时看日志。我每次调试新驱动,第一件事就是把所有关键路径的打印加上,等调通了再删掉。

好了,到这里你已经掌握了驱动开发的核心三板斧:probe、init、read。剩下的就是多练、多踩坑、多总结。记住,驱动开发没有捷径,但有了这套框架,你至少不会迷路。


公众号:蓝海资料掘金营,微信deep3321