第四节:设备树基础——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包含进来,再用&节点名去覆盖或追加属性。这样维护起来清爽很多。

小技巧: 我在项目中遇到过好几次这样的问题——两个dtsi文件定义了同一个节点,结果编译报错。后来我养成了一个习惯:每个dtsi文件开头都加个注释,写明这个文件定义了哪些节点,避免重复。

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里。你写设备树时,记得包含这个头文件。

注意: 不同厂家的GPIO编号方式不一样。有些用<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、高电平触发。这些数字怎么来的?看芯片手册里的中断向量表。

避坑指南: 我曾经在调试一个网卡驱动时,中断号写错了。手册上写的是IRQ 89,我直接写了89。结果驱动加载后,中断一直不触发。后来才发现,GIC的中断号是从32开始算的,SPI中断号要加上32。所以实际应该写<0 121 4>。这种坑,不看芯片手册根本发现不了。

4.5 实战经验——设备树调试三板斧

写设备树难免出错。我总结了三板斧,分享给大家:

  1. 编译检查:make dtbs编译,看有没有语法错误。常见的错误是少写了分号、花括号不匹配。
  2. 反编译验证:dtc -I dtb -O dts把编译好的dtb反编译成dts,看看是不是你想要的。有时候编译器会优化掉一些属性。
  3. 内核日志: 启动时加earlyconignore_loglevel参数,看内核解析设备树时有没有报错。比如OF: fdt: not found /soc/i2c@...这种信息。

嗯,这里还要提醒一点:设备树里的status属性默认是"okay"还是"disabled",取决于芯片的dtsi文件。如果你发现某个外设没工作,先检查一下status是不是被设成了"disabled"。我有个同事,调试了三天SD卡驱动,最后发现是dtsi里默认把SD卡控制器禁用了。你说气不气人?

好了,这一节的内容就到这里。设备树是嵌入式Linux开发的基石,一定要熟练掌握。下一节我们讲时钟和复位在设备树中的描述,那又是另一番天地了。