Debug case: a racing monitor

Recognise the symptoms of a testbench race, find the signal that changes at the sampling edge, and remove the race instead of moving it.

Lesson
6.7
Time
25 min
Runs on
Verilator
Check
5 questions

A regression passes on one simulator and fails on another, where the monitor records every read one beat late. After someone added an unrelated always block, it starts failing on the first simulator too. The driver writes rvalid and rdata with blocking assignments right after @(posedge clk), at the same edge where the monitor reads them, and which process runs first was never defined.

Stimulus changing at the sampling edge
0123rising edge1234rdata written at the edgea1b2c3raw read at the edge00 or a1a1 or b2b2 or c3c3clocking-block sample00a1b2c3order unspecified

A raw read at the edge can see either value. The sampled view always shows the value from before the edge.

The symptoms of a race

A race rarely looks like one. The usual signs are results that change between simulators, between versions of one simulator, or after edits that should not matter, such as adding a file or reordering always blocks.

Off-by-one-cycle mismatches are the most common form. The monitor records a value one beat early or late, or misses a transfer at the edge where the driver changed rvalid.

When a failure moves for reasons unrelated to the design, suspect a race before suspecting the DUT. Changing the simulator, the compile order or the random seed of an unrelated component are all good ways to provoke one on purpose.

What is racing

The driver resumes at @(posedge clk) and writes rdata = next;. The monitor, an always @(posedge clk) block, resumes at the same edge and reads rdata. Both are processes in the Active region of one time step.

The standard does not fix the order of those two processes. If the driver runs first, the monitor sees the new beat; if the monitor runs first, it sees the old one. Either is legal, so the testbench has no defined behaviour at all.

The DUT is exposed in the same way. Its always @(posedge clk) logic reads rvalid and rdata at that edge too, so even a perfect monitor cannot make this testbench correct.

Finding the racing signal

Look for testbench signals that change at exactly the timestamp of a sampling edge. In a waveform, zoom in on a rising edge and see which signals change at that same time rather than a little before or after it.

You can also count such changes in the simulation, as the example does: a small process notes every rising edge at which a bus signal changes. The time at which a write happens is fixed, even though the order of processes within that time step is not, so the count is reliable.

Driver writesChanges at a sampling edge?Race with edge readers?
blocking, right after @(posedge clk)yesyes
nonblocking, right after @(posedge clk)yes, in the NBA regionno: readers see the old value
blocking, after @(negedge clk)nono
through a clocking block with output skew #2nono

A change at the edge is a warning sign; a blocking change at the edge is a race.

Fix both sides

Fix the driver first, because the DUT reads its signals too. Driving through a clocking block puts every change at the output skew, after the edge, so neither the DUT nor any monitor can see it at that edge.

Then fix the monitor: read through a clocking block with #1step inputs and wait on @(bus.mon_cb). A monitor built that way sees pre-edge values whatever any other process does, so it stays correct even if someone later adds a racy driver.

Blocking writes at the sampling edge

@(posedge clk);
bus.rvalid = 1;
bus.rdata  = next;

The DUT and the monitor may see the old or the new beat at this edge.

Drives through the clocking block

@(bus.drv_cb);
bus.drv_cb.rvalid <= 1;
bus.drv_cb.rdata  <= next;

The pins change two time units after the edge; every reader at the edge sees the old beat.

Fixes that only move the race

Adding #1 before the driver's write moves it off the edge, but it ties correctness to the time unit and to the clock period. A later change to either brings the race back. Adding #0 moves the write into the Inactive region, which reorders it against some processes but not all.

Swapping the order of always blocks in the file, or changing a simulator option, may make the failure disappear on one tool without removing the race. A fix is real only when the standard defines the result.

A regression for the fix

Keep the change counter from the example in the testbench, and fail the test if it ever finds a bus change at a sampling edge. It costs a few lines and catches the next racy driver the day it is written.

Then run the test on every simulator you have. With the race gone, all of them must agree beat for beat, and a difference between them is worth investigating rather than explaining away.

Try it

The racy driver changed the bus at 4 rising edges, each one a point where the DUT or a raw monitor could see either value. The clean driver changed it at none, and the clocking-block monitor recorded the three beats in order.

In the racy driver, change @(posedge clk) to @(negedge clk) in both places and predict the first line again.

RUNVerilator · simulator service
Edit the code, then run it
Expected: racy driver: bus changed at 4 rising edges clean driver: bus changed at 0 rising edges clean bus, beats sampled by the monitor: a1 b2 c3

Check yourself

Predict the outputRising edges are at 5, 15, 25 and so on. The process counts rising edges at which a or b changes. Predict the exact output.
module tb;
  logic clk = 0;
  always #5 clk = ~clk;
  logic a = 0, b = 0;
  int n;
  time last = -1;
  always @(a or b)
    if ($time % 10 == 5 && $time != last) begin n++; last = $time; end
  initial begin
    @(posedge clk); a = 1;
    @(negedge clk); b = 1;
    @(posedge clk); a <= 0;
    #20 $display("%0d", n);
    $finish;
  end
endmodule
ConceptA test passes on simulator A and fails on simulator B, with the monitor one beat late. Which explanation is most likely?
ConceptWhich change removes the race instead of moving it?
Read the waveformRising edges are at 5, 15 and 25. Which signal changes at a sampling edge?
Change times of three testbench signals
0123time571015rvalidrdata00a1rlast

Each column is a moment at which something changes; values are shown after the change.

Spot the bugEach option is the driver loop that offers three beats. The harness counts rising edges at which the bus changes, and the result must be 0. Which option races?

Remember

  • Failures that change with the simulator, or with unrelated edits, suggest a race.
  • A blocking write at a sampling edge races every process that reads the signal at that edge.
  • Find the racing signal by looking for testbench changes at the exact timestamp of a sampling edge.
  • Fix the driver and the monitor: drive and sample through clocking blocks.
  • Delays such as #1 or #0 and file reordering move a race; they do not remove it.

Practice