PCIe 从架构到 P2P 直通:一次把路由讲清楚
本文合并自两份工程文档:
pcie/pcie_architecture.md(PCIe 物理架构与软件逻辑)与
pcie/多卡数据直通_P2P_配置与数据交互.md,并按"从结构 → 到路由 → 到多卡直通"的顺序重新组织。
配套图:pcie/pcie_arche.png(子系统框图)、pcie/multi_card_p2p.drawio(多卡直通架构)。
0. 一句话结论
PCIe 的一切"走不通",几乎都能归结成一句话:某个地址没落在该落的窗口里,或者某类事务用错了路由依据。
- 配置事务按 Bus:Dev:Func 路由;
- 内存/IO 事务按 地址窗口 路由;
- 完成事务按 Requester ID 回程。
把这三条记住,后面的 ECAM、MMIO、P2P、跨卡直通都是它的推论。
1. 心智模型:RC、RP、EP 与三类事务
| 概念 | 是什么 | 关键属性 |
|---|---|---|
| RC(Root Complex) | CPU/内存与 PCIe 层次之间的"桥头堡" | 一个 RC 可以带多个 Root Port |
| RP(Root Port) | RC 内部的 Type1 桥 | 有 Primary/Secondary/Subordinate bus、Memory/Prefetch 窗口、自己的 LTSSM |
| EP(Endpoint) | 网卡/GPU/NVMe 等设备 | Type0 头,最多 6 个 BAR;配置空间 4KB(含扩展能力) |
| Switch | 多端口桥的集合 | 上游口 + 多个下游口;P2P 是否允许由 ACS 决定 |
本工程的实现方式:一个 PCIe 子系统(PCIE0~7)= 一个 RC 控制器实例,通过 fuse 的分叉(Bifurcation)
把它切成 1~4 个 Root Port(X16 / X8+X8 / X8+X4+X4 / X4×4),每个 RP 独占一段窗口与 lane 组。
SoC
└─ PCIe 子系统(本图范围;fuse 决定整体 RC / EP)
├─ 子系统级共享:SUB CSR / IOMMU / APLIC / PLIC / IDE / PHY CSR
│ pcie_data_fabric / pcie_control_fabric
├─ 控制器级:PCIe/CXL ctrl(TLP 组装、iATU、DBI)
└─ 端口级(RC 模式):pcie_ctrl_top_x16 → RP0/RP1/RP2/RP3 → PHY
别把"子系统框图"当成"单个 RP 的框图":图上
PCIe/CXL ctrl是 RC 的控制器,pcie_ctrl_top_x16才是 RP,
而SUB CSR/IOMMU/IDE/PHY CSR是 RC 级共享资源。
2. 一个子系统内部长什么样:两张 fabric

