FIFO 验证:同步 FIFO 标志正确性与 overflow/underflow 端到端实践
更新时间:2026-09-25。FIFO 是芯片里出现频率最高的小模块之一,跨时钟域用异步 FIFO,同域缓冲用同步 FIFO。它的逻辑不难,但标志位错一拍、overflow 静默吞数据,到了系统级往往表现为随机丢包,极难回溯。这一篇以深度参数化的同步 FIFO 为对象,讲一次完整的 FIFO 验证:从指针与标志的判定原理,到 testplan、激励、SVA、覆盖率和 scoreboard 的落地写法。断言写法可对照 SVA 断言实践,覆盖率组织可对照 代码功能覆盖率。
本文要回答的问题
- full、empty、almost-full、almost-empty 四个标志分别在什么拍成立,能不能有组合毛刺?
- 「读写指针同址」既可能是空也可能是满,工程上怎么区分这两种状态?
- 写满了再写(overflow)、读空了再读(underflow)时,存储器内容和指针应该如何表现?
- 指针回卷、复位后的初值、同一拍同时读写,这些边界场景怎么激励、怎么检查?
- scoreboard 的参考模型怎么建,覆盖率里 depth 与同时读写的 cross 为什么必须收?
一、先看清 DUT:同步 FIFO 的内部结构
本文的验证对象是一个标准同步 FIFO:单时钟 clk、同步低有效复位 rst_n,写侧信号为 wr_en/wdata,读侧为 rd_en/rdata,对外输出 full、empty、almost_full、almost_empty,可选 count。深度 DEPTH 和位宽 WIDTH 参数化,存储体是一块寄存器阵列或 SRAM 行为模型。内部维护写指针 wptr、读指针 rptr,均按 DEPTH 取模回卷;另有一个数据计数 count,每拍按读写情况加减。

上图有一条读代码时容易忽略的细节:rdata 通常由 mem[rptr] 直接输出,也就是说「读」这一拍数据已经在端口上,rd_en 的作用是让指针前进,而不是去发起一次存储器读。写则相反,wr_en 有效这一拍把 wdata 写进 mem[wptr],再推进指针。理解这一拍的差异,才能解释为什么 empty 期间 rdata 可能是旧数据但协议上不允许采样。
还有一种常见变体是首字直达(First-Word Fall-Through,FWFT):只要 FIFO 非空,队头数据就直接出现在 rdata 上,rd_en 表示「弹出当前队头、露出下一个」。它与标准读模式的拍序完全不同,empty 标志的含义也随之变化。开工前必须确认 DUT 属于哪一种,本篇以标准读模式为例,FWFT 只需把参考模型的出队时机整体提前一拍。深度与位宽也要在环境里参数化,建议至少用 DEPTH=1(退化边界)、4、8、16 四档配置回归——深度为 1 时空满几乎同时相邻,最能暴露阈值和标志的拍序错误。
FIFO 验证的本质可以浓缩成一句话:在任意合法的读写激励序列下,标志位如实反映容量状态,进出数据顺序与内容一致,非法操作不破坏已有数据。前半句靠标志检查,后半句靠 scoreboard,而「任意合法序列」靠受约束随机加定向边界用例覆盖。
二、标志判定:同址歧义与指针差方案
FIFO 最经典的设计陷阱是:wptr == rptr 既出现在复位后的空状态,也出现在写到最后一格追上读指针的满状态。只比较两个指针无法区分空满。工程上有两种主流解法。
第一种是额外维护计数器 count:写而不满则加一,读而不空则减一,同拍又读又写则不变。empty = (count==0),full = (count==DEPTH),标志直观、验证也简单,代价是每拍都要做加减和比较,深度大时时序稍紧。第二种是指针扩展一位:指针位宽取 log2(DEPTH)+1,低位索引存储体,最高位记录回卷次数。读写指针高位不同、低位相同表示满;高位也相同才表示空。两种实现的外部行为必须一致,FIFO 验证对标志的检查不应绑定内部实现方式。

