3. 设备树基础与Zynq实战:设备树语法、Zynq专用绑定与DT Overlay

设备树(Device Tree)是什么?说白了,它就是一块「硬件描述板」。Linux 内核通过它来知道你的板子上接了哪些外设、地址是多少、中断号是多少。没有设备树,内核就得靠一堆硬编码的板级文件来适配硬件——那维护起来简直是噩梦。

我在 Zynq 上做第一个驱动时,就被设备树折腾得不轻。明明代码逻辑没问题,驱动就是 probe 不了。后来发现,是设备树里一个 reg 属性写错了。嗯,从那以后,我养成了「先看设备树,再调驱动代码」的习惯。

3.1 设备树语法:节点、属性、中断

设备树的基本单元是节点(node)。每个节点代表一个硬件设备或总线。节点里放属性(property),描述设备的地址、中断、时钟等信息。

3.1.1 节点与属性

一个典型的设备树节点长这样:

uart1: serial@e0001000 {
    compatible = "xlnx,xuartps", "cdns,uart-r1p8";
    reg = <0xe0001000 0x1000>;
    interrupts = <0 50 4>;
    clock-frequency = <100000000>;
    status = "okay";
};

这里我解释几个关键点:

  • 节点名:serial@e0001000,@后面是基地址,方便人眼识别。
  • label:uart1,方便其他节点引用。
  • compatible:驱动匹配的关键字。内核通过它找到对应的驱动。我建议把厂商名和具体型号都写上,比如 "xlnx,xuartps" 和 "cdns,uart-r1p8",这样兼容性更好。
  • reg:地址和长度。Zynq 的地址空间是 32 位的,所以用 <0xe0001000 0x1000> 表示基地址 0xe0001000,范围 0x1000。
  • status:"okay" 表示启用,"disabled" 表示禁用。调试时我经常用它来临时关闭某个外设。
个人习惯:我写设备树时,喜欢把 label 写得短一点,比如 uart1、i2c0、spi1。这样在 overlay 里引用时,代码更干净。

3.1.2 中断描述

中断在设备树里用 interrupts 属性描述。Zynq 使用的是 GIC(通用中断控制器),中断描述格式为:

interrupts = <中断类型 中断号 触发方式>;

中断类型:0 表示 SPI(共享外设中断),1 表示 PPI(私有外设中断)。Zynq 的大部分外设都是 SPI。中断号是 GIC 的编号,从 0 开始。触发方式:1 是上升沿触发,4 是电平触发。

举个例子:

interrupts = <0 29 4>;  // SPI 中断号 29,电平触发

我曾经踩过一个坑:中断号写错了,驱动一直收不到中断。后来用 cat /proc/interrupts 一看,发现中断根本没注册上。排查了半天,原来是设备树里的中断号比实际少 1。Zynq 的 GIC 中断号从 0 开始,但有些文档从 1 开始计数,容易混淆。

注意:Zynq 的 GIC 中断号与 PS 端的中断号映射关系,一定要参考 UG585 的 Table 7-3。别凭感觉写,否则中断不工作。

3.2 Zynq 专用设备树绑定

Zynq 的设备树绑定,除了通用的节点外,还有一些专用绑定。这些绑定描述了 PS 和 PL 之间的交互。

3.2.1 顶层节点

一个典型的 Zynq 设备树顶层结构如下:

/ {
    model = "Zynq ZC706 Board";
    compatible = "xlnx,zynq-zc706", "xlnx,zynq-7000";

    chosen {
        bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw";
    };

    memory@0 {
        device_type = "memory";
        reg = <0x0 0x40000000>;  // 1GB DDR
    };

    amba: amba@0 {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;
        // 外设节点放在这里
    };
};

chosen 节点用来传递启动参数。我习惯把 console 和 rootfs 参数写在这里,方便调试时修改。memory 节点告诉内核 DDR 的起始地址和大小。

3.2.2 PS 端外设绑定

Zynq 的 PS 端外设(如 UART、I2C、SPI、GEM)都有固定的绑定。以 GEM(千兆以太网)为例:

