Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
    • Reset Architecture
    • Metastability and Synchronisation
    • Asynchronous FIFO
    • CDC Protocols
    • Reset Domain Crossing
    • CDC Verification
      • Rules
      • Simulation
      • Assertion Patterns
      • FIFO Tests
      • Signoff
      • Reading a CDC Report
      • A CDC Mini Project
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

←CDC Verification
  1. Topics
  2. RTL Design
  3. Reset & CDC
  4. CDC Verification
  5. FIFO Tests
On this page

    Part 6 of 9, lesson 31 of 34Part 6 of 9/Reset & CDC/Lesson 31 of 34Intermediate

    FIFO Tests

    A reusable FIFO scoreboard proven against dropped, duplicated and swapped words.

    A scoreboard that checks only data can miss overflow, and a test that has never failed proves nothing. A queue of accepted words, checked against every read and the depth, catches drops, duplicates and swaps.

    After this lesson you can: build a handshake-only FIFO scoreboard, state its interface contract, and add coverage, assertions, reset scenarios and mutations for an async FIFO.

    A scoreboard from handshakes

    The scoreboard watches only the handshakes. Every accepted write pushes its word into a reference queue, and every accepted read must return the word at the head of that queue. The queue size is also checked against the depth.

    The FIFO scoreboard: accepted writes fill a reference queue, accepted reads are compared with its head, and the queue size is checked against the depth

    Read it top to bottom. The scoreboard never looks inside the FIFO: it sees only words that were accepted on the write side and taken on the read side.

    • Compare with queue head: catches a wrong word, a dropped word, a duplicate and a reordering, because each one breaks the order of the queue.

    • Size check: catches overflow, a write accepted when the FIFO already holds DEPTH words. A data-only check misses it.

    • Underflow: a read with an empty queue is an error too.

    Proving the scoreboard catches bugs

    A test that has never failed proves nothing. The testbench below runs the same random traffic into four FIFO models: one correct and three with an injected bug every 50 writes.

    FIFO under test

    Scoreboard errors (seed 215)

    Correct

    0

    Drops a word every 50 writes

    21,281 (every word after the drop is compared with the wrong expectation)

    Duplicates a word every 50 writes

    1,860

    Swaps two adjacent words every 50 writes

    228

    The counts differ because each bug disturbs the data stream differently: a drop shifts every later word, while a swap disturbs two. Counts depend on the seed and simulator; record both with any result.

    Check yourself The scoreboard flags 21,281 errors for the dropping FIFO. Why not just 1?

    After one word is dropped, every later read is compared with a different expected word (shifted by one), so each read mismatches. A real testbench might stop at the first error or re-synchronise; counting all of them shows how far the corruption spreads.

    A scoreboard that checks only the data, not the occupancy, can miss overflow (writes accepted past depth). Check the queue size against the depth, as above.

    Covering an asynchronous FIFO

    • Run the scoreboard with several clock ratios and the metastability-aware pointer synchroniser (see the async FIFO topic).

    • Add coverage of full, empty, simultaneous read and write, almost-full and almost-empty, and wrap-around of the pointers.

    • Add protocol assertions: no write accepted when full, no read accepted when empty, Gray pointers one bit per step.

    • Add reset scenarios with a reset-aware FIFO model and checker: reset in the middle of traffic on both sides, then confirm empty and not-full. The simple model/checker below has no reset interface.

    • Add a mutation run (binary pointers, wrong full condition) to show the tests catch real CDC bugs.

    Make the scoreboard a library module next to the FIFO. Every project that instantiates the FIFO then reuses a scoreboard that has already been shown to catch drops, duplicates and reordering.

    Testbench

    The scoreboard, a bug-injecting FIFO model, and a testbench applying common offered enables/data to four variants. Acceptance differs with each variant's occupancy. Supply +SEED=<integer> and record the simulator/version as well as the printed seeds; PRNG sequences are tool-dependent.

    fifo_scoreboard.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    `timescale 1ns/1ps
    // Contract: pre-edge accepted-transfer strobes and show-ahead read data; no reset/flush mid-epoch.
    // Coincident accepted transfers require 0 < pre-edge occupancy < DEPTH.
    // No empty bypass or full-turnover acceptance. Registered-read FIFOs need a latency-aware checker.
    module fifo_scoreboard #(parameter int DW = 8, parameter int DEPTH = 16) (
      input  logic          wclk, rclk,
      input  logic          w_fire, r_fire,              // a word was accepted by the FIFO / taken from it
      input  logic [DW-1:0] w_data, r_data,
      output int            errors
    );
      logic [DW-1:0] q [$];
      int            max_fill;
      initial begin errors = 0; max_fill = 0; end
    
      always @(posedge wclk) if (w_fire) begin
        q.push_back(w_data);
        if (q.size() > max_fill) max_fill = q.size();
        if (q.size() > DEPTH) errors++;                  // overflow: more words stored than the FIFO can hold
      end
      always @(posedge rclk) if (r_fire) begin
        if (q.size() == 0) errors++;                     // underflow: a read with nothing written
        else begin
          logic [DW-1:0] expected;
          expected = q.pop_front();
          if (r_data !== expected) errors++;             // wrong data or wrong order
        end
      end
    endmodule
    
    // A simple FIFO model with injectable bugs, only to test the scoreboard.
    //   BUG 0: none   1: a word is dropped now and then   2: a word is duplicated now and then   3: two adjacent words are swapped
    module buggy_fifo #(parameter int BUG = 0, parameter int DW = 8, parameter int DEPTH = 16) (
      input  logic wclk, rclk,
      input  logic winc, rinc,
      input  logic [DW-1:0] wdata,
      output logic [DW-1:0] rdata,
      output logic w_fire, r_fire
    );
      if (DEPTH < 1) begin : g_depth_check
        initial $fatal(1, "buggy_fifo: DEPTH must be positive");
      end
      logic [DW-1:0] mem [DEPTH];
      int rp = 0, wp = 0, count = 0;
      assign w_fire = winc && (wp - rp < DEPTH);
      assign r_fire = rinc && (wp - rp > 0);
      assign rdata  = mem[rp % DEPTH];
      always @(posedge wclk) if (w_fire) begin
        count <= count + 1;
        if (BUG == 1 && (count + 1) % 50 == 0) begin
          // drop: the word is accepted but never stored
        end else if (BUG == 2 && (count + 1) % 50 == 0 && wp - rp < DEPTH - 1) begin
          mem[wp % DEPTH] <= wdata;  mem[(wp + 1) % DEPTH] <= wdata;  wp <= wp + 2;                    // duplicate
        end else if (BUG == 3 && (count + 1) % 50 == 0 && wp - rp >= 1) begin
          mem[(wp - 1) % DEPTH] <= wdata;  mem[wp % DEPTH] <= mem[(wp - 1) % DEPTH];  wp <= wp + 1;       // swap with the previous word
        end else begin
          mem[wp % DEPTH] <= wdata;  wp <= wp + 1;
        end
      end
      always @(posedge rclk) if (r_fire) rp <= rp + 1;
    endmodule
    
    // DEPTH > 64 must not alias storage. Two rounds also exercise address wraparound.
    module fifo_depth_check (output bit done, output int errors);
      logic clk = 0, winc = 0, rinc = 0, w_fire, r_fire;
      logic [15:0] wdata = 0, rdata;
      always #5 clk = ~clk;
      buggy_fifo #(.BUG(0), .DW(16), .DEPTH(128)) dut (
        .wclk(clk), .rclk(clk), .winc, .rinc, .wdata, .rdata, .w_fire, .r_fire
      );
      initial begin
        done = 0; errors = 0;
        for (int round = 0; round < 2; round++) begin
          for (int i = 0; i < 128; i++) begin
            @(negedge clk); winc = 1; wdata = 128 * round + i;
            @(posedge clk); if (!w_fire) errors++;
          end
          @(negedge clk); winc = 0;
          for (int i = 0; i < 128; i++) begin
            @(negedge clk); rinc = 1;
            @(posedge clk);
            if (!r_fire || rdata !== 16'(128 * round + i)) errors++;
          end
          @(negedge clk); rinc = 0;
        end
        done = 1;
      end
    endmodule
    
    // Common clock deliberately makes accepted transfers coincide at interior occupancy.
    module scoreboard_contract_check (output bit done, output int errors);
      logic clk = 0, w_fire = 0, r_fire = 0;
      logic [7:0] w_data = 0, r_data = 0;
      always #5 clk = ~clk;
      fifo_scoreboard #(.DW(8), .DEPTH(2)) dut (
        .wclk(clk), .rclk(clk), .w_fire, .r_fire, .w_data, .r_data, .errors
      );
      task automatic step(input bit w, r, input logic [7:0] wd, rd);
        @(negedge clk); w_fire = w; r_fire = r; w_data = wd; r_data = rd;
        @(posedge clk); #1;
      endtask
      initial begin
        done = 0;
        step(1, 0, 8'ha1, 0);             // empty -> one
        step(1, 1, 8'hb2, 8'ha1);         // coincident transfers at occupancy one
        step(1, 0, 8'hc3, 0);             // one -> full
        step(0, 1, 0, 8'hb2);             // full -> one (no full-turnover write)
        step(0, 1, 0, 8'hc3);             // one -> empty (no bypass)
        @(negedge clk); w_fire = 0; r_fire = 0;
        done = 1;
      end
    endmodule
    

    BUG = 0 must preserve 128 distinct words across two fill/read rounds and accept interior coincident transfers plus separate boundary transfers under its show-ahead contract. New offers stop before a bounded 128-read-cycle drain; the clean model must have accepted-write/read equality and an empty reference queue, and every model must drain its stored words. A dropped-word mutant may leave reference entries after its hardware empties; that is expected corruption, not permission to ignore a clean-model remainder. Final failures are fatal. Empty-bypass/full-turnover interfaces still require an atomic checker.

    Limits

    The scoreboard has an interface contract. w_fire and r_fire mean transfers accepted at their rising edges, with data and fire signals stable before those edges. r_data must already be the word consumed at that edge (first-word fall-through, or show-ahead), not registered read data that appears afterward.

    Limits. A registered-read FIFO needs a latency-aware response checker that also counts pending reads in its occupancy check; do not connect its read-request strobe to this r_fire. This two-process scoreboard supports coincident accepted reads and writes only when the pre-edge occupancy is strictly between 0 and DEPTH. It does not support empty bypass, or a full-turnover write accepted because a coincident read frees space: the transient queue size would depend on process order, so those interfaces need an atomic transfer collector and checker. It has no reset or flush input, so use one reset-free traffic epoch per checker instance. The BUG = 0 model rejects writes while full and reads while empty using pre-edge occupancy, so it meets these restrictions.

    • r_data must be the word consumed at that edge (show-ahead). A registered-read FIFO needs a latency-aware checker.

    • Check the queue size against the depth as well as the data, or overflow goes unseen.

    • After one dropped word, every later read mismatches, so a real bench may stop at the first error.

    Rule of thumb: a test that cannot fail on a known-bad FIFO is not a CDC test.

    How this was checked

    Testbench, run with Icarus Verilog 13.0 (2026-10-16), default seed 215: prints PASS. Counts depend on the seed and simulator; record both with any result.

    The error counts in the mutation table come from an earlier version of the bench, with independently randomised offers, and are not re-run against this controlled experiment.

    • Practice: CDC FIFO read data mismatch from clock misalignment

    • Practice: Formal-friendly CDC FIFO checkers

    • Interview: run a mock interview on this topic

    Related topics

    • Signoff

    Previous

    185 / 350

    Next: Signoff

    Up next

    Up nextSignoff

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Rules
    • Simulation
    • Reading a CDC Report