第2章:设备树语法基础——节点、属性、标签与包含文件
设备树(Device Tree)说白了就是一块“硬件描述画板”。你用它告诉内核:我这块板子上有哪些外设、它们挂在哪条总线上、地址是多少、中断怎么接。我刚开始接触设备树时,总觉得它像一堆乱糟糟的括号和分号。后来摸清了套路,才发现它其实挺有章法的。
这一章,咱们就把设备树的四个基本构件——节点、属性、标签、包含文件——彻底讲透。你掌握了这些,后面写任何设备树文件都不会发怵。
核心一句话:设备树 = 一棵树,节点是树枝,属性是树叶,标签是路牌,包含文件是工具箱。
2.1 节点(Node)——设备树的骨架
节点就是设备树里的“文件夹”。每个节点代表一个硬件设备或总线。节点从根(/)开始,一层套一层,形成树状结构。
我习惯把节点想象成公司组织架构:根节点是CEO,子节点是各部门经理,再往下是小组长。每个节点只管自己的那一摊事。
/ {
// 根节点,代表整块板子
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
device_type = "cpu";
reg = <0x0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB DDR3
};
};
看到没?cpus、memory@80000000 都是子节点。节点名字后面可以跟 @地址,用来区分同类型设备。比如两个串口:serial@44e09000 和 serial@44e0a000。
我的习惯:节点命名尽量跟芯片手册保持一致。比如UART节点就叫 uart 或 serial,别自己发明名字。内核匹配驱动时靠的是 compatible 属性,不是节点名,但好名字能让你自己少掉头发。
2.2 属性(Property)——节点的“身份证”
属性就是键值对,描述节点的各种特征。常见的属性类型有这些:
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "ti,am335x-bone-black"; |
驱动匹配的关键,厂商+设备名 |
| 32位整数 | reg = <0x44e09000 0x2000>; |
地址和长度,尖括号包起来 |
| 64位整数 | reg = <0x0 0x80000000 0x0 0x20000000>; |
高32位在前,低32位在后 |
| 布尔值 | status = "disabled"; |
用字符串"okay"或"disabled"表示 |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
方括号,十六进制字节 |
| 字符串列表 | compatible = "ti,am335x-bone-black", "ti,am33xx"; |
逗号分隔,内核按顺序匹配 |
这里有个坑,我踩过不止一次:reg属性的地址和长度是配对的。#address-cells 和 #size-cells 决定了每个地址和长度占几个32位整数。比如 #address-cells = <1>; #size-cells = <1>; 时,reg = <0x44e09000 0x2000> 表示基址0x44e09000,长度0x2000。如果 #address-cells = <2>,那地址就得写两个数。
我曾经在配置一个FPGA桥接设备时,把 #address-cells 写成了2,但reg只给了一个地址值。结果内核解析出来的地址完全错位,设备死活不工作。查了两天才发现是cells数量不匹配。记住:address-cells和size-cells必须跟reg里的参数个数严格对应。
2.3 标签(Label)——给节点起个“外号”
标签就是节点的别名。你想想看,如果节点名很长,比如 spi@ff110000,每次引用都要写全名,多累啊。标签就是用来解决这个问题的。
// 定义标签
spi0: spi@ff110000 {
compatible = "snps,dw-apb-ssi";
reg = <0xff110000 0x1000>;
interrupts = <0 36 4>;
#address-cells = <1>;
#size-cells = <0>;
};
// 在其他地方引用标签
&spi0 {
status = "okay";
cs-gpios = <&gpio1 17 0>;
};
标签用冒号跟在节点名前。引用时用 &标签名。这招在设备树覆盖(overlay)里特别有用——你可以在一个单独的文件里用 &spi0 { ... } 来追加配置,不用动原始dts文件。
我个人习惯给所有重要节点都加上标签。哪怕当前不需要引用,也先加上。因为产品迭代时,你永远不知道哪个节点后面会被覆盖。我见过太多项目后期为了加一个标签,把整个dts翻个底朝天。
2.4 包含文件(Include)——代码复用的利器
设备树支持C语言的 #include 预处理指令。这玩意儿太实用了。SoC厂商通常会提供一个 .dtsi 文件(i表示include),里面定义了芯片内部所有外设的骨架。板级开发者只需要 #include 这个文件,然后填充或覆盖自己板子特有的部分。
// imx6ull.dtsi(SoC厂商提供)
#include "skeleton.dtsi"
/ {
compatible = "fsl,imx6ull";
cpus { ... };
soc {
aips1: aips-bus@02000000 { ... };
aips2: aips-bus@02100000 { ... };
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
status = "disabled";
};
};
};
// myboard.dts(板级开发者)
#include "imx6ull.dtsi"
/ {
model = "My i.MX6ULL Board";
chosen {
stdout-path = &uart1;
};
};
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
};
你看,myboard.dts 只写了板子特有的东西——型号、控制台、使能UART。所有SoC内部细节都在 imx6ull.dtsi 里。这就是设备树的精髓:分层设计,各司其职。
避坑指南:包含文件可以嵌套,但别搞成循环包含。我曾经见过一个项目,A.dtsi包含B.dtsi,B.dtsi又包含A.dtsi,编译直接报“too many include files”。另外,.dtsi 文件里不要写 /dts-v1/; 版本声明,这个只在最终的 .dts 文件里写一次。
2.5 四个构件如何协同工作
咱们用一个实际的数据采集设备例子,把节点、属性、标签、包含文件串起来。假设你有一块基于i.MX6ULL的采集板,带一个SPI接口的ADC芯片:
// my_adc_board.dts
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "Data Acquisition Board v1.0";
compatible = "mycomp,adc-board", "fsl,imx6ull";
adc@0 {
compatible = "ti,ads1256";
reg = <0>;
spi-max-frequency = <19200000>;
vref-supply = <®_vref>;
#address-cells = <1>;
#size-cells = <0>;
};
reg_vref: regulator-vref {
compatible = "regulator-fixed";
regulator-name = "vref";
regulator-min-microvolt = <2500000>;
regulator-max-microvolt = <2500000>;
};
};
&ecspi1 {
status = "okay";
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
};
这个例子里:
- 节点:
/(根)、adc@0(ADC设备)、reg_vref(电压基准) - 属性:
compatible、reg、spi-max-frequency等 - 标签:
reg_vref和&ecspi1引用 - 包含文件:
#include "imx6ull.dtsi"
嗯,到这里你应该明白了。设备树不是什么玄学,它就是一套结构化的硬件描述语言。节点搭骨架,属性填细节,标签方便引用,包含文件实现复用。把这四个东西玩转了,后面学中断映射、GPIO控制、时钟配置,都会轻松很多。
最后提醒一句:设备树编译工具(dtc)对语法很敏感。少一个分号、多一个括号、标签重复定义,都会直接报错。我建议你每次修改dts文件后,先用 dtc -I dts -O dtb -o test.dtb test.dts 编译一下,确认没有语法错误再烧到板子上。别问我为什么强调这个——我曾经在客户现场改完dts直接烧录,结果板子起不来,场面一度非常尴尬。