☰
Zynq MPSoC PCIe Root Complex 实战:从设备树配置到设备枚举的完整避坑指南
2026/9/27 1:08:37 网站建设 项目流程

PCIe Root Complex 这个东西,在 x86 平台上你基本感知不到它的存在——BIOS 帮你把一切都配好了,插上设备就能认。但一旦切到 Zynq UltraScale+ 这种嵌入式平台,要让它当 RC(Root Complex)去枚举下游的 EP(Endpoint)设备,事情就完全不一样了:PS 端的配置、PL 端的 IP 核参数、设备树里的 ranges 映射、中断控制器的级联,每一环出问题都会让你卡在"link up 了但枚举不到设备"或者"设备认到了但 BAR 地址全是 0xFFFFFFFF"这种状态。

我前后在 ZCU102 和自制的 MPSoC 板子上搭过三套 PCIe RC 环境,踩的坑从 GT 参考时钟选错到设备树 ranges 少写一个 0 都有。这篇就把整个流程从头捋一遍,重点放在那些文档里不会明说、但实际调试中一定会遇到的地方。不管你是刚拿到板子的新手,还是已经跑通了 PetaLinux 但卡在设备树配置上的老手,下面这些内容应该都能帮你省掉几个通宵。

1. 先搞清楚 Zynq MPSoC 上 PCIe RC 的硬件拓扑

1.1 PS 端 PCIe 控制器和 PL 端 IP 核的区别

Zynq UltraScale+ 的 PCIe 实现有两条路可以走,这一点必须先分清楚,否则后面选型会走弯路。

第一条路是用PS 端集成的 PCIe 控制器。MPSoC 的 PS 里硬核集成了 PCIe Gen2 x4 的控制器(部分型号支持 Gen3),它直接挂在 AXI 总线上,通过 GTR 收发器对外连接。这条路的好处是不占 PL 资源、功耗低、驱动成熟,PetaLinux 里直接有现成的xilinx-pcie驱动。缺点是通道数和 lane 数固定,灵活性差。

第二条路是用PL 端的 PCIe IP 核(比如xdma或pcie4_uscale_plus)。这条路灵活,可以自己配 lane 宽度、可以做成 RC 也可以做成 EP,但需要自己处理 GT 的复位时序、参考时钟、AXI 互联,复杂度高不少。

对于"搭建 Root Complex"这个需求,绝大多数场景用的是 PS 端控制器。原因很简单:你要的是让 MPSoC 去枚举和管理下游设备,PS 端控制器配合 Linux 内核的pcie-xilinx-nwl或xilinx-pcie驱动就能完成,不需要在 PL 里再搭一套。下面讨论的也主要是这条路。

1.2 RC 模式下数据流是怎么走的

理解数据流对调试至关重要。在 RC 模式下,整个链路是这样的:

  • CPU 发起一个对下游 EP 设备 BAR 空间的读写请求
  • 请求经过 AXI 总线到达 PS 端的 PCIe 控制器
  • 控制器把 AXI 事务转换成 PCIe TLP(Transaction Layer Packet)
  • TLP 经过 GTR 收发器、经过 PCB 走线到达 EP 设备
  • EP 响应后原路返回

这里的关键在于地址映射。CPU 看到的是 AXI 地址空间,EP 设备看到的是 PCIe 地址空间,两者之间需要一个转换关系。这个转换关系就是设备树里ranges属性定义的。很多人在这一步翻车,就是因为没搞明白ranges里的三段地址分别代表什么。

1.3 硬件设计阶段就要确认的几件事

在动手写软件之前,硬件原理图上这几项必须确认清楚:

检查项说明常见问题
参考时钟频率GTR 的 REFCLK 是 100MHz 还是 125MHz配错直接 link 不上
参考时钟来源来自专用时钟芯片还是 EP 端提供RC 模式下通常由 RC 侧提供
PERST# 信号复位信号是否连接到 PS 的 MIO 或 PL 引脚没接的话 EP 无法正常复位
lane 映射TX/RX 差分对是否交叉正确TX 接 TX 是经典错误
电源时序EP 设备的电源和 PERST# 时序关系时序不对会导致枚举失败

提示:参考时钟这一项我要特别强调。ZCU102 上 PS 端 PCIe 的参考时钟默认是 100MHz,来自 Si5332 时钟芯片。如果你用的是自制板,一定要在 Vivado 的 PS 配置里把参考时钟频率设对,否则 GT 根本锁不住。

2. Vivado 里 PS 端 PCIe 控制器的配置细节

2.1 PS 配置界面里那几个容易忽略的选项

打开 Vivado 的 Zynq UltraScale+ MPSoC 配置界面,在 PS-PCIe 这一栏里,有几个参数需要仔细设置:

