简介:fifo_UVM.zip是一份面向数字IC验证工程师的异步FIFO验证资源包,聚焦UVM通用验证方法学在跨时钟域设计中的应用,适合希望掌握UVM环境搭建与FIFO验证流程的初学者或进阶者。压缩包共包含274个文件,约8.48MB,内部以SystemVerilog/UVM源码(sv、v、h)、仿真日志(log、dat)、VCS与Verdi工程文件(simv、db、vpd、fsdb)为主,并配有HTML/JS/PDF等说明文档与查看报告,便于对照学习。已有1638人学习下载。通过该包可完整查看异步FIFO的UVM验证环境,包括序列激励、断言检查、覆盖率收集等组件,同时可分析仿真报告与波形,理解读写指针同步、CDC处理等关键设计要点,为独立搭建复杂验证平台提供可直接参考的脚本和代码。
1. fifo_UVM.zip是个什么包:一个FIFO的UVM验证环境怎么读、怎么改、怎么跑
拿到一个 fifo_UVM.zip,第一件事不是着急编译,而是先搞清楚三件事:里面这个FIFO是同步还是异步,UVM环境是完整可跑的工程还是只搭了框架,以及你拿它是做回归验收还是做学习参照。工程师拿到这类包最常翻车的点是:文件一堆,不知道哪个是最终验证入口,也不知道改约束是改 sequence 还是改 test。这篇笔记把包拆成三块:先读懂文件结构和验证策略,再照着搭一个最小可跑的环境,最后落到编译排错和覆盖率收敛。新手能跟着一步步跑通,熟手可以重点看边界参数和坑。
2. 拆开fifo_UVM.zip的文件构成:先确认UVM在验什么
2.1 目录怎么排:RTL、sim、tb 三者分开才值得往下读
拿到包先做三件事。第一件:把压缩包顶层目录列出来看一眼。一个能长期维护的 UVM 工程,顶层通常会分成三个区域:rtl 放被测的 FIFO 实现,sim 放编译脚本、filelist 和日志落盘位置,tb 放所有验证代码。如果三者混在一个平面目录里,后面的任何一次环境升级都会变成全量排查,第一次跑通之后建议立刻做这个整理。
tb 目录内部按 UVM 组件职责再分四层:driver 和 monitor 属于 agent,负责把事务翻译成接口信号,也把接口信号采样回事务;reference model 和 scoreboard 是一对,负责产生预期并比对;sequence 目录放激励序列;test 目录放每个测试用例的启动入口。这个四层划分的价值在改代码的时候最明显。只想改激励,动 sequence;只想改采样点位,动 interface 和 monitor;想改比较规则,动 scoreboard。组件之间通过 analysis port 连接,路径清晰,不会出现“改了 A 文件,B 文件也要莫名其妙跟着改”的情况。
filelist 是验证工程最容易翻车的文件。我会检查 filelist 的先后顺序。SystemVerilog 的编译单位是文件级的,interface 定义、宏定义、uvm.svh include 要放在前面,依赖它的组件文件放后面。有些仿真器不严格检查顺序,但换了工具版本就可能报“not found”,所以不要靠编译器宽容度活着。filelist 里每一行路径尽量写相对路径,不要写绝对路径,否则别人从 zip 解压到别的目录就编不过了。
2.2 同步和异步FIFO是两套验证思路:先认清端口再看代码
打开 RTL 目录里的 FIFO 端口列表,一眼就能分出验证路线。只有 clk、rst_n、wr_en、rd_en、data_in、data_out、full、empty 这一组端口的,是同步 FIFO。同步 FIFO 的读写时钟同源,UVM 环境里只需要一个时钟域,driver 和 monitor 共用一个 clocking block,参考模型用一个计数器就能跟踪深度,逻辑非常简单。
如果端口里出现 wr_clk、rd_clk、wr_rst_n、rd_rst_n 这种对称名字,那就是异步 FIFO。异步 FIFO 的 UVM 环境必须在两个时钟域各放一套 driver 或 monitor,还要处理读写指针跨时钟域同步。通常设计里会做 gray 码转换和两级同步,验证侧至少要处理两件事:一是两个时钟域频率比变化时,满空标志在同步窗口内会滞后;二是复位释放不对称时,指针初始化状态要能在参考模型里对齐。包如果是同步 FIFO,你跑基础回归问题不大;如果是异步 FIFO,就要先确认代码里已经带了跨时钟域处理组件,没带的话先把这套补上再谈跑通。
我习惯拿一张纸把端口分类抄一遍:哪些是输入、哪些是输出、哪些是时钟复位、宽度多少。这张表后面写 interface、写 driver、写参考模型都要反复对着看。抄端口这一步看着笨,但能挡住八成低级错误。
2.3 用最小场景清单反推环境缺什么:五个场景一张表
FIFO 的验证重点不在读写数据本身,而在标志位和边界深度。空转满、满转空、深度为 1 时的读写、读满写空同时发生、复位之后马上读写,这五个场景如果 sequence 目录里没有现成的,环境就不能直接交付。先对着这五个场景过一遍 sequence 目录,再决定跑不跑回归。
| 场景 | 需要激励 | 关注信号 |
|---|---|---|
| 空转满 | 连续写,禁止读 | full 拉高的时机、数据全部正确入栈 |
| 满转空 | 连续读,禁止写 | empty 拉高的时机、数据全部正确出栈 |
| 深度为 1 的边界 | 写一拍停一拍、读一拍停一拍 | 深度为 1 时读写指针不回绕 |
| 同时读写 | wr_en 与 rd_en 同拍 | 深度不变、数据进出不冲突 |
| 复位后立即操作 | 复位释放后第一拍读写 | full/empty 初始值正确 |
这个列表是检查环境完整性的尺子,也是后面写 coverage group 的基础。拿到 fifo_UVM.zip 后,用这五个场景去和 sequence 目录一一对应,缺哪个就补哪个。如果包只有 base sequence,那验证计划就要先从补 sequence 开始。花十分钟对表,比跑完回归再看结果里缺了一大片要划算得多。
3. 照着搭一套最小可跑的FIFO_UVM环境:四个组件从接口到计分板
3.1 interface 与 clocking block:第一步先把信号采集定义对
同步 FIFO 的 interface 不需要很复杂,但 clocking block 一定要有。它规定了 driver 在哪个沿驱动、monitor 在哪个沿采样,避免大家在信号时序上各自为政。
interface fifo_if #(parameter DATA_W = 8) ( input logic clk, input logic rst_n ); logic wr_en; logic rd_en; logic [DATA_W-1:0] data_in; logic [DATA_W-1:0] data_out; logic full; logic empty; clocking cb @(posedge clk); output wr_en, rd_en, data_in; input data_out, full, empty; endclocking modport TB (clocking cb, output rst_n); modport DUT (input wr_en, rd_en, data_in, output data_out, full, empty, input clk, rst_n); endinterfaceclocking block 里的 output 和 input 方向是以测试平台视角定义的。driver 通过vif.cb.wr_en <= value这样的形式驱动信号,monitor 通过vif.cb.data_out采样。注意这里不要用阻塞赋值,clocking block 本身带有默认的 skew,用非阻塞方式写可以避免在时钟沿附近造成竞争。
modport TB 和 modport DUT 是给验证环境和 RTL 各自使用的信号视图。tb_top 例化 interface 后,接 DUT 时用.fifo_if(DUT)这种语法,RTL 只看到它需要的端口,验证代码只看到带时钟同步的 TB 视图。参数 DATA_W 要和 RTL 的数据位宽保持一致,位宽不匹配时编译不一定报错,但比对结果会莫名失败。
3.2 driver 与 monitor:激励进得去、数据出得来
driver 的核心是从 sequencer 拿事务,再把事务驱动到接口上。下面这份代码是一个最精简的同步 FIFO driver。
class fifo_driver extends uvm_driver #(fifo_trans); `uvm_component_utils(fifo_driver) virtual fifo_if vif; uvm_analysis_port #(fifo_trans) ap; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); ap = new("ap", this); if (!uvm_config_db#(virtual fifo_if)::get(this, "", "vif", vif)) `uvm_fatal("NOVIF", "fifo interface not found") endfunction task run_phase(uvm_phase phase); fifo_trans req; forever begin seq_item_port.get_next_item(req); drive_item(req); ap.write(req); seq_item_port.item_done(); end endtask protected task drive_item(fifo_trans req); @(vif.cb); vif.cb.wr_en <= req.wr_en; vif.cb.rd_en <= req.rd_en; if (req.wr_en) vif.cb.data_in <= req.data_in; endtask endclassget_next_item和item_done是一对握手。get_next_item 会阻塞直到 sequence 准备好事务,驱动完成后必须调用 item_done,sequence 的 finish_item 才会返回。ap.write(req)把当前事务转发给参考模型,保证参考模型的输入和 DUT 实际看到的激励完全一致。
@(vif.cb)每次等到时钟沿,然后在沿上更新输出。需要注意:如果 req.wr_en 为 0,data_in 就不更新,保留前值,这样更贴近真实总线的行为。如果在这里用了阻塞赋值,波形上会看到信号在时钟沿前就跳变,后面做时序检查会非常痛苦。
monitor 是 driver 的逆过程,它不产生激励,只采样并转发事务。同样需要一个 analysis port,把采样结果发给 scoreboard。
class fifo_monitor extends uvm_monitor; `uvm_component_utils(fifo_monitor) virtual fifo_if vif; uvm_analysis_port #(fifo_trans) ap; function void build_phase(uvm_phase phase); super.build_phase(phase); ap = new("ap", this); if (!uvm_config_db#(virtual fifo_if)::get(this, "", "vif", vif)) `uvm_fatal("NOVIF", "fifo monitor interface not found") endfunction task run_phase(uvm_phase phase); forever begin @(vif.cb); if (vif.cb.wr_en || vif.cb.rd_en) begin fifo_trans t = fifo_trans::type_id::create("t"); t.wr_en = vif.cb.wr_en; t.rd_en = vif.cb.rd_en; t.data_in = vif.cb.data_in; t.data_out = vif.cb.data_out; t.full = vif.cb.full; t.empty = vif.cb.empty; ap.write(t); end end endtask endclassmonitor 里我把采样条件限定在 wr_en 或 rd_en 有效时,这样不会产生大量空事务淹没 scoreboard。但如果你的 scoreboard 需要追踪“连续 idle 期间 full/empty 状态变化”,那就要在每拍都采样。这个取舍取决于验证目标,基础回归建议用上面这种精简版,做覆盖率收敛时再改回每拍采样。
3.3 参考模型与 scoreboard:预期怎么算、比较怎么对
同步 FIFO 的参考模型本质上是一个队列加一个深度计数器。下列代码用队列存放数据,并输出预期结果。
class fifo_ref_model extends uvm_component; `uvm_component_utils(fifo_ref_model) uvm_analysis_imp #(fifo_trans, fifo_ref_model) ref_in; uvm_analysis_port #(fifo_ref_out) ref_out; localparam int DEPTH = 8; bit [7:0] mem[$]; function void write(fifo_trans t); fifo_ref_out o = fifo_ref_out::type_id::create("o"); if (t.wr_en && (mem.size() < DEPTH)) begin mem.push_back(t.data_in); o.data_expect = t.data_in; end if (t.rd_en && (mem.size() > 0)) begin o.data_expect = mem.pop_front(); end o.full = (mem.size() == DEPTH); o.empty = (mem.size() == 0); o.wr_en = t.wr_en; o.rd_en = t.rd_en; ref_out.write(o); endfunction endclass这里用队列天然实现了 FIFO 的进出顺序。注意 DEPTH 必须和 DUT 的深度一致,深度不匹配时,满空标志会永远对不上。concern 点在于:这个参考模型是跟着 driver 的事务走的,而 monitor 采样的是实际接口信号,两者如果不同拍,scoreboard 会比较错位。
scoreboard 的做法是用两个 analysis imp 分别接收预期和实际,再把两路事务通过队列对齐。最简单可靠的写法如下:
class fifo_scoreboard extends uvm_scoreboard; `uvm_component_utils(fifo_scoreboard) uvm_analysis_imp #(fifo_trans, fifo_scoreboard) act_imp; uvm_analysis_imp #(fifo_ref_out, fifo_scoreboard) exp_imp; fifo_ref_out exp_q[$]; int error_cnt; function void write(fifo_ref_out o); exp_q.push_back(o); endfunction function void write(fifo_trans t); fifo_ref_out exp; if (exp_q.size() == 0) begin `uvm_error("SCB", "no expected data, check ref model path") return; end exp = exp_q.pop_front(); if (t.data_out !== exp.data_expect) `uvm_error("CMP", $sformatf("data mismatch: act=%0d exp=%0d", t.data_out, exp.data_expect)) if (t.full !== exp.full) `uvm_error("CMP", $sformatf("full mismatch: act=%0b exp=%0b", t.full, exp.full)) if (t.empty !== exp.empty) `uvm_error("CMP", $sformatf("empty mismatch: act=%0b exp=%0b", t.empty, exp.empty)) endfunction endclass两个 write 方法分别处理预期事务和实际采样事务。exp_q 队列天然解决了两路事务到达顺序不一致的问题。若 exp_q 为空时来了实际事务,说明参考模型少产生了一拍预期,通常是 driver 和 monitor 采样点不同步,优先查 clocking block 的方向配置。error_cnt 只是最简单的计数,实际项目中可以改成按错误类型分类统计。
至此,driver、monitor、ref_model、scoreboard 四个核心组件都有了。最后用 env 把它们连起来:
class fifo_env extends uvm_env; `uvm_component_utils(fifo_env) fifo_agent agent; fifo_ref_model ref_model; fifo_scoreboard sb; function void build_phase(uvm_phase phase); super.build_phase(phase); agent = fifo_agent::type_id::create("agent", this); ref_model = fifo_ref_model::type_id::create("ref_model", this); sb = fifo_scoreboard::type_id::create("sb", this); endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); agent.drv.ap.connect(ref_model.ref_in); agent.mon.ap.connect(sb.act_imp); ref_model.ref_out.connect(sb.exp_imp); endfunction endclassconnect_phase 里三条连接分别是:driver 发事务给参考模型、monitor 发采样给 scoreboard、参考模型发预期给 scoreboard。顺序无所谓,但一定要全部连上。漏掉任何一条,仿真不会报编译错误,只会在 scoreboard 里出现大量 mismatch 或者 exp_q 永远为空。
4. sequence与test的双层调度:让随机读写真正能跑起来
4.1 sequence 怎么组织:随机约束写在事务里,序列只负责调参
有一类常见误区:把随机逻辑写在 sequence 的 body 里,事务本身只有 set 方法。这会导致约束调整非常痛苦。正确做法是把随机约束放在事务类上,sequence 只负责决定跑多少笔、按什么比例跑。
class fifo_trans extends uvm_sequence_item; `uvm_object_utils(fifo_trans) rand bit wr_en; rand bit rd_en; rand bit [7:0] data_in; constraint no_wr_rd_same { !(wr_en && rd_en); } constraint wr_rd_dist { wr_en dist {1 := 70, 0 := 30}; } endclasswr_en dist {1 := 70, 0 := 30}表示有 70% 的概率产生写事务。这个比例直接决定仿真中 FIFO 深度分布。如果 wr_en 占比太高,FIFO 会长期处于满状态,empty 方向的覆盖就很难收敛;反过来 rd_en 占比高,又会一直空转。实际项目里我会先按 50:50 跑一轮冒烟,再根据覆盖率报告调整这个权重。
sequence 层写起来很短:
class fifo_rand_seq extends uvm_sequence #(fifo_trans); `uvm_object_utils(fifo_rand_seq) rand int item_cnt; constraint item_cnt_range { item_cnt inside {[1:50]}; } task body(); repeat (item_cnt) begin req = fifo_trans::type_id::create("req"); start_item(req); assert(req.randomize()); finish_item(req); end endtask endclassstart_item 到 finish_item 之间的代码会在 driver 真正取走事务之前执行,所以 randomize 应该放在这两者之间。assert 是必须的,约束一旦失败,仿真会立刻停下来,不会带着非法事务继续跑。item_cnt 作为随机量可以控制每次 sequence 的时长,这在一个 test 里启动多个 sequence 做并发时特别有用。
4.2 test 层怎么挂工厂、怎么管理 objection
test 是 UVM 树的顶层节点,它负责创建 env、下发 interface 配置、启动 sequence。interface 下发用 uvm_config_db,启动 sequence 必须配合 objection 才能让仿真保持足够时间。
class fifo_base_test extends uvm_test; `uvm_component_utils(fifo_base_test) fifo_env env; function void build_phase(uvm_phase phase); super.build_phase(phase); env = fifo_env::type_id::create("env", this); uvm_config_db#(virtual fifo_if)::set(this, "env.agent.drv", "vif", $root.tb_top.vif); uvm_config_db#(virtual fifo_if)::set(this, "env.agent.mon", "vif", $root.tb_top.vif); endfunction task run_phase(uvm_phase phase); fifo_rand_seq seq; phase.raise_objection(this); seq = fifo_rand_seq::type_id::create("seq"); assert(seq.randomize()); seq.start(env.agent.sequencer); phase.drop_objection(this); endtask endclassconfig_db 路径要诀是:set 和 get 的路径必须落在同一个组件层次上。driver 里 get 用的是空字符串,表示从当前组件自己的层次查找,所以 test 里 set 的路径要写相对 test 的完整路径env.agent.drv。很多环境跑起来 vif 为 null,九成是这里路径写错。这里周期性地 raise_objection 和 drop_objection 是为了告诉 phase 管理器:仿真还不能结束。
如果忘了 raise_objection,最常见的现象是仿真在 0 时刻就结束,log 里只有一句提示:没有 objection 拉起。另一种做法是在 sequence 里调用set_auto_objection(1),自动管理 objection,test 层就不用手动 raise 和 drop。但从可读性看,显式 raise/drop 更直观,团队协作时不容易出现“到底谁管 objection”的争论。
5. FIFO_UVM跑不起来的排查:五个高频坑和对应改法
5.1 仿真一启动就结束,run_phase 里什么都没执行
现象:log 很快打出 phase 完成,波形只有 0 时刻一条线。原因几乎都是 objection 没拉起来。test 的 run_phase 里如果没写 raise_objection,sequence 还没开始,phase 管理器就认为可以结束了。解决:在 test 的 run_phase 里 raise,sequence 结束后 drop。如果 sequence 很多,可以在 sequence 的 body 里调用 set_auto_objection(1)。
5.2 波形上信号全是 Z 或 X,DUT 完全没有反应
现象:编译通过,但 drv 的 wr_en、data_out 之类的信号全是高阻或不定态。原因通常是 interface 的变量没有初始化,或者 DUT 没有接上 interface。例如 interface 里的 logic 类型默认初始值是 Z,复位信号 rst_n 没给初值,DUT 就一直处于复位状态。解决:在 tb_top 里给 interface 信号赋初值,例如在 initial 块里把 rst_n 先拉低再释放。另外检查 DUT 例化的端口连接,确认连的是 vif 而不是复制了一份空信号。
5.3 scoreboard 里 full/empty 永远差一拍
现象:data_out 全部比对通过,但 full 和 empty 一直报 mismatch。原因是 RTL 的满空标志如果是时序逻辑输出,会晚一拍才变化,而参考模型常常直接根据队列长度同步给出结果。解决:在 scoreboard 里对预期侧的 full/empty 做一拍延迟,或用参数FULL_DLY=1控制对齐。这个参数推荐放在 uvm_config_db 里,方便不同设计复用时切换。
5.4 编译报一大堆 UVM 宏无法识别
现象:uvm_config_db、uvm_component_utils 这些名字全部报 undefined。原因不是代码写错,而是编译命令没包含 UVM 库路径,也没在最早的编译文件里 include uvm.svh。解决:在 filelist 最前面加入 UVM 库 include 路径,编译选项里加上+incdir+$UVM_HOME/src`。很多工程把 UVM 库路径写在脚本环境变量里,换机器后忘了 export,就会看到这类报错。
5.5 回归跑到一半卡死,报 timeout
现象:仿真在某一个用例上长时间不推进,最后触发仿真器 timeout。原因大概率是死锁:driver 在等 sequence 发新事务,而 sequence 在等 driver 完成上一笔,双方互相等待。FIFO 场景里很典型的一种是:FIFO 已经满,sequence 还在持续产生写事务,driver 没检查 full 就直接驱动写使能,RTL 不接收新数据,但 sequence 和 driver 还在按原节奏握手。解决:在 driver 里增加 backpressure 检查,满时挂起写驱动。
if (req.wr_en) begin while (vif.cb.full) @(vif.cb); vif.cb.wr_en <= 1'b1; vif.cb.data_in <= req.data_in; end这里关键点是在驱动写之前等待 empty/full 解除。注意 while 循环里每次都要@(vif.cb),否则会变成同一时刻空转,仿真时间不推进,照样死锁。读方向同理,empty 时不要强行读。加了这类检查后,随机 sequence 即使写出极端比例也不会卡死回归。
6. 把覆盖率当成验收标准:五个场景的覆盖组和随机种子策略
覆盖率要做在接口层,而不是只做在 sequence 层。sequence 层只能告诉你“发了什么激励”,接口层才能说明“DUT 在什么深度下看到了什么操作”。最实用的一个做法是在 monitor 里挂一个覆盖组,追踪当前 FIFO 深度和读写操作的交叉情况。
covergroup fifo_cg @(posedge vif.clk); cp_depth: coverpoint cur_depth { bins near_full = {[DEPTH-2:DEPTH-1]}; bins near_empty = {[0:1]}; bins middle = {[2:DEPTH-3]}; } cp_wr: coverpoint vif.cb.wr_en; cp_rd: coverpoint vif.cb.rd_en; cross cp_wr, cp_rd, cp_depth; endgroupcur_depth 是 monitor 里维护的当前深度变量,每个时钟沿更新。这个覆盖组把 FIFO 深度分成近空、近满、中间三段,再和读写使能交叉,能直接回答“近满状态下是否覆盖过写操作”“近空状态下是否覆盖过读操作”这类问题。前面章节提到的五个最小场景,落到覆盖组上就是这组交叉 bin 的命中情况。
回调函数里我会在每个用例结束后把覆盖率数据 dump 出来,用同一份覆盖率数据库对比各用例的增量。基础冒烟跑完,看近空和近满两个 bin 是否已经命中;没有命中就单独调 sequence 权重,而不是盲目加大回归次数。随机种子策略上,我习惯先用固定的几个种子跑通回归,确认环境稳定后再放开用随机种子做大回归。
# 用多个随机种子跑同一测试用例,确认覆盖增长后停止 for seed in 101 202 303 404 505; do run_sim +UVM_SEED=$seed +UVM_TESTNAME=fifo_rand_test donerun_sim 替换成你本机仿真器的入口命令即可。UVM_SEED 是 UVM 内置的随机种子变量,同一个测试用例换不同种子,sequence 里的随机约束就会产生不同激励分布。我发现很多环境跑了几百次回归,覆盖却一直没有增长,就是因为种子固定,每次产生的激励序列几乎一样。
我现在做这类环境的习惯是:先对着第二章那张五个场景表写覆盖组,再写 minimal 环境,跑通后固定种子做冒烟,最后放开随机种子并盯着覆盖率增长曲线。覆盖不增长时先查 sequence 约束分布,而不是拍脑袋加测试用例。换时钟频率、改数据位宽之后,近空近满的 bin 边界要重新算一遍,否则覆盖率报告好看但实际覆盖点已经失配。希望这个 fifo_UVM 环境的拆解和排查思路能帮到你。
本文还有配套的精品资源,点击获取