3. Zynq GPIO驱动从零到优化
设备树(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"表示禁用。调试时我经常用它来临时关闭某个外设。
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 开始计数,容易混淆。
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 控制器。
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。整个过程不需要重启系统,非常方便。
target 路径必须正确。如果 target 指向的节点不存在,加载会失败。我建议先用 dtc -I fs -O dts /sys/firmware/devicetree/base 导出当前设备树,确认节点路径。
3.4 知识结构图
下面这张图总结了设备树在 Zynq 平台上的核心逻辑:
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 小时的调试时间。嗯,这个账,值得算。