4、设备树实战:为Sensor添加设备树节点

设备树,说白了就是Linux内核用来描述硬件的一本「硬件说明书」。我刚开始接触嵌入式Linux时,总觉得设备树很玄乎,后来发现它其实就是一套树形结构的数据,告诉内核:你的板子上有哪些外设、它们挂在哪里、怎么配置。

今天我们就拿一个真实的Sensor来练手。假设我们有一颗I2C接口的温度传感器——TMP117,挂在I2C总线0上,地址是0x48,使用中断引脚来通知数据就绪。

4.1 设备树节点的基本结构

一个完整的设备树节点,通常包含这几样东西:

  • compatible:驱动匹配的关键字,说白了就是「我叫什么名字」
  • reg:设备的地址或寄存器基址
  • interrupts:中断号、触发方式
  • 其他属性:比如时钟频率、GPIO引脚等

来看一个实际的例子。我在项目中给TMP117添加的节点长这样:

&i2c0 {
    status = "okay";

    tmp117: temperature-sensor@48 {
        compatible = "ti,tmp117";
        reg = <0x48>;
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        #thermal-sensor-cells = <1>;
    };
};

嗯,这里要注意:@48是设备在总线上的地址,必须和reg字段一致。我曾经见过有人写@49但reg写0x48,结果内核死活匹配不上,排查了半天。

4.2 解析compatible属性

compatible是驱动和设备树之间的「暗号」。内核通过它来找到对应的驱动程序。

格式一般是:"厂商,设备型号"。比如"ti,tmp117"表示TI公司的TMP117传感器。

驱动里怎么匹配?看这段代码:

