A register sequence works perfectly on the block bench. At subsystem level the APB clock runs four times slower, and the same sequence starts reading STATUS before the DMA has even seen the GO bit. Nobody changed the sequence. It had a #40ns between the write and the read, because on the block bench that was four clocks. At the new clock it is one.
The bug is not the number. It is that a decision about time on the pins was made in the sequence, which cannot know what the pins are doing. This lesson draws the line between what a transaction says and how the driver makes it happen, using the two items of the DMA bench.
Think of a restaurant. The waiter writes an order slip: "steak, medium, no salt, after the starter". The slip says what is wanted and in what order. It never says how hot the grill is or how long to sear each side; that is the cook's job, and a different kitchen may do it differently and still serve the same dish.
A sequence item (a class extending uvm_sequence_item) is that slip. It describes one operation on an interface: the address, the data, read or write. A driver is the cook. It owns every clock edge, every handshake wait and every pin. The split is what lets one sequence run unchanged on a block bench, a subsystem bench and an emulator, and what lets one driver serve every sequence that anyone writes for that interface.
The line is not "no timing information in items, ever". The senior version is: intent may live in the item; mechanics live in the driver. "Wait three idle clocks before this beat" is intent, expressed in units of the protocol. "#30ns" or "@(posedge vif.clk)" is mechanics, and it belongs in the driver.
Item | Field | rand? | Written by | Read by |
|---|---|---|---|---|
apb_item | addr, write, data | yes | the sequence, by | the driver, to drive PADDR, PWRITE, PWDATA |
apb_item | rdata, slverr | no | the driver, in the cycle PREADY is high | the sequence, after |
axis_item | data, last | yes | the sequence | the driver; the monitor fills them from the pins |
axis_item | idle | yes | the sequence | the driver only; no monitor can see it |
Read the last column. Every field that a driver reads came from the sequence; every field the sequence reads back came from the driver. Nothing in either item points at an interface, a component or a clock.
// snippet: from the course DMA bench
class apb_item extends uvm_sequence_item;
rand bit [7:0] addr;
rand bit write;
rand bit [31:0] data; // write data
bit [31:0] rdata; // filled by the driver on reads
bit slverr; // filled by the driver
constraint word_aligned_c { addr[1:0] == 2'b00; }
`uvm_object_utils(apb_item)
function new(string name = "apb_item");
super.new(name);
endfunction
function string convert2string();
return write ? $sformatf("WR addr=%02h data=%08h%s", addr, data, slverr ? " SLVERR" : "")
: $sformatf("RD addr=%02h rdata=%08h%s", addr, rdata, slverr ? " SLVERR" : "");
endfunction
function void do_copy(uvm_object rhs);
apb_item r;
super.do_copy(rhs);
$cast(r, rhs);
addr = r.addr; write = r.write; data = r.data; rdata = r.rdata; slverr = r.slverr;
endfunction
endclassThree fields describe the request and are rand, so a sequence can randomize them. Two fields carry the answer and are not rand: the driver writes rdata and slverr when the slave completes the access, and the sequence reads them once finish_item returns. Keeping the answer in the request object is the simplest response scheme there is, and it is why the bench's driver calls item_done() with no argument (the driver handshake lesson explains the consequence).
word_aligned_c is a legality constraint: no access on this bus may be misaligned, so the rule belongs to the item and every randomize call obeys it. Test intent, such as "write KEY", does not belong here; it arrives as an in-line constraint from the sequence. The item also writes convert2string and do_copy by hand; Objects that copy, compare and print compares that with field macros.
// snippet: from the course DMA bench
class axis_item extends uvm_sequence_item;
rand bit [31:0] data;
rand bit last;
rand int unsigned idle; // clocks of TVALID=0 before this beat (driver only)
constraint idle_c { idle dist { 0 := 6, [1:3] := 3, [4:8] := 1 }; }
`uvm_object_utils(axis_item)
function new(string name = "axis_item");
super.new(name);
endfunction
function string convert2string();
return $sformatf("data=%08h%s", data, last ? " LAST" : "");
endfunction
function void do_copy(uvm_object rhs);
axis_item r;
super.do_copy(rhs);
$cast(r, rhs);
data = r.data; last = r.last; idle = r.idle;
endfunction
endclassdata and last are the payload of one stream beat. idle is different: it is a control knob, the number of clocks the driver holds TVALID low before offering this beat. It is randomized like everything else, with a distribution that makes most beats back-to-back and a few late, which is what shakes out a design that only works when data arrives smoothly. The driver implements it in two lines, counting edges of whatever clock its interface has:
// snippet: inside axis_driver::run_phase
repeat (beat.idle) @(posedge vif.clk); // intent from the item, clocks from the interface
@(posedge vif.clk);
vif.tvalid <= 1'b1;Because idle counts clocks, it means the same thing at 100 MHz and at 25 MHz. A #30ns field would not.
Virtual interface handles, events on pins, `@` or `#` statements. An item is data. Code that waits belongs in the driver.
References to components or sequencers. The item travels through whatever sequencer it is started on; it must not care which.
Handshake state, such as "waiting for PREADY". That is the driver's loop, not the transaction.
Absolute time. Express delays in protocol units (clocks, beats) and let the driver convert.
A quick review check sorts every field. Ask: could a passive monitor fill this field from the pins alone? The APB monitor can fill all five apb_item fields, so all five are payload and a scoreboard may compare them. The stream monitor fills data and last but never idle, because idle clocks leave no trace on TDATA. That makes idle a knob: useful to stimulus, invisible to checking.
Knobs are legitimate, and most production VIP has them (delays, error-injection flags, retry counts). The rule is that they must not take part in comparisons. With field macros you add UVM_NOCOMPARE to the knob; with a hand-written do_compare you leave it out. The bench sidesteps the question entirely: its scoreboard compares observed words, not items, so a knob can never cause a false mismatch.
Run dma_smoke_test with +UVM_VERBOSITY=UVM_HIGH and the monitors print every item they rebuild. An excerpt from a Verilator run (times in ps; the data values depend on the simulator and seed):
UVM_INFO dma_tb.sv(151) @ 75000: uvm_test_top.env.apb.mon [APB_MON] WR addr=0c data=a5a50f0f
UVM_INFO dma_tb.sv(151) @ 115000: uvm_test_top.env.apb.mon [APB_MON] WR addr=08 data=00000008
UVM_INFO dma_tb.sv(151) @ 155000: uvm_test_top.env.apb.mon [APB_MON] WR addr=00 data=00000003
UVM_INFO dma_tb.sv(299) @ 205000: uvm_test_top.env.in_agt.mon [AXIS_MON] data=1364cd0b
UVM_INFO dma_tb.sv(299) @ 255000: uvm_test_top.env.in_agt.mon [AXIS_MON] data=7980e079
UVM_INFO dma_tb.sv(299) @ 275000: uvm_test_top.env.in_agt.mon [AXIS_MON] data=cbccc8de
...
UVM_INFO dma_tb.sv(151) @ 475000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000002The three APB writes (KEY, LEN, CTRL) are 40 ns apart: the driver's setup clock, the access clock, one wait state and the clock it waits before starting. The sequence asked for none of that timing.
The input beats arrive 50 ns, then 20 ns apart. Those gaps are the idle knob at work; the monitor's items carry only data and last, as the monitor test predicts.
The read of STATUS returns 00000002 (DONE). The same value travelled back to the test inside the request object.
Back to the opening. The sequence wrote CTRL with GO, waited #40ns, then read STATUS once and treated BUSY == 0 as "job finished". On the 100 MHz block bench the DMA had raised BUSY long before the read. On the subsystem bench, with a slower APB clock and the bus behind a bridge, the read overtook the GO: STATUS said idle, the sequence moved on, and the test checked a job that had not started. It passed for a week, because the scoreboard happened to see the data later in the run.
The fix had two parts. The wait became a condition, not a duration: poll STATUS until DONE or ERR, with a bounded count. And where a real gap was needed, it became a knob in protocol units, like idle, that the driver turns into clocks. Rule of thumb for reviews: if a sequence contains # followed by a number, ask what condition it is standing in for.
rand int unsigned delay_ns to apb_item, and the driver does #(req.delay_ns * 1ns) before each transfer. What do you ask them to change?Express the knob in clocks (idle_clks) and have the driver count edges of its own interface clock, as axis_driver does with idle. A delay in nanoseconds means a different number of cycles on every clock frequency and lands between edges, so it can shift a drive off the edge the protocol expects. Also make sure the knob is excluded from any compare.
rdata and slverr not declared rand in apb_item?They are answers, not requests. If they were rand, every randomize() would fill them with random values, and a driver bug that forgot to write them would hand the sequence a plausible-looking random read value instead of an obvious zero. Non-random response fields also keep the constraint solver from wasting effort on them.
compare() between the axis_item objects a sequence sent and the ones the input monitor saw. Half the compares fail although the data is right. Why, and what is the fix?The monitor never sets idle, so it stays 0 in observed items while the sent items carry random idle counts. idle is a knob, so it must be excluded from comparison (UVM_NOCOMPARE with field macros, or left out of do_compare). Better still, compare observed input against observed output, as the bench does, rather than trusting the sequence's items.
Next: the item says what is legal; the sequence says what it wants this time. Constraints and randomize() with shows how the two combine, and the one-word mistake that turns a directed register write into a random one.