2. 设备树语法与工业硬件描述

设备树(Device Tree)的本质,是把“板子上有什么硬件、这些硬件怎么连、用什么参数工作”从内核代码里剥离出来,交给一份可读、可编译、可复用的描述文件。对工业场景尤其重要:同一颗 SoC 可能配不同底板、不同串口扩展芯片、不同中断和时钟走线,如果都写死在驱动里,移植和维护成本极高。本节围绕 DTS 的节点、属性、phandle、中断与时钟绑定,以一块“4 路 RS-485/RS-232 工业串口扩展板”为例,写出可编译的 DTS 片段。

一、DTS 的基本语法骨架

DTS 文件由节点(node)和属性(property)组成。节点用大括号嵌套,属性用“键 = 值”表达。根节点固定为 /,其下按总线或功能挂子节点。一个最小可编译结构如下:

/dts-v1/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    model = "Industrial 4-Port Serial Board";
    compatible = "vendor,ind-serial-board", "vendor,soc";
    #address-cells = <1>;
    #size-cells = <1>;

    chosen {
        stdout-path = &uart0;
    };
};

这里 /dts-v1/; 声明语法版本,#include 引入宏定义,model 描述板卡名称,compatible 是驱动匹配的关键字符串,按“从具体到通用”排列。#address-cells 和 #size-cells 决定子节点 reg 属性中地址和长度各占几个 32 位单元,这是后续所有地址描述的基础,必须先定义再使用。

二、节点命名与 reg 地址描述

节点名通常写成 节点名@地址,地址部分与 reg 首地址一致,便于阅读。reg 属性描述设备在父总线地址空间中的位置和长度。以串口扩展芯片挂在 I2C1 上为例,芯片地址 0x20,则:

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

    serial_exp: serial-expander@20 {
        compatible = "vendor,sc16is752";
        reg = <0x20>;
        interrupt-parent = <&gpio1>;
        interrupts = <12 IRQ_TYPE_LEVEL_LOW>;
        clocks = <&clk_uart_exp>;
        clock-names = "xtal";
    };
};

注意 &i2c1 是引用语法,表示向已有节点追加内容,而不是重新定义。工业板卡常见做法是:SoC 厂商的 .dtsi 提供 i2c1 控制器节点,板级 .dts 只负责把 status 改为 okay 并挂上外设,这样层次清晰、复用性好。

三、phandle:让节点之间“互相指认”

phandle 是设备树中节点被其他节点引用时使用的唯一标识。写法上不需要手写数字,只要在节点上打标签(label),如 serial_exp:,编译时 dtc 会自动生成 phandle。引用时用 &serial_exp。工业场景中,中断控制器、时钟源、GPIO 控制器、稳压器、复位控制器几乎都靠 phandle 连接。例如:

    interrupt-parent = <&gpio1>;
    clocks = <&clk_uart_exp>;
    vcc-supply = <&reg_3v3>;
    reset-gpios = <&gpio2 7 GPIO_ACTIVE_LOW>;

这里 <&gpio2 7 GPIO_ACTIVE_LOW> 是“phandle + 参数”的组合,表示使用 gpio2 控制器的第 7 号引脚,低电平有效。理解这一点,就能读懂绝大多数工业外设的 DTS 绑定。

四、中断与时钟绑定写法

中断绑定要回答三个问题:接到哪个中断控制器、用哪条中断线、触发方式是什么。标准写法是 interrupt-parent 加 interrupts。触发类型宏来自 irq.h,常用 IRQ_TYPE_LEVEL_LOW、IRQ_TYPE_EDGE_RISING 等。工业现场干扰大,电平触发配合硬件去抖更常见,但必须与芯片实际输出方式一致,否则会出现中断丢失或反复触发。

时钟绑定则用 clocks 和 clock-names。clocks 里按顺序列出时钟源 phandle,clock-names 给出驱动可读的名字,两者顺序必须一一对应。若扩展芯片需要外部晶振,还要在板级节点里声明固定时钟:

    clk_uart_exp: clk-uart-exp {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <1843200>;
    };

1843200 Hz 是 SC16IS752 常见外部晶振频率,属于示例参数,实际应以原理图为准。驱动通过 devm_clk_get(&pdev->dev, "xtal") 获取该时钟,从而计算波特率分频。

工业串口扩展板 DTS 节点与绑定关系 SoC 侧控制器节点 i2c1: status = "okay" clock-frequency = <400000> #address-cells = <1> 扩展芯片节点 serial-expander@20 compatible = "vendor,sc16is752" reg = <0x20> 板级资源节点 clk_uart_exp: fixed-clock reg_3v3: regulator gpio1 / gpio2 控制器 挂载 引用 芯片节点内绑定属性 interrupt-parent = <&gpio1> interrupts = <12 IRQ_TYPE_LEVEL_LOW> clocks = <&clk_uart_exp> clock-names = "xtal" vcc-supply = <&reg_3v3> phandle 生成机制 标签 serial_exp: ↓ dtc 编译 自动分配 phandle 数字 其他节点用 &serial_exp 引用 编译与检查 dtc -I dts -O dtb -o board.dtb dtc -I dtb -O dts board.dtb 检查 phandle 是否解析 检查中断/时钟引用是否有效 关键约束:interrupts 的触发类型必须与硬件一致;clocks 与 clock-names 顺序一一对应; reg 地址必须落在父总线地址空间内;phandle 引用必须指向已定义节点,否则 dtc 报错。

五、可编译的完整 DTS 片段示例

把上述内容整合成一段可直接交给 dtc 编译的板级片段。假设 SoC 的 .dtsi 已提供 i2c1、gpio1、gpio2 节点,板级文件只需追加:

/dts-v1/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    model = "Industrial 4-Port Serial Board";
    compatible = "vendor,ind-serial-board", "vendor,soc";

    clk_uart_exp: clk-uart-exp {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <1843200>;
    };

    reg_3v3: regulator-3v3 {
        compatible = "regulator-fixed";
        regulator-name = "3v3";
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
        regulator-always-on;
    };
};

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

    serial_exp: serial-expander@20 {
        compatible = "vendor,sc16is752";
        reg = <0x20>;
        interrupt-parent = <&gpio1>;
        interrupts = <12 IRQ_TYPE_LEVEL_LOW>;
        clocks = <&clk_uart_exp>;
        clock-names = "xtal";
        vcc-supply = <&reg_3v3>;
        reset-gpios = <&gpio2 7 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

编译命令为 dtc -I dts -O dtb -o ind-serial.dtb ind-serial.dts。若出现 “label or path not found”,说明引用的 phandle 标签未定义;若出现 “interrupts: bad value”,通常是触发类型宏未包含或参数个数与中断控制器 #interrupt-cells 不匹配。反向用 dtc -I dtb -O dts 反编译,可以确认 phandle 数字是否已正确生成、reg 是否落在 i2c1 地址范围内。

工业现场建议:中断优先使用电平触发并配合硬件滤波;时钟频率必须与原理图晶振一致;reg 地址、GPIO 编号、中断号在板级 DTS 中集中管理,避免散落在多个文件里造成移植遗漏。

掌握以上写法后,面对不同工业底板,只需替换 compatible、reg、中断和时钟引用,驱动主体无需改动。这正是设备树在工业外设适配中的核心价值:硬件描述与驱动逻辑解耦,让同一套驱动适配多种板卡成为可能。