☰
U-Boot下PCIe调试:链路训练与枚举的避坑指南
2026/10/6 7:11:39 网站建设 项目流程

每次排查 PCIe 问题,我都会和同事先说一句:先别急着改驱动,先把链路搞出来。驱动写得再好,LTSSM 停在 Polling 状态,后面全是白搭。这篇文章不是从 PCIe 协议第一章开始讲的长文,而是把我在 U-Boot 阶段定位 PCIe 问题的常用思路、命令和踩过的坑整理成一份可以直接参考的笔记。目标就一个:让你在 uboot 里尽快看到设备、尽快进系统,不被底层链路问题卡住。

我自己做嵌入式底层启动和内核驱动有几年时间,碰得最多的就是三类问题:uboot 阶段偶尔丢设备、量产时掉速或降 lane、以及用 pcie 网卡或 NVMe 盘做启动设备时不稳定。U-Boot 里的 PCIe 子系统说白了就是一把快刀,它要做的事情不多,但每一件事都直接影响内核能不能正常接管。这篇文章适合正在移植 uboot、或者被 PCIe 启动问题折磨的软件工程师,也适合硬件工程师拿去对照做信号排查。

1. 先把定位搞清楚:U-Boot 里的 PCIe 到底负责什么

1.1 它和内核 PCIe 驱动的本质区别

很多人在 uboot 里用pci命令能看到设备,就觉得 PCIe 没问题。这是一个非常典型的误判。内核里的 PCIe 驱动栈远比 U-Boot 完整,中断分配、MSI/MSI-X、IOMMU/SMMU 映射、错误恢复(AER)、热插拔事件、电源管理这些都归内核管。U-Boot 里的 PCIe 子系统只做三件事:把链路训练起来、把设备枚举出来、把基本资源分掉。你可以把它理解成“把客厅收拾好,等主人回来自己处理细活儿”。

所以,一个设备在 U-Boot 里能被枚举到,说明链路通信是通的,但并不能保证它在内核里能正常工作。反过来,如果 U-Boot 阶段就枚举不到,或者枚举时断断续续,那多半就是硬件问题或者初始化时序问题,不是换个内核版本能解决的。这个判断我用过很多次,基本没翻过车。

这背后的原因也简单:U-Boot 运行在裸机环境,没有中断处理器,没有页表,没有复杂的电源管理框架。它的 PCIe 代码路径非常直接,控制器驱动先把 Root Complex 初始化,然后通过配置空间读写扫描设备,最后给设备的 BAR 分配地址空间。这套流程如果用一句话说,就是“能用就行”。但“能用”和“稳定”之间,恰好隔着大量硬件细节。

1.2 从代码地图看子系统组成

先别急着改代码,花十分钟把 U-Boot 里 PCIe 相关文件过一遍,后面排查会快很多。不同版本的驱动路径略有差异,但核心文件基本固定。

路径 / 文件作用
include/pci.hPCIe 核心数据结构和 API 定义,BDF 表示、配置访问接口、控制器结构体
drivers/pci/pci-uclass.cDM 框架下 PCI 设备的类管理层,负责设备绑定、扫描、资源分配
drivers/pci/pci.c经典 PCI 枚举、配置空间读写、BAR 分配的核心实现
drivers/pci/pcie_dw.cDesignWare 控制器驱动,很多 SoC 的 PCIe 控制器都基于它
cmd/pci.cpci命令行工具,现场排查第一利器
arch/.../dts板级 PCIe 节点,控制器 reg、ranges、bus-range 都在这里

老版本 U-Boot 和带 DM 的新版本在代码结构上差别很大。如果你拿到一个 2018 年左右的 uboot,大概率还是旧的非 DM 框架,用struct pci_controller管理整个 PCIe 控制器,枚举时直接调用pci_hose_scan_bus。而 2020 年之后的版本基本都走 DM 了,控制器是一个udevice,通过uclass管理。遇到问题先搞清楚自己面对的是哪一套,别用新版本的配置宏去套旧代码。

1.3 初始化时序和常见启动路径

