1. 为什么要在FPGA上把AXI映射到PCI Express如果你手上有块带PCIe硬核的FPGA开发板又刚好需要让上位机通过PCIe总线读写FPGA内部的寄存器或存储器那AXI Memory Mapped to PCI Express这个IP核几乎是你绕不开的选择。Xilinx现在叫AMD从7系列开始就在Vivado里提供了这个IP它做的事情说起来很简单把PCIe总线上的TLP事务翻译成AXI4总线上的读写请求反过来也一样。但真正配置起来参数面板上那几十个选项足以让第一次接触的人发懵。我见过不少工程师在这个IP上栽跟头有的是BAR地址映射搞错了导致上位机根本枚举不到设备有的是AXI位宽和PCIe位宽没对齐导致数据错位还有的是中断配置不对导致DMA传输完了主机还不知道。这些问题在文档里往往一笔带过但在实际调试中能卡你两三天。这篇文章面向的是已经有一定FPGA开发基础、正在使用或准备使用AXI Memory Mapped to PCI Express IP核的工程师。我会从参数配置的底层逻辑讲起把每个关键选项背后的为什么说清楚然后给出经过实际项目验证的配置方案和性能优化手段。不管你是做数据采集、高速接口还是嵌入式系统只要涉及PCIe和AXI的桥接这里的内容都能直接参考。2. 理解这个IP到底在做什么2.1 PCIe事务层与AXI协议的语义差异PCIe和AXI虽然都是总线协议但它们的设计哲学完全不同。PCIe是树形拓扑、基于包的传输机制每个事务都要经过事务层、数据链路层和物理层TLP包里有完整的路由信息。AXI则是点对点的、基于通道的协议读写地址通道和数据通道分离通过VALID/READY握手来传输。AXI Memory Mapped to PCI Express IP的核心工作就是在这两种语义之间做转换。当主机发起一个Memory Read TLP时IP核需要把它转换成一个AXI4的读事务先在AR通道上发出读地址然后等待R通道返回数据最后把数据打包成Completion TLP发回主机。反过来当FPGA内部有Master发起AXI写请求时IP核要把它转换成Memory Write TLP发往主机。这个转换过程中有几个关键点需要注意。第一是地址映射PCIe的BAR空间地址和AXI地址之间的对应关系需要你手动配置。第二是数据宽度匹配PCIe Gen2 x1的链路宽度是1字节但AXI数据位宽通常是64位或128位IP核内部要做位宽转换。第三是事务顺序PCIe要求同一方向的Completion不能乱序但AXI的Outstanding能力允许乱序返回IP核需要做排序缓冲。2.2 核心接口信号一览这个IP对外暴露的接口分为两大块PCIe侧和AXI侧。PCIe侧直接连接到FPGA的PCIe硬核包括差分收发信号、参考时钟、复位等。AXI侧则提供两组接口一组是AXI Master用于IP核主动发起对主机内存的读写一组是AXI Slave用于主机通过BAR空间访问FPGA内部资源。AXI Slave接口的信号包括s_axi_awaddr、s_axi_awvalid、s_axi_awready、s_axi_wdata、s_axi_wstrb、s_axi_wvalid、s_axi_wready、s_axi_bresp、s_axi_bvalid、s_axi_bready以及对应的读通道信号。这些信号的名字和标准AXI4完全一致但需要注意的是这个IP的AXI Slave接口只支持32位地址位宽可配置为64位数据位宽固定为64位在UltraScale上可以配置为128位或256位。AXI Master接口用于IP核向主机内存发起读写信号命名以m_axi_开头。这个接口的存在使得FPGA可以主动把数据推到主机内存是实现DMA功能的基础。2.3 地址翻译机制地址翻译是这个IP最核心也最容易出错的部分。PCIe的BAR空间是主机分配给设备的地址窗口主机通过访问这个窗口来读写设备。IP核需要知道当主机访问BAR空间中的某个地址时应该把它翻译成AXI总线上的哪个地址。在Vivado的配置界面里你会看到PCIe to AXI Translation这个参数。它定义了一个基地址偏移量AXI地址 PCIe BAR地址 Translation Offset。举个例子如果BAR0的大小是1MBTranslation Offset设为0x00000000那么主机访问BAR0的0x100地址时AXI总线上发出的地址就是0x100。如果Translation Offset设为0x80000000那AXI地址就是0x80000100。这里有个坑Translation Offset必须是BAR大小的整数倍。如果你设了一个不对齐的值Vivado可能不会报错但实际运行时地址会错乱。我建议在配置时先把BAR大小确定下来然后根据FPGA内部AXI地址空间的规划来设置Translation Offset。3. Vivado中IP核参数配置的逐项拆解3.1 基础模式选择Bridge还是Root Port打开IP配置界面第一个要做的选择是Mode。这个IP支持两种模式Bridge和Root Port。绝大多数场景下你选Bridge模式也就是FPGA作为PCIe端点设备Endpoint连接到主机的Root Complex。Root Port模式则是让FPGA模拟一个Root Complex可以连接下游的PCIe设备这种用法比较少见通常只在特定的嵌入式场景中使用。选Bridge模式后接下来要配置PCIe的设备信息。Vendor ID和Device ID可以保持默认也可以填入你公司的实际ID。Class Code对于自定义设备来说通常设为0x058000Memory Controller或0xFF0000Other这个值会影响主机操作系统加载哪个驱动。3.2 链路参数Gen和Lane Width怎么选PCIe的链路速度和宽度直接决定了理论带宽上限。Vivado里可以配置的最大链路速度和宽度取决于你用的FPGA型号和封装。7系列FPGA通常支持Gen2 x8UltraScale可以支持Gen3 x16甚至Gen4。选择链路参数时有个基本原则不要盲目追求最高速度。Gen3的信号完整性要求比Gen2高得多PCB走线需要更严格的阻抗控制和更短的走线长度。如果你的板子是按Gen2设计的强行上Gen3会导致链路训练失败或者误码率飙升。实际配置时我通常会把Maximum Link Speed设为比硬件设计目标高一级。比如板子设计的是Gen2我就配Gen2如果板子有Gen3的潜力但不确定可以先配Gen2跑通后面再尝试Gen3。Lane Width也是类似先配x4跑通再尝试x8。3.3 BAR配置大小、类型和预取BARBase Address Register是主机访问FPGA的窗口。这个IP最多支持6个BAR但实际常用的就1到2个。每个BAR需要配置三个属性大小、类型Memory或I/O、是否预取。BAR大小的选择要看你需要暴露多少地址空间给主机。如果只是暴露几个控制寄存器4KB就够了。如果要让主机直接访问FPGA内部的大块DDR内存那可能需要配到64MB甚至更大。注意BAR大小必须是2的幂次方而且所有BAR的总大小不能超过PCIe规范的限制。BAR类型几乎总是选MemoryI/O BAR在现代PCIe设备中已经很少用了。预取属性Prefetchable需要谨慎设置如果你的BAR空间是真正的内存读写没有副作用可以设为预取如果BAR空间里是控制寄存器读操作会清除状态位之类的必须设为非预取否则主机的预取机制会导致寄存器状态被意外清除。3.4 AXI接口参数位宽、时钟和OutstandingAXI接口的配置直接影响性能和资源占用。数据位宽方面64位是最常用的选择它在资源占用和性能之间取得了较好的平衡。如果你需要更高的吞吐量可以选128位或256位但这会消耗更多的BRAM和逻辑资源。时钟配置需要注意AXI接口的时钟频率和PCIe的用户时钟user_clk是独立的。在7系列上PCIe Gen2 x1的user_clk是125MHzGen2 x4是125MHzGen3 x4是250MHz。AXI时钟可以高于或低于user_clkIP核内部有异步FIFO做时钟域 crossing。但两者频率差不要超过3倍否则FIFO深度可能不够导致性能下降。Outstanding能力决定了IP核能同时处理多少个未完成的AXI事务。这个值设得越大理论吞吐量越高但也会消耗更多的缓冲资源。对于大多数应用读Outstanding设为8、写Outstanding设为8就够用了。如果你做的是高吞吐量的DMA应用可以适当提高到16或32。4. 地址映射与中断配置的实操细节4.1 PCIe到AXI的地址翻译实战地址翻译的配置在PCIe to AXI Translation选项卡里。这里需要为每个BAR设置一个翻译偏移量。我一般会按照以下步骤来确定这个值第一步规划FPGA内部的AXI地址空间。比如我打算把BRAM放在0x00000000到0x0000FFFF把DDR控制器放在0x40000000到0x7FFFFFFF把自定义寄存器放在0x80000000开始的位置。第二步确定每个BAR需要暴露哪些地址范围。如果BAR0要暴露自定义寄存器那BAR0的Translation Offset就设为0x80000000。这样主机访问BAR0的0x0地址时AXI总线上看到的就是0x80000000。第三步检查地址对齐。Translation Offset必须是BAR大小的整数倍。如果BAR0大小是64KBTranslation Offset是0x80000000那0x80000000除以0x10000等于0x8000是整数没问题。这里有个实用技巧在Vivado的Address Editor里你可以直观地看到每个Master和Slave的地址映射关系。配置完IP后一定要在Address Editor里检查一遍确保没有地址重叠或未映射的区域。4.2 中断配置MSI还是Legacy中断配置是另一个容易出问题的地方。这个IP支持Legacy中断和MSI/MSI-X中断。Legacy中断通过物理中断线INTA/B/C/D发送MSI则是通过写主机内存的方式发送中断消息。MSI的优势很明显不需要共享中断线支持更多的中断向量而且延迟更低。如果你的主机操作系统支持MSI现代系统都支持强烈建议使用MSI。配置时需要设置MSI的向量数量通常设为1到32之间的2的幂次方。MSI-X比MSI更灵活支持更多的向量最多2048个和独立的地址/数据对。但MSI-X的配置也更复杂需要实现MSI-X Table和PBAPending Bit Array的存储。对于大多数应用MSI就足够了。中断的触发源可以来自AXI接口的多个事件AXI写完成、AXI读完成、错误响应等。你可以在IP配置里选择哪些事件触发中断。实际使用时我通常会把写完成和读完成都使能中断这样主机能及时知道数据传输的状态。4.3 复位与时钟的注意事项这个IP需要多个时钟和复位信号。sys_clk是PCIe参考时钟通常是100MHz的差分时钟。user_clk是IP核输出的用户时钟频率取决于链路配置。axi_aclk是AXI接口的时钟需要你自己提供。复位方面sys_rst_n是系统复位低有效。这个复位信号必须与sys_clk同步。IP核还会输出user_reset信号用于复位用户逻辑。注意user_reset的持续时间可能比较长特别是在链路训练期间你的用户逻辑需要能正确处理这个复位。一个常见的坑AXI复位和PCIe复位的关系。axi_aresetn是AXI接口的复位它应该在user_reset释放之后才能释放。如果你把这两个复位接在一起可能会导致AXI接口在PCIe链路还没准备好的时候就开始工作从而产生错误响应。5. 性能优化从理论带宽到实际吞吐5.1 理论带宽计算与瓶颈分析PCIe Gen2 x1的理论带宽是5GT/s × 1 lane × 8/10编码效率 4Gbps 500MB/s。Gen2 x4是2GB/sGen3 x4是4GB/s128/130编码效率。但这些是原始带宽实际可用带宽要扣除TLP包头、DLLP、ACK/NAK等开销通常只有理论值的70%到85%。AXI侧的带宽计算更直接数据位宽 × 时钟频率。比如64位AXI接口跑250MHz理论带宽是2GB/s。如果AXI时钟是125MHz带宽就是1GB/s。瓶颈往往出现在两者不匹配的地方。如果PCIe侧带宽是2GB/sAXI侧只有1GB/s那整体吞吐就被限制在1GB/s。反过来如果AXI侧带宽远大于PCIe侧那PCIe就是瓶颈。配置时需要根据实际应用的数据流向来确定哪一侧需要更高的带宽。5.2 提高吞吐量的几个关键手段第一个手段是增大Outstanding数量。前面提到过Outstanding决定了能同时处理多少个未完成事务。在延迟较高的情况下Outstanding数量直接决定了吞吐量。假设单次事务的往返延迟是1微秒Outstanding为1时每秒只能处理100万次事务Outstanding为8时就能处理800万次。第二个手段是使用更大的TLP Payload。PCIe的Max Payload SizeMPS可以配置为128字节到4096字节。更大的Payload意味着更少的包头开销从而提高有效带宽。在IP配置里你可以设置Maximum Payload Size参数。注意这个值不能超过主机Root Complex支持的最大值否则链路训练时会协商到较小的值。第三个手段是优化AXI的突发长度。AXI4支持最大256拍的突发传输。对于连续的大块数据传输使用长突发能显著减少地址通道的开销。但要注意PCIe的TLP大小限制可能会把长突发拆分成多个TLP这中间会有额外的开销。5.3 实测数据与调优记录我在一个数据采集项目里实测过不同配置下的吞吐量。硬件平台是Kintex-7 FPGAPCIe Gen2 x4AXI时钟250MHz数据位宽64位。测试方法是FPGA持续向主机内存写数据主机端用测速程序统计实际写入速率。配置项配置A配置B配置COutstanding4816MPS128B256B512BAXI突发长度1664128实测吞吐680MB/s1.2GB/s1.5GB/s从数据可以看出Outstanding从4提高到8带来了接近翻倍的性能提升这是因为延迟被更好地隐藏了。MPS从128B提高到256B也有明显效果但继续提高到512B时提升就不那么明显了因为包头开销的占比已经很低了。最终我选择的配置是Outstanding8、MPS256B、AXI突发长度64。这个配置在资源占用和性能之间取得了较好的平衡。如果资源允许Outstanding16能进一步榨取性能但会多消耗大约30%的BRAM资源。6. 调试过程中最常见的几个坑6.1 主机枚举不到设备这是最常见的问题可能的原因有好几个。首先检查PCIe参考时钟是否正常100MHz差分时钟的幅度和抖动都要在规范范围内。其次检查复位信号sys_rst_n必须在时钟稳定之后才能释放。第三检查链路训练状态可以通过IP核的cfg_ltssm_state信号来观察链路训练的状态机。如果链路训练一直停在Detect状态说明物理层没有检测到对端设备。检查TX和RX差分线是否接反了这是新手最容易犯的错误。PCIe的TX要接到对端的RXRX要接到对端的TX交叉连接。如果链路训练到了Polling状态就失败可能是参考时钟频率不对。有些板子用的是125MHz参考时钟而不是100MHz需要在IP配置里修改。6.2 BAR空间读写返回错误主机能枚举到设备但读写BAR空间时返回0xFFFFFFFF或错误响应。这种情况通常是地址翻译配置有问题。检查Translation Offset是否与BAR大小对齐检查AXI Slave接口是否正确地响应了读写请求。还有一个可能的原因是AXI互联Interconnect没有正确配置。如果你在IP核和用户逻辑之间加了AXI Interconnect或SmartConnect需要确保地址映射关系正确。我遇到过好几次是因为Interconnect的地址范围设置得太小导致部分BAR空间访问被拦截。6.3 中断收不到中断配置对了但主机收不到中断先检查MSI的地址和数据是否正确。MSI的地址通常是0xFEExxxxx的形式数据是中断向量号。这些值由主机在枚举时写入IP核的配置空间你需要确保IP核正确接收并使用了这些值。另一个常见问题是中断的清除时机。MSI是边沿触发的中断IP核发出中断请求后需要等待主机处理完并清除中断状态才能发送下一个中断。如果中断状态没有正确清除后续中断会被阻塞。6.4 性能远低于预期如果实测吞吐量只有理论值的十分之一甚至更低先检查时钟频率是否达到了预期。用ILA抓一下AXI接口的时钟和握手信号看看有没有频繁的等待周期。另一个可能的原因是地址不对齐。PCIe对地址对齐有要求如果传输的起始地址没有对齐到4字节边界会产生额外的开销。对于大块数据传输确保起始地址和传输长度都对齐到64字节或128字节边界能显著提高效率。7. 几个实际项目中的配置模板7.1 寄存器控制型应用这类应用的特点是数据量小、实时性要求高主机通过BAR空间读写FPGA内部的寄存器和FIFO。配置要点BAR0大小设为4KB或8KBTranslation Offset设为寄存器空间的基地址。AXI数据位宽64位时钟频率可以低一些100MHz左右。中断使用MSI向量数设为1。Outstanding可以设小一些4就够了因为数据量不大。7.2 高速数据采集应用这类应用需要FPGA持续向主机内存写入大量数据。配置要点BAR0用于控制寄存器BAR1用于DMA描述符。AXI数据位宽128位或256位时钟频率尽量高250MHz以上。MPS设为256B或512BOutstanding设为16或32。中断使用MSI向量数设为4到8个分别对应不同的DMA通道。7.3 嵌入式处理应用这类应用在FPGA内部集成了MicroBlaze或ARM核需要通过PCIe与主机通信。配置要点BAR0用于处理器和主机的共享内存BAR1用于处理器访问主机内存的窗口。AXI接口需要连接到处理器的AXI互联上注意地址空间的划分不要冲突。中断可以同时使用MSI和LegacyLegacy用于处理器的本地中断。8. 一些零散但重要的经验关于Vivado版本的选择我建议用2020.2或更新的版本。老版本2018.x及以前在这个IP上有一些已知的bug比如地址翻译在某些配置下会出错。新版本还改进了时序收敛算法对PCIe这种高速接口的布局布线更友好。关于仿真这个IP的仿真模型比较重仿真速度很慢。我通常只在最后阶段做完整的系统仿真前期用AXI VIPVerification IP单独验证用户逻辑。如果要做PCIe层的仿真可以用Xilinx提供的Root Complex模型但配置起来比较麻烦。关于PCB设计PCIe的差分线要走100欧姆差分阻抗长度匹配控制在5mil以内。参考时钟的走线要远离其他高速信号避免串扰。如果板子上有多个PCIe设备注意参考时钟的分配和缓冲。关于散热PCIe Gen3及以上的链路功耗比较大FPGA的结温会明显升高。如果是在密闭机箱里使用需要加散热片或风扇。我遇到过因为过热导致链路降速的情况排查了很久才发现是散热问题。关于驱动开发Linux下的PCIe驱动框架比较成熟可以用pci_alloc_consistent分配DMA缓冲区用pci_enable_msi使能MSI中断。Windows下需要用WDK开发相对麻烦一些。如果只是做原型验证可以用Xilinx提供的XDMA驱动它已经封装好了基本的读写和中断处理。关于调试工具Vivado的ILAIntegrated Logic Analyzer是必不可少的。我通常会在AXI接口和PCIe配置空间接口上都插入ILA抓取关键信号。另外主机端的lspci -vvv命令能显示设备的详细配置信息包括BAR大小、链路状态、MSI能力等是排查问题的第一手资料。关于时序收敛PCIe的user_clk频率较高时序约束要写仔细。除了Vivado自动生成的约束外还需要手动添加AXI接口的时序约束。如果时序不收敛可以尝试降低AXI时钟频率或者优化用户逻辑的流水线设计。关于功耗估算Vivado的Power Estimator可以给出大致的功耗数据。PCIe硬核的功耗与链路速度和宽度成正比Gen3 x8的功耗可能是Gen2 x4的两倍以上。如果功耗预算紧张可以考虑动态降速或降宽。关于IP核的LicenseAXI Memory Mapped to PCI Express IP是免费包含在Vivado里的不需要额外的License。但PCIe硬核的使用可能需要特定的器件License这个在购买FPGA时通常会一并提供。关于代码风格我建议把IP核的例化代码单独放在一个文件里用参数化的方式定义地址位宽和数据位宽。这样在不同的项目之间移植时只需要修改参数就能适配不同的配置。关于版本控制Vivado的工程文件.xpr和IP核的配置文件.xci都应该纳入版本控制。但生成的输出文件如.bit和.ltx不需要因为它们可以从源文件重新生成。IP核的配置参数最好也单独记录一份文档方便后续查阅。关于团队协作如果多个人同时开发一个FPGA工程建议把IP核的配置固化下来不要每个人都在自己的机器上重新配置。可以用Vivado的Write IP Cache功能把IP核的配置导出其他人直接导入即可。关于长期维护PCIe规范在不断发展新的FPGA型号也在不断推出。如果项目周期较长建议在架构设计时留出升级空间。比如AXI接口的位宽可以设计为参数化方便后续升级到更宽的位宽。地址空间的规划也要留有余量避免后续增加功能时地址不够用。