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

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
    • SVA Fundamentals
    • Sequences: Temporal Building Blocks
    • Implication and Property Operators
    • Sampled Value Functions
    • Protocol Assertion Cookbook
      • Valid/Ready Handshake
      • Req/Ack + Pulse Discipline
      • FIFO Rulesets
      • FSM Rulesets
      • Pipelines, Stall, Credits
    • Advanced SVA: Local Variables, Multiclock & More
    • Assertion Debug & Methodology
    • SVA Interview Drills
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Protocol Assertion Cookbook
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Assertions — Interview-Critical Deep Dive
  4. Protocol Assertion Cookbook
  5. FIFO Rulesets

Part 4 · Assertions (SVA) · Intermediate

FIFO Rulesets

FIFO assertions: legality, count and pointer math, flag consistency and data integrity.

On this page

    Why this matters

    A FIFO passed every flag check and still returned the wrong byte after a wrap. Nothing compared the popped data with the pushed data.

    After this lesson you can: check a FIFO's legality, count and pointer math, flags and data order, with covers for wrap and full-to-empty.

    What a FIFO checker must prove

    Four mechanisms break independently. Overflow drops data, underflow returns garbage, count drift corrupts flags at wrap, and wrong bytes pass every occupancy check.

    Watch count rise with push, then fall with pop; full and empty follow the count.

    • Slot 0: empty; the first push arrives.

    • Slot 1: count 1; still empty-free and far from full.

    • Slot 2: count 2; pushes continue.

    • Slot 3: count 3; one slot remains.

    • Slot 4: count 4, so full rises; pushes stop and pops begin.

    • Slot 5: count 3; full clears on the first drain.

    • Slot 6: count 2; draining continues.

    • Slot 7: count 1; one more pop reaches empty.

    Failure

    Rule that catches it

    Overflow

    (push && !pop) |-> !full

    Underflow

    pop |-> !empty

    Bookkeeping drift

    Count follows accepted push minus accepted pop

    Data corruption

    Popped bytes match pushed bytes in order

    The checker module

    The data-integrity rule needs the push from as many pushes back as the FIFO holds. The textbook form is one gated $past(din, k, push) per occupancy; Verilator 5.048 rejects the gate, so this checker keeps the expected order in a queue written as modeling code, which works for any occupancy. The count rule writes ~full and ~empty, because Verilator also rejects ! anywhere after ##1.

    The listing is one file you can paste into the Code Lab: the checker first, then a small design and a bench that runs legal traffic before planting one bug at a time. Every rule reports through fail(), which counts and prints the report and lets the run go on; Verilator 5.048 stops at the first $error, and production checkers often count the same way.

    fifo_checker.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
    module fifo_checker #(
      parameter int DEPTH = 16,
      parameter int DW = 32,
      parameter int PTR_W = $clog2(DEPTH),
      parameter int MAX_DRAIN = 1024
    )(
      input logic             clk, rst_n,
      input logic             push, pop, full, empty,
      input logic [PTR_W:0]   count,
      input logic [PTR_W:0]   wr_ptr, rd_ptr,
      input logic [DW-1:0]    din, dout
    );
      int n_fail = 0, n_wrap = 0, n_drain = 0;
      function automatic void fail(string msg);
        n_fail++;
        $display("[%0t] fifo_checker: %s", $time, msg);
      endfunction
    
      // Simultaneous push and pop at full is legal here (net zero).
      a_no_overflow: assert property (@(posedge clk) disable iff (!rst_n)
        (push && !pop) |-> !full)
        else fail("push while full");
      a_no_underflow: assert property (@(posedge clk) disable iff (!rst_n)
        pop |-> !empty)
        else fail("pop while empty");
    
      // Accepted push needs room or a simultaneous pop; accepted pop needs data.
      // "~" rather than "!": Verilator 5.048 rejects "!" anywhere after ##1.
      a_count_delta: assert property (@(posedge clk) disable iff (!rst_n)
        ##1 count == $past(count)
                     + (PTR_W+1)'($past(push && (pop || ~full)))
                     - (PTR_W+1)'($past(pop  && ~empty)))
        else fail("count drifted from push/pop history");
    
      // Free-running pointers with an extra MSB; truncated indexes alias full and empty.
      a_ptr_count: assert property (@(posedge clk) disable iff (!rst_n)
        count == (PTR_W+1)'(wr_ptr - rd_ptr))
        else fail("count disagrees with the pointers");
    
      a_empty_def: assert property (@(posedge clk) disable iff (!rst_n)
        empty == (count == 0)) else fail("empty flag wrong");
      a_full_def:  assert property (@(posedge clk) disable iff (!rst_n)
        full  == (count == DEPTH)) else fail("full flag wrong");
      a_excl: assert property (@(posedge clk) disable iff (!rst_n)
        !(empty && full)) else fail("full and empty together");
    
      // Data integrity. The distance back is the occupancy, so instead of
      // $past(din, k, push) for every k, a queue in modeling code keeps the order.
      logic [DW-1:0] expect_q [$];
      always @(posedge clk)
        if (!rst_n) expect_q.delete();
        else begin
          if (pop && !empty && expect_q.size() != 0) void'(expect_q.pop_front());
          if (push && (pop || !full)) expect_q.push_back(din);
        end
      a_data: assert property (@(posedge clk) disable iff (!rst_n)
        (pop && !empty) |-> dout == expect_q[0])
        else fail("popped data out of order or corrupted");
    
      c_wrap: cover property (@(posedge clk)
        $past(wr_ptr[PTR_W-1:0]) == DEPTH-1 && wr_ptr[PTR_W-1:0] == 0) n_wrap++;
      c_full_to_empty: cover property (@(posedge clk)
        full ##[1:MAX_DRAIN] empty) n_drain++;
    endmodule
    
    // A small synchronous FIFO with free-running pointers and two plantable bugs.
    module sfifo #(
      parameter int DW = 8,
      parameter int DEPTH = 8,
      parameter int AW = $clog2(DEPTH)
    )(
      input  logic clk, rst_n, push, pop,
      input  logic [1:0] bug,            // 0 none, 1 corrupt one read, 2 count a blocked push
      input  logic [DW-1:0] din,
      output logic [DW-1:0] dout,
      output logic full, empty,
      output logic [AW:0] count, wptr, rptr
    );
      logic [DW-1:0] mem [DEPTH];
      wire push_ok = push && (!full || pop);
      wire pop_ok  = pop && !empty;
      assign full  = (count == DEPTH);
      assign empty = (count == 0);
      assign dout  = mem[rptr[AW-1:0]] ^ (bug == 1 ? 8'h80 : 8'h00);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin wptr <= 0; rptr <= 0; count <= 0; end
        else begin
          if (push_ok) begin mem[wptr[AW-1:0]] <= din; wptr <= wptr + 1; end
          if (pop_ok) rptr <= rptr + 1;
          count <= count + (push_ok || (bug == 2 && push) ? 1 : 0) - (pop_ok ? 1 : 0);
        end
    endmodule
    
    Terminal
    legal traffic: fill, pass-through at full, drain, stream: 0 new report(s)
    covers: pointer wrap 2, full-to-empty 1
    [275] fifo_checker: pop while empty
    pop while empty: 1 new report(s)
    [295] fifo_checker: popped data out of order or corrupted
    a corrupted read: 1 new report(s)
    [385] fifo_checker: push while full
    [395] fifo_checker: count disagrees with the pointers
    [395] fifo_checker: count drifted from push/pop history
    [405] fifo_checker: count disagrees with the pointers
    push while full into a DUT that counts it: 4 new report(s)
    PASS: legal traffic hit both covers, and every planted bug was reported

    The textbook general check uses local variables, one thread per word in flight. That full form lives in the advanced local-variables lesson; the per-occupancy loop above covers fixed occupancy.

    Gray pointers for async FIFOs are a same-domain check. Assert the one-bit step in the pointer's own domain, never occupancy across clocks.

    design.sv
    1
    2
    3
    4
    5
    // snippet
    // Same-domain Gray step with reset and first-cycle guards:
    // a_gray_step: assert property (@(posedge clk) disable iff (!rst_n)
    //   !$isunknown(gray_ptr) && !$isunknown($past(gray_ptr)) |->
    //     ($countones($past(gray_ptr) ^ gray_ptr) <= 1));

    Common mistakes

    Common pitfall

    Asserting count == wr_ptr - rd_ptr on truncated indexes passes until the first wrap. Without the extra MSB, full and empty alias and every flag rule corrupts.

    Interview

    “What would you assert on a FIFO?”

    • Six rules: overflow, underflow, count delta, pointer math, flag definitions, data integrity.

    • Width trick: the extra MSB separates full from empty at wrap.

    • Simultaneity: push plus pop at full is legal in most designs; name the variant.

    Check yourself push and pop arrive together while full is high. Does the overflow rule fire?

    No. The antecedent push && !pop excludes the simultaneous case, whose net occupancy change is zero.

    Check yourself Pointers use exactly $clog2(DEPTH) bits. What breaks at wrap?

    Full aliases empty. Equal indexes read as both, so the flag rules corrupt; the extra MSB keeps one lap of history.

    What the run shows

    Fill, a push and pop together at full, drain, and a primed stream give no reports, and both covers hit: the pointers wrapped twice and the FIFO went from full to empty. A pop while empty and a corrupted read each give one report. A push while full into a design that wrongly counts it gives four: the protocol rule names the push, then the count and pointer rules disagree for two cycles. The first report is again the one to read.

    Remember

    • Pointers carry an extra MSB, or full and empty alias.

    • Push and pop together at full is legal in most designs: say which.

    • Data order needs a model when the distance back is the occupancy.

    Rule of thumb: every FIFO checker compares the data, not only the flags.

    How this was checked

    Run with Verilator 5.048 (--binary --timing --assert, plus --coverage when a listing has a cover property) on 2026-10-18; every bench printed the PASS line shown. Verilator stops at the first $error or failing assertion without an action block, so the benches count failures in the action block. Icarus 13 has no concurrent assertions.

    The textbook per-occupancy gated $past was replaced by a queue in modeling code; the count rule writes ~full and ~empty because Verilator 5.048 rejects ! after ##1.

    Previous

    121 / 259

    Next: FSM Rulesets

    Up next

    Up nextFSM Rulesets

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Valid/Ready Handshake
    • Pipelines, Stall, Credits
    clkpushpopcount01234321fullempty
    A depth-4 FIFO filling, then draining, with count and flags tracking every slot