almost 标志是带阈值的提前预警:almost_full 在 count >= DEPTH-AF_DEPTH(或剩余空间小于阈值)时成立,almost_empty 在 count <= AE_DEPTH 时成立。阈值一般参数化,验证时必须把阈值也拉进参数化配置矩阵,至少覆盖 0、1、DEPTH/2、DEPTH-1 这几档。需要向设计师确认的一个语义点是:almost 区间与 full/empty 是否重叠——常见约定是 full 时 almost_full 也为 1。这种约定必须写进 testplan,不能靠验证工程师猜。
三、关键波形:标志在边界上的逐拍行为
标志检查最容易漏的是「拍」。以深度 4、阈值 1 的 FIFO 为例:连续写四拍后,第四拍写使能有效时 full 往往还是 0,要到这一拍结束、指针更新后的下一拍 full 才拉高;此时外部若严格按 full 反压,就不会在 full 为 0 的第四拍多写。读侧同理,读空最后一个数据的下一拍 empty 才成立。

这张拍序图对应两条必须在 testplan 里显式列出的检查:其一,标志与 count(或指针判据)在时钟沿后严格一致,不允许出现 X 态和组合毛刺;其二,同拍同时读写时标志的演化要正确——空状态下又读又写,结果应保持空且新数据可在下一拍读出;满状态下又读又写,结果应保持满且读出的是最旧数据、写入的是最新数据。这两条是随机回归中真正能逮到设计缺陷的高频场景。
读侧的拍序是写侧的镜像:连续读到还剩两个数据时 almost_empty 拉高,读出最后一个数据的下一拍 empty 拉高;empty 之后继续给 rd_en,rdata 要么保持最后一笔、要么为约定值,但参考模型不采它。把写满读空两条拍序放在同一张图里对照,能快速判断设计师是否把标志提前或延后了一拍——这类错误在单口激励下不明显,到了上游模块按 full 做反压时才表现为间歇溢出。
标志稳定性还包括复位行为:同步复位释放后的第一拍,empty 必须为 1、full 为 0、almost_empty 视阈值为 1、两个指针归零、rdata 允许为 X 或约定值。复位期间施加读写也应被忽略。建议加一个复位中途拉低再释放的定向用例,确认存储体残留不会让标志误判;还要在非空状态下复位,复位后重新写入并读回,确认读回的是新数据而不是复位前的残留——后者是存储体未按指针重建时的典型漏出。
四、testplan 要点与激励:定向打底、随机铺面
FIFO 验证的 testplan 可以按「初值与复位、标志正确性、容量边界、非法操作、同址读写、指针回卷、数据完整性」分组,每一条都映射到具体的检查手段和覆盖仓,写法参照 testplan 与覆盖率映射。
| 测试点 | 激励方式 | 检查手段 |
|---|---|---|
| 复位初值 | 复位释放后立即采样 | SVA + 定向比对 |
| 空/满标志 | 连续写满、连续读空 | SVA 与参考 count 比对 |
| almost 阈值 | 阈值参数化多档回归 | SVA + 覆盖仓 |
| overflow | full 后继续写若干拍 | SVA 报错 + mem 完整性 |
| underflow | empty 后继续读若干拍 | SVA 报错 + 数据顺序 |
| 同时读写 | 随机同拍使能 | scoreboard 全量比对 |
| 指针回卷 | 长序列多轮填满排空 | 覆盖率 cross 保证 |
激励上分两层。定向用例负责把边界精确钉死:纯写直到满、满后再写 N 拍、纯读直到空、空后再读 N 拍、写满读空重复三到五轮验证回卷、复位中读写。随机用例负责产生人想不到的合法序列,约束方式参照 受约束随机验证:每拍 wr_en、rd_en 独立加权随机;权重随当前填充量动态调整——接近空时压低读权重、接近满时压低写权重,把激励集中在边界附近,同时保留小概率的非法操作触发错误检查。写入数据用带标签的递增或伪随机图案,让每一格的数据都可唯一追溯,避免全写 5A 这种检不出地址错位的激励。
五、SVA:写满再写、读空再读立即报错
FIFO 验证最适合用 SVA 把协议规则固化下来。下面一组断言绑定到接口上,覆盖标志一致、非法操作和读写结果。非法操作的语义需要先与设计师约定:一种是「满时写被忽略且不报中断」,另一种是给出 overflow 脉冲;无论哪种,已有数据和指针都不得被改变。
// 标志与非法操作检查
a_empty: assert property (@(posedge clk) disable iff (!rst_n)
empty |-> (wptr==rptr) /* 或 count==0,按实现选取 */);
a_full_write: assert property (@(posedge clk) disable iff (!rst_n)
(full && wr_en && !rd_en) |=> (wptr==$past(wptr))); // overflow 指针不动
a_empty_read: assert property (@(posedge clk) disable iff (!rst_n)
(empty && rd_en && !wr_en) |=> (rptr==$past(rptr))); // underflow 指针不动
a_simrw_count: assert property (@(posedge clk) disable iff (!rst_n)
(wr_en && rd_en && !full && !empty) |=> ($stable(count)));
a_rst_init: assert property (@(posedge clk)
$rose(rst_n) |-> empty && !full);
almost 标志也建议各配一条边界断言,例如 count==DEPTH-AF_DEPTH 当拍写使能有效时,下一拍 almost_full 必须成立;阈值参数变化时断言随参数自动重算,不需要逐档改代码。断言可以直接写在 interface 里,也可以用 bind 绑定到 DUT 实例,后者不改动设计源码、综合时天然剥离,更适合交付给设计团队的项目。
上图表达了检查层次:先判入口操作是否合法,再验证容量状态与状态不变量,最后由 scoreboard 兜底数据顺序。注意 overflow 用例的目的不是「证明 DUT 会报错」,而是证明它在错误激励下不损坏——断言检查的是 wptr 保持、最旧数据仍可正常读出、随后一次合法写能成功写入。如果设计约定满时写通过同拍读腾出的空间完成(满状态又读又写),断言要把这种合法情形排除,参考上一节的同拍读写语义。
六、覆盖率:depth × 同时读写的 cross
单纯收「full 出现过」「empty 出现过」远远不够,FIFO 的高风险状态集中在填充深度与操作类型的组合上。功能覆盖率建议建一个 coverage collector,订阅 monitor 广播的每拍状态,组织方式参照 代码功能覆盖率。
covergroup fifo_cg @(posedge clk);
cp_depth: coverpoint count {
bins empty = {0};
bins ae = {[1:AE_DEPTH]};
bins mid = {[AE_DEPTH+1:DEPTH-AF_DEPTH-1]};
bins af = {[DEPTH-AF_DEPTH:DEPTH-1]};
bins full = {DEPTH};
}
cp_op: coverpoint {wr_en, rd_en} {
bins idle = {2'b00}; bins write = {2'b10};
bins read = {2'b01}; bins both = {2'b11};
}
cp_wrap: coverpoint (wptr==0 && rptr==0); // 双指针回卷同址
cx_depth_op: cross cp_depth, cp_op;
endgroup
这张覆盖矩阵里有四个仓最值得盯:empty 下的 write(单写脱离空)、empty 下的 both(又读又写仍为空)、full 下的 read(单读脱离满)、full 下的 both(不读空也不溢出)。随机回归里它们天然命中概率低,收齐矩阵往往需要加定向约束补充。另有几个独立 coverpoint:两个指针各自回卷到 0、双指针同时为 0、overflow/underflow 各至少一次、复位发生在非空状态。阈值参数变化时要按阈值配置分仓合并,避免 DEPTH=8 与 DEPTH=16 的数据混在一张报告里。
七、scoreboard:用参考模型逐拍比对
FIFO 的参考模型简单到几乎没有借口不做:SystemVerilog 里一个 queue 就是黄金模型。monitor 把每拍的有效操作广播出来,scoreboard 维护一个数据队列,按同一规则更新并在每次读操作时比对队头,比对策略可参照 scoreboard 轨迹比对。
task void write_op(bit w, bit r, logic [WIDTH-1:0] d);
if (w && (ref_q.size() < DEPTH)) ref_q.push_back(d); // 满时写:忽略
if (r && (ref_q.size() > 0)) begin
exp = ref_q.pop_front(); // 空时读:不比对
if (exp !== rdata) `uvm_error("FIFO_SB", "数据或顺序不符")
end
endtask参考模型必须与设计约定严格对齐三件事:满时写是忽略还是覆盖、空时读 rdata 的合法性、同拍读写时 rdata 取旧队头还是新写入值。常见实现是读旧数据,若设计读的是绕过(first-word-fall-through 或 bypass)语义,参考模型也要相应改写。检查收尾是关键一步:每个用例结束时参考队列必须排空,最后一批数据若还压在 FIFO 里没读出来,数据完整性检查就根本没有执行——假性通过大多出在这里。
除了逐笔数据,scoreboard 还应维护一份参考 count,每拍与 DUT 的标志做全量比对;没有引出 count 时就用队列大小自行推导四个标志。这样标志检查不依赖任何内部信号,DUT 换一种指针实现(计数器方案换扩展位方案),验证环境一行不改,这正是 FIFO 验证环境最该保住的复用性。
八、常见坑与回归收尾
FIFO 模块虽小,下面这些坑在项目里反复出现,值得在 testplan 评审时逐条核对。
| 坑 | 典型表现 | 处理方式 |
|---|---|---|
| 标志差一拍 | 反压方在 full 前一刻多写一笔 | 逐拍 SVA 对齐参考 count,不接受「差不多」 |
| 阈值写死 | 改 DEPTH 后 almost 语义错位 | 阈值参数化并按档回归 |
| 激励全写相同数据 | 地址错位、高低位颠倒检不出 | 带标签递增或伪随机图案 |
| underflow 比对旧 rdata | 空读时误报错或漏报错 | 空期间不采样,只查指针不动 |
| 不排空就结束 | 最后几笔数据从未比对 | 收尾强制队列排空、used 为 0 |
| 随机不碰边界 | 回归绿但 full 从未出现 | 权重随填充量动态偏置加定向补仓 |
| 复位后残留数据 | 重新读回旧数据 | 非空复位用例 + 写入新数据回读 |
回归管理上建议把 FIFO 用例分三组:一组快速定向冒烟,每次代码变更都跑;一组深度随机,跑固定种子集合保证可复现;一组长序列(数百轮填满排空)放夜间回归,专门暴露回卷和长时间漂移问题。每次收网除了看通过率,还要核对覆盖率矩阵里四个重点 cross 仓是否齐全、全程 uvm_error/uvm_fatal 计数为零、最后一个用例结束时参考队列已排空。随机失败时保留种子与波形,用相同种子单独重放——随机问题不可复现,比失败本身更麻烦,所以种子固化应当做硬规则而不是习惯。相关流程可参照 回归管理与失败分级。
这一篇在知识体系里的位置
- 向上承接:SVA 断言实践 提供断言语法与 bind 写法,本篇是其在具体模块上的完整应用。
- 横向相关:代码功能覆盖率 讲 covergroup 机制,本篇给出 depth 与操作 cross 的实战组织。
- 方法支撑:testplan 与覆盖率映射 说明测试点如何落到检查和覆盖仓,受约束随机验证 提供激励生成方法。
- 延伸方向:异步 FIFO(带格雷码指针与双时钟域)在本篇之外还需 跨时钟域验证 的方法;scoreboard 的通用写法见 scoreboard 轨迹比对。
总结
FIFO 验证的核心是把「容量状态」和「数据完整性」两条线查到底:用指针同址加计数器或扩展位判空满,逐拍核对 full/empty 与 almost 标志,用 SVA 保证 overflow 不写坏、underflow 不读空,用 depth × 同时读写的 cross 收齐边界场景,再以 queue 参考模型的 scoreboard 逐笔比对并在收尾排空——标志不错一拍、数据不丢一笔,这个 FIFO 才算验完。