PCIe Controller 0/1 的选择:MPSoC 有两个 PCIe 控制器,Controller 0 通常映射到 GTR 的 Lane 0-3,Controller 1 映射到 Lane 4-7。具体用哪个要看你的板子走线。

Lane Width:可以选 x1、x2、x4。注意这里选的是 RC 侧支持的最大宽度,实际 link 宽度会在训练时协商。如果你只接了 x1 的设备,这里选 x4 也没关系,会自动降级。

Reference Clock Frequency:100MHz 或 125MHz,必须和硬件一致。

Max Link Speed:Gen1 (2.5GT/s) 或 Gen2 (5GT/s)。调试阶段建议先设 Gen1,稳定后再升 Gen2。

2.2 AXI 地址空间的分配逻辑

PS 端 PCIe 控制器需要几段 AXI 地址空间:

  • AXI-Lite 配置空间:用于访问 PCIe 的配置寄存器,通常分配 16MB 左右
  • AXI 内存空间(下游):用于映射 EP 设备的 BAR 空间,大小取决于你要接多少设备
  • AXI 内存空间(上游/DMA):如果 RC 要主动 DMA 到 EP,需要这段

在 Vivado 的 Address Editor 里,这些空间会自动分配,但你要检查是否有重叠。我遇到过因为地址空间重叠导致内核启动时直接 hang 住的情况。

2.3 生成 XSA 之前的检查清单

在 Export Hardware 之前,确认以下几点:

  1. PS 端 PCIe 控制器的时钟已经使能
  2. GTR 的参考时钟源选择正确
  3. 中断已经连接到 GIC(PL-PS 中断或 PS 内部中断)
  4. Address Editor 里没有地址冲突(黄色警告要处理)
  5. 如果用了 PL 端的中断聚合,确认pl_ps_irq已经连接

导出 XSA 后,就可以进入 PetaLinux 环节了。

3. PetaLinux 工程配置与内核选项

3.1 创建工程和设备树的初始配置

petalinux-create -t project -s <xsa文件路径> -n pcie_rc_demo cd pcie_rc_demo petalinux-config --get-hw-description=./

在petalinux-config的菜单里,需要关注这几项:

  • Subsystem AUTO Hardware Settings→ 确认 PCIe 相关的硬件信息已经被正确导入
  • DTG Settings→ 设备树生成器的配置,后面会用到
  • Image Packaging Configuration→ 确认 rootfs 类型

3.2 内核配置里必须打开的选项

petalinux-config -c kernel

需要确认以下配置项:

CONFIG_PCI=y CONFIG_PCI_MSI=y CONFIG_PCIEPORTBUS=y CONFIG_PCIE_XILINX_NWL=y # 如果用 NWL 驱动 CONFIG_PCIE_XILINX=y # 如果用旧版 xilinx-pcie 驱动 CONFIG_PCI_MSI_IRQ_DOMAIN=y CONFIG_PCI_HOST_GENERIC=y # 某些情况下需要

具体用哪个驱动取决于你的 Vivado 版本和 PS 配置。2019.1 之后的版本通常用pcie-xilinx-nwl,更早的版本用xilinx-pcie。选错了驱动,内核启动时 PCIe 控制器根本不会初始化。

3.3 设备树里 PCIe 节点的关键属性

这是整篇文章的核心部分。PetaLinux 生成的设备树里,PCIe 节点大概长这样:

