This page holds the complete register-layer file used throughout this section. Lessons show it piece by piece; this is the version to copy.
On EDA Playground: dma_dut.sv in the design tab, dma_tb.sv in the testbench tab, and dma_ral_tb.sv added as a second testbench file with the + button. dma_tb.sv comes from the DMA bench source (its three parts pasted together), and dma_ral_tb.sv is listed below.
Select UVM 1.2 and a simulator such as Aldec Riviera-PRO, make dma_ral_tb.sv the file that is compiled last (it includes dma_tb.sv itself), and set the top to top.
Pick a test with +UVM_TESTNAME=: dma_ral_job_test (the default), dma_ral_reset_test, dma_ral_bitbash_test, or dma_ral_busy_len_test (which ends with one UVM_ERROR on purpose).
Locally: UVM_HOME=<uvm>/src scripts/tools/uvm-bench/run.sh scripts/data/uvm-course/bench/dma_ral_tb.sv +UVM_TESTNAME=dma_ral_job_test.
// not-run: needs the UVM library. Paste into EDA Playground with UVM 1.2 selected.
// dma_ral_tb.sv - the DMA's register layer: a register model, an APB adapter, an
// explicit predictor and a user-defined backdoor. Compile with dma_dut.sv; this
// file includes dma_tb.sv (keep both files side by side) and replaces its top.
// Run: +UVM_TESTNAME=dma_ral_job_test (or dma_ral_reset_test, dma_ral_bitbash_test,
// dma_ral_busy_len_test).
`define DMA_NO_TOP
`include "dma_tb.sv"
// A read-only window into the DMA's internal registers, bound into every
// dma_dut. The register model's backdoor reads through it.
interface dma_bd_if (input logic [31:0] key, input logic [15:0] len, input logic [15:0] count);
endinterface
bind dma_dut dma_bd_if bd (.key(key), .len(len), .count(count));
package dma_ral_pkg;
import uvm_pkg::*;
import dma_pkg::*;
// ---------------------------------------------------------------- registers
// CTRL @0x00: GO is a trigger (write 1 to start, always reads 0), IRQ_EN is RW.
class dma_ctrl_reg extends uvm_reg;
`uvm_object_utils(dma_ctrl_reg)
uvm_reg_field go;
rand uvm_reg_field irq_en;
function new(string name = "dma_ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
go = uvm_reg_field::type_id::create("go");
irq_en = uvm_reg_field::type_id::create("irq_en");
// parent size lsb access volatile reset has_reset is_rand indiv
go.configure (this, 1, 0, "WO", 0, 1'b0, 1, 0, 0);
irq_en.configure(this, 1, 1, "RW", 0, 1'b0, 1, 1, 0);
endfunction
endclass
// STATUS @0x04: BUSY is read-only; DONE and ERR are set by the hardware and
// cleared by writing 1. All three change without a bus write, so all are volatile.
class dma_status_reg extends uvm_reg;
`uvm_object_utils(dma_status_reg)
uvm_reg_field busy, done, err;
function new(string name = "dma_status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
busy = uvm_reg_field::type_id::create("busy");
done = uvm_reg_field::type_id::create("done");
err = uvm_reg_field::type_id::create("err");
busy.configure(this, 1, 0, "RO", 1, 1'b0, 1, 0, 0);
done.configure(this, 1, 1, "W1C", 1, 1'b0, 1, 0, 0);
err.configure (this, 1, 2, "W1C", 1, 1'b0, 1, 0, 0);
endfunction
endclass
// LEN @0x08 and KEY @0x0C: plain RW registers (the DMA ignores writes to them
// while BUSY - see dma_ral_busy_len_test).
class dma_len_reg extends uvm_reg;
`uvm_object_utils(dma_len_reg)
rand uvm_reg_field value;
function new(string name = "dma_len_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
value = uvm_reg_field::type_id::create("value");
value.configure(this, 16, 0, "RW", 0, 16'h0, 1, 1, 0);
endfunction
endclass
class dma_key_reg extends uvm_reg;
`uvm_object_utils(dma_key_reg)
rand uvm_reg_field value;
function new(string name = "dma_key_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
value = uvm_reg_field::type_id::create("value");
value.configure(this, 32, 0, "RW", 0, 32'h0, 1, 1, 0);
endfunction
endclass
// COUNT @0x10: read-only, counts as the job runs, so volatile.
class dma_count_reg extends uvm_reg;
`uvm_object_utils(dma_count_reg)
uvm_reg_field value;
function new(string name = "dma_count_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
value = uvm_reg_field::type_id::create("value");
value.configure(this, 16, 0, "RO", 1, 16'h0, 1, 0, 0);
endfunction
endclass
// ---------------------------------------------------------------- block
class dma_reg_block extends uvm_reg_block;
`uvm_object_utils(dma_reg_block)
rand dma_ctrl_reg ctrl;
dma_status_reg status;
rand dma_len_reg len;
rand dma_key_reg key;
dma_count_reg count;
uvm_reg_map map;
function new(string name = "dma_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
ctrl = dma_ctrl_reg::type_id::create("ctrl");
status = dma_status_reg::type_id::create("status");
len = dma_len_reg::type_id::create("len");
key = dma_key_reg::type_id::create("key");
count = dma_count_reg::type_id::create("count");
ctrl.configure(this); ctrl.build();
status.configure(this); status.build();
len.configure(this); len.build();
key.configure(this); key.build();
count.configure(this); count.build();
// One APB map: base 0, 4-byte bus, little endian.
map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
map.add_reg(ctrl, 'h00, "RW");
map.add_reg(status, 'h04, "RW");
map.add_reg(len, 'h08, "RW");
map.add_reg(key, 'h0C, "RW");
map.add_reg(count, 'h10, "RO");
lock_model();
endfunction
endclass
// ---------------------------------------------------------------- adapter
// Translates between register operations and APB items. The APB driver writes
// read data and PSLVERR back into the request item, so there is no separate
// response: provides_responses = 0.
class apb_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(apb_reg_adapter)
function new(string name = "apb_reg_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 0;
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_item it = apb_item::type_id::create("it");
it.addr = rw.addr;
it.write = (rw.kind == UVM_WRITE) || (rw.kind == UVM_BURST_WRITE);
it.data = rw.data;
return it;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_item it;
if (!$cast(it, bus_item))
`uvm_fatal("NOT_APB", "bus2reg got an item that is not an apb_item")
rw.kind = it.write ? UVM_WRITE : UVM_READ;
rw.addr = it.addr;
rw.data = it.write ? it.data : it.rdata;
rw.status = it.slverr ? UVM_NOT_OK : UVM_IS_OK; // a bus error is a status, not a fatal
endfunction
endclass
// ---------------------------------------------------------------- backdoor
// A user-defined backdoor: reads KEY, LEN and COUNT straight from the DUT
// through the bound dma_bd_if, with no bus traffic and no simulation time.
// It needs no HDL-path DPI, so it works on every simulator. Writes are refused.
class dma_reg_backdoor extends uvm_reg_backdoor;
virtual dma_bd_if vif;
function new(string name = "dma_reg_backdoor");
super.new(name);
endfunction
virtual function void read_func(uvm_reg_item rw);
uvm_reg r;
if (!$cast(r, rw.element)) begin
rw.status = UVM_NOT_OK;
return;
end
rw.status = UVM_IS_OK;
case (r.get_name())
"key": rw.value[0] = vif.key;
"len": rw.value[0] = vif.len;
"count": rw.value[0] = vif.count;
default: rw.status = UVM_NOT_OK;
endcase
endfunction
virtual task write(uvm_reg_item rw);
`uvm_warning("BD_RO", "this backdoor is read-only")
rw.status = UVM_NOT_OK;
endtask
endclass
// ---------------------------------------------------------------- env
// The block env plus the register layer: the model, the adapter on the APB
// sequencer, and an explicit predictor fed by the APB monitor.
class dma_ral_env extends dma_env;
`uvm_component_utils(dma_ral_env)
dma_reg_block regmodel;
apb_reg_adapter adapter;
uvm_reg_predictor #(apb_item) predictor;
dma_reg_backdoor bd;
virtual dma_bd_if bd_vif;
bit auto_predict; // config_db knob, default 0
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db#(bit)::get(this, "", "auto_predict", auto_predict));
regmodel = dma_reg_block::type_id::create("regmodel");
regmodel.build();
adapter = apb_reg_adapter::type_id::create("adapter");
predictor = uvm_reg_predictor #(apb_item)::type_id::create("predictor", this);
if (!uvm_config_db#(virtual dma_bd_if)::get(this, "", "bd_vif", bd_vif))
`uvm_fatal("NOVIF", "dma_bd_if not set for the register env")
bd = new("bd");
bd.vif = bd_vif;
regmodel.key.set_backdoor(bd);
regmodel.len.set_backdoor(bd);
regmodel.count.set_backdoor(bd);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
regmodel.map.set_sequencer(apb.sqr, adapter);
// The explicit predictor updates the mirror from what the APB monitor sees.
// That happens when the monitor publishes the transfer - in this bench a
// moment after write()/read() return, in the same time step. Code that
// reads the mirror the instant an access returns (uvm_reg_bit_bash_seq's
// rg.get(), mirror()'s expected value) then sees the old value. Tests
// that need it turn on auto_predict as well.
regmodel.map.set_auto_predict(auto_predict);
predictor.map = regmodel.map;
predictor.adapter = adapter;
apb.ap.connect(predictor.bus_in);
endfunction
endclass
// ---------------------------------------------------------------- tests
class dma_ral_base_test extends dma_base_test;
`uvm_component_utils(dma_ral_base_test)
dma_ral_env renv;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
dma_env::type_id::set_type_override(dma_ral_env::get_type());
super.build_phase(phase);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (!$cast(renv, env)) `uvm_fatal("CAST", "env is not a dma_ral_env")
endfunction
// Register-only tests move no data, so the scoreboard's "nothing compared"
// check does not apply to them.
function void waive_empty_scoreboard();
env.sb.set_report_severity_id_override(UVM_ERROR, "SB_EMPTY", UVM_INFO);
endfunction
endclass
// Every register's reset value, read through the bus and checked against the model.
class dma_ral_reset_test extends dma_ral_base_test;
`uvm_component_utils(dma_ral_reset_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
waive_empty_scoreboard();
endfunction
task run_phase(uvm_phase phase);
uvm_reg_hw_reset_seq seq = uvm_reg_hw_reset_seq::type_id::create("seq");
phase.raise_objection(this);
seq.model = renv.regmodel;
seq.start(null);
`uvm_info("RAL", "reset values checked (registers whose fields are all volatile are skipped)", UVM_LOW)
phase.drop_objection(this);
endtask
endclass
// Walk a 1 and a 0 through every bit of the RW registers. CTRL and STATUS are
// excluded: writing CTRL.GO would start a job, and STATUS bits are W1C.
class dma_ral_bitbash_test extends dma_ral_base_test;
`uvm_component_utils(dma_ral_bitbash_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
uvm_config_db#(bit)::set(this, "env", "auto_predict", 1); // bit-bash reads rg.get() right after write()
super.build_phase(phase);
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
waive_empty_scoreboard();
endfunction
task run_phase(uvm_phase phase);
uvm_reg_bit_bash_seq seq = uvm_reg_bit_bash_seq::type_id::create("seq");
phase.raise_objection(this);
uvm_resource_db#(bit)::set({"REG::", renv.regmodel.ctrl.get_full_name()}, "NO_REG_TESTS", 1, this);
uvm_resource_db#(bit)::set({"REG::", renv.regmodel.status.get_full_name()}, "NO_REG_TESTS", 1, this);
seq.model = renv.regmodel;
seq.start(null);
`uvm_info("RAL", "bit-bash finished for LEN, KEY and COUNT (errors, if any, are reported above)", UVM_LOW)
phase.drop_objection(this);
endtask
endclass
// A whole DMA job driven through the register model: write by name, read
// STATUS and COUNT, clear DONE, and compare the mirror with a backdoor peek.
class dma_ral_job_test extends dma_ral_base_test;
`uvm_component_utils(dma_ral_job_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
uvm_status_e st;
uvm_reg_data_t v, peeked, exp_key;
dma_reg_block rm = renv.regmodel;
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this);
rm.key.write(st, 32'hCAFE_F00D);
rm.len.write(st, 6);
// GO is a trigger: write CTRL directly. update() would skip it on the next
// job, because the mirror already holds GO = 1 from this write.
rm.ctrl.write(st, 32'h3);
if (!pkt.randomize() with { n == 6; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
rm.status.read(st, v);
`uvm_info("RAL", $sformatf("STATUS=%0h: done=%0b err=%0b", v, rm.status.done.get_mirrored_value(),
rm.status.err.get_mirrored_value()), UVM_LOW)
rm.count.read(st, v);
if (v != 6) `uvm_error("RAL", $sformatf("COUNT=%0d, expected 6", v))
rm.status.write(st, 32'h2); // W1C: clear DONE
rm.status.read(st, v);
if (v[2:0] != 3'b000) `uvm_error("RAL", $sformatf("STATUS not clear after W1C: %0h", v))
exp_key = rm.key.get_mirrored_value(); // expectation first: peek() overwrites the mirror
rm.key.peek(st, peeked); // backdoor: no bus, no time
`uvm_info("RAL", $sformatf("KEY mirror=%0h, backdoor peek=%0h, status=%s",
exp_key, peeked, st.name()), UVM_LOW)
if (st != UVM_IS_OK || peeked != exp_key) `uvm_error("RAL", "KEY mirror and DUT disagree")
#100ns;
phase.drop_objection(this);
endtask
endclass
// The model meets a DUT rule it does not know: the DMA ignores writes to LEN
// while BUSY. The write goes out on the bus, the predictor updates the mirror,
// and a mirror(UVM_CHECK) read then reports the DUT's real value as a mismatch.
// This test is expected to end with one UVM_ERROR from the register model.
class dma_ral_busy_len_test extends dma_ral_base_test;
`uvm_component_utils(dma_ral_busy_len_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
uvm_config_db#(bit)::set(this, "env", "auto_predict", 1); // mirror() takes its expected value at once
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
uvm_status_e st;
uvm_reg_data_t peeked;
dma_reg_block rm = renv.regmodel;
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this);
rm.key.write(st, 32'h0000_00FF);
rm.len.write(st, 4);
rm.ctrl.write(st, 32'h3); // the DMA is now BUSY
rm.len.write(st, 9); // ignored by the DMA, believed by the mirror
`uvm_info("RAL", $sformatf("LEN mirror after the write: %0d", rm.len.get_mirrored_value()), UVM_LOW)
rm.len.mirror(st, UVM_CHECK); // reads 4, expects 9: RegModel error
// peek() after the check, not before: a peek predicts the value it reads,
// so peeking first would quietly set the mirror to 4 and hide the mismatch.
rm.len.peek(st, peeked);
`uvm_info("RAL", $sformatf("DUT LEN (backdoor peek)=%0d, mirror now %0d", peeked, rm.len.get_mirrored_value()), UVM_LOW)
if (!pkt.randomize() with { n == 4; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
rm.status.write(st, 32'h2);
#100ns;
phase.drop_objection(this);
endtask
endclass
endpackage
// ============================ top ============================
module top;
import uvm_pkg::*;
import dma_pkg::*;
import dma_ral_pkg::*;
logic clk = 0;
always #5 clk = ~clk;
rst_if rst (clk);
wire rst_n = rst.rst_n;
initial begin
rst.rst_n = 1'b0;
repeat (4) @(posedge clk);
rst.rst_n <= 1'b1;
end
apb_if apb (clk, rst_n);
axis_if s_ax (clk, rst_n);
axis_if m_ax (clk, rst_n);
irq_if irqi (clk);
initial m_ax.tready = 1'b1;
dma_dut dut (
.clk, .rst_n,
.paddr(apb.paddr), .psel(apb.psel), .penable(apb.penable), .pwrite(apb.pwrite),
.pwdata(apb.pwdata), .prdata(apb.prdata), .pready(apb.pready), .pslverr(apb.pslverr),
.s_tdata(s_ax.tdata), .s_tvalid(s_ax.tvalid), .s_tlast(s_ax.tlast), .s_tready(s_ax.tready),
.m_tdata(m_ax.tdata), .m_tvalid(m_ax.tvalid), .m_tlast(m_ax.tlast), .m_tready(m_ax.tready),
.irq(irqi.irq)
);
initial begin
uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.apb.*", "vif", apb);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.in_agt.*", "vif", s_ax);
uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.out_agt.*", "vif", m_ax);
uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top", "irq_vif", irqi);
uvm_config_db#(virtual dma_bd_if)::set(null, "uvm_test_top.env", "bd_vif", dut.bd);
run_test("dma_ral_job_test");
end
endmoduleStart with Why a register model for the walk-through.