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 = <®_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 片段示例
把上述内容整合成一段可直接交给 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 = <®_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 地址范围内。
掌握以上写法后,面对不同工业底板,只需替换 compatible、reg、中断和时钟引用,驱动主体无需改动。这正是设备树在工业外设适配中的核心价值:硬件描述与驱动逻辑解耦,让同一套驱动适配多种板卡成为可能。