ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于AXI4的FPGA DDR控制器实战:从MIG配置到带宽优化

基于AXI4的FPGA DDR控制器实战:从MIG配置到带宽优化 早先做FPGA图像采集总会被DDR带宽卡一下。我说的不是仿真里那种波形而是真正在板上用FPGA控制DDR读写通过AXI4总线接口把一帧图像倒进倒出的场景。很多初学者拿着MIG向导点几下出来一个带AXI接口的DDR控制器却不知道该从哪下手是先写个主设备还是先跑仿真这篇就把我在这条路上实际遇到的问题和验证过的写法从头到尾捋一遍。这篇内容适合两类人一类是刚接触DDR控制器IP想快速搭一个能跑的读写通路另一类是已经在用native接口但对AXI4协议不熟想改造成标准化总线的人。我尽量不说空话直接给配置、代码思路和排查链路。1. 直接一条AXI总线把DDR控制器当“搬运工”1.1 先从帧缓存场景说说为什么绕不开DDR做FPGA图像处理时最常遇到的容量瓶颈是片上BRAM。以一块典型的Kintex-7为例BRAM总共可能只有一两兆字节。存一帧1080p的RGB图像要6MB左右怎么塞都塞不下。用SRAM外部扩展也行但容量上去之后成本、引脚、布局都在劝退。DDR颗粒有容量大、价格低的优势但缺点是时序控制极其复杂刷新、预充电、行激活、写电平校准甚至PCB上等长走线都会影响跑不跑得起来。于是各家FPGA厂商都提供DDR控制器IP把底层DRAM时序做进硬核或初始化逻辑里。用户只需要在IP配置界面里选好内存颗粒然后把端口的访问请求发过去。这个IP的用户侧接口在Xilinx系列里叫native user interface现在也常见AXI4接口。Altera/Intel那边则常见Avalon-MM和AXI。我们工程中最终选了AXI4版本不是因为原生接口难用而是因为后续要对接DMA、视频处理和软核统一走AXI总线可以让各个模块之间的连接方式保持一致。使用AXI4总线去控制DDR控制器的本质是把DDR控制器抽象成一块“地址映射的内存”。你告诉它读哪个地址、读多长它负责内部调度你只需要关心数据总线上的节奏。对业务逻辑工程师来说这比直接去控制DDR的bank、row、col要友好得多。1.2 AXI4协议在这条链路里的真实定位AXI4是ARM AMBA总线家族里面向高性能内存映射传输的协议。它一共定义了五个独立通道写地址通道AW、写数据通道W、写响应通道B、读地址通道AR、读数据通道R。每个通道都是VALID/READY握手通道之间可以乱序、可以穿插。DDR控制器IP的角色是一个AXI4从设备slave。用户逻辑是AXI4主设备master比如DMA、图像帧写入模块或者Zynq里的CPU系统。主设备发起请求从设备接收请求并返回数据或响应。中间如果有多个主设备同时访问通常还要经过一个AXI InterconnectAXI互联做仲裁和路由。你在工程里看到的“AXI4接口DDR控制器”实际上是在DDR控制器内核外面包了一层AXI转native的桥。这层桥负责把AXI事务拆分成DDR控制器能理解的最小命令。比如一个AXI burst长度为64、数据宽度为128bit的写请求内部可能被拆成多个DDR命令因为DDR物理接口一次突发往往只有8个或16个内部beat。这个桥还承担了缓冲和重排的工作也因此带来了一些延迟。所以我现在习惯把这条链路理解成三层业务逻辑请求发起→ AXI协议层命令和数据的组织形式→ DDR控制器的native内核真正操作DRAM。很多人调不好DDR不是DDR控制器的原因而是AXI协议层就没握对。1.3 native接口与AXI4接口怎么选有些老工程师更愿意直接用native接口觉得信号少、时序直观。没错Xilinx MIG的native接口确实简单app_cmd、app_addr、app_en然后就是app_wdf_data、app_rd_data一个状态机就能写完。但它最大的问题是绑定厂商而且一旦涉及多主设备共享还得自己做仲裁。我的建议很直接如果你只是临时验证DDR控制器或者做单通道数据采集存储用native接口反而更快。如果你的系统里有多个模块都要搬数据或者以后要接软核、Zynq里的PS甚至要复用别人的AXI IP那直接上AXI4接口省得后面再做协议转换。从实际体验看AXI4接口的追溯性更强。Vivado里的很多IP比如VDMA、AXI DMA、DataMover都是AXI接口。你在框架层面用AXI4就可以像拼积木一样把这些模块接到同一个DDR控制器上。协议层有一点开销但相对于降低的集成成本完全值得。2. 搭环境Xilinx MIG配置和最小的AXI读写通路2.1 第一次配MIG建议修改的3个参数以Vivado里的MIG IP为例创建IP时选择“DDR3 SDRAM”或“DDR4 SDRAM”芯片型号按板卡实际颗粒来选。我第一次配的时候在三个地方踩过坑特意说一下。第一个是Memory Part。必须从列表里找到板卡实际焊接的颗粒型号。颗粒型号差一个字母时序参数就会变calibration大概率失败。如果板卡厂商只写了“512MB DDR3”你得从原理图或颗粒丝印上找到完整型号比如MT41K256M16HA-125。第二个是AXI Data Width。MIG在“AXI Interface”部分会让你选用户侧数据位宽。这个值可以跟DDR物理位宽不同。我建议让它至少等于DDR一次物理突发能传输的数据量。以16bit DDR3颗粒为例BL8意味着一次突发能传16字节。如果AXI数据宽度选64bit一个AXI burst前几个周期携带的数据量还凑合选128bit则一个AXI数据beat正好对应一次DDR突发效率高很多。第三个是AXI ID Width。默认一般是4意味着最多支持16个不同ID。如果只有一两个master默认就够。以后要接入多个AXI主设备时需要规划每个主设备的ID段别全部用同一个ID否则MIG内部没法区分不同请求流也就没法做乱序优化。还有一个容易忽略的是System Clock和Reference Clock频率。MIG内部要用参考时钟来做DDR初始化系统时钟和用户接口时钟往往不是一个频率。建议按照板卡晶振实际频率填不要随便改否则初始化的时序参数会算错。2.2 那些容易搞混的端口名和连接关系MIG生成之后顶层会有一组以DDR4/ddr3开头的物理接口信号比如ddr3_dq、ddr3_dqs_p/n、ddr3_addr、ddr3_ba等。这些信号要按约束文件连到FPGA引脚MIG会自动生成一个引脚约束文件XDC。你的业务逻辑不需要关心这些物理信号。需要关心的只有用户侧AXI接口信号以及几个状态信号。ui_clkAXI接口时钟MIG根据内部PLL生成。你的用户逻辑应该用这个时钟作为读写模块的主时钟不要自己随便创建一个时钟。ui_clk_sync_rst这是复位信号高有效。要注意它是跟ui_clk同步的不能直接用外部异步复位拉低就完事。calib_doneDDR初始化校准完成标志。只有这个信号拉高后才能向AXI接口发起读写请求。axi_awvalid/axi_awready、axi_wvalid/axi_wready、axi_bvalid/axi_bready、axi_arvalid/axi_arready、axi_rvalid/axi_rready五个通道的握手信号。连接关系上MIG的AXI接口本质是个从机所以你的master模块必须把信号连到这些端口上。如果只写testbench测试直接把主设备testbench连上去就行。我见过不少新手拿着示例设计直接把外部100MHz时钟接到sys_clk然后把用户逻辑锁在sys_clk域里操作AXI端口。这个做法是错的。MIG内部会生成一个ui_clk可能是300MHz或400MHzAXI接口所有信号都必须以ui_clk为时钟。外部clk只负责驱动PLL输入。2.3 仿真时先别急着压时序把基础读写跑通第一次做DDR仿真时先别考虑带宽和优化目标定为一个最简单的写读回环向地址0x0000_0000写64个递增数然后立刻从同一个地址读出来比较是否一致。testbench的基本流程是这样给MIG输入系统时钟和复位等待MIG内部PLL锁定。等待calib_done拉高再额外等几十个周期确保内部状态稳定。发送一个AXI写事务同时拉高AWVALID和WVALID给出地址和数据最后一个数据带上WLAST信号。等待BVALID返回这说明DDR控制器已经把数据接进去了。注意BVALID返回只代表写数据被DDR控制器接收不代表DDR物理写操作已经完成但对用户逻辑来说这已经是可用的完成标志。发送一个AXI读事务拉高ARVALID给出读地址等待RVALID每次握手收一个数据直到收到RLAST。比较读数据和写数据。这里我给一个最简的写地址通道握手示例放在SystemVerilog里写逻辑很直观// 主设备发出写地址 always_ff (posedge ui_clk) begin if (!i_rst_n) begin awvalid 1b0; awaddr 0; awlen 0; end else if (awvalid axi_awready) begin awvalid 1b0; end else if (w_req_start) begin awvalid 1b1; awaddr w_req_addr; awlen w_req_len - 1; // AXI4中AWLEN是len-1 end end仿真时让我最意外的一点是MIG的仿真模型在calib_done之前就会开始响应一些请求但其实并不会真正落数据。所以仿真脚本里一定要等calib_done后再发事务否则结果会诡异。3. 读和写看上去简单实质上全是时序细节3.1 AW和W通道的握手不一定是同时的AXI4协议允许写地址和写数据通道独立握手。这意味着从设备可以在还没有收到数据时就先接受地址也可能反过来先收数据再收地址。MIG内部的AXI桥往往带有缓冲AW和W通道的接受顺序并不固定。很多新手在状态机里习惯这样写先发地址等AWREADY拉高再发数据。但MIG不一定按这个顺序响应。如果主设备只拉高AWVALID等待AWREADY同时WVALID一直没拉起来可能一两个周期后从设备返回AWREADY但之后数据通道又迟迟不接收。你要是错误地认为“AW握手成功数据也应该能马上发”就会死等。更安全的做法是把AW通道和W通道的状态机解耦让它们两个独立发送。或者用一个“现场”寄存器记录当前事务的地址和长度只要AW通道握手成功就消除当前地址请求W通道则负责按长度发送数据两边的结束信号再汇聚到一起产生写响应。如果为了省逻辑非要串行至少要在AW握手和W握手之间避免互相依赖。实际调试时我更喜欢在写方向用一个简单的FIFO存地址另一个FIFO存数据AW通道从地址FIFO取地址W通道从数据FIFO取数据。最后B响应回来后再弹出对应的地址这样多条写事务可以并行推进。3.2 读通道是“请求一次、返回一串”读方向稍微简单一点AR通道发出地址后R通道会按照AXI数据宽度依次返回数据。一次burst读传输数据的最后一个beat上会带RLAST。读数据的返回延迟是可变的取决于DDR控制器的调度、bank状态、刷新间隔。对于单主设备、单ID的情况MIG会保证同一ID内的读数据按序返回。也就是你按顺序发AR请求返回的R数据也按同样顺序出现不会乱序。但当你使用多个ID时MIG内部可能会优先调度后面的读请求导致后面的数据先返回。此时如果用户逻辑把所有读数据混在一起用就会出错。我在工程里的处理方式是给每个读请求打一个ID标签返回数据时根据axi_rid信号来判断这笔数据到底属于哪个请求。如果系统里只用了一个固定ID则不需要额外处理。还有一个小坑AR通道和AW通道是独立通道但它们共享DDR控制器的存储阵列。如果你在读请求还没结束时就插入大量写请求可能因为bank反复切换增加延迟。协议本身允许这样但性能不好。后文优化部分再展开。3.3 时钟、复位和calib_done的关系DDR控制器上电后会经历一系列初始化PLL锁定、DDR颗粒模式寄存器配置、DQS相位校准、读写训练。整个流程结束之后calib_done信号会拉高。严格来说这之前访问DDR是未定义行为有些仿真模型会返回数据但板上极大概率出错。我习惯在主设备里做一个等待状态机localparam IDLE 3d0; localparam WAIT_CALIB 3d1; localparam RUN 3d2; always_ff (posedge ui_clk) begin if (!i_rst_n) state IDLE; else case (state) IDLE: state WAIT_CALIB; WAIT_CALIB: if (calib_done) state RUN; RUN: state RUN; endcase end这里要特别注意ui_clk_sync_rst是MIG输出的同步复位它和你顶层给的外部复位不是一个东西。外部复位释放以后MIG会开始工作但用户逻辑不要立即发请求而要等calib_done再等ui_clk_sync_rst保持无效至少几个周期然后再开始读写。如果你在MIG初始化没有完成前就用外部复位强制复位AXI接口有些版本MIG内部的状态机会被打乱导致calib_done永远不拉高。解决办法是外部复位只用来初始化MIG用户逻辑的复位由ui_clk_sync_rst和calib_done组合生成。例如assign user_rst_n ~ui_clk_sync_rst calib_done;这样用户逻辑就不会在DDR没准备好时误操作。4. 我踩过的坑calibration fail、超时和错位4.1 calibration fail不全是硬件设计错MIG上板后最常见的问题就是calibration fail日志。很多人第一反应是PCB布线有问题但实际原因可能很普通。先用Vivado的Hardware Manager看MIG的调试日志它会打印卡在哪个阶段。常见有两种Read Calibration阶段失败或Write Calibration阶段失败。Read Calibration失败往往和DQS眼图、参考电压有关Write Calibration失败则可能是数据线和DQS的相位设置不对。我遇到过的一个案例是颗粒型号选错了。原理图上是MT41K256M16HA-125IP里误选了其他厂商的兼容型号。看起来都是DDR3-1600但内部时序参数和粒度的预充电参数有细微差别结果MIG训练时写DQS中心点找不准最终calibration fail。另一个常见的原因是板卡DDR电源纹波过大。DDR3的VDDQ和VTT对电压质量敏感如果退耦电容不足MIG校准到了错误的位置看似通过实际给后续读写留下隐患。这时用示波器量一下VDDQ纹波如果超过几十mV就要检查电源设计。软件层面可以检查MIG配置里的“System Clock”和“Reference Clock”。这两个频率哪个填错都会导致DDR控制器内部延时计算错误。如果确认硬件和配置都没问题也可以试着在IP配置里把“Select Rounds”增加或者开启“Read DQS Training”的扩展模式有时候能提高校准通过率。最后提醒一点如果某片板卡偶尔能过校准、偶尔失败优先怀疑时钟抖动和电源噪声而不是代码。4.2 第一笔读卡死的排查链路“写能响应读永远等不到RVALID”是很有代表性的问题。我经历过一次花了半天才找到原因。先描述现象我写了一个发读请求的模块拉高ARVALID地址0x0000_0000等待ARREADY。波形里ARREADY能拉高说明地址已经被接受。然后我等RVALID但始终不来。第一步排查确认发送的地址对齐。AXI4要求突发首地址对齐到burst大小。比如AXI数据宽度128bit即16字节那地址低4位必须是0。如果地址是0x0000_0001MIG内部会认为这是一个非对齐访问可能直接挂起不处理也可能返回错误。我最早就是地址没有对齐DMA模块从字节偏移1开始写结果读回永远不对。第二步排查ARBURST是不是设成FIXED了。DDR读写应该用INCR突发FIXED突发会造成所有beat都访问同一个地址明显不符合DDR物理约束。AXI4里FIXED突发一般用于寄存器访问不用于DDR。第三步排查当读卡死时检查写通道是不是也卡住了。我遇到过一次因为WLAST没有拉高MIG的写数据FIFO没收到结束标志整个命令队列被写事务堵住后面的读请求全被阻塞。AXI4的W通道必须由主设备在最后一个数据beat上同时拉高WLAST否则从设备不知道burst何时结束。第四步如果读写都正常但返回数据比预期少检查AWLEN/ARLEN用法。AXI4中长度字段是“len-1”比如要传8个beatARLEN必须写7。写错长度会导致读数据量不一致最后RLAST迟迟不来。4.3 写对了却读错地址、字节使能与数据宽度还有一种情况是DDR读写都成功但读回来的数据和写入的数据对不上。这种问题最隐蔽。最常见的原因是地址递增步长算错。AXI地址在INCR模式下每个beat的地址增量是DATA_WIDTH/8字节不是按1字节递增。比如128bit数据宽度下AWLEN15表示要写16个beat地址范围跨越256字节。如果你在软件或测试逻辑里按总线事务数递增地址就必须乘上这个步长。第二个原因是WSTRB字节使能。MIG支持部分字节写。当你只想写入低8位时WSTRB8h01DDR控制器确实只会把那一个字节写入。但之后读回整块数据时其余字节可能是旧数据校验自然不过。这种情况不是DDR读写错误而是业务逻辑对“部分写”的语义没想清楚。如果不需要部分写就保持WSTRB全1。第三个原因是DDR物理位宽和AXI数据宽度不匹配带来的地址映射问题。以16bit DDR3为例一个AXI 128bit数据对应内部8个DDR突发块。看起来一片连续内存实际上MIG会自己拼。用户侧的地址依然按AXI字节地址递增不会出错。但如果你用ILA去抓native接口看到bank地址跳变会误以为逻辑出错。我建议校验程序用“地址递增全数据递增”的方式比如地址每16字节递增一次每个beat写入128bit数据数据等于低32位地址乘一个常量。这样一旦地址错位立刻能从读回数据看出规律。5. 往上走一点怎么把DDR带宽榨得更满5.1 让burst长度和数据宽度匹配DDR物理位宽能正确读写之后大家都会关心一个事为什么实测带宽只有理论带宽的一半从AXI侧看每次发起burst都要消耗一拍到数拍发送地址。如果burst长度很短比如每次只读4个字符地址开销占比很高。DDR物理层也有类似情况连续读同一个bank同一行时效率最高跨行或跨bank就要做预充电和激活额外消耗时间。因此性能第一条尽量把AXI burst length做长。AXI4支持INCR突发最大256个beat。具体多长合适要看你的数据块大小。我做图像帧缓存时让DMA按行搬运一行的字节数除以AXI步长就是AWLEN1尽量整行作为一个burst。同时还要求AXI数据宽度和DDR物理位宽匹配。比如DDR3颗粒为16bitBL8时一次物理突发16字节。如果AXI数据宽度也是128bit16字节那一个AXI beat正好对应一次DDR突发。这种匹配最好。如果DDR位宽是16bit但AXI数据宽度只有32bit一个DDR突发要被拆成4个AXI周期控制器需要做拼包动作效率会下降。一个简单计算公式单次AXI burst搬运字节数 (AXI_DATA_WIDTH/8) × (AWLEN1)如果你想每笔burst覆盖64字节128bit位宽下AWLEN就该是3因为16×464。但实际还可以更长取决于系统数据粒度。5.2 用ID区分多路请求提升bank级并行度DDR控制器的性能很大程度取决于它能同时调度多少个bank命令。多个master都发请求时如果大家共享同一个IDMIG可能会为了保持顺序而牺牲并行度如果给不同的数据通道分配不同ID控制器就有机会重新排序优先调度那些已经激活的行。我做过一个小设计视频采集DMA和CPU接口同时访问DDR。采集通道用ID 0CPU接口用ID 5。MIG在命令队列里看到两个不同ID内部调度器就会分别处理采集通道连续写不需要等待CPU读完成CPU读也可以插空。实测总线利用率提升了接近20%。当然ID也不是随意配置。MIG的AXI接口有一个ID宽度上限比如4bit最多16个ID。如果某个通道要支持乱序比如多个独立请求同时发那么每个请求还可以用不同子ID区分。但大多数情况下给每个主设备固定一个ID就够了。还有一个容易忽略的点AXI协议支持不同ID的读数据乱序返回。如果你的下游模块只按一个FIFO顺序处理读数据多ID乱序返回会搞乱数据。所以使用多ID之前务必确保接收侧能按axi_rid区分归属。5.3 量化带宽的实测方法不看数据就说性能够不够都是玄学。我会在用户逻辑里放一个带宽计数器用两个信号统计一段时间内传输的数据量。以读方向为例在R通道上axi_rvalid axi_rready为高时一个有效读数据beat。计数器把这个信号的个数乘上AXI数据宽度就是这一段时间读出的字节数。计时基准可以用一个us周期计数器比如100MHz下计时器数到100_000代表1ms。实测带宽计算公式带宽MB/s 有效字节数 / 计时周期你可以分别统计读、写、以及读写混合情况。用ILA抓一段波形看有没有长时间没有VALID READY的空闲窗口。如果这些窗口刚好和DDR刷新或bank切换对齐就说明调度层还有优化空间。另外建议对比理论带宽。DDR3-1600、16bit颗粒的理论带宽是1600MT/s × 2字节 3200MB/s。实测能达到70%到80%已经很健康。如果只有30%大概率是burst太短、地址交错太多或者读写切换频繁。这时可以对应用层做“读写分离”比如采集模块写DDR时读出模块尽量暂停一小段而不是每几十个周期就切一次方向。我在不少项目里最终会把DDR访问封装成一个简单的“连续搬移引擎”只暴露一个“源地址、目的地址、长度、开始”的接口给上层逻辑内部自动生成最长burst、自动维护ID这样既能保证效率也方便以后换平台。最后再分享一个小习惯在MIG用户侧外面加一层AXI Register Slice或Data Width Converter有时候能改善布线时序尤其是AXI接口跑到300MHz以上时。别迷信一步到位的直连条件允许时加一拍流水时序收敛会轻松很多。回读校验则可以多用AXI VIP代替手写testbench它能把地址违例、长度违例直接报出来比肉眼盯波形效率高得多。DDR控制不是写个IP就完事真正决定性能的是AXI层的事务组合先把一帧数据完整写进去再完整读出来再去看效率这个顺序谁试谁知道。
返回列表