The DMA's regression has been green for a month: smoke, random and truncate tests, hundreds of seeds, zero errors. Then a firmware engineer starts a job with LEN = 0 by accident, and the interrupt never comes on the FPGA prototype. Nobody had ever run that case. The random test's job length is constrained to 1–16, so zero was never generated, and nothing measured that it had not been. Every test passed; the plan had a hole. How do you make holes like that visible before someone else finds them?
A testplan (or verification plan) lists every feature of the design that must be verified, and for each one it answers three questions. How do we make it happen? That is stimulus. How do we know it went right? That is the check. How do we know it actually happened? That is coverage. Leave out any leg and the feature is not verified, just like a three-legged stool with one leg missing is not a stool.
Each missing leg fails differently. Stimulus without a check is the green-but-checking-nothing test: traffic flows and nothing judges it. A check without coverage is the LEN = 0 story: the scoreboard would have caught a bug, but nothing ever drove the case. Coverage without a check is the most dangerous of all: the coverage report says 100 percent, so everyone believes the feature is verified, but no check would have noticed a wrong result.
The rule in the bottom half of the picture matters as much as the three legs. Both the check and the coverage read monitored traffic. Coverage sampled from the sequence's items proves only that the stimulus was generated; if the driver dropped an item, or the DUT never accepted it, intent-based coverage still counts it.
The DMA you will verify lists the design's promises. Here are three of them as testplan rows, with the UVM part that implements each leg. Give every row a stable ID: tests, checks and coverage bins refer to it, and the closure report is organised by it.
ID | Feature | Stimulus | Check | Coverage |
|---|---|---|---|---|
F1 | Every word comes out as |
|
| LEN ranges; KEY all-zeros and all-ones |
F4 | Early TLAST ends the job with ERR |
| Test reads STATUS.ERR; scoreboard matches the words that moved | Cut position: first, middle, last-but-one |
F7 | LEN = 0 finishes at once | none yet | none yet | none yet |
Row F7 is the hook's hole, and writing it down is what exposes it. The plan has a row; the bench has nothing for it. That is the real value of a plan: it turns "we never thought of that" into an empty cell that someone has to fill or explicitly waive.
The job length comes from dma_job_cfg, whose constraint is len inside {[1:16]}. That constraint is exactly why LEN = 0 was never generated. Constraints should describe what is legal for the DUT, and LEN = 0 is legal; the 1–16 range was a convenience for short simulations, which quietly became a hole. A better version keeps short jobs common and makes zero likely enough to happen:
// snippet: a job-length distribution that covers the edges
constraint len_c {
len dist { 0 := 1, [1:16] := 8, [17:255] := 1 };
}The bench's scoreboard is the check for F1, and Your first UVM testbench walks through it. The rule it follows is the one to remember: expected values are predicted from the input-side monitor (and the observed KEY write), and compared against the output-side monitor. Predicting from the output, or from the sequence's intent, makes the check circular or blind to register-path bugs.
A covergroup in a subscriber on the APB monitor's stream measures what lengths the DUT was really programmed with. Each bin carries its plan ID in a comment or name, so the closure report maps back to rows.
// not-run: covergroups are not run in the course's Verilator flow
class dma_len_cov extends uvm_subscriber #(apb_item);
`uvm_component_utils(dma_len_cov)
covergroup cg with function sample(bit [15:0] len);
cp_len: coverpoint len {
bins len_zero = {0}; // F7
bins len_one = {1}; // F1 edge
bins len_short = {[2:16]}; // F1
bins len_long = {[17:65535]}; // F1
}
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg = new();
endfunction
function void write(apb_item t);
if (t.write && !t.slverr && t.addr == 8'h08) cg.sample(t.data[15:0]); // LEN writes
endfunction
endclassEvery bin is explicit and bounded, so 100 percent means each named case was seen. An open-ended bin such as {[17:$]} would hide whether the maximum was ever programmed.
Before any coverage tool, the log of dma_rand_test already tells you something. From the course's run (the random values 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 mismatchedThe check leg worked: 14 words (10 + 1 + 3) were predicted and matched, and nothing was left over.
The stimulus leg was narrow: three lengths, all in 1–16, and irq_en was 1 every time (the config's soft constraint). No run with this constraint can ever produce LEN = 0 or a job without the interrupt.
The coverage leg would say so: with the covergroup above, len_zero and len_long stay at zero hits however many seeds you run. That is the signal to change the constraint or add a directed test.
The opposite failure is quieter and does more damage, and it is a pattern verification leads see again and again. A team inherits a block with 98 percent functional coverage and a long green history. A silicon bug turns up in a feature whose bins were all hit. The scoreboard for that feature compares only the packet count, not the contents, because a previous engineer disabled the data compare while chasing an unrelated mismatch and never turned it back on. Coverage said every case had been exercised; nothing said every case had been checked.
Two habits prevent it. First, review checks with the same rigour as coverage: for every plan row, someone should be able to point at the line of code that would fail if the feature broke. Second, make checks prove they ran, the way the DMA scoreboard reports its match count and fails when nothing was compared. A disabled check that still prints "0 mismatched" is indistinguishable from a working one.
Some packets the sequence generated never came out of the DUT in that form: the truncating driver cut them, the DUT stopped early, or the test ended before they drained. The sequence-side number measures intent; the monitor-side number measures reality. Trust the monitor, and find out which lengths went missing and why.
No. Each row needs stimulus, a check and coverage, but one random test routinely exercises many rows, and one scoreboard checks many features at once. Directed tests are for rows random stimulus cannot reach efficiently, such as the set-beats-clear race on STATUS.
bins big = {[17:$]} and shows 100 percent. Was the maximum length ever programmed?Unknown. One hit anywhere from 17 to 65,535 fills the bin. If the maximum matters (and for a 16-bit counter it does: COUNT must not wrap), give it its own bin, such as bins len_max = {16'hFFFF};.
Next: you have the vocabulary and the plan. Your first UVM testbench puts all of it into one running bench and then breaks it.