拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA多通道DDR3读写控制器设计:从架构到上板调试

在FPGA图像处理这条路走久了你会发现一个绕不开的大件——DDR3读写控制。做摄像头采集、图像缩放、边缘检测、帧差法这些算法时行缓存LineBuffer能解决一部分行内数据的暂存问题可一旦涉及整帧缓存、多路数据并发读写片上RAM那点容量根本不够看。我最早是从MIG生成的example design起步的后来项目要求在FPGA里做多通道图像数据调度MIG那套接口裸用起来实在太痛苦命令通道、数据通道全绑在一起多路视频流上去之后优先级根本没法精细控制。所以就动了手基于纯Verilog重写一套DDR3读写控制逻辑支持多通道图像数据读写模块接口尽量清晰方便后续集成。这篇就把整个设计和调试过程记录下来给同样在做FPGA图像处理的朋友一个参考。这个项目解决的核心问题很简单让FPGA里多个图像数据源比如两路摄像头写入、一路显示刷新读取能同时访问DDR3不互相干扰而且读写效率尽量高。它适合两类人看一类是刚学完Verilog语法、想做个有点含金量的项目练手的同学另一类是项目中正被MIG接口或多通道仲裁折磨的工程师。下面全部内容基于我实际调试和上板的经验每一步都踩过坑写出来的都是验证过的方案。1. 为什么DDR3读写控制是图像处理绕不开的坑1.1 图像数据的带宽需求与现实瓶颈先算一笔账。以1080p30fps的YUV422格式为例一帧原始数据是1920×1080×2字节约3.96MB30帧每秒就是118.7MB/s的吞吐。看起来不大但做图像处理时往往不止一路两路采集加一路显示输出光纯数据搬运量就要奔着300MB/s以上去。如果在算法上再做3×3卷积、多帧缓存、金字塔降采样这类操作每帧数据会被反复读写带宽需求轻松翻倍。FPGA内部的BRAM总容量以主流的中等规模芯片来看也就几兆比特级别撑死几百KB。存两行数据做行缓存没问题存一帧1080p图像根本不可能。DDR3的优势就在于容量大、价格便宜、接口成熟单颗512MB到1GB的DDR3颗粒配合64bit位宽理论带宽能到6.4GB/sDDR3-800频率、双沿采样下所以它成了FPGA视频处理板卡上最标准的外部存储配置。但DDR3有一个天然的脾气它是随机存储不是FIFO。你在读写时不能像访问BRAM那样给定地址就完事它需要先激活行ACTIVATE、再发送列地址读写、完成后还要预充电PRECHARGE同一时刻同一bank只能有一个行打开。再加上DDR3必须定时刷新tREFI为7.8微秒这些操作都会占据命令总线。如果不做精细调度实际带宽可能连理论值的30%都用不满。1.2 纯Verilog与厂商IP方案的取舍说到DDR3控制器很多朋友的第一反应是直接用厂商IP。确实Xilinx的MIGMemory Interface Generator或者Intel的EMIF都是成熟方案初始化训练、物理层校准、时序校准全都封装好了用起来非常省事这也是绝大多数商业项目走的路。那为什么我还要自己写一套纯Verilog的控制逻辑核心原因是MIG这类IP提供给用户的是“一个完整的内存接口”而不是“一套灵活的内存调度策略”。MIG的接口支持多个命令同时下发、乱序返回这套机制对于CPU或DMA这种通用场景是极好的但在多路图像数据流场景下并不顺手。图像数据是严格顺序的流式访问我需要在同一个控制器内让A通道的写入不要堵住B通道的读取还要保证显示刷新通道的优先权。如果基于MIG再包一层多通道仲裁器等于在别人定义好的规则里做二次调度约束太多出了问题还难以调试。这里得澄清一下“纯Verilog”这个词。真正从零手写DDR3物理层包括电平校准、DQ/DQS训练、ODT配置工作量巨大而且几乎没有必要商用。我的做法是控制器主体完全用Verilog手写——包括命令生成、bank管理、仲裁调度、读写数据通路——物理层则调用厂商提供的原语或低层PHY接口。这样的好处是逻辑层完全可控任何一条命令的产生都能回溯到自己的状态机里上板定位问题比对着MIG黑盒高效得多。2. DDR3控制器的模块划分与接口定义2.1 五层架构从用户请求到DDR3颗粒写DDR3控制器最忌讳的就是把状态机铺成一个巨型FSM。命令调度、数据通路、初始化训练、物理时序全塞在一起仿真都跑不通。我采用的方案是五层结构层与层之间用FIFO或valid/ready握手信号隔离。第一层是用户接口层。面向图像处理模块提供简单的读写请求接口所有通道都复用这一层协议。第二层是多通道仲裁层根据各通道的优先级和配额决定谁的命令进入下一级。第三层是命令调度层负责将仲裁产生的访问请求拆成DDR3命令序列包括行激活、读写命令、预充电、刷新插入。第四层是时序控制层生成DDR3所需的时钟、片选、行列地址、命令使能等信号。第五层就是物理层对应的就是DDR3颗粒本身。各层职责分工如下表所示层级核心职责关键设计点用户接口层向业务模块提供读写端口隐藏DDR3细节valid/ready握手地址连续自增多通道仲裁层按优先级和配额调度多路请求轮询优先级组合单次最多连续4次调度命令调度层将地址拆分为bank/row/col生成ACT/RD/WR/PRE命令维护bank状态表合并同row访问时序控制层满足tRCD/tCL/tWTR/tRP等参数约束使用延迟计数器和发布队列物理层对接DDR3引脚的电气时序直接调用厂商原语不做自研训练2.2 用户接口信号设计整个项目的核心目标之一是“模块接口清晰”所以我在用户接口层设计上花了很多心思。接口协议参考了AXI的valid/ready握手思路但去掉了AXI复杂的通道分离和ID管理只保留最必要的信号。这样一个图像处理模块只要看到app_wr_cmd_valid拉高且ready为高就可以把数据放入写数据通道非常直观。这里给出一份简化版的接口定义信号方向含义app_wr_cmd_valid / app_wr_cmd_ready写通道写命令握手app_wr_cmd_addr[27:0]写通道写起始地址按32B对齐app_wr_cmd_len[5:0]写通道写突发长度单位是32字节app_wr_data_valid / app_wr_data_ready写数据写数据握手app_wr_data[63:0]写数据写数据总线app_rd_cmd_valid / app_rd_cmd_ready读通道读命令握手app_rd_cmd_addr[27:0]读通道读起始地址app_rd_cmd_len[5:0]读通道读突发长度app_rd_data_valid / app_rd_data_ready读数据读数据握手app_rd_data[63:0]读数据读数据总线地址按32字节对齐是因为DDR3的burst length为8时64bit位宽下一次突发正好传输64字节。我取32字节为一个最小粒度保证图像行拼接时地址对齐简单不会出现跨行错位的问题。2.3 为什么用了本地接口而不是AXI4有朋友会问既然Xilinx的MIG都支持AXI4接口为什么不直接套AXI4这要分场景看。AXI4的优势在于多master、乱序返回、outstanding transaction管理这些在SoC总线场景下是硬需求但图像流的访问模式极其规律一个通道在写一帧图像时地址是连续递增的显示读取时地址也是按固定偏移连续扫描。这类顺序访问用AXI4的复杂机制纯属杀鸡用牛刀反而会引入额外的延迟。另一个实际原因是调试成本。AXI4接口的信号数量比这套简化接口多一倍以上示波器或ILA去抓一次读写时序时要盯的信号多到头皮发麻。简化的本地接口四个通道握手逻辑一眼就能看明白哪里卡住了。当然如果后续要把这套控制器接到PS端或者带CPU的系统里再加一个AXI桥接层就足够了控制核心不用动。3. 多通道图像数据读写仲裁的设计3.1 多通道场景分析这个项目实测跑的是三通道场景通道0是摄像头0写入帧缓存区A通道1是摄像头1写入帧缓存区B通道2是显示刷新控制器从帧缓存区A或B读取数据显示。摄像头写入是持续不断的每一行数据攒够就发起一次写请求显示刷新则受限于行同步信号每行固定时间发起读请求。这里有个容易被忽略的细节三个通道虽然带宽总和不算高但它们对延迟的敏感程度完全不同。摄像头写入可以忍受轻微延迟只要FIFO不溢出就行但显示刷新绝对不能等一旦读数据回来晚了屏幕上就会出现撕裂或闪线。所以仲裁不能只做简单的轮询必须有优先级倾斜。3.2 轮询加优先级组合仲裁我的仲裁器实现思路是显示刷新通道读通道分配最高优先级两个摄像头写入通道使用带配额的轮询。这样即使两个写入通道同时有请求也不会长期占用总线而读通道一旦拉高请求会在下一个仲裁周期立即获得总线保证显示刷新不卡顿。仲裁状态机的核心代码片段大致如下// 三通道仲裁器通道2为最高优先级 always (*) begin case (grant_state) IDLE: begin if (rd_req) grant CH_RD; else if (wr0_req) grant CH_WR0; else if (wr1_req) grant CH_WR1; else grant CH_NONE; end // 当前通道忙碌时保持否则轮询 ... endcase end3.3 带宽分配与防饿死机制只靠优先级有一个隐患如果两个写入通道持续不断发请求显示刷新通道当然每次都能抢占总线但写入通道可能被长时间饿死导致摄像头FIFO溢出丢帧。解决办法是给每个通道设置最大连续调度次数。每次仲裁获胜后最多连续执行4笔突发传输然后无论是否还有请求都必须释放总线重新仲裁。这样即便显示刷新通道每次都赢它在两次刷新读请求之间的空档期内写入通道也有机会插入访问。实测下来这套组合仲裁的效果很好。用ILA抓线上总线统计三通道同时工作时的总线利用率大约在65%左右DDR3读写命令的间隔非常均匀没有出现一卡一卡的现象。值得额外说一句的是带宽充裕并不代表不需要仲裁。就算DDR3理论带宽远大于三通道需求的合计但如果不控制访问顺序读写频繁交替会导致tWTR和tRTW这些总线转换时间被反复触发实际吞吐可能暴跌三成。仲裁器的价值不只是“谁先谁后”还包括“怎么排能让总线转换次数最少”。4. 关键时序控制刷新、bank管理与读写调度4.1 刷新机制不能糊弄DDR3要求所有bank在64毫秒内完成8192次刷新平均下来每7.8微秒就要处理一次刷新请求。刷新时命令总线会被占用正在传输的数据也要等刷新完成才能继续。对于图像这种持续高速流刷新如果实现不当轻则带宽抖动重则数据写坏。我采用的是分布式刷新策略。一个refresh_timer计数器不断累加当计数到达7.8微秒对应的时钟周期数时就向命令调度层插入一个高优先级的刷新请求。仲裁层必须无条件立刻响应刷新请求因为它的事关数据可靠性。同时在刷新请求还没发出但即将到达的窗口内我会让数据调度提前做完当前burst避免出现刷新命令与正在传输的burst冲突。4.2 Bank管理策略决定效率天花板DDR3内部通常有8个bank同一bank内切换行需要先预充电再激活耗时会拉高不同bank之间则可以流水线操作一个bank在做读写传输时另一个bank可以提前激活。所以bank管理的核心是尽量让连续访问落分布在不同的bank上减少bank冲突。我的做法是为每个通道分配固定bank集合。比如摄像头0写入的数据地址映射时固定落在bank0和bank1区间摄像头1的写入落在bank2和bank3显示刷新读取在bank4到bank7间轮转。这样多通道同时访问时大多数情况落在不同bank上行激活开销大幅下降。但这个策略有一个代价单通道可用的bank数变少了如果一路摄像头要连续写16行才能凑满一帧它就只能在这两个bank之间乒乓切换。我实测下来这个代价可以接受因为DDR3的bank切换延迟远小于同一bank内的行切换延迟且通道数不多、burst长度足够时乒乓切换的间隙正好被其它通道填充。4.3 读写调度顺序优化DDR3的读写总线是共用的一旦从写模式切到读模式中间必须插入写恢复时间tWR和读延迟tWTR反过来也有等效的转换惩罚。所以仲裁器调度时要尽量避免读写交替。我在命令调度层加了一个读写合并逻辑如果当前连续收到多个写请求就把它们尽量合并执行同理读请求也尽量合并。只有当另一端请求的等待时间超过预设阈值比如256个时钟周期时才强制切换总线方向。这个阈值不能设太大否则对端延迟会增大也不能太小否则总线反复横跳效率反而低。实测1080p三通道场景下阈值设为400个时钟周期总线吞吐最高。5. 仿真验证与上板调试实录5.1 ModelSim仿真环境搭建控制器写完后不要急着上板先用仿真把功能验证清楚。仿真环境用ModelSim或Vivado Simulator都行关键是要有DDR3的仿真模型。厂商IP包里有现成的DDR3 model也可以直接从Micron/美光的官网下载对应型号的Verilog模型。仿真时最容易踩的坑是DDR3初始化过程。DDR3颗粒上电后要经历复位、训练、模式寄存器配置等一系列流程完成后才能正常访问。我的初始化状态机严格按照JEDEC标准实现先拉低复位信号至少100微秒然后发送NOP命令、执行Training这里用厂商PHY的初始化完成信号代替自研训练、配置MR0/MR1/MR2三组模式寄存器最后等PHY层输出init_done标志。给出一段伪代码描述初始化流程localparam INIT_RESET 3d0; localparam INIT_TRAINING 3d1; localparam INIT_MR0 3d2; localparam INIT_MR1 3d3; localparam INIT_MR2 3d4; localparam INIT_DONE 3d5;仿真时务必等待init_done拉高后再发起读写激励否则发送的任何命令都会被DDR3忽略读回来的数据全是未知态——这个现象很容易被误判为控制器逻辑错误。5.2 常见问题排查速查表调试过程遇到的问题我整理了一份排查表基本覆盖了FPGADDR3项目最常见的故障点问题现象原因分析解决思路读数据全部为0或高阻DDR3初始化未完成命令被忽略检查init_done信号是否拉高后再发命令某通道数据偶尔错位地址对齐错误burst长度和实际数据位宽不匹配确认地址是否按32B对齐len计算是否正确跑一段时间后系统卡死刷新漏失或仲裁死锁检查refresh定时器是否有溢出仲裁状态机是否所有状态都有出口带宽明显偏低低于40%读写交替频繁bank冲突严重调整读写合并阈值检查bank映射策略上板现象摄像头花屏数据时序约束没加或DDR3布线不规范补加create_clock约束检查飞线阻抗匹配5.3 ILA抓波形和上板带宽实测上板调试时ILA集成逻辑分析仪是你的眼睛。我习惯把DDR3控制器的命令总线、数据总线、仲裁状态机状态量全部挂在ILA上触发条件设置为某个通道写屏蔽信号下降沿这样就能抓到一次完整的多通道读写交互过程。实测的一组数据很有参考价值在DDR3时钟频率400MHz等效800MT/s、64bit位宽条件下理论峰值带宽6.4GB/s三通道图像流并发时实测有效带宽约4.2GB/s利用率接近66%。这个数字在自研控制器层面已经算不错了而且各通道延迟都在预期范围内。如果把DDR3频率提到533MHz等效1066MT/s突发效率还能再提升但布线约束会更严格板子层叠和等长控制不到位反而容易出问题。关于DDR3布线简单提一句如果你是自己画板子而不是用现成开发板走线阻抗控制单端50欧、差分100欧和组内等长DQ/DQS组内控制在±5mil内这两条是底线做不好后面调起来会非常痛苦。我见过最极端的例子一块板子因为DATA组等长偏差过大DDR3读写偶尔报错跑半天才复现一次定位到糊涂。6. 从图像场景扩展出去的思考这个控制器虽然最初是为图像数据多通道访问设计的但架构上是通用的。只要把用户接口层的请求源换掉它可以服务DMA搬运、以太网帧收发、ADC采样数据缓存等场景。模块接口清晰的好处就在这里顶层想加一个通道只需要在仲裁器里增加一个端口和对应的优先级权重命令调度和数据通路完全不用动。后续如果考虑把这套控制器接到应用处理器上可以在用户接口层外面包一个AXI桥。桥的主要工作是地址映射和命令翻译把AXI的burst请求拆成本地接口的连续地址访问其余逻辑保持不变。我在项目里已经预留了这部分接口余量实测从握手协议到AXI桥转换只花了两天时间大部分精力都花在调试AXI的outstanding机制上。最后再分享一个细节写地址和数据通道之间要有FIFO同步。图像源写入数据是连续流但仲裁器对命令是离散调度的两者速率天然不匹配。我在每个通道入口都放了一个小容量FIFO图像行缓存一行大小即可用水位信号产生请求。水下时停止发送请求水上时批量发出。这个设计看似简单却解决了百分之八十的数据通路亚稳态和背压问题。做FPGA开发就是这样技术方案看起来都是公开的但真正让系统跑稳的往往是一个又一个不起眼的细节——仲裁器里那个超时阈值、bank映射表里那个顺序、刷新定时器那个提前量。这套DDR3控制器从仿真到上板花了大约三周时间中间经历了数据错位、带宽不足、偶发卡死各种问题最后稳定跑起来的那一刻前面踩的坑都值了。如果这篇记录能帮你少走几次弯路那我就没白写。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门