A covergroup clocked on posedge clk with a coverpoint on req_id counts idle cycles, counts the same request many times while it waits, and fills in the first test. It answers "did these values appear on the pins", which is rarely the question.
The questions that find bugs in an out-of-order design are about state the design keeps internally:
Outstanding depth. How many requests were in flight when a new one arrived? Table-full and one-below-full are where allocation bugs live.
ID reuse distance. How many requests passed between two uses of the same ID? Reusing an ID right after its response tests that the old entry was fully released.
Response order. Did a later request ever complete first? A design that happens to answer in order never tests the reordering logic.
Latency range. Did responses arrive at the minimum and the maximum latency the spec allows?
None of these is a signal. Each is computed by a monitor from a stream of handshakes, which is why the model belongs on transactions.
Sample once per event that matters, with the values that describe it. The with function sample(...) form makes the arguments explicit, so the covergroup cannot read a variable that has already moved on. Sampling strategies covers the basic forms; here the choice of event is the design decision.
Sampling choice | Effect |
|---|---|
Every clock | Idle cycles and waiting cycles count as hits. Bin counts measure time, not traffic. |
On request accept | One sample per request. Depth here means "the table the request found". |
On response | Good for latency and order bins. Depth here means "what was still in flight when this one finished". |
Depth is sampled before the accepted request is added. Sampling after the increment shifts every bin by one and hides the empty-table case. The bench below catches exactly that mistake.
// not-run: covergroups are parsed but ignored by Verilator 5.048 (COVERIGN warnings, get_coverage() returns 0) and rejected by Icarus 13; needs a commercial simulator
class ooo_cov;
// One sample per accepted request; the monitor computes the arguments.
covergroup cg with function sample(int unsigned depth, int unsigned reuse_d, bit first);
option.per_instance = 1;
cp_depth: coverpoint depth {
bins empty = {0};
bins one = {1};
bins two = {2};
bins all_but_me = {3}; // 4 IDs: 3 is the deepest table a request can find
}
cp_reuse: coverpoint reuse_d iff (!first) {
bins b2b = {1}; // same ID as the previous request
bins near = {[2:3]};
bins far = {[4:$]};
}
x_depth_reuse: cross cp_depth, cp_reuse;
endgroup
function new();
cg = new();
endfunction
endclassThe iff (!first) guard keeps the first use of each ID out of the reuse bins: it has no distance, and forcing it into a bin would inflate the "far" count. In UVM the same covergroup sits in a subscriber whose write() computes the derived values and calls cg.sample().
The DUT is a small responder: one outstanding request per ID, four IDs, latency 2*id+1 cycles, so higher IDs answer later and responses come back out of order. It exports dbg_outstanding so the monitor's derived depth can be cross-checked against the design's own count.
Verilator 5.048 parses the covergroup above but ignores it (it prints COVERIGN warnings and get_coverage() returns 0), and Icarus 13 rejects it. The bench therefore uses a class that mirrors the bins one for one, with the same ranges, and checks its counts. It also counts samples that fall in no reuse bin, which must be 0.
// Out-of-order responder: one outstanding request per ID, latency 2*id+1.
module ooo_rsp (
input logic clk,
input logic rst_n,
input logic req_valid,
input logic [1:0] req_id,
output logic req_ready,
output logic rsp_valid,
output logic [1:0] rsp_id,
output logic [2:0] dbg_outstanding
);
timeunit 1ns; timeprecision 1ps;
logic [3:0] busy;
logic [2:0] timer [4];
logic [3:0] done;
always_comb begin
for (int i = 0; i < 4; i++) done[i] = busy[i] && timer[i] == 0;
rsp_valid = |done;
rsp_id = done[0] ? 2'd0 : done[1] ? 2'd1 : done[2] ? 2'd2 : 2'd3;
req_ready = !busy[req_id];
dbg_outstanding = 3'(busy[0]) + 3'(busy[1]) + 3'(busy[2]) + 3'(busy[3]);
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
busy <= '0;
for (int i = 0; i < 4; i++) timer[i] <= '0;
end else begin
for (int i = 0; i < 4; i++) if (busy[i] && timer[i] != 0) timer[i] <= timer[i] - 3'd1;
if (rsp_valid) busy[rsp_id] <= 1'b0;
if (req_valid && req_ready) begin
busy[req_id] <= 1'b1;
timer[req_id] <= 3'(2 * req_id + 1);
end
end
endmodule
// Procedural mirror of the covergroup bins (Verilator 5.048 ignores covergroups).
class ooo_cov_mirror;
int depth_bins [4]; // outstanding before accept: 0, 1, 2, 3
int reuse_bins [3]; // b2b (1), near (2-3), far (4+)
int first_use; // first use of an ID: no distance, not sampled
int no_bin; // a distance that falls in no reuse bin (0 or negative)
function void sample(int depth, int reuse_d, bit first);
depth_bins[depth]++;
if (first) first_use++;
else if (reuse_d == 1) reuse_bins[0]++;
else if (reuse_d >= 2 && reuse_d <= 3) reuse_bins[1]++;
else if (reuse_d >= 4) reuse_bins[2]++;
else no_bin++;
endfunction
endclass
Each count was worked out by hand from the ID order and the latencies, then confirmed by the run. The ten requests use IDs 0, 0, 1, 2, 3, 1, 0, 2, 3, 3. Reuse distances for the six repeated IDs are 1, 3, 5, 4, 4 and 1, so the reuse bins are 2, 1 and 3. Depth reaches 3 only once, when ID 0 arrives while IDs 1, 2 and 3 are all busy.
Three changes make the bench fail. Moving last_req[req_id] = req_count above the distance calculation makes every distance 0, which is in no reuse bin (the covergroup's near starts at 2): the reuse bins read 0, 0, 0, all six repeated uses land in no bin, and the test fails. That is a coverage bug that would otherwise report a healthy-looking model. Forcing req_ready to 1 in the DUT lets an ID be reused while busy, and the derived depth stops matching dbg_outstanding. Incrementing the monitor count before the compare also fails on the first request.
Reuse distance is a temporal measure expressed as a number, so a covergroup can bin it. The same works for latency (cycles from accept to response), burst spacing, and time since reset. Transition bins on a derived value cover order: bins drain = (3 => 2 => 1 => 0); on depth sampled at each response proves the table drained from its deepest point. Multi-cycle scenarios that are not a single number are easier as cover property, covered in Assertion-Backed Coverage.
The bin should describe the table the request found. Sampling after the increment shifts every value by one, so the empty-table case (depth 0) can never be hit and the full case looks reached one request early.
No. With four IDs and one outstanding request per ID, the deepest table a new request can find is 3; a fourth request must wait. A bin for 4 at accept time is unreachable and belongs out of the model, with the reason written down.