4. PCIe基础回顾:PCIe拓扑结构、事务层(TLP)格式、配置空间与BAR空间
各位同学,今天我们聊聊PCIe。说实话,做FPGA加速卡,PCIe是绕不开的坎。我见过不少新手,上来就撸RTL代码,结果板子调不通,最后发现是配置空间没配对。嗯,这节课我们就从根上把PCIe的底裤扒干净。
4.1 PCIe拓扑结构:树形还是网状?
PCIe的拓扑结构,说白了就是一棵树。你想想看,CPU是树根(Root Complex),下面挂着各种Switch和Endpoint。我刚开始接触时总觉得这结构太死板,后来做项目才发现,这种树形结构天生就是为了确定性延迟设计的。
一个典型的PCIe系统包含以下角色:
- Root Complex (RC):CPU侧的根节点,负责管理整个PCIe域。我习惯把它看作“交通警察”。
- Switch:相当于路由器,把一条上游链路拆成多条下游链路。注意,Switch内部其实是由多个虚拟PCI-to-PCI桥组成的。
- Endpoint (EP):我们的FPGA加速卡就属于这个角色。它只能作为事务的发起者或目标,不能转发事务。
这里有个坑:PCIe不允许环形拓扑。我曾经在项目中试图用两个Switch做冗余,结果发现协议根本不允许——每个设备只能有一个上游端口。
关键点:PCIe拓扑是严格的树形结构,任何环路都会导致配置空间枚举失败。
下面这张图是我手绘的典型PCIe拓扑,你感受一下:
4.2 事务层(TLP)格式:数据怎么打包?
PCIe通信的最小单位是TLP(Transaction Layer Packet)。说白了,就是你要发数据,得先把它装进一个信封里。这个信封有固定的格式,乱写的话对端根本看不懂。
一个标准的TLP包含以下字段:
| 字段 | 宽度 | 说明 |
|---|---|---|
| Fmt + Type | 7 bits | 指定TLP类型(Memory Read/Write、Completion等) |
| TC | 3 bits | 流量类别,用于QoS |
| TD | 1 bit | 是否包含CRC(ECRC) |
| EP | 1 bit | 毒包标记(Poisoned Packet) |
| Attr | 2 bits | 属性(如是否开启Relaxed Ordering) |
| Length | 10 bits | 数据负载长度(单位:DWORD) |
| Requester ID | 16 bits | 发起者Bus/Device/Function编号 |
| Tag | 8 bits | 事务标签,用于匹配请求和完成 |
| Address/Data | 可变 | 目标地址或数据负载 |
我的经验:调试TLP时,我最喜欢看的是Fmt+Type和Length字段。有一次板卡死活不响应读请求,最后发现是Length字段填了0——协议规定读请求的Length必须≥1。这种低级错误,查了我整整两天。
常见的TLP类型有:
- Memory Read/Write (MRd/MWr):最常用,用于访问BAR空间或DMA传输。
- Completion (Cpl/CplD):读请求的响应,带数据或不带数据。
- Configuration Read/Write (CfgRd/CfgWr):枚举阶段使用,配置设备寄存器。
- Message (Msg):用于中断(MSI/MSI-X)、错误报告等。
举个实际例子,FPGA做DMA写时,TLP大概长这样:
// 一个典型的Memory Write TLP(64位地址,4DW数据)
Byte 0: 0x40 // Fmt=2 (4DW header), Type=0 (MWr)
Byte 1: 0x00 // TC=0, Attr=0
Byte 2-3: 0x0004 // Length=4 DWORDs
Byte 4-5: 0x0010 // Requester ID (Bus=0, Dev=1, Func=0)
Byte 6: 0x01 // Tag=1
Byte 7: 0x00 // Last DW BE=0xF, First DW BE=0xF
Byte 8-15: 0x00000000_80001000 // 目标地址 (64-bit)
Byte 16-31: // 4 DW数据负载
注意:TLP的字节序是Little-Endian。我见过有人把地址高低字节搞反,结果数据写到了莫名其妙的地方。嗯,血的教训。
4.3 配置空间与BAR空间:设备怎么被发现的?
PCIe设备上电后,CPU怎么知道你的FPGA卡在哪里?答案就是配置空间。每个PCIe设备都有256字节的配置空间(Type 0),其中前64字节是标准头,后面是设备相关的能力结构。
配置空间的关键字段:
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0x00 | Vendor ID / Device ID | 厂商和设备标识(FPGA常用0x10EE for Xilinx) |
| 0x04 | Command / Status | 控制I/O、Memory、Bus Master等使能 |
| 0x08 | Revision ID / Class Code | 设备类别(加速卡通常为0x058000) |
| 0x10-0x24 | BAR0-BAR5 | 基地址寄存器,共6个,每个32位或64位 |
| 0x34 | Capabilities Pointer | 指向能力链表(如MSI、Power Management) |
BAR空间是重点。说白了,BAR就是你的FPGA卡暴露给CPU的“窗口”。CPU通过读写BAR地址,就能访问FPGA内部的寄存器或DMA缓冲区。
BAR的类型有两种:
- Memory BAR:映射到CPU的物理地址空间,支持字节、字、双字访问。我99%的项目都用这个。
- I/O BAR:古老的x86 I/O空间,现在基本不用了。你想想看,谁还用IN/OUT指令啊?
配置BAR时有个经典流程:
- CPU写0xFFFFFFFF到BAR寄存器。
- 设备返回需要的地址空间大小(比如返回0xFF000000,表示需要16MB)。
- CPU分配物理地址,写回BAR寄存器。
- 设备使能Memory Space位(Command寄存器bit 1)。
避坑指南:我曾经在配置BAR大小时,忘了考虑对齐要求。BAR的基地址必须对齐到其大小。比如16MB的BAR,基地址必须是16MB的整数倍。否则CPU枚举时会直接跳过你的设备。
最后说说MSI/MSI-X中断。传统PCI用边带信号中断,PCIe改用消息中断。FPGA通过发送一个特殊的Memory Write TLP(目标地址是CPU预先配置的MSI地址)来触发中断。这样做的好处是:
- 不需要额外的中断引脚。
- 支持多个中断向量(MSI-X支持2048个)。
- 中断传递延迟更低。
嗯,PCIe的基础知识就回顾到这里。这些内容虽然看起来枯燥,但做FPGA加速卡时,每一个细节都可能成为你调板的拦路虎。记住:配置空间是设备的身份证,BAR是设备的大门,TLP是门里跑的数据。搞懂这三样,你的加速卡就成功了一半。
我的习惯:每次新板卡回来,我第一件事就是用PCIe Analyzer抓枚举阶段的TLP。看看配置空间是不是按预期返回的,BAR大小对不对。这一步能省下后面80%的调试时间。
公众号:蓝海资料掘金营,微信deep3321