简介:面向数字IC验证求职者与UVM初学者,这份资料对应思朗科技2022提前批数字IC验证笔试题,围绕异步FIFO的UVM验证环境搭建、仿真与覆盖率收集展开,适合练习从设计到验证环境落地的完整流程,并对照笔试要求查漏补缺。7z压缩包共193个文件、约5.13MB,以图片、网页、脚本、仿真数据库和硬件描述语言源码为主,另有覆盖率与报告页面;验证工程按输入激励和输出监测分层,涵盖transaction、driver、monitor、sequencer与agent等组件,便于理解异步FIFO的驱动、采样和结果检查。已有6232人学习,可参考其Questa Sim工程结构、组件划分、覆盖率结果与错误点分析,形成自己的异步FIFO验证思路。
1. 一道「写个异步FIFO」的笔试题,为什么能筛掉大半数字IC验证候选人
数字IC验证的笔试里,异步FIFO的出现频率高得有点不讲道理。一张卷子前面还在问setup/hold和阻塞非阻塞,翻到最后一页,题目变成「请用Verilog手写一个深度16、位宽8的异步FIFO,并说明空满判断原理」,紧接着又问「如果用UVM验证,你会怎么搭环境」。四十分钟,四件事同时考:格雷码指针跨时钟域、空满判断的边界、亚稳态的保守处理、双时钟激励怎么给。
这四件事里任何一件没真正跑过仿真,答案就会露出破绽。比如空满判断写成二进制直接比,代码看着对,实际在跨域同步后会抖出假空假满;比如UVM验证环境里两个时钟用一个driver驱动,波形上看着能跑,一换时钟比就挂死。异步FIFO验证的价值就在这里——它小到可以在一张纸上画完,又大到能把数字IC验证的核心技能全串一遍。下面按我自己搭环境、跑回归、踩坑的顺序,把这个方向拆开讲。
2. 把异步FIFO塞进UVM:双时钟agent的最小可跑平台
UVM验证平台最容易犯的错,是先抄一个现成的单时钟环境,然后把时钟一分为二,指望它能跑。异步FIFO的两个端口没有共同的时序参考点,写侧只知道full,读侧只知道empty,任何一处采样时机偏了,scoreboard就会给出似是而非的结论。所以平台骨架要从interface开始就按双时钟设计,而不是事后补。
2.1 interface与clocking block:双时钟域采样的第一道保险
我一般会把所有端口收进一个带参数的interface,并且给写侧和读侧各配一个clocking block。clocking block解决的是采样和驱动的竞争问题:input #1step让采样发生在时钟沿之前的最后稳定值,output #1让驱动在沿之后一个时间单位生效,DUT在沿上采到的永远是上一拍驱动出来的值。没有这一层,monitor和driver会在同一个沿上抢信号,波形上看起来只是偶尔错一拍,查起来能耗掉一整天。
interface fifo_if #(parameter int DSIZE = 8, parameter int ASIZE = 4) (input logic wr_clk, input logic rd_clk, input logic wr_rst_n, input logic rd_rst_n); logic wr_en; logic [DSIZE-1:0] wdata; logic full; logic rd_en; logic [DSIZE-1:0] rdata; logic empty; clocking wr_cb @(posedge wr_clk); default input #1step output #1; output wr_en, wdata; input full; endclocking clocking rd_cb @(posedge rd_clk); default input #1step output #1; output rd_en; input empty, rdata; endclocking endinterfaceASIZE=4对应深度16,指针实际位宽是ASIZE+1=5,多出来的最高位用来区分绕圈,这是后面空满判断的基础。wr_cb只碰写侧信号,rd_cb只碰读侧信号,两个clocking block之间没有任何共享变量,这是双时钟环境不互相污染的前提。
提示:clocking block里不要放复位信号。复位是异步的,塞进clocking block会让它在时钟沿上被采样,释放时刻直接偏掉一拍。
2.2 写agent与读agent拆开:driver里那个while(full)该怎么写
写侧和读侧必须是两个独立的agent,各自有sequencer、driver、monitor。合成一个agent的后果是sequence启动时只能选一个时钟域做参考,另一侧的激励时序完全失控。拆开之后,写agent挂在wr_clk上,读agent挂在rd_clk上,两个sequence可以并行启动,时钟比怎么变都不影响结构。
driver里最关键的一行是满时的等待。写driver在发数据前要检查full,满就原地等,等到不满再发。这个等待如果用@(posedge wr_clk)直接写,会采到沿上的竞争值,可能在本该停的时候多发一笔。
class fifo_wr_driver extends uvm_driver #(fifo_wr_item); `uvm_component_utils(fifo_wr_driver) virtual fifo_if vif; bit ignore_full = 0; // 定向测试时置1,用来观察满时指针是否真的停住 function new(string name, uvm_component parent); super.new(name, parent); endfunction task run_phase(uvm_phase phase); vif.wr_cb.wr_en <= 1'b0; vif.wr_cb.wdata <= '0; forever begin seq_item_port.get_next_item(req); if (!ignore_full) while (vif.wr_cb.full) @(vif.wr_cb); // 用clocking block采样,避开沿竞争 vif.wr_cb.wr_en <= 1'b1; vif.wr_cb.wdata <= req.data; @(vif.wr_cb); vif.wr_cb.wr_en <= 1'b0; seq_item_port.item_done(); end endtask endclasswhile (vif.wr_cb.full)这里读的是clocking block的采样值,等价于沿前值,和DUT判断满用的是同一个时刻,不会出现「DUT认为满了、driver认为没满」的错位。ignore_full这个开关看着不起眼,但它是验证「满时写指针不动」的唯一手段——如果driver永远礼让,那一笔多余的写根本发不出去,断言也就没机会抓到问题。
读driver结构对称,把full换成empty,把wdata换成rdata,其他不变。两个driver都不做数据检查,检查全部交给monitor和scoreboard,职责分开之后,改激励不会碰坏检查逻辑。
2.3 monitor到scoreboard:用队列reference model锁住数据顺序
monitor的上报条件必须是「有效传输」,不是「信号为高」。写monitor只在wr_en && !full时上报,读monitor只在rd_en && !empty时上报。少一个条件,scoreboard就会把DUT拒收的那一笔当成真写进去,后面所有比对全错,而且错得很隐蔽。
class fifo_wr_monitor extends uvm_monitor; `uvm_component_utils(fifo_wr_monitor) virtual fifo_if vif; uvm_analysis_port #(fifo_wr_item) ap; task run_phase(uvm_phase phase); forever begin @(vif.wr_cb); if (vif.wr_cb.wr_en && !vif.wr_cb.full) begin fifo_wr_item tr = fifo_wr_item::type_id::create("tr"); tr.data = vif.wr_cb.wdata; ap.write(tr); end end endtask endclassscoreboard这一侧我用一个SV队列做reference model,写进来就push,读出去就pop并比对。队列长度上限设成DUT深度,一旦模型满了还有写事务进来,直接报错——这说明DUT在满的时候错误地接收了数据。
`uvm_analysis_imp_decl(_wr) `uvm_analysis_imp_decl(_rd) class fifo_scoreboard extends uvm_scoreboard; `uvm_component_utils(fifo_scoreboard) uvm_analysis_imp_wr #(fifo_wr_item, fifo_scoreboard) wr_imp; uvm_analysis_imp_rd #(fifo_rd_item, fifo_scoreboard) rd_imp; logic [7:0] ref_q[$]; int unsigned depth = 16; function void write_wr(fifo_wr_item tr); if (ref_q.size() < depth) ref_q.push_back(tr.data); else `uvm_error("SB", "model full but DUT accepted a write") endfunction function void write_rd(fifo_rd_item tr); logic [7:0] exp; if (ref_q.size() == 0) begin `uvm_error("SB", "model empty but DUT returned data") end else begin exp = ref_q.pop_front(); if (tr.data !== exp) `uvm_error("SB", $sformatf("order mismatch exp=%0h act=%0h", exp, tr.data)) end endfunction endclassuvm_analysis_imp_decl这两行必须写在类外面,否则两个imp的方法名会撞在一起。env里把两个agent的monitor ap分别连到scoreboard的两个imp上,连接动作放在connect_phase,不要在build_phase里提前连,那时候monitor还没构造完。
这套骨架跑起来之后,功能正确性基本有保障,但异步FIFO真正难的地方不在数据通路,而在空满标志和跨域时序。下一章把这部分拆成可执行的验证点。
3. 异步FIFO验证点拆解:空满边界、时钟比矩阵与复位时序
数据比对只能证明「读出来的顺序对」,证明不了「满的时候有没有多写一笔」「空的时候读指针有没有动」「复位释放后第一拍采样对不对」。异步FIFO的bug绝大多数藏在这三类边界里,而随机激励恰好最难命中它们。所以验证点要单独设计,定向激励打底,随机只做补充。
3.1 写满再写、读空再读:构造刚好压线的激励
最容易漏的验证点是「满后一笔」和「空后一笔」。写满depth个数据后,DUT的full应该拉高,此时再给一笔写,写指针必须纹丝不动,存储器里的数据也不能被覆盖。读空同理,empty拉高后再给读,读指针不能动,rdata不能变成X。
构造这个场景有两种写法。一种是靠driver的ignore_full开关,强行发一笔出去,让DUT自己去拒;另一种是发满depth笔之后停住,直接用断言盯住指针。我一般两种都做:定向sequence负责把水位精确推到depth,断言负责判断行为。
class fifo_full_boundary_seq extends uvm_sequence #(fifo_wr_item); `uvm_object_utils(fifo_full_boundary_seq) int unsigned depth = 16; task body(); fifo_wr_item req; repeat (depth) begin req = fifo_wr_item::type_id::create("req"); start_item(req); if (!req.randomize() with { data inside {[8'h00:8'hFF]}; }) `uvm_error("SEQ", "randomize failed") finish_item(req); end // 第depth+1笔:driver置ignore_full=1时才会真正发出去 req = fifo_wr_item::type_id::create("req"); start_item(req); req.data = 8'hA5; finish_item(req); endtask endclassrepeat(depth)这16笔发完,理论上FIFO正好满。第17笔发出去的时候,如果DUT的满判断正确,full应该阻止它写入;如果DUT的满判断晚了一拍,这一笔就会覆盖掉最早的数据,后面读出来的第一个值就不是原来的第一个值,scoreboard立刻报顺序错。这种错在纯随机激励下出现的概率很低,因为随机写读交错,水位很难精确停在depth。
读空边界对称处理:先写若干笔,再读同样多笔,然后再读一笔。检查点是empty保持为高、读指针不变、rdata不变。有些设计在空时会把rdata清零,这属于实现选择,验证时要跟设计规格对齐,不能想当然认为它该保持。
注意:水位精确的sequence一定要串行执行,不要和随机sequence并行跑。并行的时候读侧随时可能把数据取走,水位永远推不到depth,定向测试就失去意义了。
3.2 时钟比不是随便调:从1:1到1:7的组合矩阵
异步FIFO的跨域路径只有在两个时钟频率不同、或者同频不同相的时候才会被真正压到。1:1同相是最舒服的场景,两级同步触发器几乎永远采在稳定区,跑一万次也不会出问题,但这说明不了任何事。
我一般会跑一张固定的时钟比矩阵,每个组合配一条随机sequence,跑固定种子数:
| 场景 | 写时钟周期 | 读时钟周期 | 主要压什么 |
|---|---|---|---|
| 1:1 同相 | 10ns | 10ns | 基线功能,数据顺序 |
| 1:1 反相 | 10ns | 10ns | 采样窗口最差,亚稳态概率最高 |
| 1:2 | 10ns | 20ns | 写快读慢,长时间接近满 |
| 2:1 | 20ns | 10ns | 读快写慢,长时间接近空 |
| 1:7 | 10ns | 70ns | 满标志长时间有效,写侧频繁被阻塞 |
| 7:1 | 70ns | 10ns | 空标志长时间有效,读侧频繁被阻塞 |
1:1反相这一行最容易被跳过,但它恰恰是最有价值的。两个时钟同频、相位差半个周期,同步触发器采样点正好落在对方信号的跳变附近,亚稳态窗口被压到最窄,能不能扛住全看格雷码是不是真的只变一位。这一条过不了,别的组合基本都是白跑。
时钟在tb_top里用参数控制,方便回归时用plusarg覆盖:
module tb_top; logic wr_clk = 0, rd_clk = 0; real WR_HALF = 5.0; // 写时钟半周期,默认10ns real RD_HALF = 12.5; // 读时钟半周期,默认25ns initial begin void'($value$plusargs("WR_HALF=%f", WR_HALF)); void'($value$plusargs("RD_HALF=%f", RD_HALF)); end always #(WR_HALF) wr_clk = ~wr_clk; always #(RD_HALF) rd_clk = ~rd_clk; endmodule$value$plusargs读的是实数,回归脚本里给+WR_HALF=5.0 +RD_HALF=35.0就是1:7。这样一张矩阵不用改代码,换命令行参数就能跑完。注意别把半周期写成整数除法,#(5)和#(5.0)在有些仿真器上行为一致,但写成#(WR_HALF/2)就会丢精度,相位关系直接变形。
还有一类场景要单独加:时钟停振。读时钟停住若干个周期,写侧会一路写到满;写时钟停住,读侧会一路读到空。停振期间跨域同步链的输出保持不变,这是检查「满标志会不会误清」「空标志会不会误拉」的好机会。停振的时长要超过同步链的延迟,一般给5到10个对方时钟周期。
3.3 复位打在传输中间:异步复位同步释放的检查点
复位验证最忌讳只在开头拉一次。异步FIFO的读写指针分属两个时钟域,复位释放的时刻如果在两个域里不一致,指针可能从非零值开始跑,空满判断直接失效。所以复位要打在传输中间,而且要分别检查两个域的释放行为。
我的做法是写一个独立的reset sequence,在随机读写跑起来之后,随机选一个时刻拉低wr_rst_n或rd_rst_n,保持几个周期再释放。检查点有三个:复位期间empty必须为高、full必须为低;复位释放后第一个写事务的地址必须从0开始;复位前后不能有残留数据被读出来。
class fifo_mid_reset_seq extends uvm_sequence; `uvm_object_utils(fifo_mid_reset_seq) virtual fifo_if vif; task body(); repeat (20) @(posedge vif.wr_clk); fork begin vif.wr_rst_n <= 1'b0; repeat (3) @(posedge vif.wr_clk); vif.wr_rst_n <= 1'b1; end begin vif.rd_rst_n <= 1'b0; repeat (5) @(posedge vif.rd_clk); // 故意让两个域释放时刻不同 vif.rd_rst_n <= 1'b1; end join repeat (10) @(posedge vif.wr_clk); endtask endclass两个复位的保持长度故意写成3和5,就是为了制造释放时刻的错位。如果设计里复位释放没做同步,这个错位会直接把指针打到一个非法值,后面的空满判断全乱,scoreboard会以「读出的数据和模型不符」的形式报出来——但根因在复位,不在数据通路,所以看到这类报错先去查复位释放的波形,别一头扎进scoreboard。
提示:复位释放后的第一个时钟沿,clocking block的采样值可能是X。断言里要用
disable iff把复位期间屏蔽掉,否则回归日志会被复位期的失败刷屏,真正的问题反而被淹没。
这三个方向跑完,功能层面的信心基本有了。但异步FIFO最怕的是「仿真过了、上板挂了」,靠激励和比对抓不到跨域时序的偶发问题,得靠断言和覆盖率把证据留下来。
4. SVA断言与功能覆盖率:让格雷码跨域问题留下证据
比对只能抓「已经发生并且被观测到」的错误。跨域路径上的偶发采样错误,可能跑十万个周期才出现一次,出现的时候又被后面的正确数据掩盖,scoreboard根本看不出来。断言的作用是把「不允许发生的事」写成常驻检查,覆盖率的作用是证明「该压的场景都压过了」。这两样东西是异步FIFO验证里性价比最高的投入。
4.1 格雷码单比特翻转与指针单调性断言
格雷码跨域的核心假设是「每次只有一位变化」,这个假设不成立,两级同步就失去意义。所以第一条断言就盯这个:写指针格雷码相邻两次变化,异或结果必须是一热。
module fifo_sva #(parameter int ASIZE = 4) (input logic wr_clk, rd_clk, wr_rst_n, rd_rst_n, input logic wr_en, full, rd_en, empty, input logic [ASIZE:0] wgray, rgray, wbin, rbin); // 格雷码相邻变化只允许一位翻转 property p_wgray_onehot; @(posedge wr_clk) disable iff (!wr_rst_n) $changed(wgray) |-> $onehot(wgray ^ $past(wgray)); endproperty a_wgray_onehot: assert property (p_wgray_onehot); // 写指针不允许回退,只允许保持或加一(含回绕) property p_wptr_mono; @(posedge wr_clk) disable iff (!wr_rst_n) (wbin == $past(wbin)) || (wbin == ($past(wbin) + 1'b1)); endproperty a_wptr_mono: assert property (p_wptr_mono); endmodule$onehot判断的是异或结果里是否恰好只有一位为1,这正好对应格雷码的定义。$past取的是上一个采样沿的值,所以disable iff (!wr_rst_n)必不可少——复位期间指针从任意值跳回0,可能翻转多位,不屏蔽就会误报。
指针单调性这条断言抓的是另一类bug:空满判断逻辑写错导致指针被错误地加或减。正常情况下写指针只会保持或加一,回绕的时候用模运算判断,wbin == $past(wbin) + 1在5位宽下从31回到0依然成立,不需要额外处理。读侧对称写一条rbin的即可。
这两条断言都是白盒信号,需要层次化引用DUT内部指针。有人觉得这样耦合太紧,改一版设计就要改断言。我的看法是异步FIFO的指针就是它的核心协议,断言不盯这里,黑盒根本盯不住。真要做成可复用的VIP,可以把这几条断言做成bind文件,跟DUT解耦。
4.2 空满互斥与假满假空:把保守行为也覆盖进去
第二条要盯的是「满时不能写、空时不能读」。这条看起来是废话,但实现里最常见的错误就是满标志晚一拍拉高,导致最后一笔写把数据覆盖掉。
// 满标志有效后的下一个周期,写指针必须停住 property p_no_write_when_full; @(posedge wr_clk) disable iff (!wr_rst_n) full |=> $stable(wbin); endproperty a_no_write_when_full: assert property (p_no_write_when_full); // 空标志有效后的下一个周期,读指针必须停住 property p_no_read_when_empty; @(posedge rd_clk) disable iff (!rd_rst_n) empty |=> $stable(rbin); endproperty a_no_read_when_empty: assert property (p_no_read_when_empty);用|=>而不是|->是有讲究的。full可能在某个沿上刚拉高,而这一拍上还有一个已经在进行中的写事务(DUT已经接收了),这时候指针动是正常的。从下一拍开始才必须停住,|=>正好对应这个时序。
空满互斥这一条要谨慎写。full在写域产生,empty在读域产生,直接跨域比较在复位释放的瞬间可能同时为高,属于误报高发区。我一般会加两个复位都释放的条件,然后把它当成弱检查,只抓「长期同时为高」的情况:
a_full_empty_excl: assert property ( @(posedge wr_clk) disable iff (!wr_rst_n || !rd_rst_n) !(full && empty) );还有一类行为容易被当成bug报上来:假满和假空。格雷码指针同步需要两拍,同步过来的读指针永远比实际值旧,所以写侧判断满的时候会偏保守,明明还有空间就报了满。这是设计上的正确取舍,不是bug。验证时要明确「假满允许,假空允许,但真满必须报满、真空必须报空」,否则会写出一堆误报,最后没人看断言日志。
判断真满的方法是看写侧指针和读侧指针的实际差值,用层次化引用拿DUT内部的二进制指针算,不依赖同步后的值。这个检查可以放在scoreboard的check_phase里做统计,也可以写成一条只在特定条件下使能的断言。
4.3 覆盖率交叉设计:水位、时钟比、复位三轴
功能覆盖率要回答的问题是「该压的场景压到了没有」。异步FIFO的覆盖率如果只覆盖读写操作,跑一天也收不满,因为真正的边界在组合上。我一般按三个轴设计:FIFO水位、操作类型、时钟比,然后做交叉。
class fifo_coverage extends uvm_subscriber #(fifo_cg_item); `uvm_component_utils(fifo_coverage) covergroup cg; option.per_instance = 1; cp_level: coverpoint tr.level { bins empty = {0}; bins low = {[1:3]}; bins mid = {[4:11]}; bins high = {[12:15]}; bins full = {16}; } cp_op: coverpoint tr.op { bins wr_normal = {WR}; bins wr_at_full = {WR_FULL}; bins rd_normal = {RD}; bins rd_at_empty= {RD_EMPTY}; } cp_ratio: coverpoint tr.clk_ratio { bins same_phase = {RATIO_1_1_SAME}; bins opp_phase = {RATIO_1_1_OPP}; bins wr_fast = {RATIO_1_2, RATIO_1_7}; bins rd_fast = {RATIO_2_1, RATIO_7_1}; } cx_level_op: cross cp_level, cp_op; endgroup endclasscp_level的五个bin里,empty和full最容易收不满,因为随机激励下水位很少精确停在两端。这两个bin要靠3.1节的定向sequence来收,不能指望随机。cp_op里的wr_at_full和rd_at_empty对应「满时写」「空时读」两个异常操作,采集点放在monitor上报事务的时候,用当时的full/empty值打标签。
交叉覆盖cx_level_op是收覆盖率的主力。空水位下的读、满水位下的写、中间水位下的正常读写,这十个交叉点收满,基本能说明边界场景都压过了。如果某个交叉点一直空着,先别怀疑覆盖率模型写错,去查对应的sequence有没有真的跑起来——我遇到过好几次是ignore_full开关没打开,wr_at_full这个bin从头到尾就没采到过。
时钟比的覆盖放在环境级采集,不在事务级。事务里带一个clk_ratio字段,由test在启动时配置,sequence创建item的时候填进去。这样换时钟比重跑回归,覆盖率报告里能直接看出哪几种比例跑过、哪几种没跑。
5. 异步FIFO验证避坑:五个让仿真「假通过」的细节
仿真通过不等于设计正确。异步FIFO尤其容易出现「环境自己骗自己」的情况:波形看着对,日志全是PASS,换个时钟比或者上板就挂。下面五条是我自己踩过、也见过别人踩的,每条都按现象、原因、解决写清楚。
5.1 满标志采样晚一拍,scoreboard却判定通过
现象:回归日志全绿,但把波形拉出来看,FIFO满之后又写进去了一笔,最早的数据被覆盖。scoreboard没报错,因为读出来的顺序看起来还是对的。
原因:monitor用@(posedge wr_clk)直接采样,采到的是沿上的竞争值。DUT在沿上判断full并拒收,monitor在同一个沿上可能采到full拉高之前的wr_en,把这一笔当成了有效写,reference model里多push了一个数。后面读出来的数据跟模型错位,但因为错位发生在模型内部,比对时反而「自洽」了。
解决:所有采样和驱动统一走clocking block,或者至少在monitor里写成@(posedge clk); #1;再采样。改完之后重新跑一遍,如果之前所有测试都还是绿的,说明这个bug之前一直被掩盖着。
5.2 用同频同相时钟跑通就收工
现象:本地仿真跑了几十个用例全部通过,代码覆盖率90%以上,换个时钟配置或者上板,偶发丢数据,复现概率大概千分之一。
原因:1:1同相时钟下,两级同步触发器的采样点永远落在信号稳定区,亚稳态窗口根本没被压到。跨域逻辑里的时序问题在这种配置下几乎不可能暴露,跑再多周期也没用。
解决:把3.2节的时钟比矩阵做成回归的必跑项,1:1反相和1:7这两组必须过。反相那一组要跑足够多的随机种子,因为它的失败概率本来就不高,跑一两个种子碰不上。
5.3 reset没有在时钟域内同步释放
现象:复位释放后第一个写事务的地址不是0,或者empty在释放后短暂拉低又拉高。单看某个时钟域的波形一切正常,两个域对着看才发现释放时刻差了半个周期。
原因:rst_n直接连到两个时钟域的触发器上,异步复位、异步释放。释放时刻如果正好落在某个时钟沿附近,触发器的输出可能进入亚稳态,两个域恢复到的状态不一致。
解决:每个时钟域各放一个复位同步器,异步复位、同步释放,释放沿打两拍再送出去。验证侧加一条断言:复位释放后第一个时钟沿,指针必须是0、empty必须是1。这条断言在复位错位时会立刻报出来,比等数据错位再去查快得多。
5.4 只用随机激励,从不构造精确水位
现象:跑了一整天,覆盖率卡在85%上不去,满和空两个bin一直是0。日志里没有任何错误,但就是收不满。
原因:随机写的分布是均匀的,读写交错之后水位在中间来回晃,很难精确停在depth或者0。满和空这两个状态需要连续同向操作才能到达,随机激励下概率极低。
解决:定向sequence打底,先灌满再读空,把两端的水位bin收掉,随机只做补充。定向sequence要和随机sequence分开跑,别放在同一个test里并行启动,并行的时候读侧随时把数据取走,水位永远推不到顶。
5.5 覆盖率100%但断言没开
现象:覆盖率报告很漂亮,所有bin都满了,但断言日志里attempts是0,一条都没触发过。看起来验证很充分,实际上跨域检查完全没做。
原因:编译选项没打开SVA支持,或者disable iff的条件写反了,导致断言在整个仿真期间都被禁用。还有一种情况是断言模块根本没例化,bind文件漏了。
解决:回归结束时检查断言的attempts计数,任何一条核心断言的attempts为0都要当失败处理。编译脚本里把SVA相关的选项显式加上,别依赖默认值。断言不是覆盖率,它不会出现在覆盖报告里,很容易被忽略。
6. 笔试与面试的收口:把异步FIFO讲成一个能被追问三层的故事
纸面手写异步FIFO,我的习惯是先落三个锚点,再补细节。第一个锚点是位宽:指针是ASIZE+1位,多出来的最高位区分绕圈,这一句写出来,阅卷的人就知道你懂空满判断的前提。第二个锚点是格雷码转换:二进制到格雷码是bin ^ (bin >> 1),格雷码回二进制是逐位异或,两个函数各三行,写清楚比背公式有用。第三个锚点是两级同步加比较:写侧同步读指针判满,读侧同步写指针判空,满的条件是最高两位相反、其余相同。
这三个锚点写对,代码骨架就立住了。剩下的空满标志是寄存器输出还是组合输出、复位怎么接、存储器用reg数组还是例化RAM,都是可以现场解释的取舍,不影响及格线。
面试追问通常会往下走三层。第一层问「为什么用格雷码不用二进制」,答每次只变一位、同步后最多采到旧值或新值、不会出现中间态。第二层问「同步几拍,为什么」,答两拍,第一拍可能亚稳态,第二拍基本稳定,拍数越多越保守但延迟越大。第三层问「假满能不能接受」,这一层是分水岭,答能接受、假满只是损失吞吐不影响正确性的人,说明真读过设计;答不能接受、要消除假满的人,大概率只背过八股。
再往深里问「深度不是2的幂怎么办」,标准答案是改成2的幂或者用额外的计数器,但更好的回答是指出格雷码回绕的前提就是2的幂,深度不是2的幂时格雷码不再保证单比特翻转,得换方案。这个问题能答上来,基本就不用担心这一轮了。
我自己带人时常说一句话:异步FIFO的代码能在纸上默写出来不算本事,能在波形上指出假满发生在哪一拍、为什么那一拍不报错,才算真懂。每次跑完回归,我都会把断言日志翻一遍,看attempts数对不对,这比看覆盖率报告有用得多。希望帮到你。
本文还有配套的精品资源,点击获取