首页 / 资讯中心 / 文章详情

异步FIFO的UVM验证:空满判断、采样对齐与覆盖率收敛

异步FIFO的UVM验证:空满判断、采样对齐与覆盖率收敛 ★ FEATURED ARTICLE
简介面向数字集成电路验证工程师的异步 FIFO 验证学习资料包围绕 UVM 通用验证方法学系统展示了从验证环境搭建到回归测试的完整流程。内容覆盖配置验证环境、创建激励序列、编写断言与覆盖率模型、处理跨时钟域CDC问题等关键环节并配有示例代码与可运行工程适合具备一定 SystemVerilog 语言基础、希望掌握 UVM 组件交互方式的初学者和中级验证人员。压缩包共二百七十四个文件大小约八点四八兆字节以 SystemVerilog 与 Verilog 源码为核心辅以 HTML 文档、日志文件、仿真数据库和脚本配置等类型便于对照学习验证组件搭建、仿真运行与结果分析。已有 1638 人学习参考价值已获社区认可。从预览中的仿真命令、构建数据库与可执行文件可以看出资源保留了完整的配套工程结构读者可通过实际操作快速掌握异步 FIFO 读写指针同步、跨时钟域安全传输以及覆盖率收集等验证要点提升数字集成电路验证实战能力。1. 异步FIFO的UVM验证不是hello world从“空满信号全对但数据全错”说起中午刚过A同学把前仿波形图发过来FIFO的wfull、rempty看着全都对scoreboard却从第二轮burst开始疯狂刷data mismatch。这种问题十有八九不是RTL功能错而是验证平台在跨时钟域采样上没对齐。fifo_UVM.zip就是一套把异步FIFO完整UVM通用验证方法学环境拆开给你看的工程从interface、driver、monitor到scoreboard、coverage和断言每一步都有可直接跑的代码。它适合需要一份不算复杂却能触及UVM核心机制源码的人想入门数字IC验证或者想把空满判断、同步器建模、覆盖率收敛这几个点系统过一遍都能从里面找到对应模块。2. 先读懂DUT格雷码指针与跨时钟门的空满判决scoreboard才能对照着写2.1 深度16的FIFO为什么要5位格雷码指针大多数异步FIFO的RTL骨架是双口存储加读写指针再加两条跨时钟同步链。深度为16时二进制指针理论上4位就够但为了区分“刚写完一轮回到0”和“一个字节都没写”RTL普遍会用5位指针多出来的一位专用于回绕判断。选用格雷码是因为跨时钟域传指针时每次只有1 bit翻转两个域各自拿到同步后的指针再做比较不会出现多比特同时翻转带来的非法中间态。这个“n1位”的设计会直接传染给验证平台scoreboard参考模型的深度指针、断言里判断full的位宽、FIFO水位覆盖区间都要跟着指针宽度走。// 16 深异步FIFO的空满条件wr_ptr_g / rd_ptr_g 是 5 位格雷码 logic [4:0] wptr_g, rptr_g; // 本地的写读指针 logic [4:0] wptr_sync_g, rptr_sync_g; // 同步后的对端指针 wire r_empty (rptr_g wptr_sync_g); // 读侧判空全等即空 wire w_full (wptr_g {~rptr_sync_g[4:3], rptr_sync_g[2:0]});这段比较逻辑是DUT内部的核心判决。读侧判空简单两个格雷码相等就是空写侧判满则是高两位互补、低三位相同专业点叫“相隔一轮”的格雷码关系。其中[4:3]和[2:0]严格对应深度16的5位编码一旦把FIFO深度改成64这里的位宽切片和取反位置要全部重算这也是我建议把验证参考模型里的指针宽度全部用$clog2(DEPTH)1推导的原因。手写5位、手写掩码短期没问题换参数就是连环爆炸。2.2 两级同步器加了不代表复位问题也解决写侧要把读指针同步到写时钟域读侧要把写指针同步到读时钟域各自例化两级寄存器这是异步FIFO最标准的同步结构。但验证平台里真正容易漏的是复位释放语义wr_rst_n和rd_rst_n拉低可以异步拉高却不能两个域同时释放否则复位结束的瞬间两个域初始状态不在同一拍起跑之后所有空满比较都会差出不可预测的结果。UVM测试里最稳妥的办法是在reset sequence里做异步拉低、同步释放。task reset_async_release_sync(virtual fifo_if vif); vif.wr_rst_n 1b0; vif.rd_rst_n 1b0; repeat (4) (posedge vif.wr_clk); // 至少保持 4 个写时钟 vif.wr_rst_n 1b1; // 写域先同步释放 repeat (4) (posedge vif.rd_clk); vif.rd_rst_n 1b1; // 读域再释放 repeat (2) (posedge vif.wr_clk); endtask这里先统一拉低再让写域先释放、读域后释放中间间隔由各自的时钟沿控制。repeat (4)和repeat (2)不是随便拍的要按复位信号在DUT内部的最小保持宽度要求来配常见做法是至少覆盖4个目标时钟周期。值得留意的是这个任务本身没有返回值如果reset sequence在UVM里被提前raise/drop objection平台会先看到复位还没结束就跑激励所以这条任务我会强制放到sequence的body最前面。2.3 深度、宽度、同步器级数验证平台建parameter最忌讳硬编码搭建验证环境之前我先给这套资源里的关键参数建一张映射表后续所有组件引用这些参数而不是写死数字。下表是fifo_UVM.zip里最常改的几个入口参数默认值作用范围改动注意点FIFO_DEPTH16scoreboard水位、断言掩码、覆盖率bin必须为2的幂指针位宽用$clog21推导FIFO_WIDTH32driver数据、monitor采样、scoreboard比对位宽一变随机约束和期望数据mask都要跟着动SYNC_STAGE2同步器级数影响参考模型和断言改成3级后接口波形会比DUT内部再晚一拍识别WR_RST_RELEASE4复位释放保持拍数按DUT复位最小宽度要求设定这套参数表的价值在于它把你从“翻代码找数字”里解放出来。改深度时只需要动表里第一行然后全局检索[4:3]、[2:0]这类肉眼硬编码切片一个都不能留。我一般会在package顶部直接定义localparam PTR_W $clog2(FIFO_DEPTH) 1;所有比较掩码、断言位宽引它这样从16深换到64深验证平台侧就只改一个入口。3. UVM验证工程搭建interface、driver、monitor的顺序决定仿真时序是否干净3.1 fifo_if容器为什么这里不用clocking blockUVM工程里interface是最容易一上来就写错的东西。面对异步FIFO这种双时钟域DUT我通常不给每个域都加clocking block因为后仿阶段要force信号、要将采样窗口改成#1step固定clocking block会把这些操作变得非常被动。资源里的做法是用modport做方向分组写侧驱动只看到写使能、写数据和wfull读侧monitor只看到读数据、rempty和rinc复位单独引出。interface fifo_if #(parameter DEPTH16, DWIDTH32) ( input logic wr_clk, rd_clk ); logic [DWIDTH-1:0] wdata, rdata; logic winc, rinc; logic wfull, rempty; logic wr_rst_n, rd_rst_n; modport WR_DRV(output winc, wdata, wr_rst_n, input wfull); modport RD_MON(input rdata, rempty, rd_rst_n, rinc); endinterface注意modport在这里只约束方向和可见性不管时序。写侧driver只认识winc、wdata和wfull读侧monitor只认识rdata、rempty和rinc后续在test里把interface句柄赋值给driver和monitor时就通过vif的类型限定保证不会访问错信号。参数DEPTH和DWIDTH要在顶层与DUT保持一致并且UVM组件里不要复制这两个值全部走virtual fifo_if句柄的parameter。3.2 写侧driver等非满时一定要有超时保护写侧driver的工作在UVM里相对直接从sequencer拿到写事务判断FIFO非满然后在写时钟沿把winc和wdata打出去。下面这段是fifo_wr_driver的核心片段class fifo_wr_driver extends uvm_driver #(fifo_txn); virtual fifo_if.WR_DRV vif; uvm_component_utils(fifo_wr_driver) task run_phase(uvm_phase phase); init_signals(); // winc0, wdata0 forever begin seq_item_port.get_next_item(req); drive_write(req); // 单笔写事务 seq_item_port.item_done(); end endtask task drive_write(fifo_txn req); wait (!vif.wfull); // 等待写侧非满 (posedge vif.wr_clk); vif.winc 1b1; vif.wdata req.data; (posedge vif.wr_clk); vif.winc 1b0; endtask endclasswait (!vif.wfull)是这条driver最关键的语句它把“FIFO当前没满”当成握手条件。这里有个明显的隐患如果sequence连续产生写请求而FIFO持续处于满状态这个wait会一直卡住run_phase不掉但整个用例永远不会结束。正规做法是在wait外包一层超时计数器比如等待超过100个写时钟就打印UVM_ERROR退出避免回归时死锁在某个写事务上。winc和wdata都用非阻塞赋值是为了避免和monitor在同一时钟沿采样时产生先写后读的竞争。3.3 读侧monitor采样窗口用#1step比用#1更稳采样时序是异步FIFO验证平台里最出效果也最容易翻车的地方。读数据由读时钟沿驱动monitor如果直接在(posedge vif.rd_clk)后读rdata很可能读到的是上一个沿的旧数据。常见做法是每个posedge后面补一个#1step让采样点跨过非阻塞赋值更新区这时再判断rinc和rempty才有意义。task monitor_read(); forever begin (posedge vif.rd_clk); #1step; // 跨过NBA区取到稳定rdata if (vif.rinc !vif.rempty) begin fifo_txn t fifo_txn::type_id::create(t); t.data vif.rdata; t.kind fifo_txn::READ; ap.write(t); // analysis port 送scoreboard end end endtask#1step是SystemVerilog里的仿真时间步延迟它能保证采样发生在当前仿真时刻的NBA更新之后读到的数据与DUT真实输出一致。这里不建议用#1ns这种固定延迟因为一旦全局timescale从1ns/1ps换成1ps/1fs采样点就偏离了。也有工程师习惯用(negedge rd_clk)采样那个方案可行但有个前提rinc、rdata都必须是posedge域更新的信号否则负沿采到的数据天然比正沿晚半拍scoreboard比对时就得做额外偏移。3.4 sequence层把burst长度与读写类型交回给约束异步FIFO验证里一个常见误区是把时序控制全塞进driver导致driver既管握手又管事务内容。fifo_UVM.zip里的做法是把burst长度、读写类型交回给sequencedriver只负责在正确时间把事务里的信号驱动出去。class fifo_burst_seq extends uvm_sequence #(fifo_txn); rand int unsigned burst_len; constraint c_burst { burst_len inside {[1:16]}; } task body(); repeat (burst_len) begin req fifo_txn::type_id::create(req); start_item(req); assert(req.randomize() with {kind fifo_txn::WRITE;}); finish_item(req); end endtask endclass这段sequence会生成一系列写事务burst_len约束在1到16之间正好对齐FIFO深度。这里没有用virtual sequencer因为读写两个sequencer各自独立跑只有少数场景需要跨时钟域协同比如“边写边读”的死锁压力测试那才需要引入virtual sequencer在更高层调度。对于异步FIFO的日常回归写sequence和读sequence各跑各的配合复位sequence的拉开时空就足够了硬加virtual sequencer反而会让调度变复杂。4. 断言与覆盖率异步FIFO里最容易漏掉的两种验收视角4.1 断言放在接口层能快速兜住“满时钟写”类错误scoreboard负责比对数据流但它在每个时钟沿都要维护参考模型状态成本不低。断言的定位刚好相反语句简单立刻报告适合做高频语义检查。异步FIFO最经典的断言就是“写满时不能再接收写请求”。property p_no_write_when_full; (posedge wr_clk) disable iff (!wr_rst_n) (wfull winc) |- 0; endproperty assert property (p_no_write_when_full);disable iff把复位窗口内的评估完全排除避免复位释放瞬间误报。|- 0表示如果wfull和winc同时为真这一拍立即判失败。这个断言只引用wr_clk域的wfull和winc属于同域断言用起来很安全。但要注意有的DUT内部wfull由组合逻辑生成接口层看到wfull的时间可能比DUT寄存器内部早或晚一拍如果这个断言在实际项目中产生大量假失败通常的修法是把wfull换成打拍后的采样版本或者把属性改成|- ##1 0给DUT预留一拍反应时间。4.2 双时钟域的covergroup交叉覆盖要谨慎很多第一次接触异步FIFO验证的人会试着把写侧winc和读侧rinc放到同一个covergroup里做交叉覆盖结果采样时机完全混乱。正确做法是为写域和读域各建一个covergroup分别挂在各自时钟上再对“空到非空”“满到非满”这类转移路径加bin这样覆盖结果才有物理意义。covergroup fifo_rd_cov (posedge rd_clk); cp_depth: coverpoint depth_cnt { bins low {[0:3]}; bins mid {[4:11]}; bins high {[12:15]}; } cp_transition: coverpoint (rempty) { bins empty_to_data[] (1 0); // 空到非空的转移 } endgroup这里的depth_cnt不是DUT信号而是monitor在采样过程中维护的当前深度计数器。low/mid/high的区间划分针对16深FIFO如果改到64深就要重新设计bin否则高区间永远采不到覆盖率卡在很低的值。异步FIFO验收的三个关键覆盖点是空满快速翻转、半满水位读取、写满保护信号出现次数。这三个点收敛到100%之后再去追求数据通路的随机组合效率会高很多。4.3 回归命令与打印日志里不是只有error才需要看跑回归是验证工程师每天重复最多的动作但命令参数写不好会把问题藏起来。fifo_UVM.zip配套的回归方式类似下面这条./sim.sh UVM_TESTNAMEfifo_rw_stress_test UVM_VERBOSITYUVM_MEDIUM \ ntb_random_seed20240121 -cm fifo_rd_cov,fifo_wr_cov固定ntb_random_seed是为了复现一个偶发fail时能重放同样的随机种子序列-cm是给仿真器指定需要收集的covergroup名字而不是所有covergroup一股脑全开收集太多会拖慢后仿和长回归。换成其他test name后要同时确认该test所挂的sequence不会把DUT长期停在满状态否则wait超时保护会先触发日志里的UVM_ERROR会被误读成功能bug。5. 避坑记录异步FIFO验证平台最容易翻车的五个地方5.1 空满信号全对scoreboard却在第二批数据开始mismatch现象波形里wfull、rempty翻转时机完全正常数据比对前几十笔也通过从第二批burst开始scoreboard连续报错。原因monitor在(posedge rd_clk)后直接采样rdata读到的其实还是上一个沿之前的数据采样窗口错位。解决把monitor和scoreboard入口处的采样统一改成#1stepdriver侧保持posedge驱动。同一个interface内不要混用#1step和#1ns否则换timescale后问题会从偶发变成必然。5.2 把FIFO_DEPTH从16改到64断言直接数组越界现象只改了深度参数编译报数组越界一堆断言在仿真0时刻就进入error状态。原因断言和参考模型里硬编码了5位指针的[4:3]、[2:0]掩码深度一变位宽和切片全部失效。解决在package里定义localparam PTR_W $clog2(DEPTH) 1;所有比较掩码、断言位宽引用它然后全局搜索[4:3]、[2:0]这类硬编码切片改成一个入口。从那以后我每换一次FIFO深度都会强制走一遍这步检查。5.3 随机种子一换就挂log停在某个write任务里现象固定种子跑三天都没事某次回归换了个新种子用例卡在某条write路径上UVM报告既没有error也没有正常结束。原因sequence里start_item之后某个分支上没执行finish_item就break了driver的get_next_item永远等不到下一个请求最终死锁。解决sequence的每个分支都保证finish_item超时异常时用try_finish_item加超时报告回归脚本里对UVM_TIMEOUT做专门抓取这样死锁能快速定位到具体sequence文件。5.4 断言在wfull拉高前一个周期误报现象功能波形正确但p_no_write_when_full在wfull拉高前一拍就报失败。原因DUT内部wfull由组合逻辑生成接口层看到的wfull比寄存器内部版本早或晚半拍断言直接拿接口信号参与|-自然多报一拍。解决把属性里的wfull换成打拍后的采样版本或者用$past(wfull)参与比较把断言采样点位从接口层改到DUT内部信号也可以。这里没有标准答案取决于具体RTL对wfull输出打了几拍。5.5 覆盖率和数据比对都通过但换更快的读时钟后读数据在边界采样错乱现象读时钟频率调高后覆盖率照常收敛scoreboard偶发mismatch频率越高发生越频繁。原因读时钟变快后rinc和rdata与读侧采样窗口之间的余量变小#1step在快时钟下仍然可能跨过两个沿之间的时间竞争。解决把采样窗口声明从monitor拉到interface的专项采样函数里并增加边沿窗口检查用$sampled对属性做采样。修复后这套平台的读时钟频率可设置范围比之前更宽也更容易应对不同频率比的复用场景。6. 把这套UVM工程复用去新FIFO回归前固定检查的三个动作6.1 参数变更后先跑smoke test改完FIFO_DEPTH、FIFO_WIDTH、SYNC_STAGE之后第一件事不是跑长回归而是先跑一个短smoke test确认平台能正常起振。./sim.sh UVM_TESTNAMEfifo_smoke_test UVM_VERBOSITYUVM_NONE通过的条件有两条退出码为0log里没有UVM_ERROR。这一步能快速过滤掉参数没接通、interface掉线、断言位宽错误这类低级问题。只有smoke test干净了才值得把长回归挂出去。6.2 覆盖率与断言交叉确认每个新FIFO参数进来都要重新确认两个目标满到非满转移路径覆盖率达到100%写满保护断言在回归里没有error。覆盖率数据库要在长回归结束后单独合并一次不要直接在仿真log里看因为不同用例的covergroup文件名容易冲突合并后才能看到整体收敛情况。之前有一次就是没合并数据库以为覆盖率满了实际单用例各自看都缺边界bin。那之后我每次拿到类似FIFO工程都会强制走一遍复位视图检查、同步器级数确认、scoreboard baseline重标三步再跑长回归。这套流程看起来多花几分钟实际能省掉大量半夜被回归fail叫醒的麻烦。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站