每次排查 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 相关流程大概是这样的板级初始化阶段调用pci_init()触发 PCIe 控制器驱动 probe。控制器驱动做链路初始化包括 refclk 使能、PERST 复位时序处理、LTSSM 训练。链路进入 L0 后U-Boot 开始扫描总线为每个设备建立udevice。如果打开CONFIG_PCI_SCAN_SHOW启动日志会打印扫描到的设备。之后启动设备比如 NVMe 盘、网卡会主动 probe 对应子系统的设备。最终 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 总线扫描与设备发现链路训练完成之后枚举的“流水线”才开始走。整个过程可以拆成五步从 Root Complex 的 bus 0 出发遍历 devfn设备号 0~31功能号 0~7相当于把总线上的 256 个“格子”都问一遍。对每个格子读配置空间的 Vendor ID 和 Device ID读到0xFFFF表示没有设备读到有效值就是有设备。读 Header Type判断是 Endpoint0x0、PCIe-PCIe Bridge0x1还是其他类型。如果是 Bridge按bus-range给它分配下一级总线号然后递归扫描次级总线。对 Endpoint 处理 BAR分配内存或 IO 空间建立地址映射。在 U-Boot 里这个过程的函数实现主要在drivers/pci/pci.c和pci-uclass.c里。旧版本一个pci_hose_scan_bus函数能递归扫完整个树新版本则通过 DM 的 probe 过程完成。理解这个递推结构很重要因为 PCIe Switch 就是一级一级的桥任何一个中间桥的 bus 号分配出错后面的设备全部“失联”。2.3 BAR 资源分配最容易出暗坑的地方BARBase 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_PCIy CONFIG_DM_PCIy CONFIG_CMD_PCIy CONFIG_PCIE_DWy CONFIG_PCI_SCAN_SHOWy CONFIG_SYS_PCI_64BITy CONFIG_NVMEyCONFIG_PCI_SCAN_SHOW这个开关我建议调试阶段一定要开。它会在启动时把扫描到的设备列出省得每次手动敲命令。CONFIG_SYS_PCI_64BIT则要看你板子上有没有 64 位 BAR 的设备不开的话大地址空间设备会非常痛苦。还有几个历史遗留的宏比如CONFIG_PCI_PNP自动分配 BAR和CONFIG_PCI_NOSCAN不自动扫描。如果是老版本 U-BootCONFIG_PCI_PNP没开BAR 不会自动分配设备在枚举后依然不可用。新版本里这个行为被整合进 DM 枚举流程但不代表所有平台都默认合理。拿到一个陌生板子先看 defconfig 和 README 里有没有相关说明。3.2 一个典型的 PCIe DTS 节点怎么理解设备树里的 PCIe 节点看着复杂其实核心就几个属性。我拿一个通用节点拆开解释pcie0: pciefa000000 { 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 的控制器寄存器差异很大但初始化动作基本都一样使能 refclk 参考时钟确认 100MHz 时钟稳定输出。拉高 PERST 复位信号让 Endpoint 从复位中释放。配置控制器为 Root Complex 模式使能 PHY。等待 LTSSM 进入 L0判断链路速度和 lane 宽度。把配置空间访问窗口打开让 CPU 能读取设备。我踩过最痛的坑是复位时序。板子上电后refclk 稳定需要时间电源 rail 爬升也需要时间但 U-Boot 跑得太快PERST 一早就拉高Endpoint 还没来得及准备好链路就卡住了。表面看是“偶发识别不到设备”实际上是时序竞争。解决方式也很朴素在释放 PERST 之前加一个足够的延时等 refclk 和电源稳定。很多参考板都会在驱动里留一个udelay或者通过 GPIO 控制 PERST这个延时不是随便写的要结合示波器实测的电源稳定时间。量产板上宁可多等几十毫秒也不要抢那几微秒的启动时间。4. 现场排查pci 命令和打印就是你的“最小系统”4.1 pci 命令速查U-Boot 自带的pci命令是现场排查第一工具。前提是CONFIG_CMD_PCIy否则命令根本没有。最常用的四个子命令 pci enum pci pci header 1.0.0 pci display 1.0.0pci 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 与 AERU-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 出问题都按这个顺序过一遍能省很多时间refclk 是否真的稳定输出示波器量过吗频率对不等于稳定。PERST 复位时序和电源上电顺序是否明确驱动里加的延时是不是拍脑袋写的链路协商到了什么状态当前速度和 lane 数是多少有没有现场寄存器快照枚举到设备后BAR 是否分配到可访问的地址范围有没有 64 位 BAR 落在窗口外这个现象是必现还是偶发偶发问题的根因大概率在时序不在逻辑。前两个问题解决物理层第三个问题确认链路第四个问题解决地址空间第五个问题决定排查策略。很多同事改了一上午代码最后发现是 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 阶段强行冲顶。