Your test writer wants to say "run three DMA jobs: this key, these six words" and nothing else. Today that means knowing that KEY is at 0x0C, that LEN must be written before GO, that GO is bit 0 of CTRL, that each word is a stream beat with TLAST on the last, and that STATUS must be cleared afterwards. Every abstraction leak is a place a test can get the protocol wrong. How do you let tests speak in jobs while the bench still speaks APB and AXI-Stream?
Protocol layering stacks abstraction levels the way network stacks do. An upper-layer item describes an operation in the design's terms: a DMA job with a key and a payload. A lower layer is the real bus: APB accesses and stream beats. Between them sits a translator sequence: it pulls one upper item, produces the lower-layer items that implement it, and reports the upper item done when the lower work is complete.
Think of a simultaneous interpreter at a conference. The speaker (the upper-layer sequence) talks in whole sentences and waits for each to land; the interpreter (the translator) turns each sentence into the listeners' language, word by word. The speaker never learns the second language, and the listeners never hear the first. On the way back, a second interpreter (an upward monitor) listens to the bus and rebuilds the sentences, so checkers can work at the job level too.
The upper layer needs a sequencer of its own, because upper-layer sequences call start_item and finish_item like any other. That sequencer has no driver. The translator plays the driver's part: it calls get_next_item and item_done on the upper sequencer, so the rule that `item_done` is called exactly once per item applies to it too.
A job is a key and a payload. The length is the payload's size, so it can never disagree with the data. The upper sequencer is a plain uvm_sequencer parameterized by the job item; the test builds it like any component.
// snippet
class dma_job_item extends uvm_sequence_item;
rand bit [31:0] key;
rand bit [31:0] payload[];
constraint len_c { payload.size() inside {[1:6]}; }
`uvm_object_utils(dma_job_item)
function new(string name = "dma_job_item");
super.new(name);
endfunction
function string convert2string();
return $sformatf("key=%08h len=%0d", key, payload.size());
endfunction
endclass
typedef uvm_sequencer #(dma_job_item) dma_job_sequencer;
class dma_job_seq extends uvm_sequence #(dma_job_item);
`uvm_object_utils(dma_job_seq)
rand int unsigned n_jobs;
constraint n_c { n_jobs inside {[2:3]}; }
function new(string name = "dma_job_seq");
super.new(name);
endfunction
task body();
repeat (n_jobs) begin
dma_job_item job = dma_job_item::type_id::create("job");
start_item(job);
if (!job.randomize()) `uvm_fatal("RAND", "dma_job_item randomize failed")
finish_item(job); // returns when the translator calls item_done
`uvm_info("JOB", {"upper layer: done ", job.convert2string()}, UVM_LOW)
end
endtask
endclassdma_job_seq is ordinary sequence code. Nothing in it mentions a register or a beat; a test writer can constrain keys and lengths without opening the DMA spec.
The translator is a virtual-style sequence: it starts lower-layer sequences on two sequencers and holds a handle to the upper sequencer it pulls from. It runs forever, one job per loop. The payload goes out through a small sequence that sends given words, since the bench's axis_pkt_seq randomizes its data.
// snippet
class dma_job_xlate_seq extends uvm_sequence;
`uvm_object_utils(dma_job_xlate_seq)
dma_job_sequencer up_sqr;
apb_sequencer apb_sqr;
axis_sequencer axis_sqr;
virtual irq_if irq_vif;
function new(string name = "dma_job_xlate_seq");
super.new(name);
endfunction
task reg_rw(bit [7:0] addr, bit wr, bit [31:0] data, output bit [31:0] rdata);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = wr; s.data = data;
s.start(apb_sqr, this);
rdata = s.rdata;
endtask
task body();
bit [31:0] status, unused;
forever begin
dma_job_item job;
axis_words_seq beats = axis_words_seq::type_id::create("beats");
up_sqr.get_next_item(job); // pull one upper-layer item
reg_rw(KEY, 1, job.key, unused);
reg_rw(LEN, 1, job.payload.size(), unused);
reg_rw(CTRL, 1, 32'h3, unused); // IRQ_EN | GO
foreach (job.payload[i]) beats.words.push_back(job.payload[i]);
beats.start(axis_sqr, this);
wait (irq_vif.irq === 1'b1);
reg_rw(STATUS, 0, 0, status);
reg_rw(STATUS, 1, 32'h6, unused); // write 1 to clear DONE and ERR
wait (irq_vif.irq === 1'b0);
`uvm_info("XLATE", $sformatf("%s -> 5 APB accesses + %0d beats, STATUS=%08h",
job.convert2string(), job.payload.size(), status), UVM_LOW)
up_sqr.item_done(); // the upper item is finished
end
endtask
endclass
class axis_words_seq extends uvm_sequence #(axis_item);
`uvm_object_utils(axis_words_seq)
bit [31:0] words[$];
function new(string name = "axis_words_seq");
super.new(name);
endfunction
task body();
foreach (words[i]) begin
axis_item it = axis_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { data == words[i]; last == (i == words.size() - 1); })
`uvm_fatal("RAND", "axis_item randomize failed")
finish_item(it);
end
endtask
endclassThree choices in it are deliberate. item_done comes last, after the interrupt is handled, so finish_item in the upper sequence returns only when the job has really finished; the upper layer sees end-to-end completion, not "the translator has started". Every lower-layer child is started with this as parent, so if the translator ever needs to lock the APB sequencer around a job, its children pass. And the translator waits for the interrupt to fall after clearing it, so the next job's wait cannot be satisfied by the last job's level.
The handle to up_sqr is the simplest wiring. Larger benches often put a uvm_seq_item_pull_port on a lower-layer sequencer subclass, connect it to the upper sequencer's seq_item_export, and have the translator call p_sequencer.up_port.get_next_item(); packaged with the upper sequencer and the job monitor, that is a layering agent. The mechanics are the same: the translator is the upper layer's driver.
Checking at the job level needs jobs rebuilt from the bus, never from the translator, for the same reason the bench's scoreboard predicts from monitors and not from sequences. dma_job_monitor subscribes to the APB and stream-in monitors, remembers KEY and LEN from observed writes, starts collecting at a GO write, and publishes a job when LEN words (or a TLAST) have gone in:
// snippet
`uvm_analysis_imp_decl(_jreg)
`uvm_analysis_imp_decl(_jdat)
class dma_job_monitor extends uvm_component;
`uvm_component_utils(dma_job_monitor)
uvm_analysis_imp_jreg #(apb_item, dma_job_monitor) apb_export;
uvm_analysis_imp_jdat #(axis_item, dma_job_monitor) in_export;
uvm_analysis_port #(dma_job_item) ap;
bit [31:0] key, len;
bit collecting;
bit [31:0] words[$];
function new(string name, uvm_component parent);
super.new(name, parent);
apb_export = new("apb_export", this);
in_export = new("in_export", this);
ap = new("ap", this);
endfunction
virtual function void write_jreg(apb_item t);
if (!t.write || t.slverr) return;
if (t.addr == KEY) key = t.data;
if (t.addr == LEN) len = t.data[15:0];
if (t.addr == CTRL && t.data[0]) begin collecting = 1; words.delete(); end
endfunction
virtual function void write_jdat(axis_item t);
dma_job_item job;
if (!collecting) return;
words.push_back(t.data);
if (words.size() == len || t.last) begin
job = dma_job_item::type_id::create("job"); // a fresh object per job
job.key = key;
job.payload = new[words.size()];
foreach (words[i]) job.payload[i] = words[i];
collecting = 0;
`uvm_info("JOBMON", {"rebuilt ", job.convert2string()}, UVM_LOW)
ap.write(job);
end
endfunction
endclassThe test builds job_sqr and job_mon in build_phase, connects env.apb.ap and env.in_agt.ap to the monitor in connect_phase, then in run_phase hands the translator its four handles, forks xlate.start(null) with join_none, and starts dma_job_seq on job_sqr. When the upper sequence returns, every job is done; the forever translator is simply left waiting on an empty upper sequencer when the objection drops.
A scratch run of the bench with these classes (dma_layer_test). The keys and lengths are random, so yours differ:
UVM_INFO dma_tb_scratch.sv(1124) @ 225000: uvm_test_top.job_mon [JOBMON] rebuilt key=429dc9e5 len=1
UVM_INFO dma_tb_scratch.sv(1084) @ 305000: reporter@@xlate [XLATE] key=429dc9e5 len=1 -> 5 APB accesses + 1 beats, STATUS=00000002
UVM_INFO dma_tb_scratch.sv(1031) @ 305000: uvm_test_top.job_sqr@@jobs [JOB] upper layer: done key=429dc9e5 len=1
UVM_INFO dma_tb_scratch.sv(1124) @ 515000: uvm_test_top.job_mon [JOBMON] rebuilt key=cf8ef02d len=2
UVM_INFO dma_tb_scratch.sv(1084) @ 595000: reporter@@xlate [XLATE] key=cf8ef02d len=2 -> 5 APB accesses + 2 beats, STATUS=00000002
UVM_INFO dma_tb_scratch.sv(1031) @ 595000: uvm_test_top.job_sqr@@jobs [JOB] upper layer: done key=cf8ef02d len=2
UVM_INFO dma_tb.sv(411) @ 695000: uvm_test_top.env.sb [SB] 3 words matched, 0 mismatched
UVM_ERROR : 0JOBMON before XLATE: the monitor rebuilt each job as soon as its last input word went in; the translator finished later, after the interrupt and the STATUS clear.
XLATE and JOB at the same time: the upper sequence's finish_item returned in the same timestep the translator called item_done.
3 words matched: the bench's scoreboard checked the data (1 + 2 words) without knowing that a layer above existed.
Move up_sqr.item_done() to just after beats.start(), "because the job has been sent", and nothing fails. That is the problem. The upper sequence's finish_item now returns as soon as the last word has entered the DMA, before the interrupt arrives and before STATUS is read. For the last job, the test's objection can drop while the translator is still waiting for the interrupt, and the run can end with that job's STATUS never read. An ERR on the final job would then pass silently, and the log would show JOB ... done lines that run ahead of the XLATE lines.
The general rule: the translator decides what done means for an upper item, and it should mean what a test writer assumes, which is that the operation is over and its outcome is known. Put item_done after the last thing that can fail.
get_response() after each finish_item, but the translator calls item_done() with no argument. What happens?The upper sequence blocks in get_response() forever, because no response is ever put. Either the upper sequence should not ask for responses (read results back from the item after finish_item, as the bench does), or the translator should call item_done(rsp) or put_response.
Yes: it publishes when it has LEN words or sees TLAST, so a truncated job comes out with fewer words than LEN. A checker comparing it with the intended job sees the mismatch. A monitor that waited only for LEN words would never publish, and would merge the next job's words into it.
Next: the translator pushes traffic into the DUT. Some interfaces work the other way round: the DUT asks, and the bench answers. Reactive (Slave) Sequences answers the DMA's output stream.