做FPGA开发的人,十有八九会碰到这样一组需求:8路图像或者采集通道,既要同时往DDR3/DDR4里写,又要同时从里面读。Xilinx平台上的多通道读写架构本身不难搭,难点全在防冲突设计上。很多人一开始会想,DDR带宽那么大,8个通道随便分分也够用。真正做过一轮才发现,多通道同时访问DDR时,冲突无处不在,效率可以从理论值的80%直接跌到20%,严重的时候甚至死锁、丢数、数据错乱。这篇内容我就围绕一个真实做过的稳定工程“Xilinx DDR4/DDR3多通道读写防冲突设计:8通道同时读写且不冲突的稳定工程”展开,把思路、参数、代码和踩坑过程全部盘一遍。
做Xilinx平台多通道DDR访问的老手都知道,DDR控制器本身并不难例化,MIG IP核点两下就能生成一个可用的控制器。真正的分水岭在于:8个通道同时发起读写时,谁先谁后、谁占多少带宽、怎么避免Bank冲突、怎么保证读数据不乱序、怎么在上电和长时间运行后依然稳定。这套东西如果没有想清楚,稳定性就是一句空话,测试跑两天两夜也会在某个角落给你翻车。
1. 项目到底要解决什么问题:8通道并发访问的冲突根源
1.1 通道模型与项目指标
先把这个项目的需求边界说清楚。所谓8通道,指的是用户侧存在8个独立的数据源,每个数据源都需要访问同一个DDR存储空间。常见场景是8路视频/图像采集,每路采集数据持续写入DDR环形缓冲,同时算法或显示端需要从DDR读出数据进行处理。每个通道既是读方也是写方,读写比例大致相等,突发粒度从64字节到1KB不等。
给定指标很明确:8通道同时进行读写,总有效带宽不低于6GB/s,单通道平均延迟抖动不超过10%,系统连续运行72小时无错误。DDR型号在DDR3和DDR4之间做了兼容设计,以DDR4-2400、位宽64bit为主目标,同时能在DDR3-1600上降级运行。
如果只做一个通道访问DDR,这些问题根本不存在。MIG控制器本身会处理行激活、预充电、刷新调度,用户只需要按照AXI协议把请求发出去,剩下的交还给硬件。但8个通道同时发起读写请求时,控制器同一时刻只能接收一个命令,同一时刻的总线上只有一个方向的读数据或者写数据在流动。谁先被服务、谁要等多久,全由上一层仲裁逻辑决定。这个“仲裁逻辑”,才是整个工程的灵魂。
1.2 冲突的三个来源:Bank冲突、读写转向、带宽抢占
DDR和SRAM的根本区别在于,它并不是一个“随时可取”的存储。每次访问一个地址,必须先激活对应的行(Row),激活之后的读写操作都必须停留在这一行上,如果要跳到另一个行,必须先预充电(Precharge),然后再激活新行。这一系列操作带来固定延迟,比如DDR4的tRCD约14ns,tRP约14ns,tRFC更是动辄几百纳秒。如果一个控制器面前有8个通道分别访问不同的行和不同的Bank,那么仲裁器每一次换通道,都有可能让DDR控制器被迫做一次“关行开行”的动作,效率自然一落千丈。
读写转向是第二个容易被忽略的冲突源。DDR总线的数据引脚是双向的,从写切换到读,或者从读切换到写,需要插入额外的总线周转周期(Bus Turnaround)。如果仲裁策略设计得不好,8个通道里4个读4个写轮流请求,控制器就会在读写之间反复横跳,每次横跳都有几个周期的浪费。全局来看,这种浪费不致命,但和Bank冲突叠加起来,实际吞吐率很难看。
第三个冲突源是带宽抢占。仲裁说白了就是一个资源分配问题:同一时间有多个请求者,但要决定让谁先用总线。如果只用简单的固定优先级,那么高优先级通道持续有请求的话,低优先级通道可能很长时间都拿不到总线,这在嵌入式系统里属于典型的“饿死”问题。8通道应用里,任何一个通道被饿死都会造成该路数据断流,反映到系统层面就是图像花屏、丢帧、采集超时。
这三个冲突源,其实对应了三个层面的设计决策:如何分配Bank和地址,如何调度读写顺序,以及如何做通道带宽调度。下面各节逐层展开。
2. 吃透DDR时序和MIG行为,再谈怎么抢总线
2.1 DDR3与DDR4的关键差异
项目最开始设计时,我同时在DDR3和DDR4之间要做一个判断。虽然后来用兼容方案解决,但不理解两者差异,方案设计就会走弯路。DDR3和DDR4看起来接口类似,时序模型差异非常大,尤其是Bank管理方式。
DDR3内部有8个Bank,每个Bank独立做行激活和预充电。DDR4则引入了Bank Group的概念,分为4个Bank Group,每个Group内部有4个Bank。更重要的是,DDR4可以从不同的Bank Group同时读写,也就是说,如果仲裁器能保证连续命令落在不同的Bank Group上,DDR4可以利用后台并行性,减少命令间延迟。这意味着针对DDR4设计仲裁器时,“Bank Group感知”比“Bank感知”更值得优先考虑。
另外,DDR3和DDR4的命令编码、时序参数(tCK、tRCD、tRP、tRFC)也都不同,但MIG IP核把这些都封装好了,真正需要用户关心的是UI时钟频率。以DDR4-2400为例,如果UI采用4:1模式,用户侧时钟是300MHz,用户侧数据位宽是256bit,MIG一次突发传输长度是8个256bit字,即512字节。选择4:1还是2:1模式,直接影响后续FIFO位宽和仲裁粒度的设计,这一点要在工程规划初期就定下来。
| 对比项 | DDR3-1600 | DDR4-2400 |
|---|---|---|
| Bank数量 | 8 | 4个Bank Group×4个Bank |
| Bank并发性 | 支持后台Bank预充电 | 支持Bank Group间读写并发 |
| 命令架构 | 传统RAS/CAS命令 | 引入ACT命令,命令时序更复杂 |
| 典型tRCD | 13.75ns | 13.32ns |
| 运行速度 | 800MHz时钟,1600MT/s | 1200MHz时钟,2400MT/s |
| 用户侧UI时钟(4:1) | 200MHz(若32bit位宽) | 300MHz(若64bit位宽) |
实际使用中,DDR3的Bank冲突危害比DDR4更明显,因为DDR3没有Bank Group级别的并行调度。但反过来,DDR3的时序约束不如DDR4严格,布线压力小一些。要在同一块板上兼容两种颗粒,最终选择以DDR4时序为约束目标,DDR3仅作降级运行验证。
2.2 MIG用户接口的隐含行为:Outstanding与乱序
很多初学者把MIG生成的AXI接口当成普通RAM接口来使,发一个读请求就死等读数据返回。这种用法在单通道场景下没有问题,在8通道并发下性能会非常差,因为MIG内部命令队列一旦空转,DDR控制器就无法调度命令,总线利用率大打折扣。
MIG的AXI接口允许Outstanding请求,也就是在第一个读数据没有返回之前,用户可以继续发起多个读请求。MIG内部有一个命令缓冲,这些请求会被自动排入DDR控制器的内部队列,通过Bank管理和调度算法优化执行顺序。项目里把MIG配置中的Outstanding命令数设为32,对应8个通道平均每个通道4个Outstanding请求,这样即使某个通道因为外部原因短暂停供,MIG命令队列依然饱满,DDR颗粒不会空转。
要特别注意的是乱序返回的问题。DDR控制器内部为了效率会对命令做重排,读请求A和读请求B同时发出去,可能B的数据先返回。在AXI接口上,读数据通道没有ID机制的时候就无法区分返回的是哪个请求的数据。所以MIG的AXI接口默认打开了Read Data Reordering,支持多个读ID和乱序返回。这就要求仲裁器必须给每个通道的读请求分配不同ID,同时用户侧每个通道必须有独立的读数据缓冲,按ID归位之后才能向通道释放数据。
项目里最初就是没注意乱序返回,结果出现数据“串路”:通道0读出来的数据跑到通道1的FIFO里。排查了半天,最后发现是ID分配和读数据缓冲不对应。这个坑在后面会进一步讲。
2.3 带宽估算:理论带宽和实际可用带宽之间隔着一个仲裁器
这个工程的实际带宽目标定在6GB/s,很多人第一反应是随便做做就能超过。DDR4-2400、64bit位宽的理论带宽是19.2GB/s,就算效率打对折也有9.6GB/s。但这里有个关键点:理论带宽是DDR颗粒的连续读或者连续写的极限带宽,而实际应用中读写混合、Bank切换、刷新、多通道切换,这些因素都会让效率打折。
以一个典型场景估算:8个通道,每个通道每1ms写入128个64字节块,读回128个64字节块。64字节块在256bit位宽下正好是2个突发,如果刚好落在同一行,效率在80%以上;如果每个块落在不同Row,每次都要先预充电再激活,简单算一下就知道时间根本不够用。所以带宽规划必须把Bank亲和性写进仲裁策略里,同一通道连续访问相同Bank时,尽量不打断。
我做了一个粗略的可视化测试:同样8通道读写比例1:1,轮询仲裁下实测吞吐是理论值的46%,加入Bank感知之后升到63%,再加入读写字序优化,最终到71%。这还是在未做地址交错优化时的情况。所以工程规划一开始,按理论值的60%至70%做设计余量是最稳的。
3. 仲裁器设计:8通道防冲突的核心手段
3.1 轮询仲裁、加权仲裁与Bank感知仲裁
接下来是整个项目最核心的部分:仲裁器。防冲突设计的核心目标,不是让8个通道“同时发出请求还能同时被服务”,那是物理上做不到的。真正的目标是:让8个通道的请求在时间轴上排列有序,连续访问同一个Bank或者同一行,最大化DDR控制器的行命中率。
最简单的策略是轮询仲裁。仲裁器按通道0到通道7循环扫描,哪个通道有请求就服务哪个。这个策略好处是公平,各通道带宽均等,不会有饿死现象;坏处是完全不看Bank行状态。如果一个通道请求落在Bank3行A,另一个通道请求落在Bank3行B,轮询仲裁会交替服务两者,DDR控制器就反复在行A和行B之间来回切换,行命中率几乎为零。
加权仲裁在轮询基础上给每个通道分配不同权重,比如通道0权重4,通道1权重1,那么通道0可以连续服务4次后轮转到通道1。这种策略适合带宽不均衡的场景,但不解决Bank问题。
Bank感知仲裁是在选通道之前,先查看MIG反馈的“当前活动Bank”信息。如果多个通道都有请求,仲裁器优先选择目标地址落在当前活动Bank的那个通道;如果没有,则优先选择落在不同Bank Group的通道,利用DDR4的多Bank Group并行性。这个策略可以将行命中率维持在60%以上。
最终方案是一个组合策略:基础轮询保证公平,Bank Group感知优先权做第一级筛选,再叠加短突发(不超过4个突发)限制,防止单个通道长期占用总线。仲裁级别从高到低依次是:Bank Group匹配度、通道优先级、轮询指针。
3.2 读写字序优化:减少总线周转
读和写在DDR总线上切换需要额外延时,所以仲裁器还要考虑“当连续服务同一通道时,尽量让读和写各自成组”。这种做法的专业叫法是Read/Write Grouping。可以在仲裁器里设置一个状态寄存器,记录上一次服务的是读命令还是写命令。如果上一次是写命令,新到的请求中优先选择同样为写命令的通道;只有当同向请求耗尽,才切换到反向。
这个优化策略在8通道场景下效果非常明显。我实测过一组数据:同样是8通道各25%读25%写(其余为等待状态)的请求模式,不分组时DDR总线周转周期占总时间的12%~15%;分组之后,周转周期降到3%以下。代价是单次读写的延迟从均值的均匀分布变成偏斜分布,但对图像采集这种流式场景来说完全可接受。
仲裁器内部还留了一个“紧急通道”输入。通道5作为实时性要求最高的路径,可以在极端情况下强制插入,但不允许连续插入超过一次,防止插入过于频繁打破读写分组。这个设计是调了一个星期之后补上去的,算是实践中被逼出来的功能。
3.3 仲裁状态机的Verilog实现
说了这么多理论,最终还得落到代码上。仲裁器的核心是一个状态机,每个时钟周期检查一次8个通道的请求信号,经过三级筛选后产生最终grant信号。下面给出一个简化的示意代码,主要展示轮询+Bank感知的融合方式:
// 简化版仲裁器状态机:轮询基本公平 + Bank Group优先 module ddr_arbiter #( parameter N_CH = 8, parameter AXI_ID_WIDTH = 4 )( input wire ui_clk, input wire ui_rst_n, input wire [N_CH-1:0] req_valid, input wire [N_CH-1:0] [27:0] req_addr, input wire [N_CH-1:0] req_is_write, input wire [3:0] active_bank_group, output reg [N_CH-1:0] grant ); reg [2:0] round_robin_ptr; reg [3:0] last_cmd_is_write; // 判断请求地址属于哪个Bank Group function [1:0] get_bank_group; input [27:0] addr; // 假设DDR4地址映射:Bank Group位在addr[15:14] get_bank_group = addr[15:14]; endfunction always @(posedge ui_clk or negedge ui_rst_n) begin if (!ui_rst_n) begin grant <= 8'd0; round_robin_ptr <= 3'd0; last_cmd_is_write <= 1'b0; end else begin grant <= 8'd0; // 第一级筛选:优先选择目标Bank Group匹配当前活动Bank Group的通道 // 第二级筛选:匹配方向(读/写)的通道 // 第三级筛选:轮询指针 for (int i = 0; i < N_CH; i = i + 1) begin if (req_valid[i] && get_bank_group(req_addr[i]) == active_bank_group && req_is_write[i] == last_cmd_is_write) begin // 从轮询指针开始找,保证公平 if (i == round_robin_ptr || grant == 8'd0) begin grant <= (1 << i); end end end end end always @(posedge ui_clk or negedge ui_rst_n) begin if (!ui_rst_n) round_robin_ptr <= 3'd0; else if (grant != 8'd0) round_robin_ptr <= grant2chn(grant); // 更新指针到当前服务通道 end endmodule这段代码核心思路是:先计算当前活动Bank Group,再从请求队列里找与活动Bank Group匹配的通道。如果找不到,退而求其次匹配读写方向;再找不到,纯轮询选一个。注意代码里省略了权重寄存器、紧急通道逻辑和AXI ID分配,实际工程代码大约比这长三倍,但这已经足够说明仲裁方向了。
3.4 AXI通道握手与ID分配:仲裁器落地最容易出错的地方
仲裁器选出一个通道之后,并不是简单地给一个grant信号就算完事。MIG的AXI接口有四条独立通道:写地址通道(aw)、写数据通道(w)、读地址通道(ar)、读数据通道(r)。仲裁器的grant需要被拆解成“地址阶段”和“数据阶段”两组握手。
写地址阶段:选出的通道把写地址放到AW通道上,等待MIG返回AWREADY。这个阶段往往很快,只要MIG命令队列没满,基本一拍就过。写数据阶段:根据选出的通道,把相应的写数据按照WLAST标志送完一笔突发。MIG在收满一个完整突发后才会释放写地址通道的占用。因此仲裁器要在“突发长度”边界上做计数,防止通道切换发生在数据流的中间。
读通道的ID分配更为关键。每个通道分配一个固定ID,仲裁器在发起读地址请求时,把对应通道的ID放到AR通道上。读数据返回时,RID字段会标识这比数据属于哪个通道。用户侧再根据RID把数据写入对应通道的读FIFO。这样即使MIG内部乱序返回数据,也不会发生数据串路。
说到这插一句踩坑经验:最早版本我把RID分配做错了,两个相邻通道用了同一个ID。结果低速测试时数据全对,一旦并发压力上来,MIG内部重排一开,通道0的数据就会间歇性出现在通道1里。当时抓波形抓了两天,最后点上RID和通道号之间的对应关系,才定位到问题。所以ID分配这个环节,设计文档里一定要有一条铁律:每个通道独占一个ID,禁止复用。
4. 数据通路与缓存设计:让仲裁结果真正落地
4.1 为什么每个通道必须有一个独立的异步FIFO
仲裁器解决了“谁先用总线”的问题,但数据通路如果没有设计好,仲裁结果也发挥不出来。具体来说,8个通道的输入时钟可能各不相同,有的来自ADC采样时钟,有的来自图像传感器像素时钟,而MIG的UI时钟固定为300MHz。通道数据要进入DDR,必须经过跨时钟域处理,最简单的做法就是每通道一个异步FIFO。
异步FIFO放在通道与仲裁器之间,承担两个任务:一是跨时钟域,把通道数据从采集时钟同步到UI时钟域;二是缓冲突发,让DDR能够以整突发方式写入,避免碎片化访问。FIFO深度不是随便定的,要按“最大写入突发量和DDR服务等待时间”的乘积来估算。
假设通道的数据速率为200MB/s,DDR侧UI时钟300MHz,用户侧数据宽度256bit,对应带宽约9.6GB/s。当其他通道占用总线时,一个通道最多可能等待几百个周期。为了保险,每个通道FIFO深度定为512×256bit,即16KB,换算成数据速率200MB/s能缓冲约80微秒的数据,比最坏等待时间长出近一倍。实测下来这个深度在正常仲裁情况下很少用超过一半。
4.2 水线、反压与Write Leveling
异步FIFO有两个方向需要做流量控制。写方向:通道数据塞进FIFO之后,如果FIFO快满了,需要向上游发送反压信号。读方向:仲裁器从FIFO读出数据送到DDR的时候,如果DDR的写数据通道临时不接受(WREADY拉低),FIFO的读使能必须停住。
FIFO水位可以分两级设置。低水位阈值设为128,高水位阈值设为384。通道侧达到高水位后,上游立即停止产生数据;降到低水位后恢复。两级水线中间留的余量,是为了应对上游停止数据的传输延迟。视频类通道如果直接连摄像头,反压信号最好再接入帧同步信号,做到按帧级反压,而不是像素级反压,否则容易导致同一帧内出现撕裂。
关于MIG的复位时序,这里必须提一句:MIG每次上电或者复位之后,内部的DDR控制器会自动跑校准流程。校准完成前,用户侧所有AXI访问都会挂在READY信号上。设计时,所有通道的FIFO都要等待MIG的calib_done信号拉高之后才允许启动写入,否则请求会堆积在FIFO里,一旦MIG校准完成又立刻涌入大量请求,容易造成瞬时过载。实际工程里我专门做了个握手状态机,calib_done之后先发一个64字节的空读请求,确认AXI链路真的通,再放量跑数据。
4.3 地址映射:让8个通道的地址天然分离
仲裁器再怎么优化,如果地址映射做不好,行冲突依然会高发。8个通道的地址分配应该遵循一个原则:同一通道连续的数据流落在同一Bank甚至同一Row,不同通道的数据流落在不同Bank Group。
具体做法是在地址低位预留通道号位。以256bit位宽的突发为基本单位,一次突发正好对应一个Bank内的连续8列地址。把通道号编码到Bank Group地址位,让通道0固定访问Bank Group0,通道1固定访问Bank Group1,以此类推。这样8个通道天然落在8个Bank Group上,DDR4的多Bank并行可以发挥最大效率。
当然,这种固定映射会让每个通道的可用空间只有总容量的1/8,对某些应用可能不够。更灵活的方案是做成地址哈希表,或者每个通道动态维护一个Base地址寄存器,仲裁器在地址提交时动态计算Bank Group并记录在表里。固定映射适合可预测的循环缓冲,动态映射适合数据长度不定的多通道复用。项目中最终选了固定1/8映射,原因很简单:采集系统每个通道需要的数据容量大致相等,1/8划分并不会浪费。
4.4 数据校验与ECC:稳定工程的最后防线
DDR工作在高频下,特别是DDR4-2400,数据传输过程中出现单bit翻转的概率比很多人想象的高。项目里MIG开启了ECC功能。对于64bit数据宽度,ECC需要额外的8bit存储空间,MIG会自动处理校验位的读写,用户侧完全透明。
ECC开启之后,MIG的读数据返回路径上会多一级ECC校验延迟。虽然MIG文档上写的是“几乎不消耗性能”,实测在随机小突发场景下吞吐大约下降3%~4%,但在多通道并发场景下完全可接受。更重要的是,ECC上报的uncorrectable error可以作为一种系统级检测手段,把错误地址和错误时间戳打印出来,便于定位哪条数据链路出了问题。
项目里就出现过一次:系统跑30小时开始偶发数据CRC错误,看着像DDR时序余量不足,靠ECC日志功能发现错误地址集中在通道4对应的Bank Group0,进一步查布线发现通道4相关的RDLY走线长度比其余通道长了约800mil,时序裕量变差。这个如果没有ECC日志排查,光靠人工加扰测试是几乎不可能定位到具体通道的。
5. 稳定性验证与真问题排查:稳定工程的最后一道关
5.1 测试方法:从读写校验到长时间压力
验证一个多通道DDR工程,不能只看功能对不对,要看“规模化并发下稳不稳”。我的测试分四步走。第一步行校验:在通道数和数据量都较低的情况下,写固定模式数据再读回对比,确认每个通道的地址映射和ID分配正确。第二步并发递增:从2通道开始,逐步叠加到4通道、6通道、8通道,每增加一个通道都观察带宽、延迟、丢数情况。第三步带宽饱和:让8个通道同时以最大速率写同时读,持续运行2小时,看是否出现欠载或者超时。第四步长稳运行:8通道满载连续运行72小时,每小时统计一次错误计数。
长稳测试是最考验耐心也最容易出问题的环节。不要只在常温下跑,至少要在最高工作温度环境里跑一轮。我之前就有个项目在常温下跑72小时没有一点问题,换到高温箱之后,40小时开始出现间歇性读数据错误。后来定位到是MIG的DDR颗粒温度过高,导致刷新周期需要放宽,代码里把刷新率配置从1x模式改为2x模式,问题消失。
| 测试项 | 测试方法 | 通过标准 |
|---|---|---|
| 地址正确性 | 8通道固定地址模式写读 | 全通道读写数据完全一致 |
| 并发带宽 | 8通道同时满负荷读写 | 总有效带宽≥6GB/s |
| 延迟抖动 | 长时间统计单通道读延迟 | 抖动≤10% |
| 丢数检测 | 每个通道写入伪随机序列 | 72小时零丢数 |
| 错误计数 | ECC日志记录 | 不可纠正错误为0 |
| 高温长稳 | 高温环境满载运行 | 无间歇性错误 |
5.2 疑难问题排查实录
这套系统做完之后也积累了不少问题排查经验,逐个记录在这里,能帮后来者少走非常多弯路。
第一个典型问题是8通道全部启动后系统死锁。现象是MIG的AWREADY一直为低,所有通道的写请求全部堆积,系统处于假死状态。在波形上看到,死锁发生在某两个通道同时提交消息且ID分配冲突时。MIG要求不同写事务的ID不能交错,但仲裁器只管地址通道握手,忘了数据通道的WLAST还有一笔没有被及时送出。解决方式是在仲裁器内部增加“事务完成”状态,只有WLAST和B通道响应都完成后才释放该通道的总线占用。
第二个高频问题是从第4通道开始,数据偶发出现错位。排查时发现4通道之后,仲裁器的组合逻辑路径变长,导致grant信号比请求信号晚出一个周期,FIFO的读使能拉高时数据没到位,读到了上一笔的残留数据。解决方式是给grant信号增加一级寄存器打拍,相应地在通道侧延迟一拍用于对齐地址。这属于典型的时序竞争问题,模拟测试很难覆盖,长稳压测才能暴露。
第三个问题是刷新引起的带宽毛刺。运行过程中,每经过tREFI时间,DDR会停止正常访问去执行一次刷新,表现为一段约几百纳秒的吞吐下降。8通道并发场景下,这个毛刺可能造成某一通道数据传输超时。解决方式是调整仲裁的调度粒度,使每个通道的数据块尽量切小,让刷新引起的中断时间分摊到多个通道,而不是让一个通道独占整个刷新窗口。
第四个问题是读写字节使能配置不正确导致写入数据被“截断”。MIG的WSTRB信号如果处理不当,某个通道的写请求会把其他通道的数据置成无效,回读后出现异常数据。原因出在通道数据宽度和MIG数据宽度不一致时的字节使能扩展逻辑。工程里通道数据为128bit,MIG为256bit,整合时WSTRB的处理逻辑需要把128bit数据分别映射到低128bit和高128bit的字节使能上。
5.3 长时间稳定运行的能耗与散热经验
72小时稳定运行,除了逻辑正确,物理上的散热也必须有保障。8通道全速读写时,DDR控制器和FPGA内部逻辑都在高负荷运转,实测项目里单颗DDR4芯片功耗达到2.5W左右,8颗颗粒加上FPGA内部逻辑,整板温度比空载时高出20度以上。
散热设计上有几个细节要注意。DDR颗粒的供电电压和时序参数随温度变化,高温时时序裕量变小,所以PCB上DDR供电走线要尽量宽,电源纹波控制在20mV以内。另外,DDR的更新率在高温下也会发生变化,MIG里有选项可以把刷新率调整到2x模式,代价是刷新占用的带宽增加约1.5%,换来长稳可靠性大幅提升。
板级布局上,DDR颗粒和FPGA之间的走线要控制等长。数据线组、地址线组、控制线组各自的等长误差控制在50mil以内。这个误差如果不达标,低速率下几乎感觉不出来,一旦跑到DDR4-2400,就可能出现偶发数据错误。项目里专门用时序报告工具对DDR总线做了约束检查,把读写延迟链路全部过一遍,确保建立时间和保持时间都有足够裕量。
5.4 关于稳定性的核心认知
回到标题里“稳定工程”这四个字。稳定的本质,不是某一次测试跑通了,而是设计上留足了余量。时钟余量、时序余量、带宽余量、FIFO深度余量,每个维度都要在项目初期留出设计空间。很多做FPGA的开发者习惯在功能仿真通过后就认为设计已经完成,我就见过不少项目在实验室环境下功能全对,一上工程现场就出问题。原因不外乎几个:组合逻辑延时过大、FIFO深度不足、仲裁策略在极端流量下失效、地址映射与Bank调度不匹配。
做完整个项目之后,我的一个强烈体会是:多通道DDR防冲突设计的成败,在方案阶段就已经决定了。后面所有的调试工作,其实都是在为前期的设计决策买单。前期花把地址映射、仲裁优先级、FIFO深度、ID分配这些基础想清楚,后期长稳测试基本就是走个过场;如果前期草率,后面拉锯战会耗到怀疑人生。希望这份经验能给正在做类似架构的开发者一些借鉴,也欢迎在实际调试中遇到更奇怪问题的朋友一起交流,多通道DDR设计的坑,永远比理论上更多。