The plan row reads: "the output stalls for three or more cycles while TVALID is held". The coverage report shows gap=4+ hit in dma_check_test, and someone ticks the row. But in that test the output's TREADY is tied high: nothing ever stalled. The long gaps were the input driver resting between beats. The covergroup measured a number between two transfers; the plan row describes a shape in time.
Shapes in time are what SystemVerilog Assertions describe. When should a plan row become a cover property instead of a bin, where does it live, and how do its results reach the same closure report?
A covergroup is a photograph: at one sample point it records which values were present. A `cover property` is a video clip search: it watches every clock and records each time a sequence (a pattern of signal values over consecutive cycles) completes. "LEN was 16 when GO was written" is a photograph. "TVALID stayed high while TREADY was low for three clocks, then the beat moved" is a clip.
The split also decides where the coverage lives. Concurrent assertions and cover properties cannot be written inside a class. They live in modules, interfaces or checkers, usually in or bound to the interface whose signals they watch. Value coverage of transactions lives in a class, fed by a monitor.
The stream monitor publishes one item, at the edge where TVALID and TREADY are both high. Everything interesting about this picture happened before that item existed, so no covergroup sampling monitor items can see it.
A checker module with the cover properties, bound into every instance of axis_if. Binding keeps the interface file unchanged and gives every instance its own copy, named by the hierarchy (top.s_ax.u_cover.c_stall3, top.m_ax.u_cover.c_stall3), so the distinct-name problem of covergroups does not arise.
// not-run: a cover-property module, shown as an excerpt (Verilator records covers only with --coverage-user)
module axis_cover (input logic clk, rst_n, tvalid, tready);
// TVALID held through 3 clocks of TREADY low, then the transfer
c_stall3: cover property (@(posedge clk) disable iff (!rst_n)
(tvalid && !tready) [*3] ##1 (tvalid && tready));
// two transfers on consecutive clocks
c_b2b: cover property (@(posedge clk) disable iff (!rst_n)
(tvalid && tready) ##1 (tvalid && tready));
// the rule the stall cover depends on: TVALID never drops before the transfer
a_hold: assert property (@(posedge clk) disable iff (!rst_n)
(tvalid && !tready) |=> tvalid);
endmodule
bind axis_if axis_cover u_cover (.clk(clk), .rst_n(rst_n), .tvalid(tvalid), .tready(tready));Two details matter. (tvalid && !tready) [*3] ##1 (tvalid && tready) needs the transfer immediately after exactly three stalled clocks, so a stall of ten clocks matches once (from the attempt that starts three clocks before the end), and the hit count equals the number of stalls of three or more. Writing [*3:$] instead would match from every starting clock, inflating the count. And the cover sits beside the assertion it depends on: a cover proves the case happened, the assertion proves it was handled legally.
// not-run: a cover-property module, shown as an excerpt (Verilator records covers only with --coverage-user)
module apb_cover (input logic clk, rst_n, psel, penable, pready);
// an access with one wait state: PREADY low, then high
c_wait1: cover property (@(posedge clk) disable iff (!rst_n)
(psel && penable && !pready) ##1 (psel && penable && pready));
// an access with no wait state
c_nowait: cover property (@(posedge clk) disable iff (!rst_n)
(psel && !penable) ##1 (psel && penable && pready));
endmodule
bind apb_if apb_cover u_cover (.clk(clk), .rst_n(rst_n), .psel(psel), .penable(penable), .pready(pready));dma_dut is built with WAIT_STATES = 1, so every access takes exactly one wait state. c_wait1 is hit by every register access, and c_nowait can never be hit in this configuration. That is not a stimulus hole: it is an unreachable item, and the merge lesson shows how it becomes a reviewed exclusion with the parameter as its reason.
None of this replaces dma_coverage. "Which LEN classes ran, with which IRQ_EN" is a value fact about one transaction, and a covergroup with bins and crosses is the right tool. The split, side by side:
Covergroup in a class | cover property in the interface | |
|---|---|---|
Records | Values at a sample point, binned and crossed | Each completion of a sequence over clocks |
Fed by |
| The clock, every cycle |
Good for | Configuration, outcomes, value classes, combinations | Stalls, wait states, handshake order, latency windows |
Instance names | Must be set ( | Come from the hierarchy automatically |
In the report | Covergroup section, scored in % | Assertion or directive section, as hit counts |
Merged by | The vendor merge, by covergroup and instance | The same merge, by hierarchical directive name |
A per-cycle covergroup in the interface sits between the two: it samples every clock like a property, but records values and simple transitions in bins (the handshake covergroup). Use it when you want a percentage over a few states; use a cover property when the plan row is a multi-cycle shape.
The course's simulator runs neither the cover properties nor the coverpoints, so there is no cover log to show here. What the real logs do show is which tests could ever hit c_stall3 on the output side:
+UVM_TESTNAME=dma_backpressure_test (+UVM_VERBOSITY=UVM_MEDIUM)
UVM_INFO dma_tb.sv(1246) @ 1005000: uvm_test_top [BP] the slave stalled the DMA for 28 clocks
UVM_INFO dma_tb.sv(411) @ 1005000: uvm_test_top.env.sb [SB] 12 words matched, 0 mismatchedOnly dma_backpressure_test replaces the always-ready output with a reactive slave that drives TREADY low. Every other test leaves TREADY tied high, so the output-side c_stall3 is empty there by construction.
28 is the total number of stalled clocks, not the length of any one stall. Whether any single stall lasted three clocks with TVALID held is exactly the question the cover property answers and the log cannot.
When the output stalls, the DMA's one output register fills and it drops TREADY on its input, so the input-side cover can be hit by the same test.
The opening story is a real reading of this bench's logs. dma_check_test hits gap=4+, and its output TREADY never goes low. The long gaps come from the input driver's idle knob, up to 8 clocks with TVALID low. A gap between completed beats lumps two different situations together: the source had nothing to send, or the sink refused it. For the plan row "backpressure loses nothing", only the second counts, and only a check of TVALID and TREADY during the gap can tell them apart.
The fix is a modelling decision. Keep cp_gap for what it measures (input pacing) and give the backpressure row its own temporal item, c_stall3 on the output interface, mapped in the traceability table. Then the regression shows the truth: covered only by dma_backpressure_test.
c_stall3 is empty after a full regression. List two different explanations and how to tell them apart.Either no test stalls the output for three clocks (a stimulus hole: check whether any test uses the reactive slave, and its stall settings), or the binding or reset gating is wrong (a model bug: check that c_stall3 exists in the database for top.m_ax, and that rst_n is high during traffic). An empty item that is missing from the database is a model bug, not a hole.
c_stall3 use [*3] ##1 rather than [*3:$]?[*3] ##1 transfer requires the transfer right after exactly three stalled clocks, so each stall of three or more matches once. [*3:$] matches from every starting clock that leaves at least three stalled cycles, so one long stall produces many hits, and the count no longer means "number of stalls".
c_nowait on the DMA's APB interface a hole that needs a test?No. With WAIT_STATES = 1 every access has exactly one wait state, so the item is unreachable in this configuration. It becomes an exclusion whose reason names the parameter, and it is revisited if a configuration with WAIT_STATES = 0 is ever built.
Next: coverage from many tests and many kinds of item has to become one answer. Regression Merge and the Closure Loop builds it.