This page, part 2 and part 3 hold the complete source of the course's running example: the stream DMA design and its UVM testbench. Lessons show these files piece by piece; this is the version to copy.
The testbench file dma_tb.sv is 2030 lines, too long for one page, so it is listed in three parts. Part 1 (below, lines 1-840) is the base bench: the agents, scoreboard, env, sequences and the first tests. Part 2 (lines 841-1534) adds the reactive slave, reset handling, the interrupt agent, the subsystem pieces and coverage. Part 3 (lines 1535-2030) adds the predictor and comparators, the verdict server, waivers and watchdog, and ends with the top module. Every test needs all three.
Paste dma_dut.sv into the design.sv tab.
Paste Part 1 below into the testbench.sv tab, then paste Part 2 directly after it and Part 3 after that, all in the same tab. The three parts together are the one file dma_tb.sv.
Under Languages & Libraries, choose SystemVerilog/Verilog, UVM 1.2, and a simulator such as Aldec Riviera-PRO.
Set the top module to top if the tool asks, then Run. Add +UVM_TESTNAME=dma_smoke_test to the run options to pick a test explicitly.
A passing run ends with UVM_ERROR : 0 and UVM_FATAL : 0, and the scoreboard line 8 words matched, 0 mismatched.
The design file is shown in full on The DMA you will verify, with its register map and behaviour. Copy it from there.
// not-run: needs the UVM library. Paste into EDA Playground with UVM 1.2 selected. This is lines 1-840 of dma_tb.sv.
// dma_tb.sv - the complete base UVM testbench for dma_dut (UVM 1.2 / IEEE 1800.2).
// Compile with dma_dut.sv. Run: +UVM_TESTNAME=dma_smoke_test (the default).
`timescale 1ns/1ps
`include "uvm_macros.svh"
// ============================ interfaces ============================
interface apb_if (input logic clk, input logic rst_n);
logic [7:0] paddr;
logic psel, penable, pwrite;
logic [31:0] pwdata, prdata;
logic pready, pslverr;
// Monitors sample through mon_cb: #1step reads each signal just before the clock
// edge, which is exactly the value the DUT samples on that edge. Drivers drive with
// nonblocking assignments right after @(posedge clk) (see apb_driver).
clocking mon_cb @(posedge clk);
default input #1step;
input paddr, psel, penable, pwrite, pwdata, prdata, pready, pslverr;
endclocking
endinterface
interface axis_if (input logic clk, input logic rst_n);
logic [31:0] tdata;
logic tvalid, tlast, tready;
clocking mon_cb @(posedge clk);
default input #1step;
input tdata, tvalid, tlast, tready;
endclocking
endinterface
interface irq_if (input logic clk);
logic irq;
endinterface
// Reset as a testbench-controlled signal, so a test can reset the DUT mid-run.
interface rst_if (input logic clk);
logic rst_n;
endinterface
// ============================ package ============================
package dma_pkg;
import uvm_pkg::*;
// DMA register map, shared by sequences and the scoreboard.
localparam bit [7:0] CTRL = 8'h00, STATUS = 8'h04, LEN = 8'h08, KEY = 8'h0C, COUNT = 8'h10;
// ---------------------------------------------------------------- APB agent
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
endclass
typedef uvm_sequencer #(apb_item) apb_sequencer;
class apb_driver extends uvm_driver #(apb_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "apb_if not set for this driver")
endfunction
task run_phase(uvm_phase phase);
vif.psel <= 1'b0;
vif.penable <= 1'b0;
@(posedge vif.rst_n);
forever begin
seq_item_port.get_next_item(req);
drive(req);
seq_item_port.item_done(); // rdata travels back inside req itself
end
endtask
// Setup phase: PSEL=1, PENABLE=0 for one clock. Access phase: PENABLE=1, held
// until the slave answers PREADY=1; read data is valid in that cycle.
// Every wait is on the same event, @(posedge vif.clk), and every drive is a
// nonblocking assignment, so a value read right after the edge is the value
// from before it: the one the DUT sampled.
task drive(apb_item it);
@(posedge vif.clk);
vif.paddr <= it.addr;
vif.pwrite <= it.write;
vif.pwdata <= it.data;
vif.psel <= 1'b1;
vif.penable <= 1'b0;
@(posedge vif.clk);
vif.penable <= 1'b1;
do @(posedge vif.clk); while (!vif.pready);
it.rdata = vif.prdata;
it.slverr = vif.pslverr;
vif.psel <= 1'b0;
vif.penable <= 1'b0;
endtask
endclass
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
virtual apb_if vif;
uvm_analysis_port #(apb_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "apb_if not set for this monitor")
endfunction
// A transfer completes on the edge where PSEL, PENABLE and PREADY are all 1.
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready) begin
apb_item it = apb_item::type_id::create("it"); // a fresh object per transfer
it.addr = vif.mon_cb.paddr;
it.write = vif.mon_cb.pwrite;
it.data = vif.mon_cb.pwdata;
it.rdata = vif.mon_cb.prdata;
it.slverr = vif.mon_cb.pslverr;
`uvm_info("APB_MON", it.convert2string(), UVM_HIGH)
ap.write(it);
end
end
endtask
endclass
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_sequencer sqr;
apb_driver drv;
apb_monitor mon;
uvm_analysis_port #(apb_item) ap; // the agent's public output
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase); // reads is_active from config_db
mon = apb_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = apb_sequencer::type_id::create("sqr", this);
drv = apb_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
ap = mon.ap;
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
// One APB access. The test or a parent sequence fills addr/write/data.
class apb_rw_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(apb_rw_seq)
rand bit [7:0] addr;
rand bit write;
rand bit [31:0] data;
bit [31:0] rdata;
bit slverr;
function new(string name = "apb_rw_seq");
super.new(name);
endfunction
task body();
apb_item it = apb_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { addr == local::addr; write == local::write; data == local::data; })
`uvm_fatal("RAND", "apb_item randomize failed")
finish_item(it); // returns after the driver's item_done
rdata = it.rdata;
slverr = it.slverr;
endtask
endclass
// ---------------------------------------------------------------- AXI-Stream agent
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
endclass
typedef uvm_sequencer #(axis_item) axis_sequencer;
class axis_driver extends uvm_driver #(axis_item);
`uvm_component_utils(axis_driver)
virtual axis_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual axis_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "axis_if not set for this driver")
endfunction
task run_phase(uvm_phase phase);
vif.tvalid <= 1'b0;
@(posedge vif.rst_n);
forever begin
axis_item beat;
seq_item_port.get_next_item(req);
beat = pre_drive(req);
if (beat != null) begin
repeat (beat.idle) @(posedge vif.clk);
@(posedge vif.clk);
vif.tdata <= beat.data;
vif.tlast <= beat.last;
vif.tvalid <= 1'b1;
do @(posedge vif.clk); while (!vif.tready); // the beat moves on TVALID && TREADY
vif.tvalid <= 1'b0;
end
seq_item_port.item_done();
end
endtask
// Hook for derived drivers: return the beat to drive (the sequence's item, or a
// modified copy), or null to drop it. The base driver drives every item as is.
virtual function axis_item pre_drive(axis_item it);
return it;
endfunction
endclass
class axis_monitor extends uvm_monitor;
`uvm_component_utils(axis_monitor)
virtual axis_if vif;
uvm_analysis_port #(axis_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual axis_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "axis_if not set for this monitor")
endfunction
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (vif.mon_cb.tvalid && vif.mon_cb.tready) begin
axis_item it = axis_item::type_id::create("it");
it.data = vif.mon_cb.tdata;
it.last = vif.mon_cb.tlast;
`uvm_info("AXIS_MON", it.convert2string(), UVM_HIGH)
ap.write(it);
end
end
endtask
endclass
class axis_agent extends uvm_agent;
`uvm_component_utils(axis_agent)
axis_sequencer sqr;
axis_driver drv;
axis_monitor mon;
uvm_analysis_port #(axis_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = axis_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = axis_sequencer::type_id::create("sqr", this);
drv = axis_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
ap = mon.ap;
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
// A packet of n words; the last word carries TLAST.
class axis_pkt_seq extends uvm_sequence #(axis_item);
`uvm_object_utils(axis_pkt_seq)
rand int unsigned n;
constraint n_c { n inside {[1:16]}; }
function new(string name = "axis_pkt_seq");
super.new(name);
endfunction
task body();
for (int i = 0; i < n; i++) begin
axis_item it = axis_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { last == (i == n - 1); }) // i and n are not item fields
`uvm_fatal("RAND", "axis_item randomize failed")
finish_item(it);
end
endtask
endclass
// ---------------------------------------------------------------- scoreboard
`uvm_analysis_imp_decl(_apb)
`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)
// Predicts every output word from the input word and the KEY register the
// DUT was programmed with, then compares in order. The write_* methods are
// virtual so a derived scoreboard (a factory override) can extend them.
class dma_scoreboard extends uvm_scoreboard;
`uvm_component_utils(dma_scoreboard)
uvm_analysis_imp_apb #(apb_item, dma_scoreboard) apb_export;
uvm_analysis_imp_in #(axis_item, dma_scoreboard) in_export;
uvm_analysis_imp_out #(axis_item, dma_scoreboard) out_export;
bit [31:0] key;
bit [31:0] expected[$];
int unsigned n_match, n_mismatch;
function new(string name, uvm_component parent);
super.new(name, parent);
apb_export = new("apb_export", this);
in_export = new("in_export", this);
out_export = new("out_export", this);
endfunction
virtual function void write_apb(apb_item t);
if (t.write && !t.slverr && t.addr == KEY) key = t.data;
endfunction
virtual function void write_in(axis_item t);
expected.push_back(t.data ^ key);
endfunction
virtual function void write_out(axis_item t);
bit [31:0] exp;
if (expected.size() == 0) begin
`uvm_error("SB_UNEXP", $sformatf("output word %08h with nothing expected", t.data))
n_mismatch++;
return;
end
exp = expected.pop_front();
if (t.data !== exp) begin
`uvm_error("SB_MISMATCH", $sformatf("expected %08h, got %08h", exp, t.data))
n_mismatch++;
end else begin
n_match++;
end
endfunction
function void check_phase(uvm_phase phase);
if (expected.size() != 0)
`uvm_error("SB_LEFTOVER", $sformatf("%0d expected word(s) never came out", expected.size()))
if (n_match == 0)
`uvm_error("SB_EMPTY", "no words were compared: the test checked nothing")
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf("%0d words matched, %0d mismatched", n_match, n_mismatch), UVM_LOW)
endfunction
endclass
// ---------------------------------------------------------------- env
class dma_env extends uvm_env;
`uvm_component_utils(dma_env)
apb_agent apb;
axis_agent in_agt, out_agt;
dma_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// The output side is observed only: the DUT drives it.
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
apb = apb_agent::type_id::create("apb", this);
in_agt = axis_agent::type_id::create("in_agt", this);
out_agt = axis_agent::type_id::create("out_agt", this);
sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
apb.ap.connect(sb.apb_export);
in_agt.ap.connect(sb.in_export);
out_agt.ap.connect(sb.out_export);
endfunction
endclass
// ---------------------------------------------------------------- job configuration
// One DMA job as a randomizable object. A test can randomize it, or put one in
// config_db so a test further down the hierarchy picks it up.
class dma_job_cfg extends uvm_object;
rand bit [31:0] key;
rand int unsigned len;
rand bit irq_en;
constraint len_c { len inside {[1:16]}; }
constraint irq_c { soft irq_en == 1; }
`uvm_object_utils(dma_job_cfg)
function new(string name = "dma_job_cfg");
super.new(name);
endfunction
function string convert2string();
return $sformatf("key=%08h len=%0d irq_en=%0b", key, len, irq_en);
endfunction
endclass
// ---------------------------------------------------------------- variants used by factory overrides
// A scoreboard that owns end of test. It learns how many words each job owes
// from the observed LEN and GO register writes, and when every objection drops
// while words are still owed, it raises its own objection until they arrive.
// (Counting on the expected queue alone is not enough: the last input word may
// not have been published yet when the test drops its objection.)
class dma_scoreboard_eot extends dma_scoreboard;
`uvm_component_utils(dma_scoreboard_eot)
int unsigned len_reg, owed;
event drained;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void write_apb(apb_item t);
super.write_apb(t);
if (!t.write || t.slverr) return;
if (t.addr == LEN) len_reg = t.data[15:0];
if (t.addr == CTRL && t.data[0]) owed += len_reg; // GO: this job owes len_reg words
endfunction
virtual function void write_out(axis_item t);
super.write_out(t);
if (owed > 0) owed--;
if (owed == 0) -> drained;
endfunction
function void phase_ready_to_end(uvm_phase phase);
if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;
phase.raise_objection(this, $sformatf("%0d word(s) still owed by the DMA", owed));
fork begin
@drained;
phase.drop_objection(this, "all words out");
end join_none
endfunction
endclass
// Fault injection without touching the sequence: cuts every packet after beat
// `cut` by setting TLAST there and dropping the rest of the packet.
class axis_truncating_driver extends axis_driver;
`uvm_component_utils(axis_truncating_driver)
int unsigned cut = 3; // index of the beat that gets TLAST
int unsigned beat_no;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function axis_item pre_drive(axis_item it);
axis_item b;
int unsigned i = beat_no;
beat_no = it.last ? 0 : beat_no + 1; // count beats per packet
if (i > cut) return null; // after the cut: drop the beat
if (i < cut) return it;
$cast(b, it.clone()); // never edit the sequence's own item
b.last = 1'b1;
`uvm_info("TRUNC", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
return b;
endfunction
endclass
// ---------------------------------------------------------------- virtual sequences
// Reads COUNT every 50 ns until told to stop: background register traffic that
// keeps the APB sequencer busy, the way firmware polling would.
class apb_poll_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(apb_poll_seq)
bit stop;
int unsigned polls;
function new(string name = "apb_poll_seq");
super.new(name);
endfunction
task body();
while (!stop) begin
apb_item it = apb_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { addr == COUNT; write == 0; })
`uvm_fatal("RAND", "apb_item randomize failed")
finish_item(it);
polls++;
#50ns;
end
endtask
endclass
// The interrupt handler: grab the APB sequencer so the handler's accesses go
// ahead of any queued polling, read STATUS, clear DONE/ERR, then let go.
class dma_irq_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(dma_irq_seq)
bit [31:0] status;
function new(string name = "dma_irq_seq");
super.new(name);
endfunction
task access(bit [7:0] addr, bit wr, bit [31:0] data, output bit [31:0] rdata);
apb_item it = apb_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { addr == local::addr; write == wr; data == local::data; })
`uvm_fatal("RAND", "apb_item randomize failed")
finish_item(it);
rdata = it.rdata;
endtask
task body();
bit [31:0] unused;
grab(); // jump the queue, hold the sequencer
`uvm_info("IRQ", "handler has the APB sequencer", UVM_MEDIUM)
access(STATUS, 0, 0, status);
access(STATUS, 1, 32'h6, unused); // write 1 to clear DONE and ERR
ungrab();
endtask
endclass
// One whole DMA job across two agents. It has no sequencer of its own: the
// test hands it the two agent sequencers and the interrupt interface, and
// starts it with start(null).
class dma_vseq extends uvm_sequence;
`uvm_object_utils(dma_vseq)
apb_sequencer apb_sqr;
axis_sequencer axis_sqr;
virtual irq_if irq_vif;
rand bit [31:0] key;
rand int unsigned len;
constraint len_c { len inside {[2:16]}; }
bit [31:0] status; // STATUS as read by the interrupt handler
function new(string name = "dma_vseq");
super.new(name);
endfunction
task write_reg(bit [7:0] addr, bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 1; s.data = data;
s.start(apb_sqr, this); // this vseq is the parent
endtask
task body();
apb_poll_seq poll = apb_poll_seq::type_id::create("poll");
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
dma_irq_seq isr = dma_irq_seq::type_id::create("isr");
if (apb_sqr == null || axis_sqr == null)
`uvm_fatal("VSEQ", "set apb_sqr and axis_sqr before starting")
write_reg(KEY, key);
write_reg(LEN, len);
write_reg(CTRL, 32'h3); // IRQ_EN | GO
if (!pkt.randomize() with { n == len; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
fork
poll.start(apb_sqr, this); // background polling
pkt.start(axis_sqr, this); // the data, in parallel
begin // the interrupt handler
wait (irq_vif.irq === 1'b1);
isr.start(apb_sqr, this);
status = isr.status;
poll.stop = 1;
end
join
`uvm_info("VSEQ", $sformatf("job done: STATUS=%08h after %0d polls", status, poll.polls), UVM_LOW)
endtask
endclass
// ---------------------------------------------------------------- tests
class dma_base_test extends uvm_test;
`uvm_component_utils(dma_base_test)
dma_env env;
virtual irq_if irq_vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = dma_env::type_id::create("env", this);
if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
`uvm_fatal("NOVIF", "irq_if not set for the test")
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
uvm_root::get().print_topology();
endfunction
// Register helpers used by every test.
task reg_write(bit [7:0] addr, bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 1; s.data = data;
s.start(env.apb.sqr);
endtask
task reg_read(bit [7:0] addr, output bit [31:0] data);
apb_rw_seq s = apb_rw_seq::type_id::create("s");
s.addr = addr; s.write = 0; s.data = 0;
s.start(env.apb.sqr);
data = s.rdata;
endtask
// One complete job: program the DMA, stream n words, wait for the interrupt,
// return STATUS and clear its DONE/ERR bits.
task run_job(dma_job_cfg job, int unsigned n_words, output bit [31:0] status);
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
reg_write(KEY, job.key);
reg_write(LEN, job.len);
reg_write(CTRL, {30'b0, job.irq_en, 1'b1});
if (!pkt.randomize() with { n == n_words; })
`uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
reg_read(STATUS, status);
reg_write(STATUS, 32'h6); // write 1 to clear DONE and ERR
endtask
endclass
// Program KEY and LEN, start a job with the interrupt enabled, stream one
// packet of LEN words, wait for the interrupt, then check and clear STATUS.
class dma_smoke_test extends dma_base_test;
`uvm_component_utils(dma_smoke_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
bit [31:0] status, key = 32'hA5A5_0F0F;
int unsigned n_words = 8;
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this, "smoke test running");
reg_write(KEY, key);
reg_write(LEN, n_words);
reg_write(CTRL, 32'h3); // IRQ_EN | GO
if (!pkt.randomize() with { n == n_words; })
`uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
reg_read(STATUS, status);
if (status[1] !== 1'b1) `uvm_error("STATUS", $sformatf("DONE not set: STATUS=%08h", status))
reg_write(STATUS, 32'h2); // write 1 to clear DONE
reg_read(STATUS, status);
if (status[2:0] !== 3'b000) `uvm_error("STATUS", $sformatf("STATUS not clear: %08h", status))
#100ns; // let the last output word drain
phase.drop_objection(this, "smoke test done");
endtask
endclass
// Several random jobs. A job config placed in config_db by a derived test or the
// command line wins; otherwise each job is randomized here.
class dma_rand_test extends dma_base_test;
`uvm_component_utils(dma_rand_test)
int unsigned n_jobs = 3;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
dma_job_cfg job;
bit [31:0] status;
phase.raise_objection(this);
for (int j = 0; j < n_jobs; j++) begin
if (!uvm_config_db#(dma_job_cfg)::get(this, "", "job", job)) begin
job = dma_job_cfg::type_id::create($sformatf("job%0d", j));
if (!job.randomize()) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
end
`uvm_info("JOB", job.convert2string(), UVM_LOW)
run_job(job, job.len, status);
if (status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("job %0d: STATUS=%08h, expected DONE only", j, status))
end
#100ns;
phase.drop_objection(this);
endtask
endclass
// Factory override: the stream driver cuts each packet short, so the DMA must
// stop early and report ERR. No sequence or env file changes.
class dma_truncate_test extends dma_base_test;
`uvm_component_utils(dma_truncate_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
axis_driver::type_id::set_type_override(axis_truncating_driver::get_type());
super.build_phase(phase); // the env is created after the override
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, 8, status);
if (status[2] !== 1'b1) `uvm_error("STATUS", $sformatf("ERR not set after a short packet: STATUS=%08h", status))
else `uvm_info("TRUNC", "DMA reported ERR for the short packet, as expected", UVM_LOW)
#100ns;
phase.drop_objection(this);
endtask
endclass
// The test drops its objection as soon as the last input word is accepted, with
// no settling delay. The end-of-test scoreboard keeps the run phase open until
// the last output word arrives.
class dma_drain_test extends dma_base_test;
`uvm_component_utils(dma_drain_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
dma_scoreboard::type_id::set_type_override(dma_scoreboard_eot::get_type());
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this);
reg_write(KEY, 32'h1234_5678);
reg_write(LEN, 6);
reg_write(CTRL, 32'h1); // GO, interrupt not needed
if (!pkt.randomize() with { n == 6; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
phase.drop_objection(this); // the last word is still inside the DMA
endtask
endclass
// A test that would hang: the interrupt is never enabled, so it never fires.
// The global timeout ends the run with a fatal instead of burning the regression slot.
class dma_timeout_test extends dma_base_test;
`uvm_component_utils(dma_timeout_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_root::get().set_timeout(20us, 1); // replaces a +UVM_TIMEOUT=...,YES value; ,NO locks the command line
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 == 4; irq_en == 0; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 4, status); // waits for an interrupt that never comes
phase.drop_objection(this);
endtask
endclass
// A virtual sequence drives a whole job across both agents, with background
// polling and a grab-based interrupt handler on the APB sequencer.
class dma_vseq_test extends dma_base_test;
`uvm_component_utils(dma_vseq_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
dma_vseq vseq = dma_vseq::type_id::create("vseq");
phase.raise_objection(this);
vseq.apb_sqr = env.apb.sqr;
vseq.axis_sqr = env.in_agt.sqr;
vseq.irq_vif = irq_vif;
if (!vseq.randomize()) `uvm_fatal("RAND", "dma_vseq randomize failed")
vseq.start(null);
if (vseq.status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("expected DONE only, STATUS=%08h", vseq.status))
#100ns;
phase.drop_objection(this);
endtask
endclassContinue with part 2. The walk-through of every part of the base bench is in Your first UVM testbench.