4. 传感器驱动核心概念

设备树,说白了就是一块「硬件描述板」。它用树形结构告诉 Zephyr 内核:你有哪些外设、它们挂在哪条总线上、地址是多少、用什么驱动来管。我刚开始接触 Zephyr 时,总觉得设备树很玄乎,后来发现它其实就是一套标准化的「硬件说明书」。

嗯,咱们今天就把这块硬骨头啃下来。你只要掌握四个核心点:语法怎么写、节点怎么组织、compatible 怎么匹配驱动、reg 怎么描述地址。后面写传感器驱动,你天天都要跟它打交道。

4.1 设备树语法速览

设备树文件通常以 .dts 或 .dtsi 结尾。.dtsi 是公共头文件,.dts 是具体板级文件。语法其实很简单,我总结成三句话:

  • 节点用花括号:每个设备就是一个节点,用 { } 包裹。
  • 属性用键值对:属性名 = 值; 或 属性名;(布尔型)。
  • 引用用 & 符号:通过 &节点标签 来追加或修改已有节点。

核心原则:设备树只描述硬件「有什么、在哪、怎么配」,不描述驱动怎么跑。驱动怎么跑,那是 compatible 匹配之后的事。

来看一个最简单的节点示例:

/dts-v1/;

/ {
    model = "My Sensor Board";
    compatible = "mycompany,myboard";

    soc {
        i2c0: i2c@40003000 {
            compatible = "mycompany,i2c";
            reg = <0x40003000 0x1000>;
            status = "okay";
            clock-frequency = <100000>;
        };
    };
};

这里 i2c0 是节点名,i2c@40003000 是完整名称(@后面是地址)。compatible 告诉系统用哪个驱动,reg 告诉系统地址范围。后面咱们一个一个拆开讲。

4.2 节点定义与层级结构

设备树的节点,就像文件系统的目录。根节点 / 下面挂 soc、chosen、aliases 等子节点。每个子节点又可以挂自己的子节点。我习惯把这种结构想象成一棵倒挂的树——根在上,叶子在下。

节点定义的基本格式:

[标签:]节点名[@单元地址] {
    [属性定义];
    [子节点];
};
  • 标签(label):可选,用于在别处引用。比如 i2c0:,后面可以用 &i2c0 来引用它。
  • 节点名:必填,一般用设备类型,如 i2c、spi、gpio。
  • 单元地址:可选,当同类型设备有多个时,用地址区分。比如 i2c@40003000 和 i2c@40004000。

我的习惯:写传感器驱动时,我通常会在 soc 节点下找到对应的 I2C 或 SPI 总线节点,然后在里面追加传感器子节点。这样层级清晰,也方便后续维护。

举个例子,假设你的板子上有一颗温度传感器挂在 I2C0 总线上,地址是 0x48。你会在 i2c0 节点下追加一个子节点:

&i2c0 {
    status = "okay";

    temp_sensor: temperature@48 {
        compatible = "mycompany,tmp117";
        reg = <0x48>;
        label = "TEMP_SENSOR";
    };
};

这里 temperature@48 就是传感器节点,reg = <0x48> 表示它的 I2C 从设备地址是 0x48。嗯,这个写法你后面写驱动时会反复用到。

4.3 compatible 属性:驱动匹配的关键

compatible 是设备树里最重要的属性,没有之一。它就像一把钥匙,用来匹配设备树节点和驱动代码。Zephyr 的驱动框架会遍历所有设备树节点,找到 compatible 字符串与驱动注册时声明一致的节点,然后调用驱动的初始化函数。

compatible 的命名规范:

格式 示例 说明
厂商,设备型号 ti,tmp117 标准格式,厂商前缀 + 设备型号
厂商,设备型号-版本 bosch,bme280-v1 带版本号,用于区分不同硬件版本
通用类型 gpio-leds 通用设备类型,不指定厂商

我曾经踩过的坑:compatible 字符串必须与驱动中 DT_DRV_COMPAT 宏定义的值完全一致,大小写、连字符、下划线都不能错。有一次我写驱动时把 tmp117 写成了 tmp117(少了个字母),结果设备树节点死活匹配不上驱动,查了半天才发现是拼写错误。