图 2-1
pcie_arche.png:单个 PCIe 子系统的硬件框图。
上方左侧NC-NOC(非一致数据面)、右侧CC-NOC(一致性/管理面)是两个系统级入口;
中部紫色PCIe/CXL ctrl是控制器主体(AXI slave/master 端口、APB 旁路、sub_csr/ctr_csr/io_mux、PCIe CFG);
左侧浅蓝pcie_data_fabric是数据面分发网,下方浅橙pcie_control_fabric是控制面分发网;
右侧白色pcie_ctrl_top_x16 (x4/x8/x16)是端口级 wrapper,内含PIPE Mapping、PHY、DEM、phy ctrl、apb2cr;
IDE挂在数据路径上(加解密只改载荷、不改路由),RAS/*VRs kernel/P-REG是旁路与寄存器镜像,
IOMMU/SUB CSR/PCIe CFG属子系统级服务。箭头上的M/S表示该方向上的主/从角色。
打开 pcie_arche.png 能看到的关键结构,其实只有三层:
| 层 | 模块 | 作用 |
|---|---|---|
| 系统 NoC | NC-NOC(非一致)、CFG-NOC(配置)、CC-NOC(一致性/管理) | 决定流量进入子系统的"域" |
| 子系统 fabric | pcie_data_fabric、pcie_control_fabric | 数据面 / 控制面物理分离 |
| 控制器 | PCIe/CXL ctrl | TLP 组装解析、iATU 翻译、DBI 配置入口 |
| 端口/PHY | pcie_ctrl_top_x16、pcie_phy_top | 分叉后的 RP;PIPE/PHY 物理层 |
| 旁路 | IDE、RAS、MEM、*VRs kernel、P-REG | 加密、错误记录、内部 RAM、寄存器镜像 |
为什么要把两张 fabric 分开?
高带宽的内存流量走 data_fabric,低频的配置/寄存器访问走 control_fabric —— 配置访问不会挤占数据带宽,
数据通路也不会被 CSR 访问打断。
3. CPU 到 RC 不是直连:数据走 NC-NOC,配置走 CFG-NOC
这是最容易画错、也最容易在排障时被忽略的一点:
| 面 | 系统级网络 | 子系统级 fabric | 事务 | 路由依据 |
|---|---|---|---|---|
| 数据面 | NC-NOC(含 NC_NOC_PCIE_DOWN/UP 端口) | PCIE<n>_DATA_FAB → pcie_data_fabric | MWr / MRd | 地址窗口 |
| 配置面 | CFG-NOC | PCIE<n>_CFG_FAB → pcie_control_fabric | Config T0 / T1 | Bus:Dev:Func |
| 寄存器面 | 控制 / APB 通路 | SUB CSR、MISC CSR/MSG/MHU、IOMMU/APLIC/PLIC/IDE/PHY | 寄存器读写(不发 TLP) | 寄存器基地址译码 |
三个直接结论:
- 同 RC 的 P2P,TLP 根本不出
pcie_data_fabric,与 NC-NOC 无关; - 跨 RC(
PCIE0 ↔ PCIE1)走的就是这条路:发起方 RC 上行 →NC-NOC→ 按地址译码送回目标子系统
→ 目标pcie_data_fabric→ 目标 RP(见 9.1 的sing_nc.png)。前提是系统地址译码允许该窗口回送;
没配好就会落到 DDR 或直接报错,退化成"经主机内存"; - 配置与数据严格分离:枚举/DBI 访问走 CFG-NOC,绝不能用"数据窗口"去碰 DBI。
4. 配置空间与 ECAM:让配置访问变成 load/store
ECAM(Enhanced Configuration Access Mechanism) 把 PCI 配置空间映射进系统内存地址:
ECAM_base + (bus << 20) + (dev << 15) + (func << 12) + offset[11:0]
- 每 bus 1MB、每 dev 32KB、每 func 4KB;
0x000~0x0FF是标准头,0x100~0xFFF是 PCIe 扩展能力(AER、SR-IOV、ATS 都在这里);- 访问方式就是普通
load/store(但必须非缓存、强序,因为读配置可能有副作用)。
| 传统 CAM(CF8/CFC 端口) | ECAM | |
|---|---|---|
| 每功能可访问 | 256B | 4KB(含扩展能力) |
| 并发 | 有全局状态,必须串行 | 普通内存访问,可并发 |
| 本工程 | — | 每端口 64MB = 64 bus,四端口偏移 0/4M/8M/12M |
ECAM 只是"CPU 侧地址";RC 解出 bus 号后自行决定是发 Type0(目标就是本 RP 下游)还是 Type1(要过桥)。
5. 地址窗口:MMIO-L / MMIO-H 与 Type1 头
5.1 两个窗口是按"BAR 位宽"分的,不是按"寄存器 vs 缓冲"分的
| 窗口 | 承载的 BAR | 位置 | 对应 Type1 头 |
|---|---|---|---|
| MMIO-L | 所有 32 位 BAR(可预取/非预取) | 4GB 以下 | 0x20 Memory Base/Limit |
| MMIO-H | 所有 64 位 BAR(规范强制预取) | 4GB 以上 | 0x24/0x28/0x2C Prefetchable Base/Limit(+Upper) |
两个独立约束:地址位宽(32 位 BAR 出不了 4GB)与预取属性(非预取读有副作用,硬件不能缓存/合并读)。
5.2 同一段地址,要写三处,而且必须一致
| 写在哪 | 属于哪个地址域 | 作用 |
|---|---|---|
Type1 头窗口(RP 配置空间 0x20/0x24) | PCIe 总线域 | 告诉下游"这些地址归我",也是 RC 内部 P2P 路由依据 |
SUB CSR 译码窗口(SUBSYS_REG_MMIOL/MMIOH_ADDR_*) | 系统地址域 | 告诉 fabric"哪些 CPU 地址是 PCIe 访问、送给哪个 RP" |
| EP 的 BAR | 设备侧 | 目标设备认领地址 |
- 只配 Type1、不配 SUB CSR → CPU 访问 EP 时 fabric 不认这个地址 → 访问失败;
- 只配 SUB CSR、不配 Type1 → CPU 能进 PCIe,但 RC 不知道送哪个端口 → P2P 路由失败;
- 三者对齐(枚举天然保证)→ 通。
一句话:SUB CSR 决定"CPU 能不能进来",Type1 头决定"进来了往哪个端口走",BAR 决定"谁认领"。
6. 路由三定理与一次完整访问
| 事务 | 路由依据 | 谁看 |
|---|---|---|
| 配置(CfgRd/CfgWr,Type0/Type1) | Bus:Dev:Func | RC 按 bus 号判断是否跨桥 |
| 内存/IO(MWr/MRd/IOWr/IORd) | 地址(命中哪个端口的窗口) | data_fabric + RP 的 Type1 窗口 |
| 完成(Cpl/CplD) | Requester ID | 回到发起方所在端口 |
一个常被忽视的事实:源端 EP 发起 P2P 时不需要知道目标是谁,它只把"软件告诉它的地址"放进 TLP;
"地址 → 端口"的映射存在**软件(BAR 表)和硬件(窗口寄存器)**里。路由判断全交给 RC/Switch。
7. 软件视角:从复位到枚举
7.1 链路训练(LTSSM)
上电后 RP 的 LTSSM 从 Detect → Polling → Configuration → L0 走完才算链路可用;
异常时会在 Recovery(重均衡)、Disabled、Hot Reset 等状态间摆动。
本工程提供 LTSSM Tracker 记录历史状态跳变(MISC_CSR_LTSSM_TRACKER_0~4),是排查"链路起不来"的第一手材料。
7.2 初始化与枚举顺序
按 fuse 创建子系统
→ 分叉配置(bifurcation_mode / port_enable_mask / Link Number)
→ RP 复位释放 → LTSSM 训练 → 链路协商(宽度/速率)
→ 配 Type1 头(bus 范围 + MEM32/PREF64 窗口)
→ 配 SUB CSR 译码窗口
→ ECAM 扫描(bus 0..N,dev 0..31)→ 识别桥(header_type)与 EP
→ BAR 分配(32 位落 MMIO-L、64 位预取落 MMIO-H,按大小对齐)
→ iATU outbound/inbound 建立 CPU↔PCIe 地址映射
→ 中断初始化(PLIC 线中断 / APLIC MSI;MSI-X 表写入)
7.3 iATU:地址翻译的落点
| 方向 | 做什么 |
|---|---|
| Outbound | CPU 地址 → PCIe 地址;按 Region 配 base/limit/target 与类型(MEM/IO/CFG0/CFG1/MSG) |
| Inbound | PCIe 地址 → CPU 地址(EP 侧按 BAR 匹配) |
注意:iATU 不是"路由",是"翻译"。路由由窗口(data_fabric + Type1)决定,iATU 只在控制器内部搬地址。
8. P2P 直通:本质是"改目标地址"
8.1 DMA 与 P2P 是两个维度
| 回答的问题 | |
|---|---|
| DMA | 谁在搬?(设备 DMA 引擎,CPU 只配置+触发) |
| P2P | 从哪搬到哪?(设备 → 设备,绕开主机内存) |
两者不是二选一:做 P2P 恰恰要依赖 DMA,区别只在"目标地址填 DDR 还是填对端 BAR"。
8.2 配置四步
| 步骤 | 动作 | 缺了会怎样 |
|---|---|---|
| ① RC 端口窗口 | 配 Type1 头(MEM32/PREF64) | 下游 BAR 无处安放 / 窗口外 TLP 不受理 |
| ② 子系统译码 | 配 SUB CSR MMIOL/MMIOH | RC 不知道目标地址送哪个端口 |
| ③ 枚举写 BAR | 在目标 RP 窗口内给对端卡分配地址 | 无人认领 → UR |
| ④ DMA 目标 | 发起方 DAR/BUS_ADDR = 对端 BAR | 填成 DDR → 退化成"经主机内存" |
8.3 一个容易讲错的问题:SUB CSR 参与 P2P 数据路由吗?
不参与。 P2P 的 TLP 从发起方 RP 上行进入 RC 核心,RC 核心按 Type1 头窗口判断目标属于哪个 RP,
直接在 RC 内部转发 —— 不经过 CPU/fabric 入口,而 SUB CSR 恰是那个"CPU 入口"。
但"触发 P2P"几乎总需要 SUB CSR:软件要通过 CPU 去写发起方 EP 的 DMA 寄存器(EP 的 BAR0),
这一步是 CPU 访问,必须过 SUB CSR。只有"EP 自主发起 DMA(如 mailbox 命令触发)"的极端场景,
数据面才可能只用 Type1 头 + EP BAR 跑通。
9. 多卡直通:五种拓扑的能力矩阵
| # | 拓扑 | 路径 | 是否直通 | 关键配置 | 对应图 |
|---|---|---|---|---|---|
| ① | 同 RC,不同 RP | 只在 pcie_data_fabric 内按窗口转发 | ✅ | Type1 窗口 + 对端 BAR | single_rc.png |
| ② | 跨 RC,同一 NC-NOC | 上行 NC-NOC → 地址译码送回另一子系统 | ✅(需系统译码支持) | SUB CSR 窗口 + 两侧 Type1 + 地址不重叠 | sing_nc.png |
| ③ | 跨 NC-NOC | NOC 间链路(UCIe/CCIX/CXL 或 NOC-to-NOC) | ✅(需 NOC 互连) | NOC 间路由 + 一致性/顺序 | multi_nc.png |
| ④ | 同 PCIe Switch,不同下游端口 | Switch 内部转发 | ✅(需关 ACS Redirect) | ACS RR/CR 关闭 + Switch P2P 使能 | — |
| ⑤ | 跨 Socket / 机箱 | CCIX over UCIe / CXL S2S / NTB / RDMA | ✅(链路层) | 建链 + 地址域规划 + 一致性/翻译 | multi_card_p2p.drawio |
关键认知:P2P 的"范围"由它经过的组合决定,而不是由"是不是同一颗芯片"决定。
同一 RC 内的 P2P 只走pcie_data_fabric;跨 RC 必须上到NC-NOC;跨 NOC 还要再加一级 NOC 间链路。
每多一级,就多一次地址译码与转发开销。
9.1 三张图看懂 P2P 的三种范围(都是实拍架构图,不是示意)
① 子系统内部:single_rc.png

图 9-1
single_rc.png(标签"P2P子系统内部"):
- 结构:
NC-NOC0的下方挂一个Root Complex(PCIe1),其内部pcie_data_fabric汇聚出 4 个端口
RP_x16/RP_x8/RP_x4_0/RP_x4_1,每个端口下挂一个 EP(EP0~EP3)——这正是分叉模式下的物理连接。- 红色箭头就是 P2P 数据流:
EP0 → RP_x16(上行)→pcie_data_fabric→RP_x8(下行)→EP1。- 黑色双向箭头只存在于
NC-NOC0 ↔ pcie_data_fabric之间:P2P 流量不去 NC-NOC,它只负责子系统与系统侧的常规上下行。- 要点:四个 RP 共用一个
pcie_data_fabric,这就是"同 RC P2P 能一拐就到达对端"的硬件基础。
② 跨子系统、不跨 NOC:sing_nc.png

图 9-2
sing_nc.png(标签"P2P跨子系统不跨NOC"):
- 结构:同一个
NC-NOC0下挂两个独立的 RC ——Root Complex(PCIe1)与Root Complex(PCIe3);
每个 RC 只有RP_x16为实线(其余RP_x8/x4_0/x4_1与对应的 EP1~EP3 是虚线=本配置未使能)。- 红色箭头路径:
EP0(PCIe1) → RP_x16 → pcie_data_fabric → NC-NOC0 → pcie_data_fabric(PCIe3) → RP_x16 → EP0(PCIe3)。- 结论:跨 RC 的 P2P 是可以做的——但它要"上 NOC 再下 NOC",比同 RC 多两跳;
能否成立取决于 NOC 的系统地址译码是否把这个目标地址重新分配回 PCIe3 子系统。- “不跨 NOC” 的含义:两个 RC 共用同一个 NC-NOC 实例,转发出现在这个 NOC 内部,没有走 NOC 间的长链路。
③ 跨子系统、且跨 NOC:multi_nc.png

图 9-3
multi_nc.png(标签"P2P跨子系统跨NOC"):
- 结构:两个 NC-NOC 实例(
NC-NOC0与NC-NOC1)之间有一条双向互连链路;
NC-NOC0下挂PCIe1,NC-NOC1下挂PCIe4,两侧pcie_data_fabric与 RP/EP 结构与前两张一致。- 红色箭头路径:
EP0(PCIe1) → RP_x16 → pcie_data_fabric → NC-NOC0 →(NOC 间链路)→ NC-NOC1 → pcie_data_fabric → RP_x16 → EP0(PCIe4)。- 结论:跨 NOC 的 P2P 依赖 NOC 之间的物理链路(UCIe/CCIX/CXL 或 NOC-to-NOC 通道);
每一级 NOC 都要做一次地址译码与转发,因此带宽、时延、错误传播范围都会放大,需要额外的排序/一致性保障。
三张图的对比总结
| 图 | 跨越了什么 | 经过 NOC | 额外跳数 |
|---|---|---|---|
single_rc.png | 只跨 RP(同一 RC 内) | ❌ | 0(只在 data_fabric 内部转一次) |
sing_nc.png | 跨两个 RC(同 NOC) | ✅ 同一个 NC-NOC | +2(上、下各一次) |
multi_nc.png | 跨两个 RC + 跨两个 NOC | ✅✅ 两个 NC-NOC + NOC 间链路 | +3 及以上 |
9.2 Switch 场景:ACS 是开关
| ACS 位 | 想直通时 | 想隔离时(虚拟化/IOMMU 强制) |
|---|---|---|
| RR Request Redirect | 关(开着会把 P2P 请求重定向回 RC) | 开 |
| CR Completion Redirect | 关 | 开 |
| UF Upstream Forwarding Disable | 关 | 视策略 |
| SV Source Validation | 保持开 | 开 |
| TB Translation Blocking | 一般关 | 开 |
一句话:ACS 开着 = 直通被安全策略挡住;ACS 关着 = Switch 才肯让两个下游口自己聊。
9.3 跨 NOC / 跨 Socket 的手段
| 手段 | 做法 | 代价 |
|---|---|---|
| DDR 中转 | 卡A → RC0 → DDR → RC1 → 卡B | 两次 DMA,吃两遍内存带宽 |
| NTB(非透明桥) | outbound/inbound window + scratchpad + doorbell | 需要两侧软件配合、地址翻译 |
| CCIX / CXL | 链路训练后建立一致性域,加速器间对称 P2P | 需要链路与一致性层支持 |
NTB 的四个要素:outbound window(本侧→对侧)、inbound window(对侧→本侧)、scratchpad(状态交换)、doorbell(带数据触发中断)。
写数据 → clean → fence → 敲 doorbell,是固定顺序。
10. 数据交互逻辑:控制面与数据面分离
三条路径的差别:
| 路径 | 经过 | 带宽占用 | 何时用 |
|---|---|---|---|
| 经主机 DDR | A → RC → CC_NoC(DMI) → DDR → RC → B | 内存带宽 ×2 | 默认/兜底 |
| 同 RC P2P | A → RC → B(TLP 不出 RC) | 占链路带宽,不占 DDR | 同子系统/同 Switch |
| 跨 Socket | A → RC → CCIX/CXL/NTB → 对端 | 占链路带宽 | 分布式/池化 |
一致性要点:
- TLP 的 No-Snoop 位决定走
Pcie PCAIU(一致性代理,snoop CPU cache)还是NCAIU(非一致); - No-Snoop 路径必须软件维护:
写数据 → cbo.clean → fence → doorbell; - P2P 连这一步都没有 —— TLP 在 RC 内部掉头,压根不碰 DDR/CC_NoC;
- 完成同步永远要有超时与恢复路径,链路降速/掉链会变成"看起来像挂死"的长尾。
11. 排障清单:症状 → 根因
| 现象 | 根因 | 查哪里 |
|---|---|---|
| UR(Unsupported Request) | 目标地址没有 BAR 认领 / 窗口没配 | 目标 BAR 值、目标 RP 窗口、枚举日志 |
| CA(Completer Abort) | 目标设备内部错误 / 对齐非法 | 设备错误寄存器、AER 日志 |
| Completion Timeout | 目标不在(链路未训练)/ 路由到错误端口 | LTSSM 状态、Link Status |
| P2P 请求被送回主机 | ACS RR/CR 开着 | Switch 的 ACS 能力寄存器 |
| 偶发旧数据 | No-Snoop 未做 cache 维护;缺 fence | TLP 属性位、cbo.* 调用点 |
| 吞吐上不去 | 链路宽度/速率、x16 分叉成 x4、上游端口瓶颈 | Link Status 的 Width/Speed、分叉配置 |
| CPU 访问 EP 直接失败 | SUB CSR 窗口没配 / 与 Type1 不一致 | SUBSYS_REG_MMIOL/MMIOH_ADDR_* |
抓点分层:固件(枚举表、窗口回读、DMA 描述符) → RC/Switch(AER、ACS、Link Status) →
协议层(TLP 是否到达目标、Requester ID) → 系统(中断是否触发、dmesg 里的 AER/UR)。
12. 一页速查
配置访问: ECAM 地址 = base + bus<<20 + dev<<15 + func<<12 + off,
走 CFG-NOC → control_fabric → DBI,事务按 Bus:Dev:Func
内存访问: MMIO-L(32 位 BAR)/MMIO-H(64 位预取),走 NC-NOC → data_fabric,
事务按地址窗口,32 位 BAR 只能 <4GB
P2P 四步: Type1 窗口 → SUB CSR 译码 → 枚举写 BAR → DMA 目标=对端 BAR
P2P 路由: 只在 RC 内部按 Type1 头转发,不经 CPU、不用 SUB CSR
多卡拓扑: 同 RC(只走 data_fabric)✅ / 跨 RC 同 NOC(上·下 NC-NOC)✅ /
跨 NOC(NOC 间链路)✅ / 同 Switch ✅(关 ACS RR·CR) / 跨 Socket 需 CCIX·CXL
一致性: No-Snoop → 软件 clean+inval+fence;P2P 不涉及
464



被折叠的 条评论
为什么被折叠?



