1. 为什么XDMA是7系列FPGA上PCIe开发的首选方案
做过Xilinx 7系列FPGA PCIe开发的人,大概率都经历过从零手写PCIe硬核逻辑的痛苦。7系列FPGA内部集成的PCIe硬核(Integrated Block for PCIe)本身只处理物理层和数据链路层的事务,TLP包的解析、BAR空间的响应、DMA引擎的调度、中断的生成,这些统统需要自己在用户逻辑里实现。一个能跑通的基础PCIe端点设计,光TLP解码状态机就够写上千行RTL,更别提DMA的scatter-gather描述符管理和MSI/MSI-X中断处理了。
XDMA(DMA Subsystem for PCI Express)就是Xilinx官方给出的答案。它把PCIe硬核、AXI总线接口、DMA引擎、BAR地址译码、中断控制器全部封装成一个IP核,用户只需要在Vivado里配置几个参数,就能得到一个功能完整的PCIe端点。主机侧配合官方的XDMA驱动,直接就能用/dev/xdma0_h2c_0这样的字符设备做DMA读写,或者通过/dev/xdma0_user做寄存器级的AXI-Lite访问。
这个方案解决的核心问题是:让FPGA开发者把精力放在业务逻辑上,而不是PCIe协议栈的底层实现上。特别适合以下几类人:需要快速搭建PCIe数据采集卡原型的工程师、做算法加速卡验证的团队、以及刚接触PCIe协议想先跑通链路再深入细节的学习者。
我这次的项目背景是一块自制的Kintex-7 325T板卡,需要实现主机通过PCIe对FPGA内部寄存器进行配置,同时FPGA能把采集数据通过DMA高速上传到主机内存。整个链路涉及BAR空间的映射规划、AXI-Lite主机接口的配置、DMA通道的建立,以及驱动加载后的实际调试。下面把整个流程和踩过的坑完整梳理一遍。
2. 整体架构设计与BAR空间规划思路
2.1 XDMA IP的核心接口梳理
XDMA IP对外暴露的接口可以分成三大类,理解这三类接口是配置的基础。
第一类是PCIe物理侧接口,包括pci_exp_txp/txn差分发送、pci_exp_rxp/rxn差分接收、sys_clk参考时钟、sys_rst_n复位。这部分直接连到FPGA的PCIe专用管脚或者通过IBUFDS转接,7系列FPGA的PCIe硬核位置是固定的,在Vivado里选好PCIe Block Location就行。
第二类是AXI主机接口(AXI Master),XDMA作为主机去访问FPGA内部的从设备。这里有两个子接口:M_AXI用于DMA数据传输,位宽可以配64/128/256/512位;M_AXI_LITE用于主机通过BAR访问FPGA寄存器,固定32位位宽。这两个接口是XDMA主动发起的,FPGA内部需要挂对应的AXI从设备来响应。
第三类是AXI从机接口(AXI Slave),FPGA内部逻辑作为主机去访问主机内存或者配置空间。S_AXI用于FPGA主动发DMA写请求到主机内存,S_AXI_LITE用于访问XDMA内部的配置寄存器(比如DMA控制、中断使能等)。
注意:很多人第一次用XDMA会搞混
M_AXI_LITE和S_AXI_LITE的方向。记住一个原则——带M_前缀的是XDMA做主机发起的,带S_前缀的是XDMA做从机被动响应的。主机通过BAR读写FPGA寄存器走的是M_AXI_LITE,FPGA读XDMA内部状态走的是S_AXI_LITE。
2.2 BAR空间的分配策略
BAR(Base Address Register)是PCIe端点向主机声明自己需要多少地址空间的机制。XDMA IP支持最多6个BAR,但实际常用的配置是:
| BAR编号 | 用途 | 典型大小 | 映射目标 |
|---|---|---|---|
| BAR0 | XDMA内部配置寄存器 | 64KB | XDMA内部 |
| BAR1 | 用户AXI-Lite寄存器 | 1MB | M_AXI_LITE |
| BAR2 | DMA描述符/控制 | 64KB | XDMA内部 |
| BAR3-BAR5 | 可选,一般不用 | - | - |
BAR0是XDMA自己用的,里面包含了DMA引擎的控制寄存器、中断相关寄存器、以及一些状态寄存器。主机侧驱动通过BAR0来启动DMA传输、读取传输状态。BAR1才是留给用户自定义寄存器的空间,主机通过读写BAR1的偏移地址,XDMA会把这些访问转换成AXI-Lite事务发到M_AXI_LITE接口上。
BAR的大小选择有个经验法则:够用就好,不要贪大。BAR空间太大会导致主机BIOS在枚举阶段分配地址时可能失败,尤其是在系统PCIe资源紧张的情况下。我一般BAR1给1MB,对于大多数寄存器配置场景绰绰有余。如果确实需要更大的寄存器空间,可以考虑用BAR1做窗口,通过地址寄存器切换来访问更大的内部空间。
2.3 AXI-Lite主机接口的地址映射
M_AXI_LITE接口的地址映射逻辑是这样的:主机访问BAR1的偏移地址0x0000_0000到0x000F_FFFF,XDMA会把这些访问原封不动地转发到M_AXI_LITE的AXI地址通道上。也就是说,主机读BAR1+0x100,M_AXI_LITE上就会出现一个地址为0x100的读事务。
这里有个细节需要注意:XDMA的M_AXI_LITE地址位宽默认是32位,但实际有效的地址范围由BAR1的大小决定。如果BAR1配了1MB,那么只有地址的低20位是有效的,高位会被忽略。在FPGA侧做AXI-Lite从设备地址译码时,只需要译码低20位就够了。
我在实际项目中的地址分配是这样的:
0x0000_0000-0x0000_00FF:全局控制寄存器(使能、复位、状态)0x0000_0100-0x0000_01FF:采集通道0的配置寄存器0x0000_0200-0x0000_02FF:采集通道1的配置寄存器0x0000_1000-0x0000_1FFF:DMA控制寄存器组0x0000_2000-0x0000_2FFF:中断相关寄存器
这种分页式的地址规划让后续的寄存器管理清晰很多,驱动侧也好维护。
3. Vivado工程配置与IP核参数详解
3.1 PCIe硬核的基础配置
在Vivado里添加XDMA IP之前,先要确认PCIe硬核的配置。7系列FPGA的PCIe硬核配置在IP核的"Basic"标签页里:
PCIe Block Location:这个必须根据板卡原理图上PCIe接口连接到的GT Bank来选。Kintex-7 325T有三个PCIe硬核,分别在Bank 115、Bank 116、Bank 117。选错了综合能过但上板链路训练不起来。
Lane Width:根据实际硬件选x1、x2、x4、x8。我这次用的是x4,理论带宽约2GB/s(Gen2)或4GB/s(Gen3)。注意7系列FPGA的PCIe硬核最高支持Gen3 x8,但Gen3需要额外的均衡配置,调试复杂度会高不少。
Maximum Link Speed:Gen1(2.5GT/s)、Gen2(5GT/s)、Gen3(8GT/s)。建议先跑Gen1确保链路能通,再逐步往上调。我见过太多一上来就配Gen3结果链路训练失败的案例。
Reference Clock Frequency:100MHz或125MHz,取决于板卡上给PCIe提供的参考时钟。这个必须和硬件一致,否则链路训练直接失败。
3.2 XDMA IP的关键参数
XDMA IP的配置界面有好几个标签页,重点说几个容易出错的参数。
PCIe ID标签页:
- Vendor ID:默认是Xilinx的0x10EE,如果产品需要自定义可以改
- Device ID:自定义设备ID,驱动侧会根据这个ID来匹配
- Subsystem Vendor ID / Subsystem ID:子系统ID,一般和Vendor/Device保持一致即可
- Class Code:一般选"Memory Controller"或者"Other",这个影响主机BIOS对设备的识别
PCIe:BARs标签页:
- BAR0:勾选"PCIe to AXI Lite Master Interface",大小选64KB
- BAR1:勾选"PCIe to AXI Lite Master Interface",大小选1MB
- BAR2:勾选"PCIe to DMA Bypass Interface",大小选64KB
这里有个坑:BAR0和BAR1都映射到AXI-Lite接口,但BAR0是XDMA内部使用的,BAR1才是给用户的。在Vivado的Address Editor里,BAR0对应的地址范围会自动分配给XDMA内部寄存器,BAR1对应的地址范围需要手动分配给用户的AXI-Lite从设备。
PCIe:DMA标签页:
- Number of DMA Read Channels (H2C):主机到FPGA的DMA通道数,一般1-2个够用
- Number of DMA Write Channels (C2H):FPGA到主机的DMA通道数,数据采集场景一般需要2-4个
- DMA Interface:选AXI4或AXI4-Stream。AXI4适合有地址的数据搬运,AXI4-Stream适合纯数据流
- Descriptor Bypass:如果不需要scatter-gather,可以勾选这个简化设计
AXI-Lite标签页:
- AXI-Lite Interface Frequency:一般和PCIe用户时钟一致,250MHz或125MHz
- AXI-Lite Interface Protocol:AXI4-Lite
3.3 时钟与复位架构
XDMA IP会输出几个关键时钟和复位信号:
axi_aclk:AXI接口时钟,频率由配置决定,一般是250MHz(Gen2 x4)或125MHzaxi_aresetn:AXI接口复位,低有效user_lnk_up:PCIe链路建立完成指示,高有效sys_clk:参考时钟输入,100MHz或125MHz
这里有个非常重要的设计原则:所有AXI-Lite从设备的时钟和复位必须和XDMA输出的axi_aclk、axi_aresetn同步。我见过有人用板卡上的独立晶振给用户逻辑供时钟,结果AXI事务跨时钟域处理不当导致数据错乱。最稳妥的做法就是统一用XDMA输出的时钟。
复位方面,axi_aresetn在链路建立后才会释放,所以用户逻辑的复位应该以这个信号为准。如果需要额外的软复位,可以在AXI-Lite寄存器里做一个复位控制位,但要注意复位释放的同步处理。
4. AXI-Lite从设备的RTL实现要点
4.1 一个可复用的AXI-Lite从设备模板
主机通过BAR1读写寄存器,最终落到FPGA侧就是一个AXI-Lite从设备。这个从设备的RTL实现虽然不复杂,但有几个细节处理不好就会导致主机读写失败。
先看核心的握手逻辑。AXI-Lite的写通道有五个信号:awvalid/awready、wvalid/wready、bvalid/bready。读通道有三个:arvalid/arready、rvalid/rready。
一个常见的错误是awready和wready同时拉高但bvalid没有正确产生。正确的做法是:当地址和数据都握手完成后,执行写操作,然后拉高bvalid一个周期,等待bready握手后拉低。
// 写通道状态机 always @(posedge clk) begin if (!rst_n) begin awready <= 1'b0; wready <= 1'b0; bvalid <= 1'b0; end else begin // 地址握手 if (awvalid && !awready && !bvalid) begin awready <= 1'b1; wr_addr <= awaddr; end else begin awready <= 1'b0; end // 数据握手 if (wvalid && !wready && !bvalid) begin wready <= 1'b1; wr_data <= wdata; end else begin wready <= 1'b0; end // 写响应 if ((awvalid && awready || aw_hs_done) && (wvalid && wready || w_hs_done) && !bvalid) begin bvalid <= 1'b1; end else if (bvalid && bready) begin bvalid <= 1'b0; end end end这段代码看起来简单,但实际调试时最容易出问题的是awready和wready的时序关系。AXI-Lite协议允许地址和数据以任意顺序到达,所以状态机必须能处理"地址先到"、"数据先到"、"同时到达"三种情况。上面这个简化版本假设地址和数据几乎同时到达,对于XDMA的M_AXI_LITE接口来说这个假设是成立的,因为XDMA内部会把BAR访问转换成地址和数据同时有效的AXI事务。
4.2 寄存器读写与地址译码
地址译码的逻辑很直接:根据awaddr的低位来判断操作哪个寄存器。我一般用awaddr[11:0]来译码,支持4KB的寄存器空间,对于大多数应用足够了。
// 寄存器写译码 always @(posedge clk) begin if (!rst_n) begin ctrl_reg <= 32'h0; status_reg <= 32'h0; // ... 其他寄存器复位 end else if (wr_en) begin case (wr_addr[11:0]) 12'h000: ctrl_reg <= wr_data; 12'h004: threshold <= wr_data; 12'h008: div_factor <= wr_data; // ... 其他寄存器 default: ; // 未定义的地址忽略 endcase end end // 寄存器读译码 always @(*) begin case (rd_addr[11:0]) 12'h000: rd_data = ctrl_reg; 12'h004: rd_data = threshold; 12'h008: rd_data = div_factor; 12'h00C: rd_data = status_reg; // ... 其他寄存器 default: rd_data = 32'hDEADBEEF; endcase end提示:未定义地址的读返回值建议给一个固定的魔术字(比如0xDEADBEEF),这样在驱动调试时如果读到这个值,就能立刻判断出是地址译码出了问题,而不是数据本身的问题。
4.3 跨时钟域与读写冲突处理
如果用户逻辑的工作时钟和axi_aclk不同频,寄存器读写就需要做跨时钟域处理。最简单的做法是用双触发器同步器处理单比特信号,用异步FIFO处理多比特数据。但更推荐的做法是尽量让用户逻辑也跑在axi_aclk上,避免不必要的跨时钟域。
读写冲突是另一个需要注意的点。如果主机在写某个寄存器的同时,FPGA内部逻辑也在更新这个寄存器的某些位,就会出现竞争。常见的处理方式有两种:一是读写分离,主机写配置寄存器,FPGA只读;FPGA写状态寄存器,主机只读。二是用写使能脉冲,主机写入的值只在写使能有效的那个周期生效,之后FPGA内部逻辑可以自由更新。
我一般采用第一种方式,把寄存器的读写权限分得很清楚,避免出现双向访问的寄存器。如果确实需要双向访问(比如一个控制寄存器主机可读可写,FPGA内部也会修改某些位),那就用"写优先"或者"读优先"的策略,并在文档里明确说明。
5. 主机侧驱动加载与设备识别
5.1 XDMA驱动的编译与安装
Xilinx官方提供了XDMA驱动的源码,在GitHub上可以找到(搜索"Xilinx XDMA driver")。驱动支持Linux和Windows,Linux下编译安装的流程大致是:
# 进入驱动源码目录 cd xdma_driver # 编译驱动 make -C /lib/modules/$(uname -r)/build M=$(pwd) modules # 安装驱动 sudo insmod xdma.ko # 查看设备是否识别 lspci -d 10ee: ls /dev/xdma*如果lspci能看到Xilinx设备,说明PCIe链路和BAR配置都没问题。如果/dev/xdma*设备节点没有出现,那就要检查驱动加载时的日志:
dmesg | grep xdma常见的错误包括:设备ID不匹配(驱动源码里的ID表和实际IP配置不一致)、BAR空间分配失败(主机PCIe资源不足)、中断申请失败(MSI/MSI-X配置问题)。
5.2 BAR空间的用户态访问
驱动加载成功后,/dev/xdma0_user这个字符设备就对应了BAR1的AXI-Lite空间。用户态程序可以通过mmap把这段空间映射到进程地址空间,然后直接读写寄存器:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #define BAR1_SIZE (1024 * 1024) // 1MB int main() { int fd = open("/dev/xdma0_user", O_RDWR | O_SYNC); if (fd < 0) { perror("open xdma0_user failed"); return -1; } void *bar1 = mmap(NULL, BAR1_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (bar1 == MAP_FAILED) { perror("mmap failed"); close(fd); return -1; } // 写控制寄存器(偏移0x000) volatile unsigned int *ctrl = (unsigned int *)((char *)bar1 + 0x000); *ctrl = 0x00000001; // 使能采集 // 读状态寄存器(偏移0x00C) volatile unsigned int *status = (unsigned int *)((char *)bar1 + 0x00C); printf("Status: 0x%08X\n", *status); // 写通道0阈值(偏移0x100) volatile unsigned int *ch0_thresh = (unsigned int *)((char *)bar1 + 0x100); *ch0_thresh = 0x00001000; munmap(bar1, BAR1_SIZE); close(fd); return 0; }这段代码里有个关键点:mmap的时候一定要加O_SYNC标志,否则写操作可能会被CPU缓存,导致寄存器写入没有真正到达FPGA。另外,指针要加volatile修饰,防止编译器优化掉看似"多余"的读写操作。
5.3 DMA通道的建立与测试
DMA通道的测试可以用Xilinx提供的xdma_test工具,也可以自己写程序。H2C(主机到FPGA)的DMA写操作流程是:
// 打开H2C通道 int fd_h2c = open("/dev/xdma0_h2c_0", O_RDWR); // 准备数据 char *buf = malloc(4096); memset(buf, 0xAA, 4096); // 写入FPGA(地址0x0是FPGA侧的DDR或BRAM地址) ssize_t written = write(fd_h2c, buf, 4096); printf("Written %zd bytes\n", written); close(fd_h2c);C2H(FPGA到主机)的DMA读操作类似:
int fd_c2h = open("/dev/xdma0_c2h_0", O_RDWR); char *buf = malloc(4096); ssize_t read_bytes = read(fd_c2h, buf, 4096); printf("Read %zd bytes\n", read_bytes); // 验证数据 for (int i = 0; i < 16; i++) { printf("%02X ", (unsigned char)buf[i]); } printf("\n"); close(fd_c2h);DMA测试时最容易遇到的问题就是传输卡住不返回。这时候先检查FPGA侧的M_AXI接口是否正常响应,可以用ILA抓一下M_AXI的握手信号。如果M_AXI的rready或bready一直不拉高,说明FPGA侧的AXI从设备没有正确响应。
6. 调试实战中的典型问题与排查方法
6.1 链路训练失败:从LTSSM状态机入手
PCIe链路训练失败是最常见也最让人头疼的问题。现象是user_lnk_up一直为低,lspci看不到设备。排查思路是抓LTSSM状态机的状态输出。
7系列PCIe硬核有一个ltssm_state输出信号,通过ILA抓这个信号的编码值就能知道链路训练卡在哪个阶段。常见的状态编码:
| LTSSM状态 | 编码值 | 含义 | 常见问题 |
|---|---|---|---|
| Detect.Quiet | 0x00 | 检测阶段 | 参考时钟缺失 |
| Detect.Active | 0x01 | 检测到接收端 | 差分对极性反了 |
| Polling.Active | 0x02 | 轮询阶段 | 参考时钟频偏太大 |
| Configuration | 0x04-0x07 | 配置阶段 | Lane宽度协商失败 |
| L0 | 0x10 | 正常工作 | - |
如果卡在Detect阶段,检查参考时钟是否正常、差分对是否接反。如果卡在Polling阶段,检查参考时钟的频偏是否在±300ppm以内。如果卡在Configuration阶段,检查Lane宽度配置和硬件连接是否一致。
实操心得:我遇到过一次链路训练失败,查了半天发现是参考时钟的晶振焊接不良,导致频偏过大。用示波器测晶振输出,频率偏差了将近1000ppm。换了个晶振就好了。所以硬件问题永远优先于逻辑问题排查。
6.2 BAR空间读写返回0xFFFFFFFF
主机读BAR1返回0xFFFFFFFF,说明PCIe事务没有到达FPGA,或者FPGA没有正确响应。排查步骤:
确认
lspci -vv里BAR1的地址范围是否正确分配。如果BAR1显示为Region 1: Memory at <unassigned>,说明主机BIOS没有分配地址,需要检查BAR大小是否超过了系统可用资源。用ILA抓
M_AXI_LITE接口的arvalid信号。如果主机读BAR1时arvalid没有拉高,说明XDMA没有把BAR访问转发过来,可能是BAR1的配置有问题。如果
arvalid拉高了但rvalid没回,检查AXI-Lite从设备的状态机是否卡住了。最常见的原因是arready没有正确拉高,导致读地址握手失败。如果
rvalid回了但数据是0xFFFFFFFF,检查读数据译码逻辑。可能是地址译码的default分支返回了这个值。
6.3 DMA传输卡死或数据错乱
DMA传输卡死通常有几个原因:
描述符地址不对:如果用了scatter-gather模式,描述符在主机内存里的地址必须正确配置到XDMA的寄存器里。描述符地址必须是物理地址,不能是虚拟地址。
AXI事务超时:FPGA侧的M_AXI接口如果没有及时响应,XDMA内部有超时机制,超时后会报错。检查M_AXI的rready和bready是否在合理的时间内拉高。
数据位宽不匹配:XDMA的M_AXI数据位宽和FPGA侧AXI从设备的位宽必须一致。如果XDMA配了256位而FPGA侧只支持64位,数据就会错乱。
跨4KB边界:PCIe的TLP包不能跨4KB边界。如果DMA传输的地址范围跨越了4KB边界,XDMA会自动拆分,但如果FPGA侧的AXI从设备不支持非对齐访问,就会出错。
数据错乱的排查可以用ILA抓M_AXI的wdata和wstrb,对比主机侧发送的数据和FPGA侧接收的数据。如果数据整体偏移了,检查地址对齐;如果数据部分错误,检查wstrb信号。
6.4 中断不触发或频繁触发
XDMA支持MSI/MSI-X中断,主机侧驱动通过中断来通知DMA传输完成。中断不触发的常见原因:
- 中断使能位没有配置。XDMA内部有中断使能寄存器,需要通过
S_AXI_LITE接口配置。 - MSI/MSI-X的向量号配置错误。主机侧驱动申请的中断号和FPGA侧配置的不一致。
- FPGA侧的中断请求信号没有正确连接到XDMA的中断输入接口。
中断频繁触发通常是中断清除逻辑有问题。XDMA的中断是电平敏感的,FPGA侧的中断请求信号必须保持到主机侧清除中断后才能拉低。如果中断请求信号是一个脉冲,主机还没来得及响应就消失了,就会导致中断丢失。如果中断请求信号一直拉高不释放,就会导致中断风暴。
7. 性能优化与稳定性加固
7.1 DMA带宽的实测与优化
Gen2 x4的PCIe理论带宽是2GB/s,实际DMA传输能跑到1.5GB/s左右就算不错了。影响DMA带宽的因素有几个:
AXI数据位宽:XDMA的M_AXI数据位宽越大,单次传输的数据量越大,效率越高。256位比64位的带宽能提升30%以上。
描述符开销:scatter-gather模式下,每个描述符都有额外的内存访问开销。如果传输的数据块很小(比如每次只有4KB),描述符开销占比就很高。增大单次传输的数据块大小能显著提升有效带宽。
主机内存的DMA性能:主机侧的内存分配方式会影响DMA性能。用posix_memalign分配对齐的内存,避免跨NUMA节点访问,都能提升DMA带宽。
我实测下来,Gen2 x4、256位AXI位宽、每次传输1MB数据块的情况下,C2H方向能跑到1.6GB/s,H2C方向能跑到1.4GB/s。这个性能对于大多数数据采集和算法加速应用已经足够了。
7.2 长时间运行的稳定性考虑
PCIe链路在长时间运行后可能会出现性能下降甚至断链的情况。几个加固措施:
温度监控:7系列FPGA的XADC可以监控芯片温度,如果温度超过85°C,PCIe硬核可能会降速。在逻辑里加一个温度监控模块,温度过高时通过中断通知主机。
链路状态监控:定期读取XDMA的链路状态寄存器,如果发现链路降速或断链,及时记录日志并尝试重新初始化。
DMA超时处理:在驱动侧给DMA传输加超时机制,如果传输在预期时间内没有完成,就复位DMA通道并重新初始化。
ECC保护:如果FPGA侧有DDR内存,开启ECC能有效防止数据错误。XDMA的AXI接口本身没有ECC,但可以在AXI从设备侧加ECC逻辑。
7.3 驱动侧的异常恢复机制
主机侧驱动需要处理各种异常情况:设备突然消失、DMA传输超时、中断丢失等。一个健壮的驱动应该有完整的错误恢复流程:
// DMA传输超时处理示例 static int xdma_dma_transfer_with_timeout(int fd, void *buf, size_t len) { int ret; struct timeval tv; fd_set rfds; // 设置超时 FD_ZERO(&rfds); FD_SET(fd, &rfds); tv.tv_sec = 5; tv.tv_usec = 0; ret = select(fd + 1, &rfds, NULL, NULL, &tv); if (ret == 0) { // 超时,复位DMA通道 printf("DMA timeout, resetting channel\n"); xdma_reset_channel(fd); return -ETIMEDOUT; } else if (ret < 0) { perror("select failed"); return -errno; } // 正常传输 return read(fd, buf, len); }这个超时机制在实际调试中非常有用,尤其是当FPGA逻辑还在开发阶段、DMA响应不稳定的时候,没有超时机制的话主机程序会直接卡死。
8. 从调试实战中积累的经验教训
整个项目从Vivado配置到驱动调试跑通,前后花了大约两周时间。其中大部分时间不是在写代码,而是在排查各种"意料之外"的问题。有几个教训值得记录。
第一个教训:不要跳过Gen1直接上Gen2/Gen3。我一开始为了追求带宽直接配了Gen2,结果链路训练时好时坏。后来降到Gen1,链路稳定了,再逐步升到Gen2,问题就少了很多。Gen1虽然带宽低,但作为调试起点是最稳妥的。
第二个教训:ILA是PCIe调试的命根子。没有ILA,你根本不知道AXI事务有没有到达FPGA、握手有没有完成、数据对不对。我建议在M_AXI_LITE和M_AXI接口上都挂ILA,触发条件设成arvalid或awvalid的上升沿,这样每次主机访问都能抓到波形。
第三个教训:驱动日志比逻辑波形更直接。很多时候问题出在驱动侧而不是FPGA侧。dmesg里的驱动日志能直接告诉你BAR分配是否成功、中断是否申请到、DMA描述符是否有效。先看驱动日志,再看ILA波形,排查效率会高很多。
第四个教训:寄存器地址一定要做文档。项目后期寄存器越来越多,如果没有一个完整的地址映射表,改一处忘一处,很容易出现地址冲突。我后来用Excel维护了一个寄存器映射表,包括偏移地址、位域定义、读写权限、复位值,驱动和RTL都对着这个表来写,效率高了很多。
第五个教训:AXI-Lite的default分支很重要。未定义地址的读返回一个魔术字,写操作忽略但返回OKAY响应。这样主机侧访问到未定义地址时不会挂死,只是读到魔术字,方便定位问题。
这个项目后续还可以扩展的方向包括:增加scatter-gather DMA支持以提升小数据块传输效率、加入MSI-X多向量中断以支持多通道并行处理、以及通过XDMA的AXI-Stream接口对接高速ADC数据流。XDMA这个IP核的灵活性很高,把基础链路跑通之后,后面的功能扩展就是水到渠成的事情。