以常见的 DM 版本为例,U-Boot 启动过程中 PCIe 相关流程大概是这样的:

  1. 板级初始化阶段调用pci_init(),触发 PCIe 控制器驱动 probe。
  2. 控制器驱动做链路初始化,包括 refclk 使能、PERST 复位时序处理、LTSSM 训练。
  3. 链路进入 L0 后,U-Boot 开始扫描总线,为每个设备建立udevice。
  4. 如果打开CONFIG_PCI_SCAN_SHOW,启动日志会打印扫描到的设备。
  5. 之后启动设备(比如 NVMe 盘、网卡)会主动 probe 对应子系统的设备。
  6. 最终 bootm 启动内核前,可能会把 PCIe 相关的资源通过 fdt fixup 传给内核。

这里有一个关键点:U-Boot 在跳转内核时,并不会像操作系统那样把整个 PCIe 状态“移交”给内核。内核启动后会自己重新初始化控制器、重新枚举。U-Boot 最大的作用是在内核起来之前,把链路训练稳定,让关键的启动设备可用,同时提供一套独立的诊断手段。

2. 枚举过程拆解:PCIe 子系统的“流水线”怎么走

2.1 链路训练:枚举之前根本没有设备

很多人以为枚举就是拿一个循环去读配置空间,读到非 0xFFFF 就算找到了设备。理论上是这样,但有个前提:链路必须已经稳定。PCIe 是点对点串行连接,Host 和 Endpoint 要通过 LTSSM 完成状态机握手,协商速率、lane 数量、极性校准,最后进入 L0 状态,配置空间才可访问。

用生活里打电话来类比:两个人约好通话,先要拿起听筒拨号、协商编码格式,真正接通后才能说正事。链路训练就是拨号,配置空间访问就是通话。你驱动写得再好,一方没开机、线没插好、或者时钟没对上,电话永远拨不出去。

在 U-Boot 调试现场,我见到最多的链路训练失败状态就是卡在 Polling 或 Configuration。这个信息不一定有日志,很多时候要读控制器的链路状态寄存器才能看到。比如 DesignWare 控制器的PCIE_PORT_LINKSTS寄存器,里面能读出当前 LTSSM 状态、协商速度和 lane 宽度。看到状态停在 Polling,基本就是物理层有东西不对。

2.2 总线扫描与设备发现

链路训练完成之后,枚举的“流水线”才开始走。整个过程可以拆成五步:

  1. 从 Root Complex 的 bus 0 出发,遍历 devfn(设备号 0~31,功能号 0~7),相当于把总线上的 256 个“格子”都问一遍。
  2. 对每个格子读配置空间的 Vendor ID 和 Device ID,读到0xFFFF表示没有设备,读到有效值就是有设备。
  3. 读 Header Type,判断是 Endpoint(0x0)、PCIe-PCIe Bridge(0x1)还是其他类型。
  4. 如果是 Bridge,按bus-range给它分配下一级总线号,然后递归扫描次级总线。
  5. 对 Endpoint 处理 BAR,分配内存或 IO 空间,建立地址映射。

在 U-Boot 里,这个过程的函数实现主要在drivers/pci/pci.c和pci-uclass.c里。旧版本一个pci_hose_scan_bus函数能递归扫完整个树,新版本则通过 DM 的 probe 过程完成。理解这个递推结构很重要,因为 PCIe Switch 就是一级一级的桥,任何一个中间桥的 bus 号分配出错,后面的设备全部“失联”。

2.3 BAR 资源分配:最容易出暗坑的地方

BAR(Base Address Register)是 U-Boot 阶段最容易出问题的地方。枚举到设备后,U-Boot 会通过“写全 1、读回大小”的方式,计算出每个 BAR 需要多少地址空间,然后把它分配到控制器配置的地址窗口里。

这里面有几个坑,我一个个说。

