The test plan has two error rows for the DMA. An APB access to an unmapped address must answer with PSLVERR. A packet that ends with TLAST before LEN words must end the job early, set STATUS.ERR and raise the interrupt. A colleague has already started a new driver class with an inject_error knob. Before anyone writes it, ask the question that saves most error-injection code: can an ordinary, legal sequence item already ask for this fault?
Faults come in two kinds. Some are legal transactions with a wrong meaning: a perfectly formed APB write to an address the DUT does not decode, a perfectly formed stream packet that is shorter than the job. The bus protocol is obeyed; it is the content that breaks the DUT's rules. Others are protocol violations: TVALID dropped before TREADY, PENABLE without a setup phase, X on the data lines. No legal item can express those, because the driver's job is to obey the protocol.
Think of a restaurant. Ordering a dish that is not on the menu is a legal request the kitchen must handle politely. The waiter dropping the plate on the way out is a failure of the delivery itself. The first needs nothing but a different order (a sequence). Only the second needs a different waiter (a driver hook, override or callback), which is the next lesson.
The DMA decodes five registers, 0x00 to 0x10, and answers any other address with PSLVERR in the access phase. The bench's apb_rw_seq already returns slverr from the driver, so the fault is one sequence call with a different address:
// snippet: a test step, using the bench's apb_rw_seq
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = 8'h14; s.write = 1; s.data = 32'h1234_5678; // 0x14 is not decoded
s.start(env.apb.sqr);
if (s.slverr !== 1'b1) `uvm_error("SLVERR", "unmapped write was accepted")Notice what the bench's scoreboard already does with it. write_apb updates its KEY only for t.write && !t.slverr && t.addr == KEY. A write the DUT rejected must not change the scoreboard's model of the DUT, so error responses are part of normal checking, not a special case bolted on later.
The stream fault needs no new class either. The bench's run_job helper programs LEN from the job object and sends however many words it is told. Program LEN = 8, send 4, and the fourth word carries TLAST, because axis_pkt_seq always puts TLAST on its last word:
// snippet: a short-packet test, no override needed
class dma_short_pkt_test extends dma_base_test;
`uvm_component_utils(dma_short_pkt_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
dma_job_cfg job = dma_job_cfg::type_id::create("job");
bit [31:0] status;
phase.raise_objection(this);
if (!job.randomize() with { len == 8; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 4, status); // LEN=8, but the packet ends after 4 words
if (status[2:1] !== 2'b10) `uvm_error("STATUS", $sformatf("expected ERR only, STATUS=%08h", status))
#100ns;
phase.drop_objection(this);
endtask
endclassrun_job also clears STATUS with a write of 0x6 after reading it, which matters more than it looks; the war story below is about forgetting it.
It helps to know exactly what the spec promises, because every one of these is something to check:
Effect | Where you see it |
|---|---|
The word with early TLAST is still moved, XORed with KEY, and sent out with TLAST | Output monitor: a 4-word packet; the data scoreboard still matches |
BUSY falls and ERR is set; DONE stays clear | STATUS read = 0x4 |
The interrupt rises if IRQ_EN is set | irq line, or the irq agent's event |
COUNT shows the words actually moved | COUNT read = 4 |
ERR stays set until software writes 1 to bit 2 | irq stays high until the clear |
Some faults are legal in the item format but must be chosen per transaction by the sequence, randomly, across a whole regression. Then the intent travels as an item or sequence field with a soft default of "no fault", and a fault test turns it on with an inline constraint. Unconstrained, a random fault field would make most traffic faulty, so the default must be explicit:
// snippet: fault intent as a sequence field with a clean default
class dma_err_pkt_seq extends axis_pkt_seq;
`uvm_object_utils(dma_err_pkt_seq)
rand int unsigned early_last; // index of the beat that carries TLAST
constraint c_clean { soft early_last == n - 1; } // default: TLAST on the last word
constraint c_range { early_last < n; }
function new(string name = "dma_err_pkt_seq");
super.new(name);
endfunction
task body();
for (int i = 0; i <= early_last; i++) begin
axis_item it = axis_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { last == (i == early_last); })
`uvm_fatal("RAND", "axis_item randomize failed")
finish_item(it);
end
endtask
endclass
// A fault test: if (!pkt.randomize() with { n == 8; early_last == 2; }) ...Because it extends axis_pkt_seq, a test can also swap it in for every packet sequence in the bench with a factory override, which is taught in Overriding items and sequences.
A scratch run of the short-packet test, with a write to the unmapped address 0x14 first and a scoreboard that predicts ERR (built in the last lesson of this topic):
UVM_INFO scratch_tb.sv(412) @ 75000: uvm_test_top [SLVERR] write to unmapped 0x14: slverr=1
UVM_INFO scratch_tb.sv(415) @ 425000: uvm_test_top [SHORT] LEN=8, 4 words sent: STATUS=00000004
UVM_INFO dma_tb.sv(411) @ 525000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
UVM_INFO scratch_tb.sv(368) @ 525000: uvm_test_top.env.sb [SB_ERR] 1 job(s) ended in ERR as predicted, 0 in DONE as predicted
UVM_ERROR : 0slverr=1: the DUT rejected the unmapped write, and the scoreboard's KEY did not change.
STATUS=00000004: ERR set, DONE clear, BUSY clear. The job ended early as the spec says.
4 words matched: the data path still behaved correctly for the words it was given. Data checking and error checking are separate questions.
A team wrote its short-packet test by copying a clean test and deleting what looked like noise, including the STATUS write after the read. The short-packet job passed. The next job in the same test failed with an ERR status on a perfectly good packet, and sometimes, depending on the seed, it passed while reading STATUS before the job had even finished.
The DMA's interrupt is IRQ_EN & (DONE | ERR), and ERR is write-1-to-clear. Nobody cleared it, so the interrupt line simply stayed high. When the next job started, wait (irq_vif.irq === 1'b1) returned at once, the test read STATUS in the middle of the job, and it saw the stale ERR from the job before. The fix is one line, the W1C write of the bits that were read. The lesson is wider: an injected fault leaves state behind, and a fault test owns cleaning it up, or checking that the DUT's own recovery did.
Yes, when existing sequences must stay unchanged: a regression variant that cuts every packet of every sequence in the library, without editing or re-randomizing any of them. That is what the next lesson's axis_truncating_driver does. For a single directed test, the short packet is simpler and needs no new class.
dma_err_pkt_seq has rand int unsigned early_last with no constraints at all. What happens in a regression that randomizes it?Almost every packet would be faulty or out of range: an unconstrained 32-bit value is nearly always far beyond the packet length, and the loop would try to send billions of beats. The c_range constraint keeps it inside the packet and the soft c_clean constraint keeps packets clean unless a test asks otherwise.
0x2 clears DONE, which was not set; ERR stays set. The interrupt stays high, so the next job's wait for the interrupt returns at once, before the job has finished. Write back the bits you read (0x4 here) or 0x6 to clear both.
Next: a sequence can create a short packet, but it cannot cut short the packets of forty existing sequences without touching them. Driver injection hooks does that from the driver.