A random DMA test failed on two jobs out of three. Each time the DMA set STATUS.ERR: the packet had arrived one word short. The sequence had generated every item. The stream driver had called item_done for every one, and its own debug message said it had seen TREADY for the first beat. The DUT never took that beat. Driver and DUT had looked at the same signal on the same clock edge and disagreed about its value.
How can two parts of one simulation see different values at the same instant, and how do you build a monitor and driver that never do?
A simulator processes each instant of time in ordered regions. When a clock edge happens, every process waiting on it wakes in the Active region, in an order the standard leaves undefined. Flip-flops evaluate their inputs there, and their outputs change later, in the NBA (nonblocking assignment) region. A testbench thread that reads a signal right after @(posedge clk) is racing every other process that woke on the same edge: if one of them changes that signal with a blocking assignment, the reader may see either value.
A clocking block removes the race for sampling. Its input skew #1step means: sample in the Preponed region of the edge's time step, before anything in that instant has run. Like a photo-finish camera that fires just before the line, it records the value every signal had before the edge, which is exactly the value the DUT's flip-flops capture on that edge. Whatever happens during the edge cannot change the picture.
Put the bench's two rules on this picture. The APB and stream monitors sample in Preponed, through mon_cb. (The interrupt monitor reads its one level line directly at the clock edge: irq changes only on a clock edge and the monitor looks for a change with !==, so a one-cycle-late view costs nothing. A fast multi-bit bus gets a clocking block.) Drivers change pins in NBA, by waiting on @(posedge clk) and assigning with <=. Every read a driver makes right after the edge happens in Active, before NBA, so it too sees pre-edge values. No reader is ever racing a writer.
// snippet: from the course DMA bench
interface apb_if (input logic clk, input logic rst_n);
logic [7:0] paddr;
logic psel, penable, pwrite;
logic [31:0] pwdata, prdata;
logic pready, pslverr;
// Monitors sample through mon_cb: #1step reads each signal just before the clock
// edge, which is exactly the value the DUT samples on that edge. Drivers drive with
// nonblocking assignments right after @(posedge clk) (see apb_driver).
clocking mon_cb @(posedge clk);
default input #1step;
input paddr, psel, penable, pwrite, pwdata, prdata, pready, pslverr;
endclocking
endinterfacemon_cb is declared in the interface, never in a class (a clocking block in a class does not compile). default input #1step sets the sampling point for every input, and the signal list makes each one readable as vif.mon_cb.psel. The monitor waits with @(vif.mon_cb), which fires on the clocking event, after the samples for that edge have been taken. There are no outputs in mon_cb: the bench's drivers do not drive through a clocking block, for a reason the war story explains.
Every wait in apb_driver and axis_driver is @(posedge vif.clk), and every pin assignment is <=. Two consequences follow. A driver's if (!vif.pready) right after the edge reads the pre-edge value, which matches what the monitor's mon_cb sampled and what the DUT saw. And every thread in the bench, in every agent, wakes on the same event object, so a hand-off from one thread to another (the APB driver finishing the GO write, the stream driver starting its first beat) cannot slip by an edge.
The model below has a driver that changes a signal with a blocking write on the clock edge (the risky style), a monitor that reads the raw signal right after @(posedge clk), and one that samples through a #1step clocking block. The reference is the value the signal held mid-cycle.
// A blocking write on the clock edge races a raw read; a #1step clocking
// block always returns the value from before the edge.
module tb_sample;
logic clk = 0;
always #5 clk = ~clk;
logic v = 0; // driven with a BLOCKING write on the edge: risky style
logic pre_edge = 0; // the value v held in the middle of the cycle
int raw_saw_new = 0, cb_ok = 0, errors = 0;
clocking mon_cb @(posedge clk);
default input #1step;
input v;
endclocking
always @(negedge clk) pre_edge = v; // reference: the settled value
initial begin // the "driver"
repeat (8) begin
@(posedge clk);
v = ~v; // changes v in the same time step as the edge
end
end
initial begin // a monitor that reads the raw signal
repeat (8) begin
@(posedge clk);
if (v !== pre_edge) raw_saw_new++; // order-dependent: may see the new value
end
end
initial begin // a monitor that samples through mon_cb
repeat (8) begin
@(mon_cb);
if (mon_cb.v === pre_edge) cb_ok++;
else errors++;
end
#1;
if (errors == 0 && cb_ok == 8)
$display("PASS: mon_cb returned the pre-edge value 8 of 8 times (the raw read saw the new value %0d times on this simulator)", raw_saw_new);
else
$display("FAIL: mon_cb disagreed with the pre-edge value %0d times", errors);
$finish;
end
endmoduleOn Verilator the raw read sees the new value all eight times; on another simulator, or after an unrelated edit changes process order, it may see the old one. That number is not something to rely on, which is the point. The clocking block's answer is the same everywhere.
A monitor can check protocol rules in its run_phase, but pin-level rules are better written as concurrent assertions in the interface (or in a checker module bound to it). They sample with the same preponed semantics, run every cycle whether or not a monitor exists, travel with the interface into passive chip-level benches, and are reused unchanged by formal tools and emulators. A sketch for the bench's APB interface:
// snippet: assertions that could live in apb_if
property p_setup_then_access; // a new transfer starts with a setup cycle, then access
@(posedge clk) disable iff (!rst_n)
(psel && !penable) |=> (psel && penable);
endproperty
property p_hold_until_ready; // control and data stay stable through the access phase
@(posedge clk) disable iff (!rst_n)
(psel && penable && !pready) |=> (psel && penable && $stable({paddr, pwrite, pwdata}));
endproperty
property p_no_x_on_strobes; // out of reset the strobes are always 0 or 1
@(posedge clk) disable iff (!rst_n) !$isunknown({psel, penable});
endproperty
a_setup: assert property (p_setup_then_access);
a_hold: assert property (p_hold_until_ready);
a_no_x: assert property (p_no_x_on_strobes);Check | Best home | Why |
|---|---|---|
Handshake order, signal stability, no X on strobes | SVA in the interface or a bound checker | Cycle-accurate, always on, reusable in formal and emulation, works with no UVM monitor |
Address in the register map, legal burst for this configuration | Monitor or scoreboard class | Needs configuration and transaction context that assertions do not have |
Turning checks off for an error-injection test | Both: | The test, not the checker, decides when a violation is expected |
Look at the monitor's completion test: if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready). If PSEL is X, the condition is not true, so the monitor silently skips the cycle. A DUT bug that puts X on a strobe turns into missing transactions, which the scoreboard may blame on something else entirely. Decide the policy explicitly. On control lines (strobes, valid, ready), X out of reset is a bug: report it, from an assertion like a_no_x or with an uvm_error in the monitor using $isunknown. On data lines, X is legal when not qualified (PWDATA during a read, TDATA while TVALID is low) and a bug when qualified. During reset, X is expected; gate checks with disable iff (!rst_n). Use the 4-state operators (===, !==, $isunknown) for these checks; == against X yields X, and an if treats X as false.
Raise objections. A monitor sees traffic, not intent; a passive monitor on a bus that never goes idle would hold the test open forever. End of test belongs to the test, or to a scoreboard that uses phase_ready_to_end to wait for owed items; see End-of-test strategy.
Change an item after publishing it, or reuse one object for several publishes (see The monitor).
Drive or force any signal, even "just to help" with a default value. A monitor that drives is not reusable as a passive observer.
The random test from the opening is dma_rand_test. With the drivers on @(posedge clk) and nonblocking drives, and the monitors on mon_cb, all three jobs complete and every word is checked (the random keys and lengths differ by simulator and seed):
UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
UVM_INFO dma_tb.sv(731) @ 645000: uvm_test_top [JOB] key=0e564b57 len=1 irq_en=1
UVM_INFO dma_tb.sv(731) @ 885000: uvm_test_top [JOB] key=5a1793f4 len=3 irq_en=1
UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched
UVM_ERROR : 010 + 1 + 3 = 14 words: no beat lost at a job boundary, which is exactly where the bug used to strike.
This happened while building the course bench. The first version gave each interface a driver clocking block, and each driver waited on its own: the APB driver on @(apb_vif.drv_cb), the stream driver on @(axis_vif.drv_cb). Both blocks were clocked by the same posedge clk.
Symptom: dma_rand_test failed on 2 of 3 jobs with STATUS.ERR; each packet arrived one word short, although the sequence generated N items and the driver called item_done N times.
Root cause: the APB driver finished the GO write on an edge. In the same time step, the stream driver received its first item and executed @(axis_vif.drv_cb). The two clocking blocks' events fire in an undefined order within the time step, and this time the stream block's event had not fired yet, so the stream driver caught the current edge instead of the next one. From then on its clocking-block drives and samples were offset from what the DUT saw: it "saw" TREADY for the first beat, but the DUT never took that beat.
Fix: every driver waits on @(posedge clk), one shared event, and drives with nonblocking assignments; monitors keep sampling through mon_cb with #1step. The rerun above is the result.
The lesson is not "clocking blocks are bad". It is that every thread that hands work to another must wait on the same event. Separate clocking blocks are separate events, even on one clock. The bench keeps clocking blocks where they remove a race (sampling) and plain @(posedge clk) with NBAs where threads must stay in step (driving).
@(posedge vif.clk) and reads vif.tready directly. It works today. What change elsewhere could break it without touching the monitor?Anything that makes TREADY change in the Active region of the edge: a testbench responder that drives TREADY with a blocking assignment, a TREADY that is combinational from a signal driven that way, or a simulator or compile-order change that reorders processes. The raw read then races the writer. Sampling through a #1step clocking block makes the monitor immune to all of them.
a_setup fires and fails the test. What is the right fix?Make the check controllable, not weaker: guard it with a control bit (disable iff (!rst_n || !chk_en)) that the test clears for the window in which it injects, or have the test demote the expected failure. The test owns the decision that a violation is expected; the assertion stays on for every other test.
TDATA carries meaning only when TVALID is high; while it is low, the protocol does not care what TDATA holds, and many designs leave it undriven. TVALID decides whether a transfer happens at all, so an unknown TVALID makes the bus state undefined, and a monitor's if would silently treat it as no transfer.
Next: every class in this topic reached the pins through a virtual interface. In Interface abstraction you will see where that handle comes from, why parameterized interfaces make it painful, and the abstract-BFM pattern that emulation-friendly benches use instead.