gem0: ethernet@e000b000 {
    compatible = "cdns,zynq-gem", "cdns,gem";
    reg = <0xe000b000 0x1000>;
    interrupts = <0 22 4>;
    clocks = <&clkc 30>, <&clkc 30>, <&clkc 13>;
    clock-names = "pclk", "hclk", "tx_clk";
    phy-mode = "rgmii-id";
    status = "disabled";
};

这里 clocks 和 clock-names 是 Zynq 特有的。每个外设需要哪些时钟,在 UG585 里都有明确说明。我建议你对照 UG585 的 Clock Tree 来写,别自己猜。

3.2.3 PL 端 IP 核绑定

PL 端(FPGA 逻辑)的 IP 核,比如 AXI GPIO、AXI DMA、VDMA,都需要在设备树里描述。以 AXI GPIO 为例:

axi_gpio_0: gpio@40000000 {
    compatible = "xlnx,xps-gpio-1.00.a";
    reg = <0x40000000 0x10000>;
    interrupts = <0 59 4>;
    interrupt-parent = <&intc>;
    gpio-controller;
    #gpio-cells = <2>;
};

这里 interrupt-parent 指定中断控制器为 &intc(即 PS 端的 GIC)。gpio-controller 和 #gpio-cells 告诉内核这是一个 GPIO 控制器。

关键点:PL 端 IP 核的地址由 Vivado 的地址映射决定。每次修改 PL 设计后,记得重新生成设备树,否则地址对不上。

3.3 设备树覆盖(DT Overlay)

DT Overlay 是什么?它允许你在系统运行时动态加载或卸载设备树片段。说白了,就是「热插拔」设备树。在 Zynq 上,这个功能特别适合 PL 端动态加载的场景。

3.3.1 Overlay 的基本结构

一个 Overlay 文件(.dtbo)的结构如下:

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &amba;
        __overlay__ {
            my_ip: my_ip@43c00000 {
                compatible = "my,ip-core-1.0";
                reg = <0x43c00000 0x10000>;
                interrupts = <0 55 4>;
                interrupt-parent = <&intc>;
            };
        };
    };
};

/plugin/ 是 Overlay 的标志。fragment@0 是一个片段,target 指定要挂载到哪个节点下。__overlay__ 里放你要添加的节点。

3.3.2 编译与加载

编译 Overlay:

dtc -@ -I dts -O dtb -o my_ip.dtbo my_ip.dts

-@ 选项表示生成 Overlay 符号表,这是必须的。

加载 Overlay:

mkdir /sys/kernel/config/device-tree/overlays/my_overlay
cat my_ip.dtbo > /sys/kernel/config/device-tree/overlays/my_overlay/dtbo

卸载 Overlay:

rmdir /sys/kernel/config/device-tree/overlays/my_overlay

我曾经在项目里用 Overlay 实现 PL 端的动态重配置。FPGA 加载一个新的 bitstream 后,再加载对应的 Overlay,驱动就能自动 probe。整个过程不需要重启系统,非常方便。

避坑指南:Overlay 的 target 路径必须正确。如果 target 指向的节点不存在,加载会失败。我建议先用 dtc -I fs -O dts /sys/firmware/devicetree/base 导出当前设备树,确认节点路径。

3.4 知识结构图

下面这张图总结了设备树在 Zynq 平台上的核心逻辑:

Zynq 设备树核心结构 Zynq 设备树核心结构 / (根节点) chosen (启动参数) memory (DDR) amba (总线) PS 外设 (UART/GEM) PL 外设 (AXI GPIO) DT Overlay 动态加载/卸载 适合 PL 动态重配置

3.5 实战建议

最后,我总结几条实战经验:

场景 建议
编写设备树 先看 UG585 的 Memory Map 和 Interrupt Table,确认地址和中断号
调试设备树 用 dtc -I fs -O dts /sys/firmware/devicetree/base 导出当前设备树,检查节点是否正确
PL 端 IP 核 每次修改 Vivado 设计后,重新生成设备树,不要手动改地址
DT Overlay 先确认 target 路径,再加载。用 ls /sys/kernel/config/device-tree/overlays/ 查看加载状态

设备树是 Zynq 驱动开发的基石。你花 30 分钟把设备树写对,后面能省 3 小时的调试时间。嗯,这个账,值得算。