Suppose you put the stream covergroup in the driver, where axis_item.idle is right at hand. The item's constraint gives idle == 0 a weight of 6 in 10, so a bin for back-to-back beats would fill within the first few beats of every test, and the plan row "back-to-back beats" would be signed off. On the pins, not one pair of beats in the whole regression was back to back: this driver always drops TVALID for at least a clock between beats. The covergroup would be measuring the knob, not the bus.
The DMA bench's real coverage component, dma_coverage, reports that row as a hole in every test, and that hole led to a driver fix. Where should coverage listen, and what exactly should it sample?
Picture a traffic survey. You can read the bus company's timetable, or you can lay a counting cable across the road. Only the cable tells you how many buses actually passed, and when. A monitor is the cable: it turns pin activity into transactions and broadcasts them on an analysis port. A coverage component subscribes to those ports exactly as the scoreboard does. It never drives, never raises an objection, and never reads anything the stimulus side knows but the pins do not show.
UVM's uvm_subscriber #(T) is a component with one built-in analysis export and a write(T t) method to fill in. It fits a coverage collector that listens to one stream. dma_coverage needs two streams of different types (APB transfers and input beats), so it extends uvm_component and declares two analysis imps with uvm_analysis_imp_decl, the same pattern the scoreboard uses (several analysis inputs).
Read the arrows. The coverage component gets the same observations as the checkers, through the same fan-out. Nothing reaches it from a sequence or a driver, so nothing it records can be intent.
Each uvm_analysis_imp_decl(SUFFIX) macro declares an imp class whose write calls write_SUFFIX in the owner. Two macros give two differently named methods in one component.
// snippet: the declarations just above dma_coverage in the bench
`uvm_analysis_imp_decl(_cov_apb) // the imp calls write_cov_apb(apb_item)
`uvm_analysis_imp_decl(_cov_in) // the imp calls write_cov_in(axis_item)// snippet: from dma_coverage in the course DMA bench
uvm_analysis_imp_cov_apb #(apb_item, dma_coverage) apb_export;
uvm_analysis_imp_cov_in #(axis_item, dma_coverage) in_export;A covergroup declared with function sample(...) receives its values at the moment of the call. Nothing is read from class fields that may have changed in between, the sample point is visible in the code, and the same covergroup can be fed from different places. Two of the four:
// snippet: from dma_coverage in the course DMA bench
covergroup job_cg with function sample(bit [15:0] len, bit irq); option.per_instance = 1;
cp_len: coverpoint len {
bins zero = {0};
bins one = {1};
bins few = {[2:4]};
bins many = {[5:15]};
bins max_pkt = {16};
bins over = {[17:$]};
}
cp_irq: coverpoint irq;
len_x_irq: cross cp_len, cp_irq;
endgroup
covergroup stream_cg with function sample(int unsigned gap, bit early); option.per_instance = 1;
cp_gap: coverpoint gap {
bins back_to_back = {0};
bins short_gap = {[1:3]};
bins long_gap = {[4:$]};
}
cp_tlast: coverpoint early {
bins on_time = {0};
bins early = {1};
}
endgroupA covergroup declared inside a class is an embedded covergroup, and the language allows it to be constructed only in the class constructor. Forget the new() and the first sample() is a null-handle crash.
// snippet: from dma_coverage in the course DMA bench
function new(string name, uvm_component parent);
super.new(name, parent);
apb_export = new("apb_export", this);
in_export = new("in_export", this);
job_cg = new();
status_cg = new();
clear_cg = new();
stream_cg = new();
endfunctionEvery covergroup instance needs its own name, or the report cannot tell instances apart. Put this component in each half of the two-DMA subsystem bench and, without names, both instances report under the same covergroup; with set_inst_name and the component's full path they become uvm_test_top.env.dma0.cov.job_cg and uvm_test_top.env.dma1.cov.job_cg. Names built from the hierarchy are also stable from run to run, which a regression merge depends on.
// snippet: from dma_coverage in the course DMA bench
function void build_phase(uvm_phase phase);
super.build_phase(phase);
job_cg.set_inst_name({get_full_name(), ".job_cg"}); // distinct names per instance
status_cg.set_inst_name({get_full_name(), ".status_cg"});
clear_cg.set_inst_name({get_full_name(), ".clear_cg"});
stream_cg.set_inst_name({get_full_name(), ".stream_cg"});
endfunction// snippet: from dma_coverage in the course DMA bench
virtual function void write_cov_apb(apb_item t);
if (t.slverr) return;
if (t.write && t.addr == LEN) len_reg = t.data[15:0];
if (t.write && t.addr == CTRL) begin
irq_en = t.data[1];
if (t.data[0]) begin // GO: one job with this length
job_cg.sample(len_reg, irq_en);
hit(len_bin(len_reg));
hit(irq_en ? "irq_en=1" : "irq_en=0");
beats_in_job = 0;
end
end
// A clear counts only for a bit that was actually set: writing 1 to an
// already-clear W1C bit exercises nothing.
if (t.write && t.addr == STATUS) begin
bit [1:0] cleared = t.data[2:1] & {err_seen, done_seen};
if (cleared != 0) begin
clear_cg.sample(cleared);
if (cleared[0]) hit("clear=done");
if (cleared[1]) hit("clear=err");
end
done_seen &= !t.data[1];
err_seen &= !t.data[2];
end
if (!t.write && t.addr == STATUS) begin
done_seen = t.rdata[1];
err_seen = t.rdata[2];
if (t.rdata[2:1] != 0) begin
status_cg.sample(t.rdata[1], t.rdata[2]);
hit(t.rdata[2] ? "outcome=err" : "outcome=done");
end
end
endfunctionPSLVERR transfers are skipped. A failed access changed nothing in the DUT, so it must not cover anything.
LEN is latched, not sampled. A LEN write is configuration; the job only exists when GO is written, so job_cg samples at GO with the latched value.
The outcome is sampled on a STATUS read that shows DONE or ERR. Reads of an idle STATUS say nothing about how a job ended.
A clear is sampled only for bits that were set, the fix described in the model design lesson.
The stream side has no field that says "gap". The input monitor publishes a beat when it sees TVALID and TREADY high on a clock edge, so the gap is computed from the times of consecutive beats.
// snippet: from dma_coverage in the course DMA bench
virtual function void write_cov_in(axis_item t);
int unsigned gap;
bit early;
gap = (beats_in_job == 0) ? 0 : int'(($time - last_beat_t) / 10ns) - 1;
last_beat_t = $time;
beats_in_job++;
early = t.last && (beats_in_job < len_reg);
stream_cg.sample(gap, early);
if (beats_in_job > 1) hit(gap == 0 ? "gap=0" : (gap <= 3 ? "gap=1-3" : "gap=4+"));
if (t.last) hit(early ? "tlast=early" : "tlast=on-time");
endfunctionTwo things to notice. The 10ns is the clock period written into the formula, so a clock change would silently rescale every bin; a sturdier version has the monitor count clocks between transfers and publish the count as an observed field. And the gap counts every clock without a transfer, whether TVALID was low (the source was idle) or TREADY was low (the DMA stalled it). This covergroup cannot tell an idle from a stall; lesson 6 shows what can.
// snippet: from dma_env_check in the course DMA bench
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
apb.ap.connect(pred.apb_export);
in_agt.ap.connect(pred.in_export);
if (use_class_cmp) begin
pred.exp_ap.connect(class_cmp.before_export); // expected
out_agt.ap.connect(class_cmp.after_export); // actual
end else begin
pred.exp_ap.connect(cmp.before_export);
out_agt.ap.connect(cmp.after_export);
end
apb.ap.connect(cov.apb_export);
in_agt.ap.connect(cov.in_export);
endfunctionThe connections use the agent-level ap, never agt.mon.ap, so the env does not depend on what is inside an agent. The coverage component is one more subscriber on ports that already fan out to the predictor and scoreboard.
+UVM_TESTNAME=dma_check_test
UVM_INFO dma_tb.sv(1528) @ 1305000: uvm_test_top.env.cov [COV] covergroups: job 0.0%, status 0.0%, clear 0.0%, stream 0.0%
UVM_INFO dma_tb.sv(1530) @ 1305000: uvm_test_top.env.cov [COV] plan items hit: 9 of 17
UVM_INFO dma_tb.sv(1531) @ 1305000: uvm_test_top.env.cov [COV] plan holes: len=0 len=16 len>16 irq_en=0 outcome=err clear=err gap=0 tlast=earlyThe report comes from uvm_test_top.env.cov, the component's full name, which is also the prefix of every covergroup instance name.
gap=1-3 and gap=4+ were hit; gap=0 was not, in 14 beats with idle == 0 drawn for most of them.
The 0.0% figures are Verilator ignoring coverpoints; the tally is the evidence on this simulator.
Here is the stream driver's loop. After each handshake it drops TVALID, and the next beat waits for at least one more clock edge before raising it again:
// snippet: from axis_driver in the course DMA bench
task run_phase(uvm_phase phase);
vif.tvalid <= 1'b0;
@(posedge vif.rst_n);
forever begin
axis_item beat;
seq_item_port.get_next_item(req);
beat = pre_drive(req);
if (beat != null) begin
repeat (beat.idle) @(posedge vif.clk);
@(posedge vif.clk);
vif.tdata <= beat.data;
vif.tlast <= beat.last;
vif.tvalid <= 1'b1;
do @(posedge vif.clk); while (!vif.tready); // the beat moves on TVALID && TREADY
vif.tvalid <= 1'b0;
end
seq_item_port.item_done();
end
endtaskSo idle == 0 means "no extra idle clocks" in the driver's own terms, and the smallest gap the bus can ever show is one clock. A covergroup sampling idle in the driver would mark back-to-back as covered on roughly 60% of beats. dma_coverage, sampling observed beat times, reports gap=0 as a hole in every test. Coverage did its real job here: it exposed a driver limitation that no checker could, because every beat that did arrive was correct. The closure lesson shows the driver change that closes it.
A field the monitor fills is observation; a field only the driver reads is intent. axis_item carries both kinds, so check which one a covergroup samples.
Passive reuse breaks driver-side coverage. At subsystem or chip level an agent goes passive and its driver disappears; coverage on monitors keeps working.
dma_env twice. If each copy had a dma_coverage without the set_inst_name calls, what would the coverage report show?Two instances of each covergroup under the same name, so dma0's holes and dma1's holes could not be told apart, and a merge across runs could not line instances up reliably. Naming each instance from get_full_name() gives ...dma0.cov.job_cg and ...dma1.cov.job_cg.
write_cov_apb return at once when t.slverr is set?A transfer that ended with PSLVERR did not change any register, so counting it could cover a job or a clear that never happened. On this DMA, PSLVERR comes only from unmapped addresses, so the guard matters most when a later test injects bus errors.
stream_cg reports gap=4+ as hit. Does that prove the DMA was tested under backpressure?No. In dma_check_test the output's TREADY is tied high, so nothing ever stalls the DMA. The long gaps come from the driver's random idle clocks on the input. Because the gap counts every clock without a transfer, it cannot tell an idle source from a stalled one. A stall needs coverage of TVALID high with TREADY low.
Next: the component samples the right traffic. Bins, Illegal Bins and Ignore Bins decides which values count, which are ignored, and which should never happen at all.