static const struct of_device_id tmp117_of_match[] = {
    { .compatible = "ti,tmp117" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, tmp117_of_match);

static struct i2c_driver tmp117_driver = {
    .driver = {
        .name = "tmp117",
        .of_match_table = tmp117_of_match,
    },
    .probe = tmp117_probe,
    .id_table = tmp117_id,
};

为什么要有of_match_table?我个人的习惯是:即使你的设备是I2C或SPI总线,也一定要加上设备树匹配表。这样内核在枚举设备时,会优先通过compatible来匹配,而不是靠传统的ID表。兼容性更好,也更容易调试。

4.3 解析reg属性

reg属性描述的是设备的地址信息。对于I2C设备,它就是从设备地址;对于SPI设备,它是片选号;对于内存映射设备,它是基地址和长度。

在驱动里,我们用of_get_property或者of_i2c_get_board_info来读取。不过更常见的做法是:I2C核心层已经帮我们解析好了,在probe函数里直接拿client->addr就行。

但如果你想手动解析,可以这样:

struct device_node *np = client->dev.of_node;
u32 addr;

if (of_property_read_u32(np, "reg", &addr)) {
    dev_err(&client->dev, "failed to get reg property\n");
    return -EINVAL;
}
dev_info(&client->dev, "sensor address: 0x%02x\n", addr);

你想想看,如果设备树里reg写错了,这里读出来的地址就是错的,后续I2C通信全废。所以我在项目中每次都会加这么一段打印,确认地址对不对。

4.4 解析interrupts属性

中断属性稍微复杂一点。它通常包含两个值:中断号和触发方式。

触发方式常用的有:

宏定义 说明
IRQ_TYPE_EDGE_RISING 1 上升沿触发
IRQ_TYPE_EDGE_FALLING 2 下降沿触发
IRQ_TYPE_LEVEL_HIGH 4 高电平触发
IRQ_TYPE_LEVEL_LOW 8 低电平触发

驱动里解析中断的典型写法:

int irq_num;

irq_num = of_irq_get(np, 0);
if (irq_num < 0) {
    dev_err(&client->dev, "failed to get irq\n");
    return irq_num;
}

dev_info(&client->dev, "irq number: %d\n", irq_num);

/* 注册中断 */
ret = devm_request_threaded_irq(&client->dev, irq_num, NULL,
                                tmp117_irq_handler,
                                IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                "tmp117", client);

我曾经踩过一个坑:设备树里写的是IRQ_TYPE_EDGE_FALLING,但驱动里注册时用了IRQF_TRIGGER_LOW。结果中断触发方式不匹配,传感器数据就绪了,中断却一直不来。排查了整整一下午才发现是触发方式写错了。

注意:设备树里的interrupts属性和驱动里request_irq的触发标志必须一致。否则中断要么不触发,要么频繁误触发。

4.5 使用of_函数族读取设备树信息

Linux内核提供了一套of_开头的函数,专门用来读取设备树属性。我把常用的几个列出来:

函数 用途
of_property_read_u32 读取32位整数属性
of_property_read_string 读取字符串属性
of_property_read_u32_array 读取整数数组
of_get_property 获取属性原始数据
of_irq_get 获取中断号
of_iomap 将地址映射到虚拟空间
of_find_node_by_path 通过路径查找节点
of_find_compatible_node 通过compatible查找节点

来看一个综合的例子,把前面讲的都串起来:

static int tmp117_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct device_node *np = client->dev.of_node;
    struct tmp117_data *data;
    u32 temp;
    int ret;

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

    /* 读取自定义属性:采样频率 */
    ret = of_property_read_u32(np, "ti,sample-rate", &temp);
    if (ret) {
        /* 没读到就用默认值 */
        temp = 4;  /* 默认4Hz */
        dev_warn(&client->dev, "use default sample rate: %d\n", temp);
    }
    data->sample_rate = temp;

    /* 读取中断 */
    data->irq = of_irq_get(np, 0);
    if (data->irq < 0) {
        dev_info(&client->dev, "no irq configured, use polling mode\n");
    }

    /* 读取reg地址(其实client->addr已经有了,这里演示用) */
    of_property_read_u32(np, "reg", &temp);
    dev_info(&client->dev, "device addr: 0x%02x, irq: %d\n",
             temp, data->irq);

    /* 注册中断 */
    if (data->irq > 0) {
        ret = devm_request_threaded_irq(&client->dev, data->irq, NULL,
                                        tmp117_irq_handler,
                                        IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                        "tmp117", data);
        if (ret) {
            dev_err(&client->dev, "failed to request irq\n");
            return ret;
        }
    }

    i2c_set_clientdata(client, data);
    return 0;
}
我的经验:在probe函数里,尽量用devm_系列函数(设备资源管理),比如devm_kzallocdevm_request_threaded_irq。这样在设备卸载或驱动出错时,内核会自动帮你释放资源,省去很多麻烦。

4.6 调试技巧

设备树写对了没有?驱动读到了没有?我一般用这几个方法验证:

  • 查看设备树是否被正确解析:在/sys/firmware/devicetree/base/目录下,可以看到内核解析后的设备树结构
  • 查看设备是否被注册ls /sys/bus/i2c/devices/,看看有没有你的设备
  • 查看驱动是否匹配cat /sys/bus/i2c/devices/0-0048/driver,看驱动名是否正确
  • 使用devicetree.org的语法检查工具:编译设备树时加上DTC_FLAGS=-@可以生成符号表,方便调试

我记得有一次,设备树编译通过了,驱动也加载了,但probe就是不调用。最后发现是status = "okay"写成了status = "ok"。内核只认"okay""disabled",写"ok"会被当成无效状态,直接跳过。

4.7 小结

设备树实战这块,说白了就是三件事:

  1. 在设备树里把硬件描述清楚(compatible、reg、interrupts)
  2. 在驱动里用of_函数把属性读出来
  3. 确保设备树和驱动的匹配条件一致

你只要把这三步走通,Sensor驱动移植就完成了80%。剩下的就是具体的寄存器读写和数据处理逻辑,那些反而是相对固定的套路。

下一章我们会深入Sensor的电源管理和休眠唤醒机制,到时候再聊。