Day 22 / 25
← Day 21 Day 23 →
HomeVerificationDay 22 — FIFO Verification
Track 5 — Protocol & IP Verification

FIFO Verification

By EcrioniX · Updated June 2026

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.

⏱ 32 min read📖 Day 22 of 25🎯 SVA · Coverage · Reference Model · Corner Cases
Writer wr_en / data_in Synchronous FIFO FULL 0 1 2 3 Reader rd_en / data_out empty nearly full full
Contents
  1. FIFO Specification and Signal Interface
  2. SVA Assertions — Overflow, Underflow, Flag Accuracy
  3. Queue-Based Reference Model
  4. Functional Coverage — Fill Level Bins
  5. Full SV Testbench with Tasks
  6. Asynchronous FIFO Verification
  7. Corner Cases
  8. Debug Tips
  9. FAQ

1. FIFO Specification and Signal Interface

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:

SignalDirectionWidthDescription
clkInput1System clock — all sampling on rising edge
rst_nInput1Active-low synchronous reset — empties FIFO
wr_enInput1Write enable — push data_in when high and !full
rd_enInput1Read enable — pop and present data_out when high and !empty
data_inInputWData to push into FIFO
data_outOutputWData popped from FIFO (registered — valid 1 cycle after rd_en)
fullOutput1High when FIFO contains N entries — push ignored when full
emptyOutput1High when FIFO contains 0 entries — pop ignored when empty
countOutputlog2(N)+1Current number of entries in the FIFO (optional diagnostic)
Flag semantics matter: Some FIFO implementations assert 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.

2. SVA Assertions — Overflow, Underflow, Flag Accuracy

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 (.*);

3. Queue-Based Reference Model

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

4. Functional Coverage — Fill Level Bins

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
Coverage closure tip: The 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.

5. Full SV Testbench with Tasks

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

6. Asynchronous FIFO Verification

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:

6.1 Gray Code Pointer Verification

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);

6.2 Metastability Injection

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

7. Corner Cases

Corner CaseDescriptionExpected BehaviourHow to Test
Simultaneous push+pop at thresholdwr_en && rd_en when count == N-1count stays at N-1; full never assertsDirected test: fill to N-1, apply sim push+pop for 10 cycles
Simultaneous push+pop when count==1wr_en && rd_en when count == 1count stays at 1; empty never assertsFill to 1, apply sim push+pop for 10 cycles
Back-to-back pushes until fullwr_en held high for N consecutive cyclesfull asserts on cycle N; write N+1 ignoredDrive wr_en=1 for N+2 cycles, check full timing and count
Reset during writerst_n deasserts mid-pushFIFO empties; push data discardedAssert rst_n=0 in middle of wr_en=1 sequence
Read after resetrd_en immediately after reset deassertiondata_out undefined; empty asserted; DUT ignores rd_enDeassert reset, immediately apply rd_en
Pointer wrap-aroundWrite pointer wraps from N-1 to 0count and flags correct after wrap; no data corruptionFill/drain 3x DEPTH writes total
Single-cycle full recoveryPop while full, then immediately pushfull deasserts, new push accepted within 1 cycleFill, pop, push within same clock window

8. Debug Tips

Bug: full asserts one cycle late. The most common FIFO bug. Write count==N data words but full asserts on cycle N+1. Fix: check whether full is registered (should be combinational off count) or check the pointer comparison logic for off-by-one.
Bug: data_out shows stale value. If data_out doesn't update after rd_en, check whether the DUT uses a registered or combinational output. A registered output is valid on the clock edge after rd_en — testbench must sample 1 cycle later.
Waveform checklist: When a mismatch fires, check (1) write pointer, (2) read pointer, (3) count, (4) full/empty flags — all on the same failing cycle. Most FIFO bugs are visible in the pointer difference vs count value.
Formal verification: FIFO safety properties (no overflow, no underflow, full ↔ count==N, empty ↔ count==0) are excellent targets for bounded model checking. Run them with a depth of 2×DEPTH cycles to prove pointer wrap-around is correct.

Key Takeaways — Day 22

Frequently Asked Questions

What SVA assertions are essential for FIFO verification?
The four essential FIFO SVA assertions are: (1) no push when full — assert that wr_en is never high when full is high; (2) no pop when empty — assert that rd_en is never high when empty is high; (3) full flag accuracy — if count reaches DEPTH then full must be asserted; (4) empty flag accuracy — if count is 0 then empty must be asserted. These catch the most common FIFO bugs: flag timing, overflow, and underflow.
What is a queue-based reference model for FIFO verification?
A queue-based FIFO reference model uses a SystemVerilog queue (dynamic array with push_back/pop_front) to mirror the expected FIFO contents. On every observed push (wr_en && !full), the reference model calls ref_q.push_back(data_in). On every observed pop (rd_en && !empty), it calls expected = ref_q.pop_front() and compares expected against the DUT's data_out. This gives exact cycle-accurate checking without implementing any FIFO RTL in the testbench.
How do you verify an asynchronous FIFO?
Async FIFO verification requires two separate clock domains in the testbench. Key checks are: (1) gray code pointer crossing — ensure only 1 bit changes per pointer increment using an SVA popcount property; (2) metastability injection — inject random delays (0–3 ns) on the synchronised pointers; (3) full/empty flag timing — flags may be pessimistic (assert early) but must never be optimistic; (4) boundary conditions — fill to N-1, check full not yet asserted, then add one more. SVA clock domains must be declared on the correct edge of each respective clock.
What functional coverage bins are needed for FIFO verification?
A complete FIFO covergroup should include bins for: empty state (count==0), single entry (count==1), half-full (count==DEPTH/2), nearly full (count==DEPTH-1), full (count==DEPTH), simultaneous push and pop (wr_en && rd_en), push only, pop only, and reset during operation. The simultaneous push/pop bin is especially important because it exercises the pass-through path and exposes pointer arithmetic bugs that only manifest at threshold boundaries.
← Day 21
AXI4 Verification
Next → Day 23
CDC Verification
Static CDC analysis, metastability injection, gray code pointer SVA, and CDC sign-off methodology.