第一,64 位 BAR 的分配。有些设备(尤其现代 NVMe 盘和显卡)的 BAR 是 64 位的,占两个 BAR 寄存器。如果 U-Boot 没有打开CONFIG_SYS_PCI_64BIT,或者 DTS 里ranges没有包含高地址区间,设备就可能被分配到一个它根本够不着的位置,结果是枚举能看到,但后续读写全挂。遇到这类问题,先看pci header输出的 BAR 值,看它是不是一个超过 4GB 的高地址。

第二,桥的窗口没开。PCIe Bridge 本身有一组 Memory Base/Limit 寄存器,用来声明下游设备占用的访问窗口。有时候 Endpoint 的 BAR 分得没错,但中间 Bridge 的窗口没覆盖到,CPU 想访问下游设备时会直接“绕路失败”。这个问题在带 PCIe Switch 的板子上特别典型,也最容易让人绕圈子。

第三,U-Boot 的资源分配和内核经常不一致。U-Boot 会把 BAR 放在它认为合适的地方,但内核启动后会重新枚举、重新分配。所以你在 U-Boot 里看到某个 BAR 地址是0x80000000,不代表进内核后还是这个地址。如果只是启动阶段用一下,不用太纠结;但如果 U-Boot 阶段要通过 DMA 访问设备,就得留意地址是否在控制器可访问的范围内,否则 DMA 会跑飞。

3. 实操配置:从 DTS 到 Kconfig 的完整接入

3.1 Kconfig 里需要关注哪些开关

相比内核,U-Boot 的 PCIe 配置宏要少很多,但每一个都很关键。以 DesignWare 控制器的平台为例,下面这一组是基础:

CONFIG_PCI=y CONFIG_DM_PCI=y CONFIG_CMD_PCI=y CONFIG_PCIE_DW=y CONFIG_PCI_SCAN_SHOW=y CONFIG_SYS_PCI_64BIT=y CONFIG_NVME=y

CONFIG_PCI_SCAN_SHOW这个开关我建议调试阶段一定要开。它会在启动时把扫描到的设备列出,省得每次手动敲命令。CONFIG_SYS_PCI_64BIT则要看你板子上有没有 64 位 BAR 的设备,不开的话,大地址空间设备会非常痛苦。

还有几个历史遗留的宏,比如CONFIG_PCI_PNP(自动分配 BAR)和CONFIG_PCI_NOSCAN(不自动扫描)。如果是老版本 U-Boot,CONFIG_PCI_PNP没开,BAR 不会自动分配,设备在枚举后依然不可用。新版本里这个行为被整合进 DM 枚举流程,但不代表所有平台都默认合理。拿到一个陌生板子,先看 defconfig 和 README 里有没有相关说明。

3.2 一个典型的 PCIe DTS 节点怎么理解

设备树里的 PCIe 节点看着复杂,其实核心就几个属性。我拿一个通用节点拆开解释:

