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

  • Start Here5
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive39
  • SystemVerilog Assertions — Interview-Critical Deep Dive41
    • 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
      • Checking PacketFIFO
    • Advanced SVA: Local Variables, Multiclock & More
    • Assertion Debug & Methodology
    • SVA Interview Drills
  • Functional Coverage in SystemVerilog25
  • Testbench Architecture in SystemVerilog24
  • Advanced Language & Integration26
  • Senior & Interview Preparation23
  • Principal-Level Verification34
Loading lesson

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

    Part 4 of 9, lesson 21 of 41Part 4 of 9/Assertions (SVA)/Lesson 21 of 41Intermediate

    FIFO Rulesets

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

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

    PacketFIFO step: overflow and underflow. Keep a checker-side count occ of accepted minus taken bytes. A byte accepted while occ equals the depth is an overflow, and a byte taken while occ is 0 is an underflow. PacketFIFO's in_ready and out_valid already prevent both, so the rules only fire on a design bug.

    design.sv
    1
    2
    3
    4
    5
    // snippet
    a_no_overflow: assert property (@(posedge clk) disable iff (!rst_n)
      (occ == DEPTH) |-> !push) else fail("overflow: byte accepted while full");
    a_no_underflow: assert property (@(posedge clk) disable iff (!rst_n)
      (occ == 0) |-> !pop) else fail("underflow: byte taken while empty");

    Output from the complete listing in Checking PacketFIFO:

    Terminal
    [945] pf_checker: overflow: byte accepted while full
    [955] pf_checker: out_data changed while stalled
    in_ready stuck high, fifth byte into a depth-4 FIFO: 2 report(s)
    [975] pf_checker: underflow: byte taken while empty
    out_valid stuck high, one read of an empty FIFO: 1 report(s)

    The planted design bug here is in_ready stuck high. The overflow report comes first; the second line is its echo, because the extra byte overwrote the oldest one while out_valid was waiting.

    Common mistakes

    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.

    “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.

    • 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.

    The PacketFIFO step output was copied from a Verilator 5.048 run (--assert --coverage) of the Checking PacketFIFO listing on 2026-10-25.

    Previous

    123 / 275

    Next: FSM Rulesets

    Up next

    Up nextFSM Rulesets

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

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