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.
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 writes | Changes at a sampling edge? | Race with edge readers? |
|---|---|---|
blocking, right after @(posedge clk) | yes | yes |
nonblocking, right after @(posedge clk) | yes, in the NBA region | no: readers see the old value |
blocking, after @(negedge clk) | no | no |
through a clocking block with output skew #2 | no | no |
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.
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 c3Check yourself
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
#1or#0and file reordering move a race; they do not remove it.
Practice
- The monitor that saw the future Debug · Medium