Reset, wrap and reproducible stress

Stress the FIFO at a depth that is not a power of two, with resets in the middle of traffic, and make every random run reproducible from a printed seed.

Lesson
7.6
Time
30 min
Runs on
Verilator
Check
5 questions

A random regression failed once in 2,000 runs: a read returned an item written before a reset. The failure needed three things at once: a reset while the FIFO held data, a write pointer that had already wrapped, and a depth of 3. The log did not show the seed, so nobody could run that test again, and the bug stayed open for a month.

An item survives a reset
012345resetitems held (model)23010read_validread_data0022returns 22, written before the reset

One column per rising edge. The model expects the item written after the reset; the FIFO returns an older one.

Depth 3 on purpose

A FIFO of depth 4 hides pointer bugs, because a 2-bit counter that overflows happens to wrap at the right place. Depth 3 is the smallest depth where that shortcut breaks, so the stress test in this lesson uses it.

A depth only tests wrapping if the stimulus actually wraps. fifo_scenarios counts laps: every DEPTH accepted writes since the last reset, any correct design's write pointer is back at slot 0. It works this out from the model's own counts, without looking inside the FIFO.

Stress also needs a mix that reaches the boundaries often. With writes and reads each requested about half the time at depth 3, the FIFO spends many cycles empty or full, which is where most FIFO bugs live. The counts in the example confirm it, with dozens of refused writes and reads per run.

The scoreboard needs no change for depth 3. It reads depth from its constructor, and nothing in it assumes a power of two.

Reset in the middle of traffic

The generator's constraint reset dist {0 := 40, 1 := 1} produces a reset in about one operation in 41, at random moments. Many of those resets land while the FIFO holds data, which is the case that matters. Everything stored must be discarded, and a read accepted just before the reset must still be answered after it.

The scoreboard handles reset as one more edge. It deletes exp_q, and because it clears due before deciding the edge, it expects no answer after the reset edge itself. fifo_scenarios counts resets that happen while holding data, so a run where every reset found the FIFO empty is visible in the report.

A seed you can replay

Each run in the example starts by printing its seed. The generator seeds its own process with that number, so all the operations it creates follow from it. As Module 5 explained, the same seed, code and simulator version give the same stimulus.

Run a fixed list of seeds in the nightly regression, so a failure can be repeated at once, and add a few new seeds each night to explore. Either way, print the seed before any stimulus, so a run that crashes or hangs still names it.

Keep the list of seeds in the regression script rather than in the testbench, so adding a seed never changes the code. Vary the depth as well: the same seeds at depths 1, 3 and 5 cover the extreme case, where every accepted write fills the FIFO, as well as ordinary ones.

When a failure appears, rerun its seed with the same code first. Only change the stimulus once the failure repeats, because any change, even an added debug object, can shift the random sequence.

Drain, then decide

A stress run ends in a fixed order. The generator sends its last random operation and five reads to drain the FIFO. The test then waits until the driver has driven everything, and for two more edges, so the last operation is sampled and its answer checked.

Stop when the generator is done

do @(posedge clk);
while (!gen.done);
report();

Reports while the last operations are still in the mailbox or on the pins, so their checks never run.

Stop when everything was checked

do @(posedge clk);
while (!(gen.done && drv.driven == gen.sent));
repeat (2) @(posedge clk);
#1 report();

Every operation has been driven, sampled and answered before the report.

The #1 steps off the clock edge, so the report never races with the monitor and scoreboard working on that same edge. Two more conditions finish the job: the model must be empty after the drain, and a watchdog must be running.

The watchdog is a separate process that waits a fixed time, here 100,000 time units, and calls $fatal if the test has not finished by then. It costs one line, and it turns a hung simulation into a clear failure with a message instead of a regression timeout.

LayerComponentJobReusesStatus
Testfifo_test, fifo_configchooses depth, seed and length, runs the environment, gives the verdictModule 4 classeslesson 7.7
Generatorfifo_generatorcreates fifo_op objects: directed, then seeded randomModules 4 and 5built in 7.3
Mailboxesmailbox #(fifo_op), mailbox #(fifo_obs)carry operations and observations between processesModule 3built in 7.3
Driverfifo_driverdrives one operation per clock through drv_cbModule 6built in 7.3
Monitorfifo_monitorsamples the pins through mon_cb into a new fifo_obsModule 6built in 7.4
Scoreboardfifo_scoreboardpredicts each edge with the queue exp_q and comparesModules 2 and 4built in 7.4
Assertionsfifo_propschecks timing rules on the pins at every edgeModule 6built in 7.4
Coveragefifo_coverage, fifo_scenariosmeasures which situations the stimulus reachedModule 5 stimulusbuilt in 7.5
DUTfifothe design under testlesson 7.1 rulesbuilt in 7.2

