第3章 设备树(Device Tree)实战:DTS语法、绑定文档解读、在Jacinto平台上添加自定义外设节点
设备树,说白了就是一块「硬件描述板」。它告诉内核:你有哪些外设、它们挂在哪条总线上、寄存器地址是多少、中断号是什么。没有设备树,内核就得靠一堆硬编码的板级文件来认硬件——那维护起来简直是噩梦。
我在 Jacinto 平台上最早踩的坑,就是设备树里一个 reg 属性写错了字节数,结果 SPI 控制器死活不干活。查了两天才发现,嗯,就是少了个 0x 前缀。从那以后,我对 DTS 的每个字段都格外小心。
3.1 设备树基础语法——你得先会「读树」
设备树的源文件是 .dts,编译后变成 .dtb。内核启动时,Bootloader 把 .dtb 传给内核,内核解析后就知道硬件长什么样了。
一个典型的节点长这样:
/dts-v1/;
/ {
model = "TI Jacinto 7 EVM";
compatible = "ti,j721e-evm", "ti,j721e";
chosen {
stdout-path = &main_uart0;
};
memory@80000000 {
device_type = "memory";
reg = <0x00 0x80000000 0x00 0x80000000>;
};
main_uart0: serial@2800000 {
compatible = "ti,j721e-uart", "ti,omap4-uart";
reg = <0x00 0x02800000 0x00 0x100>;
interrupts = <GIC_SPI 192 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&k3_clks 146 0>;
status = "okay";
};
};
这里有几个关键点:
- 节点名:
serial@2800000,@后面是基地址,方便人眼识别。 - 标签:
main_uart0:,方便在其他地方引用,比如&main_uart0。 - 属性:
compatible是驱动匹配的关键,内核靠它找到对应的驱动。 - reg:地址和长度,格式是
<地址高位 地址低位 长度高位 长度低位>,64位平台尤其要注意。
reg 属性时,我会先查 TRM 里的 memory map 表格,确认外设的地址范围和长度。长度写小了,驱动访问会越界;写大了,可能和其他设备冲突。
3.2 绑定文档——驱动和设备的「契约」
绑定文档(Binding Document)是内核里 Documentation/devicetree/bindings/ 目录下的 .yaml 文件。它规定了某个外设节点必须有哪些属性、可选哪些属性、值的格式是什么。
举个例子,TI Jacinto 上的 McASP(多通道音频串行端口)绑定文档会告诉你:
| 属性 | 是否必须 | 说明 |
|---|---|---|
compatible |
是 | 必须包含 "ti,j721e-mcasp" |
reg |
是 | 寄存器基地址和长度 |
interrupts |
是 | 中断号,GIC 类型 |
clocks |
是 | 时钟句柄,来自 k3_clks |
dmas |
否 | DMA 通道,如果使用 DMA 传输则需要 |
status |
否 | 默认 "okay",禁用时写 "disabled" |
我曾经犯过一个错:给 McASP 配了 DMA 通道,但忘了在绑定文档里查 dmas 属性的格式。结果 DMA 请求号写反了,音频数据全乱套。后来我养成了习惯——每加一个新节点,先翻绑定文档,对着格式写属性。
dt-bindings 检查你的 DTS 是否符合规范。如果 compatible 字符串写错了,编译不会报错,但驱动加载时会直接跳过你的设备。
3.3 在 Jacinto 平台上添加自定义外设节点
假设我们要在 Jacinto 7 的 MAIN 域添加一个自定义的 GPIO 扩展器,挂在 I2C0 总线上。地址是 0x20,支持 8 个 GPIO 口。
步骤很简单,但每一步都有坑:
- 找到正确的
.dtsi文件——Jacinto 7 的 MAIN 域设备树在arch/arm64/boot/dts/ti/k3-j721e-main.dtsi。别加到 WAKEUP 域去,否则驱动找不到时钟。 - 在 I2C0 节点下添加子节点——I2C0 的标签通常是
&main_i2c0。 - 写对
compatible字符串——假设我们的驱动是my_gpio_expander,那就写"my,gpio-expander"。 - 配置
reg为 I2C 地址——0x20,注意是 7 位地址,不要写 8 位。 - 添加
gpio-controller和#gpio-cells——告诉内核这是个 GPIO 控制器。
最终代码长这样:
&main_i2c0 {
status = "okay";
clock-frequency = <400000>;
my_gpio: gpio-expander@20 {
compatible = "my,gpio-expander";
reg = <0x20>;
gpio-controller;
#gpio-cells = <2>;
interrupt-parent = <&main_gpio0>;
interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
status = "okay";
};
};
这里有个细节:#gpio-cells = <2> 表示引用这个 GPIO 时,需要两个参数——GPIO 编号和标志位。比如 &my_gpio 3 GPIO_ACTIVE_LOW。
核心要点:设备树不是写完了就完事。你得用 dtc 工具编译成 .dtb,然后反编译回来检查有没有丢失属性。我每次都会做这个「编译-反编译」验证,确保设备树没有被编译器优化掉关键信息。
3.4 设备树知识体系总览
下面这张图帮你理清设备树的核心逻辑:从 DTS 源文件到内核驱动匹配的完整链路。
你看,从 DTS 到驱动匹配,中间每一步都有讲究。绑定文档和 DTS 语法是左右两条「护栏」,保证你写的设备树能被内核正确理解。
3.5 避坑指南——我替你踩过的雷
- 地址写错:Jacinto 7 是 64 位平台,
reg属性要用两个<>表示高 32 位和低 32 位。我见过有人只写一个<0x2800000 0x100>,结果内核把地址解析成0x0000000002800000,长度变成0x0000000000000100——虽然碰巧对了,但遇到高地址外设就崩。 - 中断号搞混:Jacinto 有 MAIN 域和 WAKEUP 域,中断控制器不同。MAIN 域用
GIC_SPI,WAKEUP 域用GIC_SPI但中断号偏移不同。查 TRM 时一定要确认是哪个域的。 - 时钟依赖没配:有些外设需要多个时钟,比如 McASP 需要
ahclkx和auxclk。只配一个时钟,驱动 probe 会失败,而且日志里只打印一句clk_prepare_enable failed——排查起来很痛苦。 - status 忘记写:默认
status = "disabled",如果你不显式写"okay",内核不会使能这个设备。我调试时经常怀疑驱动有问题,结果发现是设备树没打开。
cat /proc/device-tree/ 看内核解析后的设备树;第二,dmesg | grep -i "of_" 看设备树解析日志;第三,反编译 .dtb 对比原始 .dts。这三招能解决 90% 的设备树问题。
设备树这东西,说难不难,说简单也不简单。关键是多写、多查、多验证。你想想看,一个 reg 属性写错,可能让你浪费一整天。但只要掌握了语法和绑定文档的套路,后面就顺了。