FIFOs are among the most verification-intensive modules in SoC design — deceptively simple in spec but rich in corner cases. A complete FIFO verification plan covers SVA assertions for flag accuracy, a queue-based reference model, functional coverage of all fill levels, and targeted corner-case tests for simultaneous push/pop, reset during operation, and back-to-back full conditions.
A synchronous FIFO with parameterised depth N and width W exposes these signals. Every verification plan begins with a precise signal table — ambiguity in flag semantics causes more FIFO bugs than any RTL error:
| Signal | Direction | Width | Description |
|---|---|---|---|
clk | Input | 1 | System clock — all sampling on rising edge |
rst_n | Input | 1 | Active-low synchronous reset — empties FIFO |
wr_en | Input | 1 | Write enable — push data_in when high and !full |
rd_en | Input | 1 | Read enable — pop and present data_out when high and !empty |
data_in | Input | W | Data to push into FIFO |
data_out | Output | W | Data popped from FIFO (registered — valid 1 cycle after rd_en) |
full | Output | 1 | High when FIFO contains N entries — push ignored when full |
empty | Output | 1 | High when FIFO contains 0 entries — pop ignored when empty |
count | Output | log2(N)+1 | Current number of entries in the FIFO (optional diagnostic) |
full after the last write (count reaches N), others assert it one cycle before. Verify flag timing against spec before writing any assertions — mismatched timing assumptions are the #1 false failure in FIFO verification.Four SVA properties form the safety net for any FIFO. These assertions should live in a bind file so they can be attached to any FIFO instance without modifying RTL:
// fifo_assertions.sv — bind to DUT at elaboration module fifo_sva #(parameter int DEPTH = 16, WIDTH = 8) ( input clk, rst_n, wr_en, rd_en, input [WIDTH-1:0] data_in, data_out, input full, empty, input [$clog2(DEPTH):0] count ); default clocking cb @(posedge clk); endclocking default disable iff (!rst_n); // P1: No push when full (overflow prevention) property no_overflow; @(posedge clk) disable iff(!rst_n) (wr_en && full) |-> 0; // must never occur endproperty NO_OVERFLOW: assert property(no_overflow) else `uvm_error("FIFO_SVA", "Overflow: wr_en high when FIFO is full"); // P2: No pop when empty (underflow prevention) property no_underflow; @(posedge clk) disable iff(!rst_n) (rd_en && empty) |-> 0; endproperty NO_UNDERFLOW: assert property(no_underflow) else `uvm_error("FIFO_SVA", "Underflow: rd_en high when FIFO is empty"); // P3: full flag accuracy — count==DEPTH implies full asserted property full_flag_acc; @(posedge clk) disable iff(!rst_n) (count == DEPTH) |-> full; endproperty FULL_FLAG: assert property(full_flag_acc) else `uvm_error("FIFO_SVA", "full not asserted when count==DEPTH"); // P4: empty flag accuracy — count==0 implies empty asserted property empty_flag_acc; @(posedge clk) disable iff(!rst_n) (count == 0) |-> empty; endproperty EMPTY_FLAG: assert property(empty_flag_acc) else `uvm_error("FIFO_SVA", "empty not asserted when count==0"); // P5: count stability on simultaneous push+pop property count_stable_on_sim_push_pop; @(posedge clk) disable iff(!rst_n) (wr_en && rd_en && !full && !empty) |=> (count == $past(count)); // net count unchanged endproperty SIM_PUSH_POP: assert property(count_stable_on_sim_push_pop) else `uvm_error("FIFO_SVA", "count changed on simultaneous push+pop"); // P6: count increments by 1 on pure push property count_inc_on_push; @(posedge clk) disable iff(!rst_n) (wr_en && !rd_en && !full) |=> (count == $past(count) + 1); endproperty COUNT_INC: assert property(count_inc_on_push); // Cover: FIFO reaches full COV_FULL: cover property(@(posedge clk) full); // Cover: FIFO reaches empty after being non-empty COV_EMPTY: cover property(@(posedge clk) !empty ##1 empty); endmodule // Bind statement in tb_top.sv bind sync_fifo fifo_sva #(.DEPTH(16),.WIDTH(8)) u_sva (.*);
The reference model mirrors the FIFO's behaviour using a SystemVerilog queue. It is the golden truth — every push enqueues to ref_q, every pop dequeues from ref_q, and the scoreboard compares ref_q.pop_front() against the DUT's data_out:
// fifo_ref_model.sv — standalone reference model class class fifo_ref_model extends uvm_component; `uvm_component_utils(fifo_ref_model) parameter int DEPTH = 16; parameter int WIDTH = 8; logic [WIDTH-1:0] ref_q[$]; // golden storage int error_count = 0; uvm_analysis_imp #(fifo_trans, fifo_ref_model) analysis_export; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); analysis_export = new("analysis_export", this); endfunction // Called on every observed DUT transaction function void write(fifo_trans t); case (t.op) PUSH: begin if (ref_q.size() < DEPTH) ref_q.push_back(t.data_in); else `uvm_error("REF", "Push when model full — overflow!"); end POP: begin if (ref_q.size() == 0) begin `uvm_error("REF", "Pop when model empty — underflow!"); return; end begin logic [WIDTH-1:0] expected_data = ref_q.pop_front(); if (expected_data !== t.data_out) begin error_count++; `uvm_error("REF_MISMATCH", $sformatf("data_out mismatch: expected=0x%02h got=0x%02h", expected_data, t.data_out)); end else `uvm_info("REF_MATCH", $sformatf("MATCH 0x%02h", t.data_out), UVM_HIGH); end end SIM_PUSH_POP: begin // simultaneous push+pop logic [WIDTH-1:0] expected_data = ref_q.pop_front(); ref_q.push_back(t.data_in); // push after pop to keep order if (expected_data !== t.data_out) begin error_count++; `uvm_error("REF_SIM", $sformatf("Sim push/pop mismatch: exp=0x%02h got=0x%02h", expected_data, t.data_out)); end end endcase endfunction function void report_phase(uvm_phase phase); `uvm_info("REF", $sformatf("FIFO Reference Model: %0d errors | queue depth at end: %0d", error_count, ref_q.size()), UVM_NONE); endfunction endclass
Coverage drives test closure. The FIFO covergroup must capture every structurally distinct fill state and all important stimulus combinations. Without bins for the threshold boundaries, constrained-random rarely hits them without bias:
covergroup fifo_cg @(posedge clk); option.name = "fifo_cg"; option.comment = "FIFO fill level and stimulus coverage"; option.per_instance = 1; // Fill level bins: 0, 1, mid, N-1, N CP_FILL: coverpoint count { bins empty = {0}; bins single = {1}; bins low = {[2:DEPTH/4-1]}; bins mid = {[DEPTH/4:DEPTH*3/4]}; bins high = {[DEPTH*3/4+1:DEPTH-2]}; bins nearly_full = {DEPTH-1}; bins full_state = {DEPTH}; } // Operation type: push, pop, simultaneous, idle CP_OP: coverpoint {wr_en, rd_en} { bins idle = {2'b00}; bins push = {2'b10}; bins pop = {2'b01}; bins sim_op = {2'b11}; } // Cross: what operation at what fill level? // Critical: push at nearly_full, pop at single CX_FILL_OP: cross CP_FILL, CP_OP { // Illegal: push at full, pop at empty illegal_bins overflow = binsof(CP_FILL.full_state) && binsof(CP_OP.push); illegal_bins underflow = binsof(CP_FILL.empty) && binsof(CP_OP.pop); } // Reset during operation CP_RESET: coverpoint rst_n { bins in_reset = {0}; bins out_of_reset = {1}; } // Transition: back-to-back pushes until full CP_FULL_SEQ: coverpoint full { bins rise = (0 => 1); // transition 0→1 (FIFO became full) bins fall = (1 => 0); // transition 1→0 (first read after full) } endgroup
mid bin is usually hit early; the hard bins are nearly_full (count==N-1) and single (count==1). Add a directed test that fills to N-1 then checks full is not yet asserted before biasing random to hit these bins.A self-contained SystemVerilog testbench wraps the reference model, assertions, and coverage into tasks. This structure also works as the DUT-level testbench before the full UVM environment is built:
module tb_fifo; parameter int DEPTH = 16; parameter int WIDTH = 8; logic clk = 0, rst_n; logic wr_en, rd_en; logic [WIDTH-1:0] data_in; logic [WIDTH-1:0] data_out; logic full, empty; logic [$clog2(DEPTH):0] count; always #5 clk = ~clk; // 100 MHz sync_fifo #(.DEPTH(DEPTH),.WIDTH(WIDTH)) dut (.*); // Reference queue (golden model) logic [WIDTH-1:0] ref_q[$]; int errors = 0, checks = 0; task automatic reset_dut(); rst_n = 0; wr_en = 0; rd_en = 0; data_in = 0; ref_q = {}; repeat(4) @(posedge clk); rst_n = 1; @(posedge clk); $display("[TB] Reset complete"); endtask task automatic push(input logic [WIDTH-1:0] d); if (full) begin $display("[TB] Push skipped — FIFO full"); return; end @(negedge clk); wr_en = 1; data_in = d; @(posedge clk); ref_q.push_back(d); @(negedge clk); wr_en = 0; endtask task automatic pop(); logic [WIDTH-1:0] exp; if (empty) begin $display("[TB] Pop skipped — FIFO empty"); return; end @(negedge clk); rd_en = 1; @(posedge clk); // data latches @(posedge clk); // registered output valid exp = ref_q.pop_front(); checks++; if (data_out !== exp) begin errors++; $error("[TB] POP mismatch: exp=0x%02h got=0x%02h", exp, data_out); end @(negedge clk); rd_en = 0; endtask task automatic fill_to_full(); int i; for (i = 0; i < DEPTH && !full; i++) push($urandom_range(0,255)); $display("[TB] Filled to full. count=%0d", count); endtask task automatic drain_to_empty(); while (!empty) pop(); $display("[TB] Drained to empty"); endtask // Corner case: simultaneous push+pop at various fill levels task automatic sim_push_pop(input logic [WIDTH-1:0] d); logic [WIDTH-1:0] exp; if (empty || full) return; exp = ref_q.pop_front(); @(negedge clk); wr_en = 1; rd_en = 1; data_in = d; @(posedge clk); ref_q.push_back(d); @(posedge clk); // registered output valid checks++; if (data_out !== exp) begin errors++; $error("[TB] SIM_PUSH_POP mismatch: exp=0x%02h got=0x%02h", exp, data_out); end @(negedge clk); wr_en = 0; rd_en = 0; endtask initial begin $dumpfile("fifo_tb.vcd"); $dumpvars(0, tb_fifo); // Test 1: Basic fill and drain reset_dut(); fill_to_full(); drain_to_empty(); // Test 2: Random push/pop sequence reset_dut(); repeat(200) begin if ($urandom_range(0,1)) push($urandom_range(0,255)); else pop(); end // Test 3: Simultaneous push/pop reset_dut(); repeat(4) push($urandom); // fill to count=4 repeat(50) sim_push_pop($urandom); // Test 4: Reset during operation reset_dut(); push(8'hAA); push(8'hBB); @(negedge clk); rst_n = 0; repeat(2) @(posedge clk); rst_n = 1; ref_q = {}; @(posedge clk); if (!empty) $error("[TB] FIFO not empty after reset!"); $display("\n[TB] =============================="); $display("[TB] Total checks : %0d", checks); $display("[TB] Total errors : %0d", errors); if (errors == 0) $display("[TB] *** FIFO TEST PASSED ***"); $display("[TB] =============================="); $finish; end endmodule
Asynchronous FIFOs cross clock domains — the write side runs on wr_clk and the read side on rd_clk. Verification must handle three unique challenges not present in sync FIFOs:
Write and read pointers are sent across the clock domain crossing in Gray code to ensure only 1 bit changes per increment, preventing metastability from multi-bit transitions. An SVA can verify this property:
// Gray code property: only 1 bit may change per cycle function automatic int popcount(logic [ADDR_W:0] v); int c = 0; for (int i = 0; i <= ADDR_W; i++) c += v[i]; return c; endfunction property gray_wr_ptr; @(posedge wr_clk) disable iff(!rst_n) (wr_en && !full) |=> (popcount(wr_ptr_gray ^ $past(wr_ptr_gray)) == 1); endproperty GRAY_WR: assert property(gray_wr_ptr) else $error("Gray code violation on write pointer!"); property gray_rd_ptr; @(posedge rd_clk) disable iff(!rst_n) (rd_en && !empty) |=> (popcount(rd_ptr_gray ^ $past(rd_ptr_gray)) == 1); endproperty GRAY_RD: assert property(gray_rd_ptr);
Inject random 0 to 3 ns delays on the synchronised pointer to stress flag timing without simulating actual metastability (which has no deterministic simulation model):
// Metastability injection model for async FIFO module meta_inject #(parameter int W = 5) ( input logic [W-1:0] ptr_in, output logic [W-1:0] ptr_out ); real delay_ns; always @(ptr_in) begin delay_ns = $urandom_range(0,300) / 100.0; // 0–3 ns #(delay_ns) ptr_out = ptr_in; end endmodule
| Corner Case | Description | Expected Behaviour | How to Test |
|---|---|---|---|
| Simultaneous push+pop at threshold | wr_en && rd_en when count == N-1 | count stays at N-1; full never asserts | Directed test: fill to N-1, apply sim push+pop for 10 cycles |
| Simultaneous push+pop when count==1 | wr_en && rd_en when count == 1 | count stays at 1; empty never asserts | Fill to 1, apply sim push+pop for 10 cycles |
| Back-to-back pushes until full | wr_en held high for N consecutive cycles | full asserts on cycle N; write N+1 ignored | Drive wr_en=1 for N+2 cycles, check full timing and count |
| Reset during write | rst_n deasserts mid-push | FIFO empties; push data discarded | Assert rst_n=0 in middle of wr_en=1 sequence |
| Read after reset | rd_en immediately after reset deassertion | data_out undefined; empty asserted; DUT ignores rd_en | Deassert reset, immediately apply rd_en |
| Pointer wrap-around | Write pointer wraps from N-1 to 0 | count and flags correct after wrap; no data corruption | Fill/drain 3x DEPTH writes total |
| Single-cycle full recovery | Pop while full, then immediately push | full deasserts, new push accepted within 1 cycle | Fill, pop, push within same clock window |