第四节:设备树基础——DTS语法、绑定文档与GPIO/中断描述
各位同学,咱们今天聊聊设备树。说实话,我刚入行那会儿,设备树还是个新鲜玩意儿。那时候做Linux驱动,板级文件里全是C语言的结构体,一个平台设备一个平台设备的注册。后来ARM Linux社区全面转向设备树,我一开始还挺抵触的——毕竟老办法用顺手了嘛。但用了一段时间后,我不得不承认:设备树确实是个好东西。
设备树,说白了就是一套描述硬件信息的“配置文件”。它用文本形式把CPU、内存、外设、中断、GPIO这些东西的拓扑关系说清楚。内核启动时解析这个文件,动态构建出设备模型。这样做的好处很明显:换一块板子,改改设备树就行,不用重新编译内核。
4.1 DTS语法基础——别被那些花括号吓到
DTS的语法其实很简单。我经常跟团队里的新人说:你就把它当成一个JSON文件,只不过换成了C语言风格的写法。来看个最基本的例子:
/dts-v1/;
/ {
model = "Rockchip RK3568 EVB1 Board";
compatible = "rockchip,rk3568-evb1", "rockchip,rk3568";
chosen {
stdout-path = &uart2;
};
memory {
device_type = "memory";
reg = <0x0 0x00200000 0x0 0x7fe00000>;
};
};
看到没?根节点用/表示,里面可以嵌套子节点。每个节点都有属性,属性值可以是字符串、32位整数、64位整数,或者它们的数组。嗯,这里要注意:reg属性里的地址和大小,默认是32位的。如果你用64位地址,得加上#address-cells = <2>。
我个人习惯把设备树文件按功能模块拆分。比如rk3568.dtsi放芯片内部外设,rk3568-evb.dts放板级差异。用#include包含进来,再用&节点名去覆盖或追加属性。这样维护起来清爽很多。
4.2 绑定文档解读——别瞎猜,看文档
绑定文档,英文叫Binding Document。这东西是设备树和驱动之间的“契约”。驱动开发者通过它知道设备树里该写什么属性,设备树编写者通过它知道驱动需要什么信息。
绑定文档通常放在内核源码的Documentation/devicetree/bindings/目录下。每个外设都有对应的YAML文件或文本文件。我建议你养成一个习惯:写设备树之前,先翻翻绑定文档。我曾经有一次,凭经验写了一个GPIO按键的设备树节点,结果驱动死活不认。折腾了半天,才发现新版本内核改了属性名——从gpios改成了gpio。你说冤不冤?
来看一个典型的绑定文档结构:
# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/gpio/gpio-leds.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#
title: GPIO LEDs
maintainers:
- Jacek Anaszewski <jacek.anaszewski@gmail.com>
properties:
compatible:
const: gpio-leds
led-0:
type: object
properties:
gpios:
maxItems: 1
label:
description: Label for this LED
linux,default-trigger:
description: Default trigger for this LED
required:
- compatible
additionalProperties: false
看到required字段没?那里面列出的属性是必须写的,少一个驱动就加载不了。我见过有人把compatible拼写错了,结果驱动根本没匹配上。这种低级错误,查绑定文档就能避免。
required字段是硬性要求,optional字段按需添加。写设备树时,先确认compatible字符串是否和驱动里的一致。
4.3 GPIO在设备树中的描述——从硬件管脚到软件节点
GPIO在设备树里怎么描述?说白了就是告诉内核:这个设备用了哪个GPIO控制器、哪个引脚、以及引脚的电平极性。来看一个实际例子:
&gpio0 {
/* GPIO0_A1 作为按键输入 */
key-power {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&key_power_pin>;
power-key {
gpios = <&gpio0 RK_PA1 GPIO_ACTIVE_LOW>;
linux,code = <KEY_POWER>;
label = "Power Key";
debounce-interval = <10>;
};
};
};
这里gpios属性有三个值:GPIO控制器引用、引脚编号、电平标志。GPIO_ACTIVE_LOW表示按下时引脚为低电平。我刚开始做的时候,经常搞反这个标志——明明按下按键应该触发事件,结果死活没反应。后来用示波器一量,才发现电平极性写反了。
瑞芯微的芯片里,GPIO引脚编号通常用宏定义,比如RK_PA1表示GPIO0组的第1个引脚。这些宏定义在include/dt-bindings/pinctrl/rockchip.h里。你写设备树时,记得包含这个头文件。
<phandle 0 1>这种格式,有些用<phandle GPIO_ACTIVE_HIGH>。一定要看对应芯片的绑定文档,别想当然。
4.4 中断在设备树中的描述——中断控制器与中断号
中断描述比GPIO稍微复杂一点。因为它涉及中断控制器、中断号、触发类型等多个维度。来看一个典型的中断描述:
&i2c1 {
status = "okay";
clock-frequency = <400000>;
touchscreen: ft5x06@38 {
compatible = "edt,edt-ft5x06";
reg = <0x38>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PB5 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;
};
};
这里interrupt-parent指定了中断控制器是哪个GPIO组。interrupts属性有两个值:引脚编号和触发类型。IRQ_TYPE_EDGE_FALLING表示下降沿触发。我建议你用边沿触发而不是电平触发,因为电平触发容易产生中断风暴——如果中断服务程序没及时清除中断源,会一直触发。
为什么会这样?你想想看,电平触发是持续有效的。如果硬件没把电平拉回去,CPU就会一直响应中断。而边沿触发只关心跳变瞬间,处理完就完事了。我在一个项目中遇到过这个问题:一个触摸屏用电平触发,手指一放上去,CPU占用率直接飙到100%。后来改成边沿触发,问题就解决了。
对于多级中断控制器,比如GIC(通用中断控制器),描述方式会复杂一些:
gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3";
reg = <0x0 0xfd400000 0x0 0x10000>,
<0x0 0xfd460000 0x0 0x10000>;
interrupt-controller;
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
};
#interrupt-cells = <3>表示每个中断描述需要3个cell:中断类型(SPI、PPI等)、中断号、触发标志。比如<0 89 4>表示SPI类型、中断号89、高电平触发。这些数字怎么来的?看芯片手册里的中断向量表。
<0 121 4>。这种坑,不看芯片手册根本发现不了。
4.5 实战经验——设备树调试三板斧
写设备树难免出错。我总结了三板斧,分享给大家:
- 编译检查: 用
make dtbs编译,看有没有语法错误。常见的错误是少写了分号、花括号不匹配。 - 反编译验证: 用
dtc -I dtb -O dts把编译好的dtb反编译成dts,看看是不是你想要的。有时候编译器会优化掉一些属性。 - 内核日志: 启动时加
earlycon和ignore_loglevel参数,看内核解析设备树时有没有报错。比如OF: fdt: not found /soc/i2c@...这种信息。
嗯,这里还要提醒一点:设备树里的status属性默认是"okay"还是"disabled",取决于芯片的dtsi文件。如果你发现某个外设没工作,先检查一下status是不是被设成了"disabled"。我有个同事,调试了三天SD卡驱动,最后发现是dtsi里默认把SD卡控制器禁用了。你说气不气人?
好了,这一节的内容就到这里。设备树是嵌入式Linux开发的基石,一定要熟练掌握。下一节我们讲时钟和复位在设备树中的描述,那又是另一番天地了。