The layered testbench after lesson 7.6.

Try it

Two seeds, 300 random operations each, at depth 3: about 38 laps of the ring and 4 to 7 resets while holding data per run, with no errors and an empty model after the drain. The writes that were never read back are the items discarded by those resets.

Change seeds to '{11, 11} and predict the second pair of lines. Then set DEPTH to 4 and predict whether laps goes up or down.

RUNVerilator · simulator service
Edit the code, then run it
Expected: seed 11: 315 edges, 125 writes, 119 reads, 0 errors, 0 left after drain laps=39 write_blocked=24 read_blocked=38 both_at_full=7 both_at_empty=16 reset_holding=4 seed 12: 315 edges, 123 writes, 113 reads, 0 errors, 0 left after drain laps=38 write_blocked=29 read_blocked=30 both_at_full=12 both_at_empty=11 reset_holding=7

Check yourself

Predict the outputThree generators produce their operations into separate mailboxes. Each number is 1 if two streams are identical. Predict the exact output.
// One clock cycle of requests: what the driver offers to the FIFO.
class fifo_op;
  rand bit       reset, write_en, read_en;
  rand bit [7:0] write_data;
  constraint c_reset { reset dist {0 := 40, 1 := 1}; }
  function fifo_op copy();
    copy = new();
    copy.reset = reset; copy.write_en = write_en;
    copy.read_en = read_en; copy.write_data = write_data;
  endfunction
  function string show();
    return $sformatf("rst=%0d w=%0d r=%0d d=%h", reset, write_en, read_en, write_data);
  endfunction
endclass

// Directed operations first, then seeded random ones, all through a mailbox.
class fifo_generator;
  mailbox #(fifo_op) req;
  int                n_random, seed, sent;
  bit                done;
  function new(mailbox #(fifo_op) req, int n_random, int seed);
    this.req = req; this.n_random = n_random; this.seed = seed;
  endfunction
  task send(bit reset, bit write_en, bit read_en, bit [7:0] write_data);
    fifo_op op = new();                        // a new object for every operation
    op.reset = reset; op.write_en = write_en;
    op.read_en = read_en; op.write_data = write_data;
    req.put(op);
    sent++;
  endtask
  task run();
    fifo_op op;
    process::self().srandom(seed);             // same seed, same operations
    send(1, 0, 0, 0);                          // start from reset
    for (int i = 0; i < 5; i++) send(0, 1, 0, 8'h10 + 8'(i));   // fill past full
    send(0, 1, 1, 8'hEE);                      // read and write at full
    repeat (n_random) begin
      op = new();
      if (!op.randomize()) $fatal(1, "fifo_op randomize failed");
      req.put(op);
      sent++;
    end
    for (int i = 0; i < 5; i++) send(0, 0, 1, 0);               // drain
    done = 1;
  endtask
endclass

module tb;
  mailbox #(fifo_op) m1 = new(), m2 = new(), m3 = new();
  fifo_generator g1 = new(m1, 20, 11);
  fifo_generator g2 = new(m2, 20, 11);
  fifo_generator g3 = new(m3, 20, 12);
  function automatic string stream(mailbox #(fifo_op) m);
    fifo_op op;
    string s = "";
    while (m.try_get(op)) s = {s, op.show(), ";"};
    return s;
  endfunction
  initial begin
    string a, b, c;
    g1.run();
    g2.run();
    g3.run();
    a = stream(m1); b = stream(m2); c = stream(m3);
    $display("%0d %0d", a == b, a == c);
  end
endmodule
Spot the bugEach option is the end-of-test wait in a stress run, placed before the final checks. The test passes only if every operation was driven, sampled and answered. Which option ends too early?
ConceptWhy does the stress test use depth 3 rather than depth 4?
ConceptA stress run fails at operation 4,512. Which line in the log makes it possible to run exactly that test again?
ConceptA run reports reset_holding=0: every reset found the FIFO empty. What has it failed to test?

Remember

  • Stress at a depth that is not a power of two, and count laps to prove the pointers really wrapped.
  • Reset at random moments, and require at least one reset while the FIFO holds data.
  • Seed the generator from a number printed before any stimulus, and rerun a failing seed unchanged first.
  • End only after every operation is driven, sampled and answered, then check that the model is empty.
  • A watchdog turns a hang into a clear failure.

Practice