首页 / 资讯中心 / 文章详情

PCIe RC与EP模式详解:从枚举原理到调试实战

PCIe RC与EP模式详解:从枚举原理到调试实战 ★ FEATURED ARTICLE
PCIe 这玩意儿刚接触的时候总觉得它就是个插槽——显卡插上去、固态插上去能亮就行。但真到了调试阶段尤其是当你面对一块空板子、一颗还没跑起来的芯片或者一块死活枚举不出来的加速卡时你才会意识到PCIe 不是一根线它是一套有主从关系、有状态机、有严格握手流程的总线协议。而 RC 和 EP 这两个角色就是这套协议里最根本的身份划分。我做了几年硬件底层调试踩过最多的坑几乎都和角色没搞对有关明明该配成 RC 的端口被设成了 EP结果链路训练都过不去明明是个 EP 设备却指望它主动去扫描别人。所以这篇就围绕 PCIe 的 RC 模式和 EP 模式把这两个角色的本质、配置逻辑、枚举过程、常见故障和调试手段掰开揉碎讲一遍。不管你是刚入行的固件工程师还是做 FPGA 原型验证的老手或者是被掉卡、降速、AER 报错折磨过的驱动开发者这篇应该都能给你一些能直接上手的东西。1. 先把 RC 和 EP 的身份关系讲透1.1 为什么 PCIe 一定要分主从很多人第一次听到 RCRoot Complex和 EPEndpoint会下意识觉得这是两个设备类型其实更准确的说法是这是同一条 PCIe 链路上两个端点各自扮演的角色。PCIe 的拓扑本质上是一棵树树根就是 RC树叶就是各种 EP中间可能还有 Switch 做扩展。这个树形结构不是随便定的它直接继承自当年 PCI 总线的主机桥 设备模型。为什么非要分主从因为 PCIe 的配置空间访问机制决定了必须有一个发起者。在 PCI 总线时代CPU 通过 Host Bridge 去访问设备设备自己不会主动去访问别的设备。PCIe 延续了这个思路RC 是唯一有权发起配置请求Configuration Request的角色EP 只能被动响应。换句话说枚举这件事只有 RC 能干EP 天生就是被枚举的对象。这个设计带来的直接后果是如果你把两个都配成 RC 的端口对接链路可能能训练起来但配置空间访问会乱套——两边都想当老大谁都不服谁。反过来两个 EP 对接那更惨根本没人发起枚举链路即使 up 了也是一潭死水。所以对接之前先把角色定清楚这是所有后续工作的前提。1.2 RC 到底复杂在哪里RC 之所以复杂是因为它要干一大堆管家的活。它内部通常包含几个关键组件主机桥Host Bridge、配置空间管理单元、以及一个或多个 Root Port。每个 Root Port 下面可以挂一条独立的 PCIe 链路每条链路就是一棵子树的根。RC 的核心职责有这么几项。第一是发起配置周期也就是我们常说的枚举去读取每个 EP 的 Vendor ID、Device ID、BAR 大小等信息然后给它们分配总线号、设备号、功能号和内存/IO 地址空间。第二是地址映射把 CPU 的物理地址空间和 PCIe 设备的 BAR 空间对应起来这样 CPU 访问某段内存时请求能被正确路由到对应的设备。第三是中断管理处理 INTx、MSI、MSI-X 这些中断机制。第四是错误处理包括 AERAdvanced Error Reporting的根端口侧处理。正因为职责多RC 的实现通常和 SoC 强绑定。比如 x86 平台上RC 是集成在 CPU 或 PCH 里的ARM 平台上很多 SoC 会提供可配置的 PCIe 控制器你可以把它配成 RC 模式也可以配成 EP 模式。这个可配置就是很多坑的来源——配置寄存器写错一位角色就反了。1.3 EP 的被动不等于简单EP 看起来被动但它内部一点都不简单。一个典型的 EP 要实现配置空间Type 0 Header、BAR 地址译码、DMA 引擎、中断生成、以及各种 Capability 结构Power Management、MSI、PCIe Capability 等。尤其是 DMAEP 要主动往主机内存写数据这时候它发出的 Memory Write TLP 必须能被 RC 正确路由和接收。EP 还有一个容易被忽略的点它需要正确声明自己的能力。比如它支持多大的 Max Payload Size、支持哪些电源状态、是否支持 FLRFunction Level Reset。这些信息都写在配置空间的 Capability 寄存器里RC 在枚举时会读这些值然后据此配置链路。如果 EP 把这些字段写错了轻则性能上不去重则枚举直接失败。我见过一个典型案例某 FPGA 做的 EPMax Payload Size 字段写成了 4096但实际硬件只支持 256。RC 读到这个值后按 4096 去发 TLP结果 EP 侧接收缓冲溢出链路直接报错降速。所以 EP 的配置空间不是填个 ID 就行每个字段都要和实际硬件能力对齐。2. 枚举过程RC 是怎么一步步认全设备的2.1 从链路训练到配置空间可访问枚举的前提是链路已经训练成功Link Up。链路训练是物理层的事双方通过交换 TS1/TS2 有序集来协商速率Gen1/2/3/4/5和链路宽度x1/x2/x4/x8/x16。这个过程不需要软件干预是硬件自动完成的。但要注意链路训练成功不代表配置空间就能访问中间还隔着 LTSSM 状态机的完整跳转。链路 Up 之后RC 才能发起配置读。这里有个细节配置请求分 Type 0 和 Type 1。访问直接挂在 Root Port 下的设备用 Type 0访问更下层总线上的设备用 Type 1。RC 在枚举时会先读自己 Root Port 下的 Bus 0、Device 0、Function 0确认有没有设备响应。判断设备是否存在的方法很经典读 Vendor ID如果返回 0xFFFF说明这个位置没有设备或者设备没准备好。这个 0xFFFF 判断是所有枚举代码的基石几乎所有找不到设备的问题最后都归结到为什么读出来是 0xFFFF。2.2 总线号、设备号、功能号的分配逻辑PCIe 的寻址用的是 BDFBus/Device/Function三元组。Bus 号 8 位Device 号 5 位Function 号 3 位。一条总线最多 32 个设备每个设备最多 8 个功能。RC 在枚举时会给每个发现的桥设备Switch 或 PCIe-to-PCI 桥分配一个新的 Bus 号范围。具体流程是这样的RC 先扫描 Bus 0 上的所有 Device/Function。如果发现某个设备是桥Header Type 寄存器 bit 7 1就给它分配一个空闲的 Bus 号然后递归扫描这个新总线。这个过程叫深度优先遍历。分配完所有总线号后还要回填桥设备的 Subordinate Bus Number 和 Secondary Bus Number 寄存器这样后续的配置请求才能被正确路由。这里有个常见的坑Bus 号范围配置错误会导致下游设备完全不可见。比如一个 Switch 下面挂了三层设备但 Subordinate Bus Number 只填了下一层的号那第三层的设备就永远枚举不到。我在调试一块多级 Switch 的背板时就因为这个字段少填了一位导致最底层的 SSD 死活认不出来查了两天才定位到。2.3 BAR 空间的探测与分配枚举完 BDF 之后下一步是给每个 EP 的 BARBase Address Register分配地址空间。BAR 的探测方法很巧妙先往 BAR 寄存器写全 10xFFFFFFFF然后读回来读回的值里为 0 的位表示这个 BAR 需要多少空间为 1 的位是可配置的地址位。举个例子如果一个 32 位 Memory BAR 读回来是 0xFFFFF000说明它需要 4KB 空间低 12 位为 0。RC 根据这个信息从可用的地址池里分配一段对齐的地址写回去。对于 64 位 BAR要连续读写两个 32 位寄存器。注意探测 BAR 时一定要先保存原值探测完再恢复或写入新值。有些 EP 在 BAR 被写全 1 的瞬间会触发异常行为尤其是那些 BAR 和内部寄存器共用译码逻辑的设计。BAR 分配完之后CPU 就能通过访问对应的物理地址来读写 EP 的内部寄存器和内存了。这一步如果出错典型症状是设备能枚举到但驱动一访问就挂或者读出来的全是 0xFF。3. 可配置控制器同一颗芯片怎么在 RC 和 EP 之间切换3.1 配置引脚与寄存器两种切换方式很多 SoC 和 FPGA 的 PCIe 控制器是双模的既能当 RC 也能当 EP。切换方式主要有两种一种是硬件引脚 strapping上电时采样某个引脚的电平来决定模式另一种是通过软件写配置寄存器。引脚 strapping 的方式最直接但也最容易出错。我遇到过一块板子原理图上模式选择引脚默认拉高RC 模式但实际焊接时上拉电阻虚焊导致引脚浮空芯片采样到低电平进了 EP 模式。结果就是整块板子作为 RC 去枚举下游设备时完全没反应因为它在等别人来枚举它。这种问题用示波器量一下引脚电平就能发现但如果没有怀疑到模式问题很容易往链路训练方向去查白白浪费时间。软件切换的方式更灵活但要注意切换时机。通常需要在控制器初始化之前完成模式配置一旦链路训练开始再改模式往往需要复位整个控制器。有些控制器甚至要求模式配置后必须伴随一次硬复位才能生效。3.2 模式配错后的典型症状模式配错的症状其实很有辨识度我总结了几种症状可能原因排查方向链路一直不 Up双方都是 EP没人发起训练检查模式配置链路 Up 但枚举不到设备本端是 EP却在等别人枚举确认本端角色枚举到设备但 BAR 全 0RC 侧 BAR 分配逻辑未执行检查枚举代码配置读返回 0xFFFF下游设备未上电或模式错误量设备供电和模式引脚链路反复 Up/Down模式切换导致状态机震荡检查模式配置稳定性这张表里的每一行我都在实际项目中遇到过至少一次。尤其是链路 Up 但枚举不到设备这一条特别有迷惑性——链路 Up 说明物理层没问题人会本能地往协议层或软件层查但如果本端其实是 EP 模式那它压根就不会去发起枚举自然什么都找不到。3.3 用 lspci 和寄存器 dump 确认角色在 Linux 环境下确认 RC 角色相对容易lspci能看到 Root Port 和下游设备。但如果本端是 EPlspci里是看不到自己的EP 不会出现在自己的枚举结果里。这时候要看控制器的寄存器。以常见的可配置控制器为例通常会有一个 Mode 或 Type 寄存器读出来能明确当前是 RC 还是 EP。另外RC 模式下控制器会有一组 Root Port 相关的寄存器比如 Link Capabilities、Root ControlEP 模式下则有一组 Device 相关的寄存器。通过 dump 这些寄存器的存在与否也能反推当前模式。对于 FPGA 实现还可以直接看 IP 核的配置参数。Xilinx 的 PCIe IP 在例化时会指定Device/Port Type这个参数直接决定了生成的是 RC 还是 EP 逻辑。如果综合后的网表里 Root Port 相关模块被优化掉了那基本可以确定配成了 EP。4. 调试实战掉卡、降速、AER 报错怎么查4.1 掉卡问题的分层排查思路掉卡是 PCIe 调试里最笼统也最头疼的问题。我一般按层来排查物理层、链路层、事务层、软件层。物理层先看供电和参考时钟。PCIe 设备对参考时钟的抖动很敏感如果时钟质量差链路可能能训练起来但跑一会儿就断。用示波器量一下 REFCLK 的峰峰值和抖动能排除一大类问题。另外金手指接触不良、连接器氧化也是常见原因尤其是那些经常插拔的测试板。链路层看 LTSSM 状态。如果链路反复在 Detect 和 Polling 之间跳说明对端设备没响应或者信号质量太差。如果卡在 Configuration 状态可能是速率协商失败可以尝试强制降到 Gen1 看是否能稳定。事务层看有没有 AER 报错。AER 寄存器会记录 Correctable 和 Uncorrectable 错误比如 Receiver Error、Bad TLP、Completion Timeout 等。这些错误码能精确定位到是哪一层的哪种问题。软件层看驱动日志和枚举结果。dmesg里通常会有链路训练和枚举的详细输出lspci -vvv能看到链路的当前速率、宽度和各种 Capability。4.2 降速降宽的根因定位链路降速比如从 Gen3 降到 Gen1或降宽从 x4 降到 x1通常有两个原因信号完整性问题和配置问题。信号完整性问题最常见。PCIe Gen3 及以上对通道损耗要求很严如果 PCB 走线太长、过孔太多、或者用了劣质连接器眼图就会闭合链路训练时会自动降速以保证稳定。这种情况用误码率测试仪或者示波器看眼图能确认。配置问题则包括双方支持的速率不匹配、链路宽度协商被限制、或者某个 Lane 的极性反了。PCIe 支持 Lane Reversal 和 Polarity Inversion正常情况下硬件会自动处理但如果配置寄存器里禁用了这些功能就可能出现降宽。我遇到过一次很隐蔽的降宽一块 x8 的卡插在 x16 插槽里但只跑在 x4。查了半天发现是插槽的某几个 Lane 在 PCB 上没走线设计时为了省成本只连了 x4但插槽物理上是 x16 的。这种物理 x16、电气 x4的设计如果不看原理图根本发现不了。4.3 AER 报错的解读与处理AERAdvanced Error Reporting是 PCIe 的错误报告机制分 Correctable 和 Uncorrectable 两类。Correctable 错误通常是瞬时的硬件能自动恢复比如 Bad DLLP、Replay Timer Timeout。Uncorrectable 错误则可能导致数据丢失或链路中断比如 Completion Timeout、Unsupported Request、Malformed TLP。解读 AER 报错的关键是看错误来源和错误类型。AER Capability 结构里有 Header Log 寄存器会记录出错 TLP 的前 16 字节通过解析这个能知道是哪个设备的哪个请求出了问题。处理 AER 报错的一般思路是先确认是可纠正还是不可纠正可纠正的如果频繁出现也要重视可能是信号质量的早期征兆不可纠正的要立即定位通常伴随功能异常。有些 AER 错误可以通过写寄存器清除但清除之前一定要先记录否则现场就丢了。提示AER 的 Root Error Status 寄存器能告诉你错误是从哪个 Root Port 报上来的多端口系统里这个信息很关键。5. 热插拔与稳定性那些容易被忽略的细节5.1 热插拔功能的硬件与软件配合PCIe 热插拔Hot-Plug不是插上就能用那么简单它需要硬件和软件配合。硬件上插槽需要 Presence Detect 引脚来检测卡是否插入需要 Power Controller 来控制插槽供电需要 Attention Button 和 Attention Indicator 来做用户交互。软件上需要 Hot-Plug 驱动来响应这些事件执行上电、枚举、下电等操作。热插拔最容易出问题的地方是时序。比如卡插入后Presence Detect 信号变化软件收到中断然后给插槽上电等待电源稳定再释放 PERST# 复位最后开始枚举。这个流程里任何一步时序不对都可能导致枚举失败。我见过因为上电后等待时间不够PERST# 释放太早导致设备还没准备好就被枚举读出来全是 0xFFFF。另外热插拔对电源的要求也高。插槽的 12V 和 3.3V 供电要能承受插拔瞬间的浪涌电流否则可能触发过流保护导致整个插槽断电。5.2 兼容性问题的常见来源PCIe 兼容性问题往往出现在新设备 老平台或者非标设备 标准平台的组合上。常见来源有配置空间字段不规范有些设备把某些 Capability 字段填得不符合规范标准 RC 能容忍但严格的 RC 就会报错。时序不满足规范比如 CRSConfiguration Request Retry Status处理不当设备还没准备好就返回非 CRS 响应。电源管理行为异常L1 低功耗状态进入/退出时序不对导致链路挂死。MSI/MSI-X 中断向量分配冲突多设备系统里中断资源没分配好。解决兼容性问题通常需要抓包分析。用 PCIe 协议分析仪抓配置周期和 TLP对比规范看哪一步不符合。没有分析仪的话就只能靠 RC 侧的日志和寄存器 dump 来推断。5.3 稳定性测试该怎么做PCIe 稳定性不是能跑起来就行要经过压力测试。我一般会做这几类测试第一是长时间读写压力测试用工具持续对设备做 DMA 读写跑几个小时甚至几天看有没有 AER 报错或链路中断。第二是电源状态切换测试反复在 L0、L1、L2 之间切换验证电源管理逻辑。第三是热插拔循环测试反复插拔设备验证热插拔流程的健壮性。第四是温度循环测试在高温和低温下分别跑压力测试看信号余量是否足够。这些测试里温度循环最容易暴露问题。很多信号完整性问题在常温下看不出来一到高温眼图就闭合了。所以如果设备要在宽温环境下工作温度测试不能省。6. 仿真与原型验证中的角色配置6.1 仿真环境里怎么搭 RC 和 EP做 PCIe 仿真验证时通常需要一个 RC 模型和一个 EP 模型对接。RC 模型一般用 VIPVerification IP来实现EP 模型可以是自己写的 RTL也可以是 VIP。仿真环境里配置角色比硬件简单因为都是参数化的改个参数就行。但仿真也有仿真的坑。最常见的是仿真里的链路训练和真实硬件不一致。有些 VIP 会简化 LTSSM跳过某些状态导致仿真能过但上板就挂。所以仿真验证的重点应该放在事务层和配置空间逻辑上物理层的验证还是要靠硬件测试。另一个坑是时序收敛。仿真里时间是无所谓的但真实硬件里配置请求的响应时间是有要求的。如果 EP 响应太慢RC 可能已经超时了。仿真时要注意给配置响应加上合理的延迟模型。6.2 FPGA 原型验证的角色选择用 FPGA 做 PCIe 原型验证时角色选择取决于验证目标。如果要验证一个 EP 设备的设计那 FPGA 就配成 EP插到主机的 PCIe 插槽里让主机的 RC 来枚举它。如果要验证 RC 的逻辑那 FPGA 配成 RC下游接真实的 EP 设备。Xilinx 的 PCIe IP 在例化时Device/Port Type参数决定角色。配成 EP 时IP 会生成 Type 0 配置空间配成 RC 时生成 Root Port 逻辑。需要注意的是RC 模式下 IP 占用的资源明显更多因为它要处理枚举、地址映射等逻辑。FPGA 做 EP 时一个常见需求是让主机能识别到一个看起来正常的设备。这就要求配置空间里的 Vendor ID、Device ID、Class Code 等字段填得合理。有些主机 BIOS 对未知设备会拒绝分配资源所以最好用一个已知的 ID或者确保 Class Code 正确。6.3 仿真与实测结果不一致的排查仿真过了、上板挂了这是最让人抓狂的情况。排查这种问题我一般从这几个方向入手先确认仿真和实测的配置是否完全一致。有时候仿真用的参数和实际综合时用的参数不一样比如链路宽度、速率、BAR 大小。这种低级错误其实很常见。再确认仿真有没有覆盖到出问题的场景。比如仿真里只测了单设备枚举实测时下游挂了多个设备那多级枚举的逻辑可能就没验证到。最后确认物理层因素。仿真里没有信号完整性、没有时钟抖动、没有电源噪声这些在实测里都可能成为问题。如果仿真和实测在事务层行为上一致但实测链路不稳定那基本就是物理层的事。7. 一些零散但实用的经验关于速率和带宽的换算很多人会搞混。PCIe Gen2 x1 的原始速率是 5 GT/s但因为是 8b/10b 编码有效带宽要打八折约 500 MB/s。Gen3 改用 128b/130b 编码效率高很多8 GT/s x1 约 985 MB/s。算带宽的时候一定要把编码开销算进去不然会高估。关于配置空间的读写有个细节值得注意配置读是 Non-Posted 的必须等 Completion 返回。如果 Completion 超时RC 会报 Completion Timeout 错误。调试时如果看到大量 Completion Timeout要检查 EP 是不是响应太慢或者请求是不是发到了不存在的设备。关于 BAR 大小和地址对齐64 位 BAR 的地址必须 8 字节对齐而且高低 32 位要一起配置。有些驱动在配置 64 位 BAR 时只写了低 32 位导致地址错乱。这种问题在 32 位系统上尤其常见因为 32 位系统可能没有足够的地址空间分配给 64 位 BAR。关于 MSI-X它的 Table 和 PBA 结构可以放在 BAR 里也可以放在 MSI-X Capability 指定的位置。配置时要注意 Table Size 字段和实际分配的向量数一致否则中断可能丢失。关于链路训练时间PCIe 规范要求链路必须在 100ms 内完成训练从 PERST# 释放算起。如果超过这个时间还没 Up主机可能就会放弃。所以如果链路训练慢要查是不是参考时钟稳定太慢或者电源爬升太慢。最后说一个我踩过的坑PERST# 信号的释放时机。规范要求 PERST# 在电源稳定后至少保持 100ms 才能释放。我有一次为了加快启动速度把这个时间缩短到了 50ms结果大部分板子能起来但有一批就是枚举失败。后来老老实实按 100ms 来问题就没了。这种规范里的最小值真的不能随便压缩省那几十毫秒换来的是不稳定完全不划算。
阅读完成 · 觉得有帮助?
咨询建站