日常调试都会配置 BAR、调试驱动、跑带宽,但深究底层 TLP 报文交互、地址映射原理,多数人都模糊不清。
咱们把PCIe的核心概念串一遍,帮你把碎片知识拼成体系。同时加入高频误区纠正和网上少见的联调暗坑,让你从“能用”进阶到“用好”。
PCIe是什么?——比你想象的简单
PCIe是CPU和外设之间的点对点高速通道。和PCI的共享总线不同,PCIe是串行差分、全双工的。
text
PCI(并行共享总线):
CPU ←→ [共享总线] ←→ [设备1] [设备2] [设备3]
PCIe(串行点对点):
CPU ←→ [Switch] ←→ [设备1] [设备2] [设备3]
每个设备有一条或多条lane。lane越多,带宽越高:
| Lane数 | 单向带宽(Gen3 x1) | 实际可用 |
|---|---|---|
| x1 | 约1GB/s | 网卡、串口 |
| x4 | 约4GB/s | NVMe SSD |
| x8 | 约8GB/s | 数据采集、加速卡 |
| x16 | 约16GB/s | GPU |
❗ 误区纠正:很多工程师把H2C和C2H的方向搞反。
H2C = Host → Card(PC到FPGA),C2H = Card → Host(FPGA到PC)。FPGA采集系统上传数据,走的是C2H通道。后文统一沿用此定义。
TLP——PCIe传输的基本单元
TLP(Transaction Layer Packet)是PCIe通信的基本单元。每次读写,本质上都是一次TLP的发送和接收。
四种核心TLP
| TLP类型 | 全称 | 方向 | 用途 |
|---|---|---|---|
| Memory Read | 内存读请求 | H2C | PC读取FPGA的BAR空间 |
| Memory Write | 内存写请求 | H2C | PC写入FPGA的BAR空间 |
| Completion Data | 读请求的响应数据 | C2H | 携带数据返回 |
| Completion Status | 操作完成状态 | C2H | 无数据的响应(如错误) |
text
Memory Read(H2C):
PC ──[MRd TLP]──→ FPGA
PC ←──[CplD TLP]─── FPGA (FPGA返回数据)
Memory Write(H2C):
PC ──[MWr TLP + 数据]──→ FPGA (无需响应)
FPGA主动发送(DMA C2H):
FPGA ──[MWr TLP + 数据]──→ PC内存 (零拷贝上传)
❗ 误区纠正:很多人以为Memory Write也必须等待响应。错误。Memory Write是Posted请求,发送后无需Completion,DMA就是利用这个特性实现高带宽上传。
TLP的结构
每个TLP由三部分组成:
text