在 Zephyr 驱动中,匹配过程是这样的:

  1. 设备树编译时,每个节点生成一个 DEVICE_DT_DEFINE 宏。
  2. 驱动注册时,通过 DT_DRV_COMPAT 声明自己支持的 compatible。
  3. 系统启动时,遍历所有设备树节点,找到 compatible 匹配的节点,调用驱动的初始化函数。

说白了,compatible 就是设备树和驱动之间的「接头暗号」。暗号对上了,驱动就接管这个设备。

4.4 reg 属性:地址与大小描述

reg 属性描述设备的地址空间。它的格式是:

reg = <地址1 长度1>, <地址2 长度2>, ...;

每个 <地址 长度> 对称为一个「reg 单元」。对于不同的总线,reg 的含义不同:

总线类型 reg 含义 示例
内存映射外设 基地址 + 寄存器区域大小 reg = <0x40003000 0x1000>
I2C 设备 从设备地址(长度通常为 1) reg = <0x48>
SPI 设备 片选编号(长度通常为 1) reg = <0>

你想想看,为什么 I2C 设备的 reg 只有一个地址值?因为 I2C 总线上每个设备只有一个 7 位或 10 位从地址,不需要长度信息。而内存映射外设需要知道寄存器区域的起始地址和大小,所以需要两个值。

关键点:reg 属性的解析方式由 #address-cells 和 #size-cells 决定。前者表示地址占几个 32 位整数,后者表示长度占几个 32 位整数。默认情况下,#address-cells = <1>,#size-cells = <1>。

举个例子,如果你的传感器是 SPI 设备,片选为 0,reg 就写成:

spi_sensor: sensor@0 {
    compatible = "mycompany,spi-sensor";
    reg = <0>;  /* 片选 0 */
    spi-max-frequency = <1000000>;
};

如果是内存映射的 GPIO 控制器,reg 就写成:

gpio0: gpio@50000000 {
    compatible = "mycompany,gpio";
    reg = <0x50000000 0x1000>;  /* 基地址 0x50000000,大小 4KB */
};

4.5 知识结构总览

下面这张图把设备树的核心概念串起来了。你可以把它当作一张「思维导图」,后面写驱动时随时回来对照。

设备树核心知识结构 设备树 (Devicetree) 语法基础 节点定义与层级 compatible 属性 reg 属性 键值对 引用 & 标签:节点名 子节点嵌套 驱动匹配 命名规范 地址+长度 总线差异 核心:compatible 匹配驱动,reg 描述地址 节点层级反映硬件拓扑

4.6 实战:写一个传感器设备树节点

咱们把前面学的知识串起来,写一个完整的传感器设备树节点。假设你有一颗 BME280 温湿度气压传感器,挂在 I2C1 总线上,地址是 0x76。

第一步,找到板级设备树文件(比如 boards/arm/myboard/myboard.dts),在 I2C1 节点下追加:

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

    bme280: bme280@76 {
        compatible = "bosch,bme280";
        reg = <0x76>;
        label = "BME280";
    };
};

第二步,在驱动代码中,通过 DT_DRV_COMPAT 声明匹配:

#define DT_DRV_COMPAT bosch_bme280

/* 驱动初始化函数 */
static int bme280_init(const struct device *dev)
{
    /* 获取设备树配置 */
    uint16_t addr = DT_INST_REG_ADDR(0);
    /* ... 初始化代码 ... */
    return 0;
}

DEVICE_DT_DEFINE(DT_NODELABEL(bme280),
                 bme280_init,
                 NULL,
                 NULL,
                 NULL,
                 POST_KERNEL,
                 CONFIG_SENSOR_INIT_PRIORITY,
                 &bme280_driver_api);

小技巧:Zephyr 提供了大量 DT_* 宏来读取设备树属性。比如 DT_REG_ADDR(node_id) 读取 reg 地址,DT_PROP(node_id, label) 读取 label 属性。写驱动时多用这些宏,少写硬编码。

嗯,到这里你应该对设备树有了一个整体的认识。说白了,它就是硬件和驱动之间的「翻译官」。你只要把设备树节点写对,compatible 和 reg 填准确,Zephyr 就能自动帮你把驱动挂载上去。后面写传感器驱动时,你会反复用到这些知识。