pcie0: pcie@fa000000 { compatible = "vendor,pcie-rc"; reg = <0x0 0xfa000000 0x0 0x1000>, <0x0 0xfa001000 0x0 0x1000>; #address-cells = <3>; #size-cells = <2>; device_type = "pci"; ranges = <0x02000000 0x0 0x80000000 0x0 0x80000000 0x0 0x10000000>; bus-range = <0x0 0xff>; status = "okay"; };
  • reg描述控制器本身占用的寄存器空间和把配置空间映射到 CPU 地址的窗口。
  • device_type = "pci"告诉系统这是一个 PCIe Host Bridge。
  • ranges是最容易看错的地方。它表示的是 PCIe 总线地址到 CPU 地址的映射关系。0x02000000表示非预取内存空间,后面三组数字分别是高地址单元、PCI 地址、CPU 地址、地址长度。翻译过来就是:PCI 侧0x80000000开始的 256MB 空间,映射到 CPU 侧同一地址。
  • bus-range是允许使用的总线号范围。如果你板子上有 Switch,或者需要挂多个设备,空间要留足;很多奇怪问题都是这里写的bus-range = <0x0 0x0>,一扩展就全乱。

调试时建议把ranges的地址窗口先调大一点,比如映射 1GB 甚至 2GB,避免 BAR 分配时撞墙。等稳定后再收窄。

3.3 控制器驱动接入的关键动作

U-Boot 里的 PCIe 控制器驱动,核心工作其实不在“枚举”,而在“把链路搞出来”。不同 SoC 的控制器寄存器差异很大,但初始化动作基本都一样:

  1. 使能 refclk 参考时钟,确认 100MHz 时钟稳定输出。
  2. 拉高 PERST 复位信号,让 Endpoint 从复位中释放。
  3. 配置控制器为 Root Complex 模式,使能 PHY。
  4. 等待 LTSSM 进入 L0,判断链路速度和 lane 宽度。
  5. 把配置空间访问窗口打开,让 CPU 能读取设备。

我踩过最痛的坑是复位时序。板子上电后,refclk 稳定需要时间,电源 rail 爬升也需要时间,但 U-Boot 跑得太快,PERST 一早就拉高,Endpoint 还没来得及准备好,链路就卡住了。表面看是“偶发识别不到设备”,实际上是时序竞争。

解决方式也很朴素:在释放 PERST 之前,加一个足够的延时,等 refclk 和电源稳定。很多参考板都会在驱动里留一个udelay或者通过 GPIO 控制 PERST,这个延时不是随便写的,要结合示波器实测的电源稳定时间。量产板上,宁可多等几十毫秒,也不要抢那几微秒的启动时间。

4. 现场排查:pci 命令和打印就是你的“最小系统”

4.1 pci 命令速查

U-Boot 自带的pci命令是现场排查第一工具。前提是CONFIG_CMD_PCI=y,否则命令根本没有。最常用的四个子命令:

=> pci enum => pci => pci header 1.0.0 => pci display 1.0.0

pci enum手动触发一次枚举。某些配置下 U-Boot 启动时不会自动扫描,或者扫描失败后想重来一次,这个命令最直接。pci不带参数会列出当前所有总线上的设备,输出里能看到 BDF(总线号:设备号:功能号)、厂商 ID 和设备 ID。

pci header用来查看某个设备的配置头,包括 Vendor ID、Device ID、Class Code、BAR 值。这是判断设备是否正常初始化的关键。pci display则是把整个 256 字节配置空间以原始字节形式打印出来,适合自己解析多值情况。

我在实际调试时最常用的一套操作是:先pci enum,立刻pci,如果空说明链路没起来;如果能列出但启动设备找不到,则用pci header看该设备的 BAR 分配情况。这一套走完,大致能定位问题在链路层、枚举层还是驱动层。

4.2 怎么从打印日志里判断 PCIe 是否真的稳定

光看设备在不在列表里还不够。我一般还会结合启动日志和dm tree输出判断控制器是否 probe 成功、设备是否绑定到正确驱动。

=> dm tree

这个命令会打印当前设备模型里的所有设备树节点,包括 PCIe 控制器和它底下的 PCI 设备。如果控制器出现了但设备没有绑定,说明扫描阶段就出了问题;如果设备绑定到了pci_bus_generic而不是具体驱动,那就要检查 compatible 是否写对。

日志层面,CONFIG_PCI_SCAN_SHOW打开后,U-Boot 启动时会打印类似这样的信息:

PCIe link up, gen3 x4 Scanning PCI devices on bus 0 Bus 0: 01.00 0 144d:a822

能看到“link up”和进扫描列表,基本可以确认链路和枚举都正常。如果日志里只有“scanning”但列表为空,重点排查链路状态寄存器和 PERST 时序。如果连“link up”都没有,第一步就要量参考时钟。

4.3 什么时候必须动用示波器和协议分析仪

软件手段只能帮你定位“哪一层出了问题”,解决“信号为什么有问题”最终还是要靠硬件工具。我个人经验:一旦确认链路训练失败或者量产偶发掉卡,别再反复改软件参数碰运气,直接用示波器量三样东西:refclk 的波形、PERST 的时序曲线、以及电源 rail 的上电时序。

比如 refclk,只有频率对没用,还要看幅度、抖动和是否稳定。PCIe 参考时钟通常要求 100MHz,带展频的话还有特定调制要求。示波器上如果发现 refclk 上升沿毛刺多,或者 PERST 释放时间比 refclk 稳定时间早了几百微秒,问题的方向就很清楚了。

如果条件允许,上协议分析仪直接抓 LTSSM。它能精确告诉你链路卡在哪个状态、是哪一端主动发起去速率、lane 是在哪个阶段掉的。我见过最神奇的案例,是端设备在 Configuration 阶段因为 lane 极性不对反复重试,协议分析仪一眼看出,软件调了一周才发现是差分线接反了一组。

5. 高频故障:掉速、掉卡、AER 的典型现场与解法

5.1 Link Training 失败的几种现场

每个板子的链路失败都像“同一种病,不同的炎症部位”。我按症状列一个速查表,方便照方抓药。

现场优先怀疑建议动作
完全没有任何设备,日志里无 link uprefclk 没起、PERST 没释放示波器量时钟和复位,确认 PERST 确实拉高
偶发识别不到,上电顺序不同表现不同电源 rail 稳定时间和 PERST 竞争修改驱动延时,拉长 PERST 释放前的等待
gen3 训练不过,降到 gen1 能过均衡参数、信号完整性余量不足先固定 gen1 验证链路本质,再调 PHY 均衡
lane 只能 x1,明明插的是 x4 卡差分线断线、接收端端接缺失、mSATA/PCIe 转换接触查原理图走线,量四对差分信号,不要先改软件
枚举成功,但访问配置空间偶发超时控制器配置窗口没映射完全检查 DTS 的 reg 和 ranges 是否覆盖完整

第一个要排除的永远是信号完整性和时序,不要一上来就怀疑驱动。PCIe 对信号质量的要求比一般嵌入式总线苛刻得多,一句话总结:硬件不稳定时,软件能让它稳定是运气,不能让它稳定才是常态。

5.2 掉速、降 lane 与 AER:U-Boot 阶段能做什么、不能做什么

掉速和降 lane 是量产现场最头疼的问题。设备在开发板上一路 gen3 x4 跑得好好的,换到量产机箱里变成 gen1 x1,甚至干脆不认盘。这类问题通常是信号链路边界状态:参考时钟抖动偏大、连接器接触不良、均衡参数不匹配,或者相邻信号串扰。

U-Boot 阶段能做的是两件事:一是通过链路状态寄存器确认当前协商的速度和 lane 数,二是临时把控制器固定到低速度档位,判断信号余量。我之前在调试时,会把 gen3 先固定成 gen2,再固定成 gen1,逐级往上测。gen1 能稳定、gen3 掉速,大概率是均衡参数或者 PCB 阻抗问题。

AER 则是另一个话题。U-Boot 本身基本不处理 AER,它甚至连像样的错误中断服务都没有。所以如果你发现内核里报 AER,尤其是 Unsupported Request 或 Corrupted Data,别急着骂内核,先回想一下 U-Boot 阶段对设备做了什么。有一种很常见的情况:U-Boot 枚举时对设备发了它不支持的配置请求,设备返回 UR,但 U-Boot 当时没有感知,错误状态滞留在设备里,内核接管后一访问就报错。

对于这种情况,我的建议是:U-Boot 阶段额外主动读一次 AER 相关的 capability 寄存器,把错误状态清掉。如果控制器平台支持pci_aer_clear之类的接口就用接口,没有就写一个简单的配置空间读写,把 AER Uncorrectable Error Status 寄存器写 1 清零。这个操作很多人忽略,但它能解决相当一部分“内核阶段莫名 AER”的怪问题。

5.3 热插拔、Switch、网卡等场景的补充

U-Boot 默认不管理系统级热插拔功能。它没有完整的热插拔事件中断处理,也没有 surprise removal 恢复能力。如果你的产品依赖 pcie 热插拔,那就明确一点:热插拔动作需要在操作系统启动之后完成。U-Boot 阶段你要保证的是“设备上电前就在位,并且链路已经训练稳定”,而不是期待 U-Boot 在启动过程中感知一个正在插入的设备。

但这不代表热插拔和 U-Boot 完全无关。我遇到过一类问题:设备在 U-Boot 阶段正常枚举,但因为使用了 PCIe Switch,枚举时总线号分配和 Switch 下游设备的资源窗口处理不当,导致后续插入新设备时根本没有可用总线号。这种情况下,打开bus-range的余量是最直接的解法。U-Boot 里多留点总线号不改硬件,成本很低,收益很大。

另外,如果用 PCIe 网卡做网络启动,比如比较常见的 Realtek 千兆网卡,要注意 32 位系统或者 32 位地址窗口的限制。网卡的 DMA 地址如果落在 32 位之外,在 U-Boot 里做 TFTP 启动会非常玄学,能 ping 通但传输慢、卡顿、甚至直接死机。检查一下 DTS 的ranges是不是只映射了低 32 位地址空间,必要时把地址窗口设高,或者换用 32 位 DMA 的网卡。

6. 我的避坑清单和个人建议

6.1 排查前先问自己的五个问题

我把自己多次来回折腾总结成五个必问题,每次 PCIe 出问题都按这个顺序过一遍,能省很多时间:

  1. refclk 是否真的稳定输出?示波器量过吗?频率对不等于稳定。
  2. PERST 复位时序和电源上电顺序是否明确?驱动里加的延时是不是拍脑袋写的?
  3. 链路协商到了什么状态?当前速度和 lane 数是多少?有没有现场寄存器快照?
  4. 枚举到设备后,BAR 是否分配到可访问的地址范围?有没有 64 位 BAR 落在窗口外?
  5. 这个现象是必现还是偶发?偶发问题的根因大概率在时序,不在逻辑。

前两个问题解决物理层,第三个问题确认链路,第四个问题解决地址空间,第五个问题决定排查策略。很多同事改了一上午代码,最后发现是 PERST 拉高太快,连寄存器快照都没留。

6.2 和硬件同事配合时,软件能给的“实锤”

软件和硬件扯皮的时候,最忌讳说“感觉”、“好像”、“偶尔”。要拿实锤。我最常用的一招,是在 U-Boot 里写一个小脚本,连续做多次复位枚举,把每次的链路状态寄存器和设备扫描结果打印出来,存成日志。

这个脚本甚至可以自动化:循环执行pci enum,等待、复位控制器、再枚举,把结果 append 到内存或者通过串口输出。如果一百次里有三次枚举失败,并且失败时的链路状态寄存器都停在同一个值,硬件工程师基本无话可说,只能乖乖配合你查信号。

另外一个实用小技巧:生产测试时,可以用CONFIG_PCI_SCAN_SHOW的启动日志做批量筛查。把每次开机的日志抓到上位机,判断有没有出现设备丢失,比人工盯 print 输出高效得多。这招我在产线验证 NVMe 启动盘的不稳定问题时用过,跑一晚上能拿到几百次开机的统计数据,比什么都强。

6.3 最后分享一个小技巧

调试 U-Boot PCIe 过程中,我养成了一个习惯:每次开机都手动记录从pci enum到设备可见的时间间隔。这个时间虽然用秒表测不准,但可以从串口日志的时间戳上大致看出来。正常链路从复位到 L0 通常在毫秒级,如果明显偏慢,比如花了上百毫秒甚至更多,链路重试的次数就异常。

曾经有块板子偶发启动慢,现象是系统能起、但比别人慢十几秒,查来查去最后发现是链路在 Configuration 阶段反复重试,协商 gen3 失败回落 gen1,再重新训练,整个过程重复了几轮。这种问题在启动日志里一点错误痕迹都没有,只有时间戳暴露了异常。从那次之后,我给自己的板卡都默认在 U-Boot 阶段强制固定一个协商速度档位,先保证稳定,再追求最高速率。速度这种事情,内核起来之后有的是机会重新协商,不用在 bootloader 阶段强行冲顶。

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

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

立即咨询