Header是核心,包含:
-
Fmt:读还是写,是否有数据,Header长度
-
Type:Memory、IO、Config还是Message
-
Requester ID:谁发的(总线:设备.功能号)
-
Tag:用来匹配请求和响应
-
Address:目标地址(PC内存地址或BAR空间地址)
-
Length:以DW(双字 = 4字节)为单位的数据长度。PCIe协议内长度统计默认以DW为基础单位,便于理解计数逻辑。
具体编码值参考PCIe规范,工程中由IP核自动生成,无需手动构造。
关键理解:PC读取FPGA BAR空间时,先发一个MRd TLP,FPGA收到后发回CplD TLP。这两次TLP往返的延迟是PCIe读操作慢的根本原因。DMA绕过了这个延迟——因为DMA是FPGA主动写PC内存,只有单向TLP。
BAR空间——地址映射的本质
BAR(Base Address Register)是FPGA在PC内存空间中占的一块地址区间。驱动通过访问这个地址来读写FPGA的寄存器。
BAR空间是怎么建立起来的?
这个过程叫BAR空间枚举,发生在系统启动时:
-
系统BIOS扫描PCIe总线,发现FPGA设备,读取其BAR配置寄存器(初始全1)
-
BIOS写入全1值,读取返回的“可用的地址位数”,然后分配一个合适的物理地址并写入BAR
-
驱动读取BAR寄存器的值,得到FPGA寄存器的基地址(物理地址)
-
驱动将基地址加上内部偏移(如
0x100),访问具体寄存器
驱动代码示例:
c
#define FPGA_BASE_ADDR 0xA0000000 // BAR0映射的物理地址
#define REG_CTRL 0x100 // FPGA内部寄存器偏移
// 访问FPGA的控制寄存器
uint32_t val = *(volatile uint32_t *)(FPGA_BASE_ADDR + REG_CTRL);
FPGA端:BAR空间的实现
在Xilinx PCIe IP核里,BAR空间通过AXI接口映射:
verilog
// PCIe IP核生成的AXI从接口
// IP核把PC发来的BAR地址翻译成AXI读写
// PC访问BAR0偏移0x100,实际映射到AXI从设备地址0x100
// AXI从设备地址映射(通过Address Editor配置)
// BAR0 64KB → AXI 0x00000 ~ 0x0FFFF
// 偏移0x000 ~ 0x0FF : 控制寄存器
// 偏移0x100 ~ 0x1FF : 状态寄存器
// 偏移0x200 ~ 0x2FF : DMA描述符表
// 偏移0x300 ~ 0x3FF : 中断控制寄存器
BAR的三种类型
| BAR类型 | 大小 | 用途 |
|---|---|---|
| Memory BAR(32位) | 最大2GB | 寄存器、配置空间 |
| Memory BAR(64位) | 超过2GB | 大容量缓冲区 |
| IO BAR | 仅32个地址 | 遗留兼容,基本不用 |
FPGA一般用64KB的Memory BAR就够了。PCIe地址空间理论最大64TB(2^46),64KB是沧海一粟。
❗ 误区纠正:Prefetchable属性不能乱用
可预取(Prefetchable):适合批量连续读取的内存区域(如DMA缓冲区)。PC可以提前发起预读,不影响正确性。
不可预取(Non-Prefetchable):寄存器类随机访问空间绝对禁止开启预取,否则读操作会破坏寄存器状态(比如读清中断位会被预读误触发)。
Xilinx PCIe IP核快速配置
Vivado里配置PCIe IP核,这几个参数最容易配错。
关键配置项
1. Link Speed 和 Lane Width
text
Speed: Gen3 (8GT/s) ← 最高,兼容性好
Lane Width: x8 ← 根据硬件走线选择,采集卡一般x8
2. BAR Settings
text
BAR0: Enabled, 64KB, Memory, Non-Prefetchable
→ 映射控制/状态寄存器
BAR2: Enabled, 1MB, Memory, Prefetchable
→ 映射DMA描述符缓冲区(必须是Prefetchable)
⚠️ BAR2必须设为Prefetchable(可预取),否则驱动用mmap映射时会失败。可预取空间适合批量连续读取数据,寄存器类随机访问空间不可开启预取属性。
3. Device ID / Vendor ID
text
Vendor ID: 0x10EE ← Xilinx原厂固定ID,不可修改
Device ID: 0xFFFF ← 自定义,每个项目不同,用于区分设备
⚠️ 注意:Vendor ID 是Xilinx官方分配的唯一标识,不可改动。仅Device ID可自定义,以避免与其它设备ID冲突导致系统无法正确识别。
4. MSI / MSI-X
text
MSI-X: Enabled, Table Size = 16 ← 推荐用MSI-X
MSI: Disabled
MSI-X比MSI好在哪?MSI最多32个中断向量,MSI-X支持2048个,且每个向量可绑定不同地址——这对多通道DMA很重要。
一个调试场景:PC认不到PCIe设备
这是最常见的开局问题。
排查清单:
硬件层面
-
□ 插槽金手指是否氧化?换个插槽试试
-
□
lspci -vv查看Link Speed和Lane Width是否达标 -
□ 参考时钟是否正确?(PCIe要求100MHz差分,抖动<1ps RMS)
IP核层面
-
□ Vendor/Device ID是否与驱动匹配?(Vendor ID必须为0x10EE)
-
□ BAR是否成功分配?(
dmesg | grep -i pcie查看BAR分配日志) -
□ Reference Clock是否接对?
驱动层面
-
□ Vendor/Device ID是否匹配驱动里的值?
-
□ 内核模块签名问题?(SECURE_BOOT环境下需要签名)
bash
# 查看PCIe设备枚举信息
lspci -vv -d 10ee:
# -vv 显示详细信息,-d 10ee 只显示Vendor ID为10ee的设备
# 如果设备不在列表里 → 硬件或IP核配置问题
# 查看BAR地址分配
lspci -vv -d 10ee: -xxx
# -xxx 显示原始配置空间(看BAR寄存器的原始值)
网上少见的联调暗坑(实战经验)
通用教程只会讲正常流程,下面几个坑是你真正调试时会遇到的。
暗坑1:H2C和C2H的TLP互相抢占带宽
当PC同时读取FPGA状态寄存器(H2C MRd)和FPGA主动上传数据(C2H MWr)时,两者共享同一链路。如果寄存器访问频繁,会抢占DMA上传的带宽,导致C2H实测吞吐骤降。
解决:将寄存器访问和DMA数据分到不同Virtual Channel,或在FPGA内部对C2H流量赋予更高优先级。多通道采集卡等高带宽场景,优先划分独立虚拟通道保障上传业务优先级。
暗坑2:BAR地址重分配后驱动偏移失效
很多驱动代码里硬编码了BAR的物理地址(比如从lspci抄下来的0xA0000000)。但系统重启、换插槽、BIOS升级后,BAR地址可能重新分配。驱动还访问旧地址,轻则读写失败,重则内核崩溃。
正确做法:驱动必须使用pci_resource_start(pdev, bar_num)动态获取BAR地址,绝不可硬编码。
暗坑3:参考时钟抖动超标,能枚举但带宽跑不满
lspci显示链路为Gen3 x8,实际测带宽只有理论值的60%。常见原因是100MHz参考时钟抖动超差(>1ps RMS),导致PCIe物理层频繁重传。
解决:用示波器测量参考时钟的眼图和抖动,确保<1ps RMS。PCIe Gen3对时钟要求很高,普通晶振可能不合格,需要专用时钟芯片。
暗坑4:预取BAR空间随机读会篡改DMA描述符
如果DMA描述符表放在Prefetchable BAR中,而PC软件不小心对该区域执行了预读操作(比如调试器扫描内存),预取机制会随机读取描述符内容,破坏描述符链,导致DMA传输出错或卡死。
解决:DMA描述符表放在Non-Prefetchable空间,或者确保软件不会对该区域做随机读访问。
自检表
-
□ Link Speed = Gen3,Lane Width = x8(根据硬件)
-
□ BAR0 = 64KB Non-Prefetchable(寄存器空间)
-
□ BAR2 = 1MB Prefetchable(DMA描述符缓冲区)
-
□ Vendor ID = 0x10EE(固定),Device ID自定义且不与已有设备冲突
-
□ MSI-X Enabled,Table Size ≥ 16
-
□ 参考时钟 = 100MHz差分,抖动 < 1ps RMS
-
□ 已用
lspci -vv确认设备枚举和BAR分配
核心观点总结
协议层TLP决定数据交互规则,IP核BAR决定硬件寻址边界。二者配合,才能实现真正的高速传输。
普通配置只能让设备“认得到”,吃透报文与地址映射,才能解决丢包、带宽不足、中断异常等疑难问题。
记住:
-
Memory Write无需响应 → DMA零拷贝的基石
-
Prefetchable属性不能乱用 → 寄存器空间绝对不可预取
-
Vendor ID不可改 → 改了设备消失
-
参考时钟抖动直接影响有效带宽 → 示波器验证
最后
PCIe是FPGA和PC之间最快的数据通道,也是驱动工程师和逻辑工程师最常吵架的地方。
理解TLP和BAR,你就掌握了PCIe的核心。
下一期我们聊XDMA——SG DMA是怎么工作的,描述符表怎么配置,H2C和C2H通道怎么配合实现零拷贝上传。

362

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



