PCIe 四架构实测(零):为什么要做这个实验——硬件平台、数据帧格式与测量口径
系列:《PCIe 四架构实测》第 0 篇 · 开篇
平台:XC7K325TFFG900-2I · Vivado 2023.1 · XDMA · PCIe Gen2 ×8 · Windows 上位机(Python + ctypes)
读法:这一篇不含任何"架构二"的实现细节——它只干一件事:把后面四篇要用的规矩立好(平台、时钟、帧格式、指标定义)。后面每一篇都会回头引用它。看到表格别跳过。
文章目录
- PCIe 四架构实测(零):为什么要做这个实验——硬件平台、数据帧格式与测量口径
- @[toc]
- 0 这一篇要交给你什么
- 1 为什么要做这个实验
- 1.1 场景:数据要从 FPGA 跑到主机
- 1.2 最容易犯的错:拿"链路速度"当"系统速度"
- 1.3 四个问题 + 架构二的答案(先剧透)
- 2 四种架构是什么
- 2.1 为什么先做 ②
- 3 硬件平台基线
- 3.1 这块板
- 3.2 基线数字(后面每篇都要引用)
- 3.3 主机软件栈全景
- 3.4 XDMA 平台级配置(四架构共用)
- 4 时钟规划:为什么 `user_clk` 必须是 250 MHz
- 4.1 时钟从哪来、到哪去
- 4.2 250 MHz 是被"逼"出来的,不是挑出来的
- 5 理论天花板:这张算式是全系列的分母
- 5.1 逐层剥
- 5.2 再往上,还有两刀
- 6 统一假数据帧格式
- 6.1 为什么必须有帧
- 6.2 帧长什么样
- 6.3 `FLAGS` 里的 `arch` / `chan` / `data_mode`
- 6.4 校验字:这里有个真实的工程取舍
- 6.5 一条必须记住的规律:实体帧长 = `N + 36`
- 7 四项指标的严格定义
- 7.1 吞吐(Throughput)
- 7.2 带宽利用率(Utilization)
- 7.3 延迟(Latency)——这里有两个完全不同的数,务必分清
- 7.4 稳定性(Stability)
- 7.5 ★ 每一项指标怎么测的(全系列总表)
- 8 主机软件栈:驱动 + 为什么 Python 就够
- 8.1 驱动从哪来
- 8.2 为什么 Python + ctypes 就够了
- 9 Windows 环境隔离清单
- 10 系列导航
- 附:本篇用到的图与复现方式
- 下一篇
文章目录
- PCIe 四架构实测(零):为什么要做这个实验——硬件平台、数据帧格式与测量口径
- @[toc]
- 0 这一篇要交给你什么
- 1 为什么要做这个实验
- 1.1 场景:数据要从 FPGA 跑到主机
- 1.2 最容易犯的错:拿"链路速度"当"系统速度"
- 1.3 四个问题 + 架构二的答案(先剧透)
- 2 四种架构是什么
- 2.1 为什么先做 ②
- 3 硬件平台基线
- 3.1 这块板
- 3.2 基线数字(后面每篇都要引用)
- 3.3 主机软件栈全景
- 3.4 XDMA 平台级配置(四架构共用)
- 4 时钟规划:为什么 `user_clk` 必须是 250 MHz
- 4.1 时钟从哪来、到哪去
- 4.2 250 MHz 是被"逼"出来的,不是挑出来的
- 5 理论天花板:这张算式是全系列的分母
- 5.1 逐层剥
- 5.2 再往上,还有两刀
- 6 统一假数据帧格式
- 6.1 为什么必须有帧
- 6.2 帧长什么样
- 6.3 `FLAGS` 里的 `arch` / `chan` / `data_mode`
- 6.4 校验字:这里有个真实的工程取舍
- 6.5 一条必须记住的规律:实体帧长 = `N + 36`
- 7 四项指标的严格定义
- 7.1 吞吐(Throughput)
- 7.2 带宽利用率(Utilization)
- 7.3 延迟(Latency)——这里有两个完全不同的数,务必分清
- 7.4 稳定性(Stability)
- 7.5 ★ 每一项指标怎么测的(全系列总表)
- 8 主机软件栈:驱动 + 为什么 Python 就够
- 8.1 驱动从哪来
- 8.2 为什么 Python + ctypes 就够了
- 9 Windows 环境隔离清单
- 10 系列导航
- 附:本篇用到的图与复现方式
- 下一篇
0 这一篇要交给你什么
| 读完你应当能做到 | 对应小节 |
|---|---|
| 说清"四种 DMA 架构"各自是什么、代价是什么、什么时候该用哪个 | §2 |
| 报出这个平台的基线与理论天花板,并知道数字是怎么算出来的 | §3、§5 |
说清user_clk为什么必须是 250 MHz(而不是随便挑的) | §4 |
| 画出本项目统一的假数据帧格式,并解释每个字段为什么存在 | §6 |
| 说清"吞吐 / 利用率 / 延迟 / 稳定性"这四个词的严格口径 | §7 |
| 复现出实验环境:驱动怎么装、Windows 要关哪些东西 | §8、§9 |
| 知道后面四篇各讲什么、先读哪篇 | §10 |
一句话概括这一篇:“先把尺子做出来,再去量东西。”
1 为什么要做这个实验
1.1 场景:数据要从 FPGA 跑到主机
做数据采集卡的人都会撞上同一堵墙:
板子上采到的数据,怎么最省事、最快地送进主机内存?
FPGA 里数据是流(一个拍一个拍地出),主机里数据是内存(一个地址一个地址地放)。这两边天生不一样,中间必须有个"搬运工"。这个搬运工就是 DMA;在 Xilinx 的生态里,用得最多的搬运工叫XDMA(DMA Subsystem for PCI Express)。
于是问题变成:这块搬运工的活,有几种干法?每种干法在吞吐、延迟、资源上各是什么代价?
1.2 最容易犯的错:拿"链路速度"当"系统速度"
新手(包括我)第一反应是翻数据手册:
PCIe Gen2 ×8 ⇒ 5.0 GT/s × 8 lane ⇒ 4 000 MB/s。
然后得出结论:“我这板子能跑 4 GB/s。”
这句话是错的——它只是链路的能力,不是系统的能力。真实的数据要经过这么多手:
传感器/ADC → FPGA 内部逻辑 → DMA 引擎 → PCIe 链路 → 主机根复合体 → 驱动 → 操作系统 → 你的程序 ① ② ③ ④ ⑤ ⑥ ⑦第 ③ 步(链路)确实是 4 GB/s 级的,但第 ⑥⑦ 步(驱动 + 操作系统 + 你自己的程序)很可能比它慢一个数量级。
这个实验的全部意义,就是把这七段路拆开,量出每一段到底吃掉多少。结论会让你吃惊:在这块板子上,第 ③ 步(大家最关心的 PCIe 链路)根本不是瓶颈。
1.3 四个问题 + 架构二的答案(先剧透)
| 问题 | 量什么 | 架构二实测 | 折算成线速 |
|---|---|---|---|
| ① 板上能跑多快 | FPGA 侧产数速率 | 3 942.7 MB/s | 98.6% |
| ② 主机实收多少 | 主机 read 到的有效载荷 | ≈ 133 MB/s | 3.3% |
| ③ 延迟多大 | FPGA 内部回环 / 端到端往返 | 8 ns/54.0 µs(P50,4 KB 请求) | — |
| ④ 长时间稳不稳 | 30 分钟分段长跑 | 9 171 万帧(91 708 725)零错 / 零丢 / 零超时 | — |
🔴① 和 ② 差了约 30 倍。这两个数都是真的、都测对了——它们量的不是同一样东西。"差 30 倍"从头到尾就是这个系列要讲的事(篇 2 讲 ②,篇 3 讲 ③,篇 4 讲 ④ 和"为什么②这么低还不怪链路")。
2 四种架构是什么
"架构"在这里不是指硬件电路,而是指数据在 FPGA 内部走哪条路。四种典型做法:
一句话总结每种:
| 架构 | 一句话 | 主打 | 代价 |
|---|---|---|---|
| ① DDR 大缓存 | 先把数据攒进 DDR3(2 GB),攒成大块再整块搬走 | 能吸收突发、能重传、容量大 | 多两趟 DDR 读写;时序收敛最难(MIG 是硬骨头) |
| ② Streaming 直通 | 数据不在 FPGA 里停,一拍 16 B 直接过 | FPGA 侧延迟最低、零拷贝、逻辑最少 | 无弹性缓冲、不能重传——丢了就是丢了,只能靠帧头发现 |
| ③ BRAM 轻缓存 | 用片上 BRAM(256 KB)当小缓冲 | 延迟比 DDR 低,能吸收小抖动 | 容量小;吞吐仍被 C2H 卡住,收益有限 |
| ④ 多通道 | 直通 +2 条 C2H / 2 条 H2C 并行 | 唯一能顶开单通道天花板的做法 | 逻辑面积 ×N;主机侧要分核/多进程 |
2.1 为什么先做 ②
- 它是基线。②是四种里逻辑最简单、延迟最低的一种,它测出来的数字 ≈ “这块板 + XDMA 的物理上限”。后面三种拿它做对照才有意义。
- 它的结论最反直觉——“板上 98.6% 线速,主机只有 3.3%”,这句话足以撑起一整篇文章。
- 换架构时唯一变量干净。四种架构共用同一套帧格式、同一套寄存器、同一套上位机,换架构只换"中间那段数据通路"。这是数据可比的前提(见上面那张图的底注)。
⚠️一个必须提前说的坑:架构四我说的是"2 条通道",不是我原计划的 4 条。原因是本器件的XDMA IP 通道数上限就是 2(见 §3.4)。这不是取舍,是硬约束。
3 硬件平台基线
3.1 这块板
注意实物图左侧那排金手指旁边,板上丝印就印着PCIe 2.0 X8——这不是我挑的配置,是这块板子线路只有 8 个 lane、且只跑到 Gen2。
3.2 基线数字(后面每篇都要引用)
| 项 | 值 | 说明 |
|---|---|---|
| FPGA | XC7K325TFFG900-2I(Kintex-7,-2 速度等级) | 325K 逻辑单元 |
| 板载内存 | DDR3 2 GB / 64 bit / 1600 MT/s | 理论1600 MT/s × 8 B = 12.8 GB/s;只有架构一用 |
| PCIe | Gen2 ×8(5.0 GT/s × 8 lane) | 板卡线路上限 |
| 参考时钟 | 100 MHz 差分(板载晶振) | 给 PCIe 硬核用 |
| 用户时钟 | user_clk= 250 MHz | 见 §4 |
| AXI 数据位宽 | 128 bit= 一拍 16 字节 | 全设计统一"一拍 16 B" |
| 开发工具 | Vivado 2023.1 | 全程统一,四套工程同版本 |
| DMA IP | XDMA(DMA/Bridge Subsystem for PCI Express) | AXI-Stream 接口模式 |
| 主机 | Windows + Python 3 + ctypes | 不写一行 C(见 §8) |
3.3 主机软件栈全景
这张图请记住一个结构:一个设计里其实只有两条通路——一条控制路(主机读写寄存器,慢但对,走 AXI-Lite),一条数据路(高速流,走 AXI-Stream / DMA)。后面所有问题几乎都能归到"这两条路里谁在挡道"。
3.4 XDMA 平台级配置(四架构共用)
| 配置页 | 项 | 值 | 为什么 |
|---|---|---|---|
| Basic | 模式 | AXI-Stream(架构二/四);AXI-MM(架构一/三) | 这是"架构"这个变量本身 |
| Basic | Device / Vendor ID | 7028/10EE | 主机按这个认设备 |
| Basic | 链路速度 / 宽度 | 5.0 GT/s / ×8 | 板卡线路只有 Gen2 ×8 |
| Basic | 参考时钟 | 100 MHz | 板载差分晶振 |
| Basic | AXI 数据位宽 | 128 bit | 250 MHz × 128 bit = 4 GB/s= 线速 |
| Basic | 通道数(C2H / H2C) | 1 / 1 | 架构四改成 2 / 2;⚠️ 本器件上限就是 2 |
| Basic | RIDs | C2H 32 / H2C 16 | 允许同时挂起多少条读请求 |
| PCIe | AXI-Lite Master | 开启(窗口 1 MB) | 控制流的唯一入口 |
| PCIe | BAR | BAR0 = AXI-Lite | 寄存器窗口 |
| MSI-X | 中断 | 开启(1 vector) | 主机靠它知道"数据到了" |
| 缓存 | HAS_CACHE | 0 | AXI-Stream 模式不涉及 |
🔴“通道数上限 = 2” 的依据(官方 PG195 原文):
“最多 4 条主机到卡(H2C/读取)数据通道(针对 7 系列 Gen2 IP 最多 2 条数据通道)。最多 4 条卡到主机(C2H/写入)数据通道(针对 7 系列 Gen2 IP 最多 2 条数据通道)。”
XC7K325T 走的就是7 系列 Gen2 IP⇒2 条是天花板;想要 4 条得换 UltraScale 器件。别的教程里写"XDMA 支持 4 通道"是对的,但那说的是 UltraScale。看手册一定要看"针对哪个器件"那一句。
顺带一个有用的事实:AXI 数据位宽在 7 系 Gen2 IP 上只有 64 bit 和 128 bit 两档(也是 PG195 写的)。这个限制直接决定了 §4 的时钟选择。
4 时钟规划:为什么user_clk必须是 250 MHz
时钟是这类工程里最容易"抄错了还不知道"的地方。这里把来龙去脉讲清楚。
4.1 时钟从哪来、到哪去
板载 100 MHz 差分晶振 └─ util_ds_buf(差分转单端) └─ xdma_0 / refclk (PCIe 硬核的参考时钟) └─ [PCIe 硬核内部 PLL + 时钟域切换,XDMA 自己搞定] └─ xdma_0 / axi_aclk = user_clk = 250 MHz └─ 扇出到全部用户逻辑(data_gen / data_verify / stream_mux / ts_counter / 寄存器块)关键点:用户逻辑里没有第二个时钟。全设计只有一个用户时钟域,PCIe 时钟域到用户时钟域的跨越由 XDMA 内部完成。所以:
- 用户逻辑内部不需要写任何异步 FIFO;
- 不存在 CDC 问题——这是这个工程时序能收敛的前提(篇 1 / 篇 3 的时序部分都建立在这条上)。
这也是"为什么用 IP 而不是自己写 PCIe 逻辑"的最大好处之一:跨时钟域这个最容易翻车的地方,IP 厂商替你做了,而且是验证过的。
4.2 250 MHz 是被"逼"出来的,不是挑出来的
想让用户时钟不成为瓶颈,只有一条路:让它的数据搬运能力 ≥ 链路能力。
链路能力 = 4 000 MB/s 用户时钟需满足 : 频率 × 位宽 ÷ 8 ≥ 4 000 MB/s代入可选位宽(7 系 Gen2 IP 只有 64 / 128 bit 两档):
| AXI 位宽 | 需要的频率 | 7 系能做到吗? |
|---|---|---|
| 64 bit | 4000 ÷ 8 = 500 MHz | ❌ 用户逻辑跑 500 MHz 在 -2 器件上是自找麻烦 |
| 128 bit | 4000 ÷ 16 =250 MHz | ✅ 常规做法,时序有余量 |
| 256 bit | 125 MHz | ❌ 7 系 Gen2 IP 不支持 256 bit 数据路径 |
所以:128 bit × 250 MHz = 4 GB/s = 线速——用户时钟恰好不成为瓶颈,也没有浪费。这就是 250 这个数的全部理由。
💡 换个说法:如果哪天有人告诉你"用户时钟随便给 200 MHz 也行",你要能立刻算出来——
200 MHz × 128 bit = 3.2 GB/s,用户时钟已经自己把带宽砍掉了 20%,你测出来的所有吞吐数字都会被这个错误的天花板压住,而且看起来"像是 PCIe 不行"。
5 理论天花板:这张算式是全系列的分母
5.1 逐层剥
5.0 GT/s × 8 lane = 40 GT/s ← 链路物理速率("GT/s"是每秒传输次数,不是每秒比特) 40 × 8/10 = 32 Gb/s ← Gen1/Gen2 用 8b/10b 编码,每 10 bit 只有 8 bit 是数据 32 Gb/s ÷ 8 = 4 GB/s ← 换成字节 = 4 000 MB/s ← ★ 全系列所有百分比的分母三个容易记错的点:
GT/s不是Gb/s。5.0 GT/s 是"每秒 50 亿次传输",经过 8b/10b 之后实际数据率是它的 0.8 倍。忘了这个 0.8,你的天花板会凭空高 25%。8b/10b是 Gen1/Gen2 的编码。Gen3 换成 128b/130b,开销从 20% 降到 1.5%——所以 Gen3 的"到账率"看起来高得多。本板是 Gen2,老老实实乘 0.8。÷ 8是把比特换成字节。这是最常见的一处笔误:把32 Gb/s直接当成32 GB/s,结果算出 4 倍的天花板。
5.2 再往上,还有两刀
链路不是纯数据管道,上面还跑着协议:
| 层 | 开销来源 | 剩下的比例 |
|---|---|---|
| TLP 层 | 每个 TLP(事务层包)有包头、还有流控信用维护 | 约88% ~ 92%(读偏低、写偏高) |
| 软件层 | 驱动 + 操作系统 + 你的程序 | 这一层没有理论值,得实测 |
⇒链路真正能交给软件的是 3.5 GB/s 量级。而架构二实测主机侧只有133 MB/s。
所以本文反复说的那句话:瓶颈不在链路。篇 2 会把 133 MB/s 这个数的成因拆到底。
🎯这张算式是后面所有百分比的标尺:说"98.6% 线速",就是
3942.7 ÷ 4000;说"只用 3.3%",就是133 ÷ 4000。分母永远是这个 4 000 MB/s。
6 统一假数据帧格式
6.1 为什么必须有帧
直通架构没有存储、不能重传。那怎么知道数据对不对、有没有丢?
唯一的办法:让数据自己带身份证。这就是"帧"的由来——每帧前面带一个头,说明"我是谁、第几号、什么时候生的",尾巴上带一个校验字,说"我整帧是完整的"。
主机(或 FPGA 里的解析器)只要逐帧对齐,就能算出丢帧率、错误率。没有帧头,就什么都判不了,只能看到一堆字节。
6.2 帧长什么样
+-------- 32 B 帧头 --------+------------ N 字节载荷 ------------+--- 4 B ---+ | MAGIC SEQ TS_TX FLAGS ... | 计数器(第 k 个字 = k) | 校验字 | +---------------------------+-------------------------------------+-----------+ 实体帧长 = N + 36 字节| 偏移 | 字段 | 宽度 | 含义 |
|---|---|---|---|
0x00 | MAGIC | 4 B | 0xA5C30000 | arch<<8 | ver;架构二 =0xA5C30200。扫流时看第一个字就知道"是不是我们的帧" |
0x04 | SEQ | 4 B | 帧序号,每通道独立递增,首帧 = 0 |
0x08 | TS_TX | 6 B | 组帧时刻的 48 bit 时间戳(250 MHz 秒表,回绕周期 13 天) |
0x0E | FLAGS | 2 B | [15:12] arch / [11:8] chan / [7:4] data_mode / [3:0] rsv=0 |
0x10 | LENGTH | 4 B | 载荷字节数N(注意:不是帧总长) |
0x14 | SEED | 4 B | PRBS 种子(可复现随机数据用) |
0x18 | RSV | 8 B | 常规帧全 0;回应帧的[47:0]放到达时刻TS_RX |
0x20 | PAYLOAD | N B | 计数器载荷:第 k 个 32 bit 字 = k(“位置即期望值”,验证器不用查表就能对拍) |
| 末尾 | 校验字 | 4 B | 见 §6.4 |
为什么帧头是 32 B 而不是更短:32 B = 128 bit × 2 整拍(AXI 位宽是 128 bit)。这样帧头永远占满整数拍、不产生尾拍,也不用处理"帧头被切成两半"的麻烦。同时 4 字节对齐满足所有 DMA 的对齐要求。
6.3FLAGS里的arch/chan/data_mode
| 位域 | 名称 | 取值 | 谁写 / 谁读 |
|---|---|---|---|
[15:12] | arch | 1=DDR / 2=Streaming / 3=BRAM / 4=多通道 | data_gen按当前位流写死;解析器核对 |
[11:8] | chan | 架构一~三恒 0;架构四 = 0 或 1 | 按通道实例化时写死;多通道时用来分桶统计 |
[7:4] | data_mode | 0=计数器 / 1 | 解析器按此模式"重算期望值"再比对 |
[3:0] | rsv | 恒 0 | 解析器检查非 0 即报格式错——多一层"帧头没被撕坏"的判定 |
💡为什么这些信息要写进帧里,而不是只放在寄存器:因为配置会变、数据会滞留。中途从计数器模式换成 PRBS,DDR 里还存着旧模式的帧;数据落地成文件后板卡可能都下电了。帧内字段是唯一跟着数据走的元数据。这叫"自描述"。
另外注意
MAGIC的[11:8]也编了arch——同一信息编了两遍是有意的:单 bit 翻转想同时骗过两处,概率远小于骗过一处。
6.4 校验字:这里有个真实的工程取舍
先把结论说清楚:本项目的"校验字"是 32 位加法校验和(把帧头 8 个字 + 全部载荷字,按小端 32 bit 累加,模 2³²),不是标准 CRC32。字段名沿用了文档里的CRC32(寄存器也叫CRC_ERR),但实现是加法校验和——RTL 里对此有明确注释。
为什么要换掉 CRC32?答案是时序收敛。
标准 CRC32 是一个串行 LFSR:每个字节都要经过几级异或,而且下一拍要用上一拍的结果——这是一条又长又无法并行化的组合路径,在 250 MHz(4 ns)下是典型的关键路径来源。
而加法校验和可以做成闭式:
载荷是"第 k 个字 = k"的计数器 ⇒ 载荷字之和 = 0 + 1 + 2 + ... + (nw-1) = nw(nw-1)/2 ← 一次乘法就能算出来! ⇒ 校验字 = Σ(帧头 8 字) + nw(nw-1)/2 ← 只剩一级 32 bit 加法(约 0.5 ns)于是这条路径从"几十级串行异或"变成一次乘加,时序立刻轻松。
⚠️代价必须说清:加法校验和的检错能力弱于 CRC32(比如它抓不住"两个字整体调换位置"这类错误)。本项目接受这个代价,是因为:
① 载荷有独立的逐 lane 位置比对(第 k 字必须等于 k),这才是主力检查;
② 校验字只当"帧头 + 载荷整体没被撕坏"的兜底;
③ 换来的时序余量对 250 MHz 下的直通设计是刚需。这是一次典型的"用检错强度换时序余量"的取舍——不是偷懒,是算过账的。
6.5 一条必须记住的规律:实体帧长 =N + 36
| 名词 | 含义 |
|---|---|
payload_len = N | 只算载荷(计数器那部分) |
实体帧长 =N + 36 | 帧头 32 B + 载荷 N + 校验字 4 B ——真正在总线上跑的长度 |
算吞吐、选块长、算拍数,都必须用实体帧长。把这两者搞混是本项目里真实踩过的坑(篇 2 详细讲)。
🔎篇 2 的伏笔:实体帧长必须是16 字节的整数倍(因为一拍是 16 B)。这条约束会让特定的帧长"刚好整除",另一些"最后剩 4 个字节"——而后者实测能让吞吐掉 20%。看到
N + 36这个式子,就该想到它。
7 四项指标的严格定义
"吞吐多少"这句话在没有口径的情况下毫无意义。下面四个词在本系列里各只有一个意思,请以此为准。
7.1 吞吐(Throughput)
只算有效载荷字节数 ÷ 耗时(MB/s)。
帧头、校验字一律不算。所以同一份数据流,"按实体帧算"和"按有效载荷算"能差几个百分点——本系列统一用有效载荷。
7.2 带宽利用率(Utilization)
吞吐 ÷ 4 000 MB/s。
分母就是 §5 那张算式的结论,全系列唯一。
7.3 延迟(Latency)——这里有两个完全不同的数,务必分清
| 名字 | 量的是什么 | 架构二实测 |
|---|---|---|
| FPGA 内部回环延迟 | FPGA收完最后一拍到发出第一拍之间隔了多少拍 | 2 拍 = 8 ns(ILA 逐拍量的) |
| 端到端往返 RTT | 主机发出请求到收完响应 | 54.0 µs(P50,4 KB 请求) |
差了约 7500 倍,而且两个数都是对的——它们量的是完全不同的两段路。篇 3 整篇都在讲这件事,以及为什么"用帧内两个时间戳相减"得到的不是回环延迟。
延迟一律报P50 + P99(中位数 + 尾延迟)。只报平均值的延迟数据基本没有信息量 —— 平均值会掩盖掉"偶尔卡一下"这种最要命的现象。
7.4 稳定性(Stability)
分段长跑,每段读一次寄存器增量,看四条判据。
四条判据(来自本项目上位机的tc03):
ΔCRC_ERR = ΔSEQ_ERR = 0(没有错帧)- 末段吞吐 ≥ 最快段 × 98%(没有明显衰减)
- 全程无读超时
- FPGA 的
FRAMES_OK增量等于主机实际发帧数(两侧对齐)
⚠️ 长跑必须先"钉核"(把线程绑定到指定的 CPU 核)。不钉核的话,你测的其实是Windows 调度器,不是你的板子——篇 4 会给出实锤证据。
7.5 ★ 每一项指标怎么测的(全系列总表)
这是本系列最实用的一张表——任何数字,都能在这里找到它的来源。
| 指标 | 怎么测 | 工具 / 命令 | 关键注意点 |
|---|---|---|---|
| FPGA 侧产数速率 | 理论 =4000 ÷ rate_div;实测 = 帧计数增量 × 帧长 ÷ 时间窗 | 读0x3C TX_COUNT | 算实体帧长,别只算载荷 |
| 主机实收吞吐 | 收到的有效载荷字节数÷ 耗时(QueryPerformanceCounter) | tc01/scan | 组帧与拷贝必须移出计时窗,否则测的是 Python 不是 PCIe |
| 带宽利用率 | 吞吐 ÷ 4 000 MB/s | 计算 | 分母唯一 |
| FPGA 逻辑延迟 | ① 响应帧内两个时间戳相减;② ILA 逐拍数 | tc04+ ILA | 两种口径差 7500 倍,必须注明是哪种 |
| 真回环延迟 | ILA 量s_axis_tlast握手沿 →m_axis_tvalid上升沿 | ILA(触发echo_req) | 实测2 拍 = 8 ns |
| 端到端 RTT | 主机QPC:发请求前打 t0,收完响应打 t1;报 P50+P99 | tc04 --mode wait | 一问一答;拷贝在窗外 |
| 错误率 / 丢帧率 | 读FRAMES_OK/CRC_ERR/SEQ_ERR/TX_COUNT | regs | 丢帧率 = 1 − (OK+CRC+SEQ)/TX_COUNT;只在 GEN 模式下有意义 |
| 稳定性 | 分段长跑,每段读寄存器增量,四条判据自动判定 | tc03 | 必须先钉核 |
| CPU 频率(对照量) | CallNtPowerInformation读每个逻辑核当前 MHz(纯 ctypes) | tc03内置 | 全核平均值没有诊断力,必须看线程所在的那个核 |
| 线程落核 | GetCurrentProcessorNumber定时采样 | tc03内置 | 是模态值(最常驻的核),不是逐帧真值 |
8 主机软件栈:驱动 + 为什么 Python 就够
8.1 驱动从哪来
| 项 | 内容 |
|---|---|
| 来源 | Xilinx 官方仓库dma_ip_drivers(内含 Windows 驱动源码 + 示例程序) |
| 编译 | 用WDK编(仓库里有现成的工程文件) |
| 装之前 | 管理员 cmd:bcdedit /set testsigning on→重启(驱动没有正式签名,必须开测试签名模式) |
| 装完认设备 | 设备管理器里出现4 个节点:xdma0_h2c_0/xdma0_c2h_0/xdma0_user/xdma0_control |
| 寄存器读写 | 用仓库自带的xdma_rw.exe,或自己用DeviceIoControl写 |
⚠️设备 ID 必须对上:本设计是
10EE:7028。如果INF里写的是别的 ID,驱动装上也是"部分节点出现"。看到一个叫DEV_7018的设备,那是残留的旧设备记录,不是你的板子——上位机只认DEV_7028。
8.2 为什么 Python + ctypes 就够了
这是很多人的疑问:“性能测试不用 C 能行吗?”
能,而且在这个场景下 C 帮不了你。理由:
- 瓶颈不在语言,在系统调用。架构二实测主机侧只有 133 MB/s——慢在"驱动 + 操作系统 + 内核往返"这一段,不在你的循环里。用 C 写同样的逻辑,还是走同一套
ReadFile,一分钱省不出来。 - 数据直接从设备读到 Python 的可写缓冲区。
ctypes让 Python 能拿到一块原生内存,ReadFile直接往里写,零中间拷贝。这一步用 C 也是这样。 - 解析热点可以用 numpy。本项目里
memoryview+numpy视图把解析开销压到可忽略(实测比最初实现快了几十倍),解析根本不是瓶颈。
🔴唯一必须遵守的纪律:计时窗里只准有设备 I/O。组帧、算校验、打印、拷贝数据——全部移到窗外。否则你测到的是 Python 的解释器速度,而它会伪装成"PCIe 很慢"。这条纪律在篇 4 会被反复引用。
9 Windows 环境隔离清单
Windows 会主动干扰你的测量。开测前逐项确认(本系列全部测试都在这个状态下跑的):
| 项 | 设置 | 为什么 |
|---|---|---|
| PCIe ASPM | 关闭(电源选项 → 高级 → PCI Express → 链接状态电源管理 → 关闭) | 省电模式会拉高延迟、拉低吞吐,而且是动态的,让结果不可复现 |
| 驱动签名 | bcdedit /set testsigning on(重启生效) | 不装驱动就没得测 |
| Defender 实时扫描 | 关闭(或对测试目录豁免) | 它会插在文件 I/O 路径上 |
| 线程绑核 | 测试程序用SetThreadAffinityMask钉核 | 不钉核 = 测 Windows 调度器 |
| 固定 PCIe 插槽 | 前后用同一个槽 | 换槽 = 换根复合体路径,数据不可比 |
| 后台程序 | 关浏览器 / 关同步盘 / 关更新 | 它们抢 CPU 和内存带宽 |
| 预热 | 每次测前先跑几十帧预热 | 第一次访问总有冷启动开销 |
🔴重要预告:上面这张清单,在第四篇会被证明远远不够。
我们实测发现,同一句命令连跑两次,快慢方向居然相反;最后定位到的原因是CPU 频率(本机 14 个核里 8 个跑 3600 MHz、6 个跑 4200 MHz),而有意思的是——你把电源计划切成"高性能 + 处理器最小状态 100%",反而会把快的那一档彻底消灭。
那一篇就是整张清单的血泪补充版。
10 系列导航
| # | 标题 | 讲什么 | 依赖 |
|---|---|---|---|
| 0 | 本篇:为什么做这个实验——平台、帧格式、口径 | 立规矩 | — |
| 1 | 《PCIe AXI-Stream 架构(一):从零搭一个 XDMA 直通工程——逐模块讲清数据流与控制流》 | 9 个 BD 组件、5 个自写模块、三条通路、寄存器表、bring-up 六步、4 个真坑 | 本篇 |
| 2 | 《PCIe AXI-Stream 架构(二):XDMA 吞吐只有 133 MB/s?一次完整的瓶颈定位》 | 板上 98.6% 线速 vs 主机 3.3%;帧长对齐规则;为什么换 RTL 救不了 | 本篇 §5 §7 |
| 3 | 《PCIe AXI-Stream 架构(三):直通延迟到底是 8 ns 还是 60 µs?》 | 8 ns 与 54 µs 的关系;ILA 逐拍证据;RTT 由什么组成 | 本篇 §7.3 |
| 4 | 《PCIe AXI-Stream 架构(四):你测到的可能不是 PCIe,是 Windows——主机侧测量的四个陷阱》 | 同命令两跑结果相反的原因;两档位因果归因;长跑稳定性 | 本篇 §9 |
建议读法:
- 只想学怎么搭 XDMA 直通工程→ 直接读篇 1(它自洽)。
- 只关心性能为什么上不去→ 读篇 2 + 篇 4(一个讲"是什么",一个讲"别把测量假象当结论")。
- 想学怎么量硬件延迟→ 读篇 3。
附:本篇用到的图与复现方式
| 文件 | 内容 |
|---|---|
fig/csdn_p0_arch.png | 四种架构对比(数据路径 / 代价 / 适用场景) |
fig/csdn_p0_system.png | 平台基线(主机软件栈 · PCIe 链路 · FPGA 用户逻辑) |
fig/csdn_p0_budget.png | 理论天花板算式(4000 MB/s 怎么来的、被什么吃掉) |
fig/csdn_p1_frame_format.png | 统一假数据帧格式(字节偏移 + 拍内字段序) |
fig/实物图.jpg | 板卡实物(金手指丝印PCIe 2.0 X8) |
所有结构图都由脚本生成(tools/make_csdn_figures.py,matplotlib),改参数可重出——读者可照着复现,不必相信我截的图。
下一篇
规矩立完了。篇 1开始干活:从空目录搭一个能跑通的 XDMA AXI-Stream 直通工程——9 个 BD 组件一个都不留白,三条数据通路逐拍讲清,控制信号"谁接谁、谁故意不接谁"全部列出来,最后附上我真实踩过的 4 个坑。