&pcie { status = "okay"; reset-gpios = <&gpio 38 GPIO_ACTIVE_LOW>; interrupt-map-mask = <0 0 0 7>; interrupt-map = <0 0 0 1 &pcie_intc 0>, <0 0 0 2 &pcie_intc 1>, <0 0 0 3 &pcie_intc 2>, <0 0 0 4 &pcie_intc 3>; pcie_intc: interrupt-controller { interrupt-controller; #address-cells = <0>; #interrupt-cells = <1>; }; };

看起来不复杂,但每个属性都有讲究。下面逐个拆解。

4. 设备树配置的深水区:ranges、中断和复位

4.1 ranges 属性的三段地址到底怎么填

ranges是设备树里最容易出错的地方。它的格式是:

ranges = <flags child_address parent_address size>;

对于 PCIe 节点,通常有两段 ranges:一段是配置空间,一段是内存空间。

ranges = <0x02000000 0x0 0xE0000000 0x0 0xE0000000 0x0 0x10000000>, <0x43000000 0x80 0x00000000 0x80 0x00000000 0x0 0x80000000>;

第一段0x02000000表示 32 位内存空间,child 地址0xE0000000,parent 地址也是0xE0000000,大小0x10000000(256MB)。第二段0x43000000表示 64 位预取内存空间。

踩坑点:child 地址和 parent 地址不一定要相同,但如果你不确定,设成相同是最安全的。另外 size 字段的位数要和#size-cells匹配,PCIe 节点的#size-cells通常是 2,所以 size 要写成两个 cell。

4.2 中断映射的级联关系

PCIe 的中断是级联的:EP 设备的 INTx 中断先到 PCIe 控制器的中断控制器(pcie_intc),然后再映射到 GIC。interrupt-map就是定义这个映射关系的。

interrupt-map-mask = <0 0 0 7>; interrupt-map = <0 0 0 1 &pcie_intc 0>, <0 0 0 2 &pcie_intc 1>, <0 0 0 3 &pcie_intc 2>, <0 0 0 4 &pcie_intc 3>;

interrupt-map-mask是<0 0 0 7>,意味着只匹配最后一个 cell 的低 3 位(也就是 INTx 的编号 1-4)。前面的 0 表示不关心设备号和功能号。

如果这里配错了,表现是设备能枚举到,但中断收不到。调试时可以看/proc/interrupts里有没有对应的 PCIe 中断条目。

4.3 reset-gpios 的极性和时序

reset-gpios定义的是 PERST# 信号。这里有两个坑:

极性:GPIO_ACTIVE_LOW表示低电平有效,也就是拉低时复位。但有些板子的电路上加了反相器,实际极性可能相反。如果内核启动时 EP 设备一直枚举不到,先查这个。

时序:PCIe 规范要求 PERST# 在参考时钟稳定后至少保持 100ms 的低电平。内核驱动通常会处理这个时序,但如果你用的是自定义 GPIO,可能需要自己加延时。

4.4 一个完整的设备树配置示例

把上面的内容整合起来,一个可用的 PCIe RC 设备树节点是这样的:

&pcie { status = "okay"; compatible = "xlnx,nwl-pcie-2.11"; reg = <0x0 0xfd0e0000 0x0 0x1000>, <0x0 0xfd480000 0x0 0x1000>, <0x80 0x00000000 0x0 0x1000000>; reg-names = "breg", "pcireg", "cfg"; interrupts = <0 151 4>, <0 152 4>, <0 153 4>, <0 154 4>; interrupt-names = "misc", "msi0", "msi1", "legacy"; interrupt-map-mask = <0 0 0 7>; interrupt-map = <0 0 0 1 &pcie_intc 0>, <0 0 0 2 &pcie_intc 1>, <0 0 0 3 &pcie_intc 2>, <0 0 0 4 &pcie_intc 3>; msi-parent = <&pcie_intc>; ranges = <0x02000000 0x0 0xE0000000 0x0 0xE0000000 0x0 0x10000000>, <0x43000000 0x80 0x00000000 0x80 0x00000000 0x0 0x80000000>; pcie_intc: interrupt-controller { interrupt-controller; #address-cells = <0>; #interrupt-cells = <1>; }; };

注意reg属性里的三段分别对应 breg(桥寄存器)、pcireg(PCIe 控制器寄存器)和 cfg(配置空间)。这些地址来自 Vivado 的 Address Editor,不能随便填。

5. 上电调试:从 link up 到设备枚举的完整排查链路

5.1 第一步:确认 link 是否建立

系统启动后,第一件事是看 link 状态:

cat /sys/kernel/debug/pcie/link_status # 或者 lspci -v

如果lspci什么都不显示,说明 link 没建立。这时候要查:

  • GT 参考时钟是否正常(用示波器量)
  • PERST# 是否已经释放(拉高)
  • LTSSM 状态机的状态(有些驱动会打印)

5.2 第二步:link up 了但枚举不到设备

这是最常见的情况。link 建立了,但lspci里看不到设备。原因通常有:

  1. 配置空间访问失败:检查ranges里的配置空间地址是否正确
  2. EP 设备没有正常初始化:有些 EP 需要额外的初始化序列
  3. 地址映射错误:BAR 地址读出来是 0xFFFFFFFF

排查方法:

# 查看 PCIe 控制器的寄存器状态 devmem 0xFD0E0000 # 查看内核日志 dmesg | grep -i pcie

5.3 第三步:设备认到了但 BAR 分配失败

如果lspci能看到设备,但 BAR 地址是 0xFFFFFFFF 或者分配失败,问题在ranges的内存空间配置。检查:

  • ranges里的 size 是否足够大
  • 是否有地址冲突(和其他外设的地址空间重叠)
  • 内核的pci=realloc参数是否需要加上

5.4 第四步:中断收不到

设备能正常工作但中断不触发,检查:

cat /proc/interrupts | grep pcie

如果没有 PCIe 相关的中断条目,说明interrupt-map配置有问题。另外要注意 MSI 和 INTx 的区别:现代设备通常用 MSI/MSI-X,需要确认msi-parent指向正确。

5.5 一个真实的排查案例

我之前遇到过一块自制板,link up 正常,lspci能看到设备,但读写 BAR 空间时系统直接挂死。排查过程:

  1. 先确认 link 状态 → 正常
  2. 确认设备枚举 → 正常
  3. 读写 BAR → hang

最后发现是ranges里的内存空间地址和 DDR 的地址范围重叠了。CPU 访问那个地址时,AXI 互联不知道该路由到 DDR 还是 PCIe,导致总线死锁。把 PCIe 的内存空间改到0xE0000000之后问题解决。

这个案例说明:ranges 里的地址不能和系统中其他设备的地址空间重叠,这是硬性要求。

6. 几个能省你大量时间的实操技巧

6.1 用 lspci 的 -vvv 看详细信息

lspci -vvv

这个命令能看到每个设备的 BAR 分配、中断信息、link 状态、协商速率等。调试阶段建议一直开着。

6.2 通过 sysfs 直接读写配置空间

# 读取配置空间 hexdump -C /sys/bus/pci/devices/0000:01:00.0/config # 写配置空间(谨慎操作) echo "value" > /sys/bus/pci/devices/0000:01:00.0/config

6.3 内核启动参数里加上 pci 调试信息

在bootargs里加上:

pci=verbose pci=earlydump

这样内核启动时会打印详细的 PCIe 枚举过程,方便定位问题。

6.4 用 devmem 直接读 PCIe 控制器寄存器

# 读取 PCIe 控制器的 link 状态寄存器 devmem 0xFD0E0000 32

具体的寄存器偏移要看 Xilinx 的文档(UG1085)。这个方法在驱动还没加载起来的时候特别有用。

6.5 设备树修改后的快速验证流程

每次改完设备树,不需要重新编译整个系统:

# 只重新编译设备树 petalinux-build -c device-tree # 把生成的 dtb 拷贝到 SD 卡 cp images/linux/system.dtb /media/sdcard/ # 重启后在 u-boot 里手动加载新的 dtb

这样可以节省大量编译时间。

7. 关于驱动选型和版本兼容性的经验

7.1 nwl-pcie 和 xilinx-pcie 的区别

Xilinx 的 PCIe 驱动有两个版本:

  • xilinx-pcie:旧版驱动,支持 Gen1/Gen2,代码简单但功能有限
  • nwl-pcie:新版驱动(基于 Northwest Logic 的 IP),支持更多特性,是 2019.1 之后的默认选择

选哪个取决于你的 Vivado 版本和 PS 配置。如果内核启动时看到xilinx-pcie: probe failed之类的错误,大概率是驱动选错了。

7.2 内核版本对 PCIe 支持的影响

不同版本的 PetaLinux 使用的内核版本不同,PCIe 子系统的行为也有差异。比如:

PetaLinux 版本内核版本PCIe 驱动注意事项
2018.34.14xilinx-pcie需要手动配置 ranges
2019.24.19nwl-pcie默认配置基本可用
2020.25.4nwl-pcie支持 Gen3
2021.25.10nwl-pcie设备树节点有变化

如果你在升级 PetaLinux 版本后 PCIe 不工作了,先检查设备树节点的 compatible 属性是否匹配新驱动的要求。

7.3 关于 MSI 中断的配置

现代 PCIe 设备基本都用 MSI/MSI-X 中断。在设备树里,msi-parent属性指向 MSI 控制器:

msi-parent = <&pcie_intc>;

如果 MSI 不工作,设备可能会回退到 INTx,但性能会差很多。检查/proc/interrupts里是否有 MSI 中断条目。

8. 写在最后的一些个人体会

搭 PCIe RC 这件事,硬件和软件的耦合度非常高。我见过太多人软件调了半天,最后发现是硬件上参考时钟没接对。所以我的建议是:先确认硬件没问题,再动软件。具体来说,上电后先用示波器确认参考时钟、PERST# 和电源时序,这三样没问题了再开始调软件。

另外,设备树这个东西,改的时候一定要小步走。每次只改一个属性,改完就验证。我见过有人一次性改了 ranges、中断和复位三个地方,结果出问题了根本不知道是哪个引起的。

还有一点:Xilinx 的文档(特别是 UG1085 和 PG201)一定要看,但不要全信。文档里的示例代码和实际能跑的代码之间往往有差距,以实际调试结果为准。

最后说一个我踩过的坑:有一次用 PetaLinux 2020.2 搭 RC,设备树里ranges的 64 位内存空间地址写成了0x0 0x00000000,结果内核启动时直接 panic。原因是这个地址和系统 RAM 的起始地址冲突了。改成0x80 0x00000000之后正常。这个教训告诉我:64 位地址的高 32 位不能随便填 0,要根据实际的地址映射来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询