2、设备树语法:节点、属性、标签、包含文件、宏定义
好,咱们今天聊聊设备树的基础语法。说实话,这部分内容看起来琐碎,但却是你写设备树的「基本功」。我见过不少工程师,上来就复制粘贴别人的dts,结果板子跑不起来,查半天发现是语法写错了。嗯,咱们先把地基打牢。
2.1 节点:设备树的骨架
设备树本质上是一棵树。每个硬件设备就是一个节点。节点用花括号 {} 包裹,里面可以嵌套子节点。
最基本的节点长这样:
/dts-v1/;
/ {
model = "My Board";
compatible = "vendor,my-board";
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x00>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
};
看到没?根节点是 /,下面挂了一个CPU节点和一个内存节点。每个节点都有一个名字,比如 cpu@0。@后面的数字是地址,用来区分同类型设备。我在项目中遇到过有人把两个CPU都写成 cpu@0,结果内核只认了一个——这种低级错误其实挺常见的。
节点命名规则:
- 节点名格式:
名称@地址 - 名称尽量用通用名,比如 cpu、memory、i2c、spi
- 地址部分用十六进制,小写
2.2 属性:描述设备的语言
节点里放的是属性。属性就是键值对,用来描述设备的特征。常用的属性类型有几种:
| 属性类型 | 写法 | 说明 |
|---|---|---|
| 字符串 | compatible = "vendor,device"; |
用来匹配驱动,最重要 |
| 32位整数 | reg = <0x10000000 0x1000>; |
地址和大小,用尖括号 |
| 64位整数 | reg = <0x00 0x80000000 0x00 0x10000000>; |
两个32位拼一个64位 |
| 布尔值 | status = "disabled"; |
或者 "okay",没有true/false |
| 字节数组 | local-mac-address = [00 11 22 33 44 55]; |
用方括号,十六进制 |
| 字符串列表 | interrupt-names = "rx", "tx"; |
用逗号分隔 |
这里我要特别强调一下 compatible 属性。说白了,它就是设备和驱动之间的「暗号」。内核通过这个字符串找到对应的驱动。我建议你养成好习惯:格式用 厂商名,设备名,比如 ti,am335x-uart。我曾经接手过一个项目,compatible 写成了 uart,am335x,结果驱动死活匹配不上——折腾了两天才发现是顺序反了。
我的小技巧:写 reg 属性时,地址和长度成对出现。比如 reg = <0x10000000 0x1000> 表示从 0x10000000 开始,大小 4KB。如果有多段地址,就写多对:reg = <0x10000000 0x1000 0x20000000 0x2000>;
2.3 标签:给节点起个外号
标签就是节点的别名。用 标签名: 节点名 的格式。为什么要用标签?你想想看,如果设备树里有个串口节点,你在好几个地方都要引用它,每次都写完整路径多麻烦。
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <32>;
};
// 在其他地方引用
&uart0 {
clock-frequency = <24000000>;
};
看到 &uart0 了吗?这就是通过标签引用节点。我在项目中习惯给所有重要外设都加上标签,比如 i2c0、spi1、mmc0。这样后面要修改或者引用的时候,直接 &i2c0 就搞定了,不用去翻整棵树找路径。
注意:标签名不能重复。整个设备树里,标签是全局唯一的。如果你不小心写了两个 uart0,编译器会报错。嗯,我犯过这个错,编译时一脸懵。
2.4 包含文件:别重复造轮子
设备树支持 #include 预处理指令。这跟C语言的头文件是一个道理。常用的包含文件有:
.dtsi文件:设备树包含文件,放通用定义.h文件:头文件,放宏定义
举个例子:
#include "soc.dtsi"
#include "pinctrl.h"
/ {
model = "My Board";
compatible = "vendor,my-board";
&uart0 {
pinctrl-0 = <&uart0_pins>;
status = "okay";
};
};
这里 soc.dtsi 里定义了 SoC 的公共部分,比如所有外设的基地址、中断号。你的板级文件只需要 #include 进来,然后修改或启用需要的部分就行。我建议你把 SoC 的通用配置都放到 .dtsi 里,板级差异放到 .dts 里。这样换板子时只需要改 .dts,省事不少。
2.5 宏定义:让设备树更清爽
设备树里可以用宏。宏定义通常放在 .h 文件里,然后在 .dts 里 #include 进来。比如:
// pinctrl.h
#define PIN_CTRL_DEFAULT 0x00
#define PIN_CTRL_PULLUP 0x01
#define PIN_CTRL_PULLDOWN 0x02
// board.dts
#include "pinctrl.h"
&uart0_pins {
pinmux = <PIN_CTRL_PULLUP>;
};
你看,用宏代替魔法数字,代码可读性一下子就上来了。我在项目中经常把 GPIO 编号、时钟频率、中断号都定义成宏。比如:
#define UART0_BASE 0x10000000
#define UART0_IRQ 32
&uart0 {
reg = <UART0_BASE 0x1000>;
interrupts = <UART0_IRQ>;
};
为什么要这么做?你想想看,如果哪天 UART0 的基地址变了,你只需要改一个宏,而不是满世界找 0x10000000。我曾经维护过一个项目,里面散落着几十个硬编码的地址,改起来简直要命。从那以后,我坚持用宏定义所有关键数值。
总结一下我的习惯:
- SoC 通用配置放
.dtsi - 板级差异放
.dts - 所有数值常量用宏定义
- 重要节点加标签,方便引用
- compatible 字符串严格按
厂商,设备格式
好了,设备树的基础语法就这些。说白了就是节点套节点,属性描述设备,标签方便引用,包含文件避免重复,宏定义让代码更干净。下一节咱们聊聊地址映射和中断,那才是真正让人头疼的地方。