An earlier version of the course bench had a line in status_cg that looks like a safety net: illegal_bins both = {2'b11}; // the DMA never reports both. If DONE and ERR are ever read together, a commercial simulator raises a run-time error. A reviewer nods: an impossible state, guarded.
Then a test leaves an ERR uncleared and starts another job, and STATUS reads 0x6. The DUT is behaving exactly as its spec says. So who was wrong, and what does illegal_bins actually claim? This lesson is about the vocabulary of a covergroup, and about the responsibility each word carries.
Think of a sorting office. Each bin is a labelled pigeonhole; a sampled value lands in every pigeonhole whose label matches it, and a pigeonhole with at least one letter counts as covered. An ignore bin is a sign saying "not our department": matching values are thrown away and the pigeonhole does not exist for the score. An illegal bin is an alarm: a value that must never be posted has arrived, and the simulator reports an error. A default bin is the floor pile, which nobody counts.
Without explicit bins, the tool creates automatic bins: one per value for small types, or values grouped into at most auto_bin_max (64 by default) buckets for wide ones. A 1-bit coverpoint gets two automatic bins, auto[0] and auto[1]. Explicit bins are how you make the buckets match plan rows.
The order matters. A value listed in both an illegal bin and a regular bin is illegal. A value listed in both an ignore bin and a regular bin is ignored. Neither illegal nor ignore bins appear in the coverage denominator.
// 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;
endgroupThe boundaries come from the spec, not from round numbers. Zero is the special case where DONE is set at GO; one is a single-beat job; 16 is the largest job the random constraint creates; 17 and up are legal (LEN is 16 bits) but never random.
`[17:$]`: $ in a range means the largest value of the coverpoint's type, here 65535.
The names avoid keywords. The bench first named bins small and large, which are SystemVerilog keywords (charge strengths), and it failed to compile. So do matches and program.
`cp_irq` has no bins list, so its 1-bit value gets auto[0] and auto[1].
// snippet: from dma_coverage in the course DMA bench
covergroup status_cg with function sample(bit done, bit err); option.per_instance = 1;
cp_outcome: coverpoint {err, done} {
bins done_only = {2'b01};
bins err_only = {2'b10};
bins both = {2'b11}; // legal: ERR survives GO unless cleared
ignore_bins idle = {2'b00}; // a read before the job ends says nothing
}
endgroupThe coverpoint is the concatenation {err, done}, so ERR is the high bit: 2'b01 is DONE alone, 2'b10 is ERR alone. The four kinds of bin behave like this:
Kind | In the denominator? | When a value matches | Use it for |
|---|---|---|---|
bins | Yes | Hit count + 1 | Plan items |
ignore_bins | No | Dropped silently | Legal values the plan excludes on purpose, such as an idle STATUS read |
illegal_bins | No | Simulator run-time error | Values the spec forbids |
default | No, and it cannot be crossed | Dropped from the score | Rarely: it hides values instead of measuring them |
The illegal-bin error comes from the simulator's coverage engine, not from UVM. It does not pass through the report server, so a run can end with UVM_ERROR : 0 printed and an illegal-bin error further up the log. A regression must catch simulator errors (log scan or exit status), and the real check for a forbidden state belongs in an assertion or the scoreboard. Treat an illegal bin as a second tripwire, never as the only one.
// snippet: from dma_coverage in the course DMA bench
covergroup clear_cg with function sample(bit [1:0] w1c); option.per_instance = 1;
cp_clear: coverpoint w1c {
bins clear_done = {2'b01};
bins clear_err = {2'b10};
bins clear_both = {2'b11};
}
endgroupclear_both can be hit only when DONE and ERR were both set before the write. Keep that in mind for the war story below.
A wildcard bin treats ? (and x or z) in its values as "either". The keyword is what turns those characters into don't-cares. A view of the full 3-bit STATUS read might look like this (not in the bench):
// snippet: a wildcard view of STATUS {ERR, DONE, BUSY}
cp_status: coverpoint rdata[2:0] {
wildcard bins busy = {3'b??1}; // BUSY, whatever DONE and ERR say
wildcard bins ended_err = {3'b1?0}; // ERR set, not busy
}A transition bin such as (2'b10 => 2'b11) matches when two consecutive samples of the same coverpoint have those values. status_cg is sampled only on STATUS reads that show an outcome, so consecutive samples are whole jobs apart: a transition bin there would mean "a DONE read, then an ERR read some jobs later", not a DUT state change. A real transition needs a sample on every clock, which means a covergroup in the interface (this one is not in the bench):
// snippet: inside axis_if, sampled on every clock out of reset
covergroup hs_cg @(posedge clk iff rst_n);
cp_hs: coverpoint {tvalid, tready} {
bins idle = {2'b00, 2'b01};
bins stall = {2'b10};
bins xfer = {2'b11};
bins stall_xfer = (2'b10 => 2'b11); // a stall, then the transfer
bins long_stall = (2'b10 [*3:16] => 2'b11); // 3 to 16 stalled clocks first
bins b2b = (2'b11 => 2'b11); // back-to-back transfers
}
endgroup
hs_cg hs = new();On the input side of the current bench, b2b would stay empty for the same reason gap=0 does: the driver never holds TVALID across two beats.
A scratch test (not part of the course bench) runs a job with LEN = 8 and a 3-word packet, so the DMA ends it early with ERR, and deliberately does not clear ERR. It then starts a 4-word job with IRQ_EN off and polls STATUS until DONE. Scratch run, Verilator:
+UVM_TESTNAME=dma_both_bits_test (scratch run)
UVM_INFO closure_tb.sv(81) @ 315000: uvm_test_top [BOTH] job 1 ended early: STATUS=00000004, ERR not cleared
UVM_INFO closure_tb.sv(88) @ 685000: uvm_test_top [BOTH] job 2 done: STATUS=00000006
UVM_INFO dma_tb.sv(411) @ 825000: uvm_test_top.env.sb [SB] 7 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0STATUS=00000004 after job 1: ERR alone, as the early TLAST demands.
STATUS=00000006 after job 2: DONE and ERR. GO clears DONE but never ERR, exactly as the spec says.
7 words matched: the data path was right throughout. With the earlier illegal_bins both, a commercial simulator turned this final read into a coverage error.
The comment says "the DMA never reports both". What is true is that the bench's tests never let it: run_job writes 0x6 after every job, so ERR is always cleared before the next GO. The illegal bin encoded a habit of the testbench as a property of the DUT. The first test that skipped the clear, which is precisely the spec's "ERR survives GO" row, would fail the regression for a design that did nothing wrong, and the natural reaction, deleting the line, would leave that spec row with no coverage at all.
The fix is a decision, not an edit. Find the spec sentence that forbids the value. If there is one, keep the illegal bin and add the matching assertion. If there is not (as here), make it a regular bin tied to its plan row, bins done_and_err = {2'b11}; // ERR survives GO, and write the test that hits it. The course bench made exactly this change: status_cg now reads bins both = {2'b11}; // legal: ERR survives GO unless cleared.
status_cg had no ignore_bins idle line, would its coverage percentage change?No. With explicit bins, a value that matches no bin is simply not counted, so 2'b00 would fall through either way. The ignore bin documents a decision (idle reads are deliberately out of scope) and keeps that decision if someone later adds a catch-all bin. On an automatically binned coverpoint, ignore bins do change the bins that are created.
bins done_then_err = (2'b01 => 2'b10); to cp_outcome. What would a hit actually mean?That one outcome read showed DONE and the next outcome read showed ERR. status_cg is sampled only on reads that show an outcome, so the two samples belong to different jobs. It says nothing about the DUT's state machine. Transition bins need a coverpoint sampled every clock.
small and large to few and many?small and large are SystemVerilog keywords (charge strengths for trireg nets), so using them as bin names is a compile error. matches and program are keywords too.
Next: single coverpoints say which values occurred. Crosses and Plan Traceability asks which combinations occurred, and how every plan row finds its bins.