ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PCIe DMA定制固件开发:TLP、中断与仿真全解析

PCIe DMA定制固件开发:TLP、中断与仿真全解析 这段时间一直在折腾一块PCIe接口的FPGA板卡系统能枚举到设备、驱动也能加载但DMA就是跑不起来。查了一天一夜最后在ILA里抓到TLP包头发现是Tag字段被锁死成一个固定值导致Completion包始终匹配不上。这种问题在PCIe DMA开发里简直太典型了——底层协议栈被IP核包住之后TLP层的行为就像个黑盒不懂的人根本不知道从哪里下手。所以我把这套定制的完整过程整理出来。这里不打算讲那些虚头巴脑的概念直接围绕三个核心点展开TLP读写事务到底怎么走、中断上报逻辑怎么搭、FPGA仿真怎么做到和真实链路1:1。这篇文章适合正在做PCIe DMA设备开发、被固件折磨得想摔板子的朋友也适合刚入门想搞懂DMA全链路的新手。按这套流程做完你会对整个数据通路有个非常清醒的认识。1. 定制固件到底在“定制”什么——PCIe DMA设备的运行机制拆解很多朋友拿到“定制固件”这个词第一反应是改驱动或者刷BIOS。实际上PCIe DMA设备的固件开发核心工作全在FPGA侧——你要决定硬件上电后初始状态是什么、描述符怎么管理、中断以什么方式上报、数据从哪个口进哪个口出。这些东西组合在一起才是真正意义上的固件。1.1 一块DMA板卡从插上电到干活的完整链路先把全链路捋一遍不然后面看TLP和中断都会是散的。第一步板卡插进PCIe插槽后链路两端开始做LTSSM训练从Detect到L0需要过完整个状态机。这不归固件管是PCIE PHY和链路层硬件自动完成。第二步系统枚举设备读取配置空间里的Vendor ID、Device ID、BAR地址大小等信息给设备分配内存地址空间。第三步驱动加载后会读取并映射BAR空间在系统内存里分配DMA缓冲区然后构建描述符链。第四步驱动把描述符地址写入FPGA侧寄存器通知DMA控制器有活干了。第五步DMA控制器自己去主机内存里拉取描述符解析出源地址、目的地址、长度等信息然后真正开始搬运数据。第六步数据搬完之后DMA控制器写状态回内存再触发一个中断告诉驱动“活干完了”。这六步里第一步到第三步基本是PCIe协议栈的固定流程能定制的地方不多真正的固件开发集中在第四步到第六步——描述符管理、数据搬运、状态回写、中断上报。1.2 所谓“单人固件”本质是一套带参数的硬件配置标题里写着“单人固件”听起来玄乎实际就是“一个人一块板子一个环境”的专属配置版本。通用固件要兼容各种主板枚举、各种BIOS设定、各种驱动版本所以默认参数会留很多余量。而专属固件的思路正好相反你具体用哪块主板、装哪个系统、跑什么业务就把对应的参数调到最优省掉兼容性代价。这里面可调的东西包括但不限于配置空间里的Device ID和Subsystem ID驱动靠这个认设备BAR空间的窗口大小有的场景要一整个64位地址空间有的只要32位就够DMA通道数量。XDMA IP默认可以开多个通道但如果业务是单通道大块搬运就可以把多通道合并成一条高吞吐通道中断模式。MSI还是Legacy INTx这个必须跟主板和系统匹配选错了中断根本进不来描述符预取深度。描述符缓存开深一点DMA引擎预取的描述符更多大流量场景不容易断流。这些参数落到FPGA里就是一堆寄存器的默认值和一段上电初始化时序。别的行业改配置叫“调参”在FPGA世界里这就是改代码、改逻辑、重新综合实现所以它被叫“固件开发”而不是“改配置”。1.3 固件里真正需要手写的Verilog逻辑用Xilinx的XDMA IP核的话PCIe协议栈那部分被IP包掉了但要写的东西依然不少。我自己这套定制固件里手写模块主要有五个描述符解析模块、地址转换模块、数据转发模块、中断控制模块、寄存器读写模块。描述符解析模块负责从主机内存拉回描述符拆出里面的src、dst、len、flag等字段按字节序填到内部寄存器。地址转换模块负责处理主机物理地址与FPGA内部AXI地址空间之间的映射有时候还要处理不连续物理页的scatter-gather列表。数据转发模块是纯吞吐关键路径AXI读写命令的生成、数据对齐、字节使能都在这里性能高低就看这段逻辑写得好不好。中断控制模块管理完成计数、中断屏蔽、中断合并。寄存器读写模块则是驱动与硬件对话的窗口。我在第一次写的时候把所有逻辑堆在一个顶层模块里后续调试改一个信号就要全盘重新综合效率极低。后来学乖了按功能切模块每个模块给独立的状态寄存器调试时直接读寄存器定位问题省了一大半时间。2. FPGA工程搭建与XDMA IP配置——仿真和上板共用的前置条件这章讲工程落地的具体操作。就算你是第一次接触Vivado按这个流程走下来也能把工程跑通。2.1 为什么选XDMA IP而不是从零写PCIe核之前有个朋友问我自己撸一个PCIe DMA控制器可行不可行。理论上是可行的但实践上你会被三座大山压垮PHY层的高速收发器配置和链路训练、数据链路层的ACK/NAK重传和流量控制、事务层的TLP组包拆包和乱序处理。这三样每一样都是一整个学期的课程设计量而且自己写的协议栈在兼容性上很难跟商业IP比。XDMA IP把这三层全包了对外暴露干净的AXI接口和配置寄存器接口。你要做的是把IP的寄存器接口和AXI数据通路对接好然后专心写自己的逻辑。这就像买发动机和变速箱你不需要自己锻造曲轴只管把底盘和电控做好就行。2.2 Vivado工程搭建与XDMA参数配置打开Vivado新建工程芯片型号按板卡选然后从IP Catalog里搜XDMA双击打开配置界面。这里有几个关键配置必须跟你的板卡和业务对上第一个是DMA Interface也就是数据通路接口类型。选AXI4-MM的话DMA引擎会把主机内存地址直接映射成AXI读写的地址适合大块数据搬运选AXI4-Stream的话数据是以流的形式往里灌适合连续采样的场景比如ADC采集卡。我这套固件用的是AXI4-MM因为要支持scatter-gather链表灵活度更高。第二个是Data Width。XDMA的AXI数据位宽可选64、128、256、512。位宽越大带宽越高但内部逻辑越复杂时序收敛难度也越大。第一次做建议选256bit原因很简单256bit配合256MHz跑出来的带宽是8.5GB/s左右对PCIe 3.0 x4链路来说已经够了而时序压力比512bit小得多。第三个是DMA Channels。XDMA在MM模式下通常提供2个通道H2CHost to Card和C2HCard to Host各一个。有些版本支持4通道。单通道跑大块搬运的人可以把两路合并成一路把描述符环深度调大虽然寄存器配置稍微复杂一点但吞吐提升明显。第四个是Interrupt配置。这里要选MSI还是Legacy中断。我的建议是只要系统支持MSI就尽量用MSI不用考虑。Legacy INTx是共享中断线多个设备共享一条物理中断线驱动里要做中断服务例程的遍历判断又慢又容易出问题。2.3 配置完IP之后最容易忽略的三个细节第一XDMA IP的时钟配置。IP内部有AXI时钟和用户逻辑时钟如果你用的是内部时钟注意驱动里读到的时钟频率要和实际综合一致。你仿真时随便设了个100MHz上板实际是250MHz那所有超时计数、中断节流时隙全部会偏表现出来就是“仿真好好的上板就废”。第二复位逻辑。IP会给一个axi_aresetn信号这个信号必须在内部时钟稳定后至少保持几个周期低电平再拉高释放。有些板卡的全局复位是按钮和电源监控给的直接怼到AXI复位上会导致初始化时序不满足我见过不少板卡莫名其妙起不来最后查出来是复位释放时沿和时钟沿竞争。第三约束文件里的PCIe差分引脚。XDMA的example design里自带一套完整的约束模板你要做的不是从零写约束而是把模板里的引脚位置改成你板卡实际的位置。注意时钟差分对、复位极性都要跟原理图对上差分对极性接反的话训练根本完成不了。配置完IP点生成Vivado会自动把PCIe硬核、DMA引擎、AXI接口的example design代码列出来在example design基础上改是你最好的起点。3. TLP层核心机制——从描述符到数据完成的全事务拆解TLP是Transaction Layer Packet的缩写它是PCIe协议栈真正干活的最小单位。搞DMA不懂TLP就像修车不懂点火顺序真要出问题只能干瞪眼。这章把TLP掰碎了讲。3.1 TLP头就是快递单你可以把TLP理解成一个快递包裹。TLP header是运单信息payload是箱子里装的货。Header里写了从哪来、到哪去、货物多长、是读还是写。PCIe链路上跑的高效数据搬运本质就是一连串的TLP交换。从功能上分TLP大致就这么几类Memory Read/Write Request TLP内存读写请求数据搬运的主力Completion TLP读请求的回应包携带读回来的数据Message TLP消息中断MSI就是这个类型Configuration Read/Write TLP配置空间访问枚举阶段用的。DMA工作的时候主要的TLP交互就是Memory Write、Memory Read、Completion这三种。3.2 一次DMA搬运涉及哪些TLP序列拿一次C2H传输举例也就是从FPGA侧把DMA缓冲区数据搬到主机内存。完整的TLP序列如下序列1驱动在主机内存里构建好描述符结构需要发送一个Memory Write TLP把描述符内容写进FPGA一个固定的缓存区。序列2FPGA的DMA引擎向主机内存发出一个Memory Read TLP读的是描述符链表的首地址。序列3主机内存控制器返回Completion with Data TLP数据内容就是描述符本身。FPGA侧DMA引擎收到后解析描述符。序列4FPGA引擎开始真正的数据搬运通过一系列Memory Write TLP把数据写到目标地址。大块数据会拆成多个TLP每个最大4KB有效载荷PCIe协议规定单TLP最大就是4KB。序列5FPGA写完成状态到描述符的最后一个字段再发一个状态更新的Memory Write TLP。序列6FPGA触发MSI中断一个Message TLP发到主机指定的地址驱动就收到通知了。我头一次看这个序列的时候最大的顿悟是原来FPGA去“读”主机内存不是直接在物理层拉数据而是发送一个Memory Read请求然后等主机的Completion回来。这个机制决定了DMA性能的上限场景里让读请求尽量并发、不要串行等待是优化吞吐的核心思路。3.3 TLP头的字段拆解与一次实际抓包对照一个典型的64位地址Memory Read Request TLP头是16字节关键字段用表格列出来Byte字段含义0Fmt Type包类型如读请求、写请求、完成包1-2Length有效载荷长度单位是4字节(DWord)3Requester ID发起者的总线号/设备号/功能号5-6Tag请求者生成的事务标签Completion靠它匹配请求7Last/First DW BE首尾数据的字节使能8-11Address[31:0]目标地址低32位12-15Address[63:32]目标地址高32位Tag字段就是前面开头我说到的那个坑。DMA引擎发给主机的每个请求都要带一个唯一的Tag主机的Completion回来时带同样的Tag请求方才能把数据和请求对起来。我那次就是Tag被写死成1所有读取请求的Completion全是同Tag协议栈直接疯掉了。Requester ID也很重要它必须和配置空间里的Bus/Dev/Fn一致否则系统会认为请求端非法而直接丢弃。这个字段通常是IP核自动填的如果你的设计是自己做了封装就得留意它有没有被正确传递。3.4 提一个抓TLP的实操工具经验很多人问“怎么抓到PCIe链路上的TLP”。如果你有PCIe协议分析仪那当然好但那玩意动辄几十万不是所有人都能配。退而求其次我常用的方法是在XDMA IP的逻辑例化里面把关键的AXI信号拉出来接ILA观察DMA引擎发给用户逻辑的命令。比如在AXI-MM模式下观察m_axi_araddr、m_axi_rdata、m_axi_awaddr这些信号能看到FPGA请求主机内存的地址和数据。还有一种方式是读XDMA IP自带的状态寄存器比如XDMA_STAT里的TLP计数、错误计数。如果TLP错误计数在涨那基本可以断定TLP层有异常再回到ILA去抓细节。先看计数器定位方向再用ILA看具体波形这个组合能省下大量时间。4. 中断逻辑——从描述符完成到上报CPU的完整路径DMA跑完数据CPU怎么知道方案无非两种轮询状态寄存器或者等中断。轮询简单但吃CPU尤其在DMA频繁完成的小包场景下CPU会被拖死。所以正规的DMA固件都走中断这一章专门把中断逻辑掰透。4.1 中断在DMA固件里的角色和触发时机中断不是随便什么时间都能触发的。它必须在正确的时间点上报而且要防抖防重复。正常的中断触发时机有这几处描述符链上的一个节点处理完成整个描述符链全部处理完遇到错误比如地址越界或者Master Abort。每个时机对应不同的驱动处理逻辑所以固件里必须把这些事件区分开。我在中断控制模块里维护了一个事件寄存器每一位对应一种事件驱动读这个寄存器就知道发生了什么而不是收到一个中断再靠猜。4.2 MSI中断的原理和FPGA侧实现方式MSI全称是Message Signaled Interrupt它的机制和传统硬件引脚中断完全不一样。传统INTx中断是拉低引脚、释放引脚靠电平或边沿触发。MSI则是直接向主机内存写一个特定数据本质就是发一个Memory Write TLP。具体流程是这样系统启动时驱动配置MSI的Capability结构主机固件会在MSI配置里写入一个中断消息地址和一个中断消息数据。FPGA侧在需要触发中断时就像写内存一样往这个地址写入这个数据中断就被触发了。这个写入动作在总线上看就是一个普通的Memory Write TLP。所以“FPGA产生MSI中断”在硬件上根本不是拉个引脚而是发起一次写操作。很多第一次接触MSI的人会卡在这个点上——代码里看着像写内存实际上是在触发中断。下面是我在固件里用的一个简化的中断控制模块示例完成计数达到阈值才允许上报中断module irq_ctrl #( parameter C_IRQ_COUNT_WIDTH 32 )( input wire axi_aclk, input wire axi_aresetn, input wire dma_done_pulse, input wire dma_error_pulse, input wire [C_IRQ_COUNT_WIDTH-1:0] irq_threshold, input wire irq_enable, output reg msi_irq_request, output reg [C_IRQ_COUNT_WIDTH-1:0] irq_pending ); always (posedge axi_aclk, negedge axi_aresetn) begin if (!axi_aresetn) begin irq_pending h0; msi_irq_request 1b0; end else begin if (dma_done_pulse irq_enable) begin if (irq_pending irq_threshold) begin msi_irq_request 1b1; end irq_pending irq_pending 1b1; end if (dma_error_pulse irq_enable) begin irq_pending h0; msi_irq_request 1b1; end if (irq_clear) begin msi_irq_request 1b0; end end end endmodule这段代码里dma_done_pulse每完成一次搬运就来一个脉冲irq_pending做完成计数msi_irq_request在达到阈值时拉高一个周期。irq_clear是驱动写完中断服务程序之后清中断的寄存器。这种“计数到阈值再中断”的机制对高频小包场景特别管用避免每个小包都中断一次导致的中断风暴。4.3 Legacy INTx中断为什么越来越让人头疼Legacy中断走的是物理中断线问题一大把。首先是共享中断多个设备可能挂在同一条中断线上驱动要遍历找哪个设备触发了中断开销不小。其次是断言和撤销的时序要求非常严格当设备的中断撤销被主机的写操作延迟时很容易丢中断。最麻烦的是平台差异。不同主板对INTx的内部路由方式不一样有的走中断控制器有的通过固件模拟成虚拟线。同一套固件在A主板上工作正常插到B主板就中断失灵这类问题排查的成本极高。所以我现在新做的设计一律只用MSILegacy代码只保留了兼容分支。4.4 中断风暴和中断丢失的排查经验中断风暴就是硬件疯狂触发中断CPU中断服务程序忙不过来整个系统响应变慢甚至死机。最常见的原因是描述符回写的状态没有及时更新驱动把同一个描述符重复提交硬件以为有新活就一直干活一直中断。解决方法是给中断加上节流窗口两次中断之间至少隔N微秒换算成周期就是给节流计数器。中断丢失的现象是DMA已经完成了但驱动一直没收到中断只能靠超时兜底。通常原因有几个MSI Capability里的Enable位没置1硬件发的中断被当成普通写操作直接丢弃或者中断请求信号只维持了一个周期被IP核内部的处理逻辑漏掉了。我踩过一次坑就是msi_irq_request只拉高一个周期但IP核要求至少保持两个周期才能稳定识别改成保持到被软件清除后问题就消失了。5. FPGA仿真1:1到底怎么做到——testbench搭建与验证要点标题里写了“FPGA仿真1:1”这不是营销话术而是一种工程要求。仿真的目的不是“样子像”而是把上板后要发生的事务级行为完整跑一遍保证板卡到手即跑通不用反复烧写调试。5.1 为什么说上板前必须先过仿真这一关DMA固件的调试效率和普通逻辑完全不是一个量级。普通FPGA逻辑改了代码重新编译烧进去还能看现象。DMA固件改一次要重新综合、布局布线动辄两三个小时然后还要重启电脑、加载驱动、跑测试程序、抓波形。一个最简单的时序问题往返几次一整天就没了。仿真则是在软件环境里跑出整个链路改完代码几十秒就能看波形。所以有价值的DMA工程一定是有完善的仿真环境在前面挡着。所谓仿真1:1就是要让仿真时发生的TLP交互顺序、AXI吞吐行为、中断时序与上板后看到的完全一致。5.2 搭建一个能跑全事务的testbenchXDMA IP自带一个仿真模型它模拟了配置空间、中断机制、DMA引擎还带BFMBus Functional Model。用这个模型你可以直接在testbench里发起寄存器读写操作模拟驱动的行为。下面是一段testbench骨架核心逻辑就是模拟驱动做三件事配置MSI、提交一个描述符、等中断module tb_xdma_dma; // 时钟和复位生成 bit axi_aclk; bit axi_aresetn 0; // XDMA IP的BFM接口 xdma_bfm_if bfm(); // 被测设计例化省略连线... xdma_wrapper dut ( .axi_aclk (axi_aclk), .axi_aresetn (axi_aresetn), .bfm (bfm) ); task send_descriptor(input logic [63:0] src_addr, input logic [63:0] dst_addr, input logic [31:0] length); // 写BAR地址0寄存器填入描述符基地址 bfm.write(BAR0_ADDR REG_DESC_BASE, src_addr); // 写长度寄存器 bfm.write(BAR0_ADDR REG_DESC_LEN, length); // 写tail寄存器通知DMA引擎有新描述符 bfm.write(BAR0_ADDR REG_DESC_TAIL, 32h1); endtask task check_completion(); logic [31:0] status; bfm.read(BAR0_ADDR REG_STATUS, status); if (status[0] ! 1b1) $fatal(1, DMA transfer did not complete); endtask initial begin // 复位释放 repeat (10) (posedge axi_aclk); axi_aresetn 1; repeat (10) (posedge axi_aclk); // 配置MSI中断使能 bfm.write(BAR0_ADDR REG_MSI_ENABLE, 32h1); // 提交DMA搬运请求 send_descriptor(32h1000_0000, 32h2000_0000, 4096); // 等待中断上报 wait (bfm.msi_irq_asserted); // 读取完成状态 check_completion(); $display(TEST PASSED); $finish; end endmodule这里有几个点必须注意描述符地址要按64字节对齐这是PCIe协议的基本要求长度字段的单位是字节还是DWord不同IP定义不一样tail寄存器写法的物理意义是“发布描述符”必须放在最后顺序反了硬件会看不到新的描述符。5.3 三个衡量“1:1”是否达标的标准第一个是参数一致。仿真里用的IP配置必须和上板工程完全一致不能仿真时开MSI上板关MSI或者仿真时256bit AXI上板改成128bit。任何一个配置差异都会让仿真失去验证意义。第二个是时钟一致。我在仿真里直接约束和上板一样的AXI时钟频率比如250MHz。如果你仿真只跑100MHz时序裕量全被吃掉了上板才发现高频率下握手不满足。第三个是软件一致。仿真里模拟的驱动行为要和真实驱动的初始化序列、描述符格式、中断处理流程一致。最好把真实驱动里初始化硬件的那段逻辑照着搬进testbench仿真跑通了驱动那边的配合就八九不离十。我自己的经验是第一次跑仿真别贪多先跑一个最小请求比如4KB的DMA搬运。跑通了再扩到大数据块、多描述符、错误注入。最小请求跑通意味着数据通路已通后续都是性能调优的活。6. 实测中最常踩的坑与排错清单——每一条都是真金白银的教训这部分是纯经验把我在DMA定制固件过程中遇到过的真实问题整理成清单每一条后面都有当时的定位思路。你在做的时候对应着查能省掉大量试错时间。6.1 问题排查速查表现象最常见根因定位手段系统无法枚举到设备差分引脚约束错误、链路训练失败看LTSSM状态寄存器驱动加载失败设备ID不匹配或BAR空间分配异常查lspci输出、改固件里的配置空间IDDMA传输超时描述符地址未对齐或描述符格式错误仿真抓描述符解析模块波形数据搬完后无中断MSI Enable位没置1或中断请求脉冲宽度不足抓中断控制模块输出查MSI配置寄存器小包场景CPU占用过高中断风暴节流参数没生效查看中断间隔调整阈值寄存器吞吐远低于预期读请求串行未并发TLP队列深度太浅抓AXI读写通道的pending请求数预取开深上板结果和仿真不一致时钟频率或复位时序不一致对照仿真和上板的约束文件6.2 四个值得单独拿出来说的经典案例第一个案例描述符地址没对齐导致总线错误。当时我在testbench里把描述符地址设成了一个非64字节对齐的值结果BFM模型在仿真里一直报错。后来一查PCIe规范描述符环的基地址必须按64字节对齐这是硬件协议的死要求。驱动分配内存时如果不做对齐处理就要在固件里做地址掩码截断否则直接总线异常。第二个案例MSI中断触发过晚。现场反馈“数据早就进来了但CPU迟迟不响应”。原因是FPGA的中断逻辑里有好几个时钟域的同步链每跳一级都多出几拍延迟中断上报被拖了很多个微秒。解决办法是把中断请求信号在DMA完成状态写回的同时就打拍而不是等状态走到AXI总线上才产生。第三个案例Legacy中断在笔记本上不工作。同一套固件插台式机正常插一台笔记本就鬼一样地丢中断。查到最后发现是笔记本的中断控制器对INTx的路由方式不同虚拟中断线需要额外一个时钟周期的保持时间。这个案例让我彻底倒向MSI再也不碰INTx。第四个案例仿真全通上板死。仿真里AXI时钟设的跟实际不一致导致时钟域交叉的信号同步出问题。这个前面提过这里再强调一次仿真里的时钟频率、复位释放时间、寄存器初始值全部要和上板工程严格一致一条都不能改。6.3 我给自己的固件里加的一个“后悔药”设计调试DMA固件最痛苦的就是不知道系统跑到哪一步了。后来我在固件里加了一组debug寄存器把LTSSM状态、TLP发送计数、TLP接收错误计数、DMA描述符处理计数、中断触发计数全部映射到寄存器空间。驱动加载失败或者DMA不工作时先读这些寄存器看一眼。比如LTSSM停在Polling状态那是链路层问题别在应用逻辑上瞎找TLP错误计数在涨那是事务层问题直接去抓TLP头。这套“从寄存器读数定位故障层级”的方法比我早期靠猜高效太多。6.4 给新入坑的朋友的三条建议最后说三条实在的。第一条能仿真解决的事绝不上板仿真环境花再多时间搭都值。第二条需求文档里写清楚中断模式、通道数、吞吐指标再开始动工不然做到一半改通道配置会让人崩溃。第三条调试时手上常备一份TLP协议头字段速查表地址偏移、字节顺序、位宽这些细节临时查手册非常耽误事。DMA固件的难度不在某个单独的技术点上而在全链路的耦合关系。搞清楚TLP、中断、描述符和数据通路这几块怎么衔接定制固件就不是玄学了。其实说到底所谓100%成功靠的不是某个灵丹妙药而是每一步都按规矩来参数配置跟板卡对上、仿真环境跟真实对齐、中断时序反复验证、寄存器调试手段提前埋好。这些做到位了成功率自然就上来了。
返回列表