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拓扑,你感受一下:

Root Complex (RC) Switch 1 Endpoint 1 (GPU) Endpoint 2 (FPGA) Endpoint 3 (NVMe) Switch 2 (可选) 图例: Root Complex Switch Endpoint FPGA加速卡 注:虚线表示可选链路,实际拓扑中每个Endpoint只能有一个上游端口。

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时有个经典流程:

  1. CPU写0xFFFFFFFF到BAR寄存器。
  2. 设备返回需要的地址空间大小(比如返回0xFF000000,表示需要16MB)。
  3. CPU分配物理地址,写回BAR寄存器。
  4. 设备使能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