第2章:设备树语法入门
好,咱们正式开始啃设备树的语法。说实话,很多嵌入式工程师第一次看到.dts文件时,第一反应是「这什么鬼?长得像C语言又不是C语言」。别急,我当年也是这样。花半小时搞懂它的骨架,后面就顺了。
2.1 DTS文件长什么样?
一个典型的.dts文件,结构其实很简单。我习惯把它想象成「一棵倒着长的树」——根节点在最上面,下面挂着一堆子节点。每个节点代表一个硬件设备或总线。
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a8";
reg = <0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
};
看到没?最外层是根节点/,里面包了cpus、memory这些子节点。每个节点用花括号{}包起来,最后用分号;结束。嗯,这里要注意:分号不能丢,丢了编译器直接报错,我踩过这个坑。
2.2 节点(node)和属性(property)
节点就是硬件单元。比如一个I2C控制器、一个GPIO引脚、一个SPI Flash,都对应一个节点。节点里面放的是属性——描述这个硬件的信息。
属性分两种:
- 标准属性:比如
compatible、reg、status,内核规定好的 - 自定义属性:厂商自己加的,比如
max-frequency、gpios
举个例子,一个I2C设备节点:
&i2c0 {
status = "okay";
clock-frequency = <400000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
这里eeprom@50是子节点,@50表示它的I2C地址是0x50。compatible告诉内核用哪个驱动,reg就是地址。我在项目中遇到过有人把reg写成了addr,结果驱动死活匹配不上——因为内核只认reg。
核心原则:节点描述「有什么硬件」,属性描述「硬件长什么样」。
2.3 标签(label)和引用
标签这东西,说白了就是给节点起个外号。方便你在别的地方引用它。比如:
i2c0: i2c@44e0b000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "ti,omap4-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
};
这里的i2c0:就是标签。后面你想修改这个节点,直接用&i2c0引用它:
&i2c0 {
status = "okay";
clock-frequency = <400000>;
};
为什么要这么干?因为设备树文件经常被多个板子共用。基板定义好i2c0的寄存器地址和中断,具体板子再通过&i2c0打开它、配频率。这样代码复用性极高。
我的习惯:标签名尽量跟节点名保持一致,比如uart0对应uart@44e09000。不然过两个月你自己都忘了哪个标签是哪个。
2.4 include机制
设备树支持#include,跟C语言一样。但注意,它用的是C预处理器,所以你可以用#define定义宏。
实际项目中,一个完整的设备树文件通常长这样:
#include "am33xx.dtsi"
#include "am335x-bone-common.dtsi"
#include "am335x-boneblack-common.dtsi"
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
};
.dtsi文件是「设备树包含文件」,相当于头文件。SoC厂商会提供soc.dtsi,里面定义好所有内置外设的节点。板级厂商再#include它,然后修改或启用需要的节点。
我曾经接手一个项目,板子改了三次PCB,每次只改几个GPIO和I2C地址。我直接把公共部分放.dtsi,每个板子一个.dts,只写差异部分。嗯,维护起来轻松多了。
注意:#include和/include/是两回事。前者是C预处理器指令,支持宏定义;后者是dtc编译器自己的语法,功能弱很多。我建议统一用#include。
2.5 避坑指南
最后分享几个我踩过的坑:
- 分号漏了:每个属性结束都要分号,每个节点结束也要分号。漏一个,编译报错,而且报错信息经常指向下一行,很坑。
- 标签重复:不同
.dtsi里定义了同名标签,编译不会报错,但后面的会覆盖前面的。我曾经查了半天,才发现是标签冲突。 - reg格式搞错:
reg = <0x80000000 0x10000000>,第一个是起始地址,第二个是长度。有人写成reg = <0x80000000>,少了长度,内核直接懵。 - compatible拼写:驱动匹配靠字符串比较,拼错一个字母就匹配不上。我建议直接从驱动源码里复制,别手敲。
好了,设备树语法就这些。说白了就是「节点套节点,属性填里面,标签方便引用,include复用代码」。下一章咱们聊聊怎么编译和调试设备树,那才是真正动手的时候。