To truly understand UVM, you need to see how all the components work together. Below is a complete, end-to-end example for a Generic Memory Interface.
Example Specification:
- Interface: 8-bit Address, 8-bit Data, Write Enable.
- DUT: A simple synchronous memory.
- Testbench: Full UVM architecture including Agent, Scoreboard, and Environment.
1. The Design (DUT) & Interface
The DUT is a simple memory, and the interface defines the signals and clocking blocks used for synchronization.
// Simple Memory DUT
module memory_dut (
input logic clk, rst_n,
input logic [7:0] addr,
input logic wr_en,
input logic [7:0] wdata,
output logic [7:0] rdata
);
logic [7:0] mem [255:0];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rdata <= 8'h00;
else if (wr_en) mem[addr] <= wdata;
else rdata <= mem[addr];
end
endmodule
// UVM Interface
interface mem_if (input logic clk, rst_n);
logic [7:0] addr, wdata, rdata;
logic wr_en;
clocking drv_cb @(posedge clk);
default input #1ns output #1ns;
output addr, wr_en, wdata;
input rdata;
endclocking
clocking mon_cb @(posedge clk);
default input #1ns output #1ns;
input addr, wr_en, wdata, rdata;
endclocking
endinterface
2. Sequence Item & Sequence
The sequence item defines the data packet (transaction), and the sequence generates a stream of randomized transactions.
// Transaction Class
class mem_item extends uvm_sequence_item;
rand logic [7:0] addr;
rand logic wr_en;
rand logic [7:0] wdata;
logic [7:0] rdata; // Sampled, not randomized
`uvm_object_utils_begin(mem_item)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(wr_en, UVM_ALL_ON)
`uvm_field_int(wdata, UVM_ALL_ON)
`uvm_field_int(rdata, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "mem_item");
super.new(name);
endfunction
endclass
// Stimulus Sequence
class mem_sequence extends uvm_sequence #(mem_item);
`uvm_object_utils(mem_sequence)
virtual task body();
repeat(10) begin
req = mem_item::type_id::create("req");
start_item(req);
if (!req.randomize()) `uvm_error("SEQ", "Randomization failed")
finish_item(req);
end
endtask
endclass
3. Agent, Driver & Monitor
The Agent encapsulates the Sequencer, Driver, and Monitor. The Driver converts transactions to pin-level activity, while the Monitor samples it back.
// Sequencer Typedef
typedef uvm_sequencer #(mem_item) mem_sequencer;
// Driver: Pin-level Handshake
class mem_driver extends uvm_driver #(mem_item);
`uvm_component_utils(mem_driver)
virtual mem_if vif;
function void build_phase(uvm_phase phase);
if (!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
`uvm_fatal("DRV", "Virtual interface not found")
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
drive_item(req);
seq_item_port.item_done();
end
endtask
task drive_item(mem_item item);
@(vif.drv_cb);
vif.drv_cb.addr <= item.addr;
vif.drv_cb.wr_en <= item.wr_en;
vif.drv_cb.wdata <= item.wdata;
if (!item.wr_en) begin
@(vif.drv_cb);
item.rdata = vif.drv_cb.rdata;
end
endtask
endclass
// Monitor: Sampling & Analysis
class mem_monitor extends uvm_monitor;
`uvm_component_utils(mem_monitor)
virtual mem_if vif;
uvm_analysis_port #(mem_item) mon_ap;
function new(string name, uvm_component parent);
super.new(name, parent);
mon_ap = new("mon_ap", this);
endfunction
function void build_phase(uvm_phase phase);
if (!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
`uvm_fatal("MON", "Virtual interface not found")
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
mem_item item = mem_item::type_id::create("item");
@(vif.mon_cb);
item.addr = vif.mon_cb.addr;
item.wr_en = vif.mon_cb.wr_en;
item.wdata = vif.mon_cb.wdata;
if (!vif.mon_cb.wr_en) begin
@(vif.mon_cb);
item.rdata = vif.mon_cb.rdata;
end
mon_ap.write(item);
end
endtask
endclass
// Agent: Encapsulating components
class mem_agent extends uvm_agent;
`uvm_component_utils(mem_agent)
mem_sequencer sqr;
mem_driver drv;
mem_monitor monitor;
function void build_phase(uvm_phase phase);
sqr = mem_sequencer::type_id::create("sqr", this);
drv = mem_driver::type_id::create("drv", this);
monitor = mem_monitor::type_id::create("monitor", this);
endfunction
function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
4. Environment & Scoreboard
The Environment instantiates and connects components like the Agent and Scoreboard using TLM ports.
// Simple Scoreboard
class mem_scoreboard extends uvm_scoreboard;
`uvm_component_utils(mem_scoreboard)
uvm_analysis_imp #(mem_item, mem_scoreboard) mon_export;
logic [7:0] model_mem [255:0];
function new(string name, uvm_component parent);
super.new(name, parent);
mon_export = new("mon_export", this);
endfunction
function void write(mem_item item);
if (item.wr_en) model_mem[item.addr] = item.wdata;
else if (model_mem[item.addr] !== item.rdata)
`uvm_error("SCB", $sformatf("Mismatch! Addr:%0h Exp:%0h Got:%0h",
item.addr, model_mem[item.addr], item.rdata))
endfunction
endclass
// Environment
class mem_env extends uvm_env;
`uvm_component_utils(mem_env)
mem_agent agent;
mem_scoreboard sb;
function void build_phase(uvm_phase phase);
agent = mem_agent::type_id::create("agent", this);
sb = mem_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
agent.monitor.mon_ap.connect(sb.mon_export);
endfunction
endclass
5. Test & Top (The Harness)
The Test starts the sequence, and the Top module connects the virtual interface and starts the simulation.
// UVM Test
class mem_test extends uvm_test;
`uvm_component_utils(mem_test)
mem_env env;
task run_phase(uvm_phase phase);
mem_sequence seq = mem_sequence::type_id::create("seq");
phase.raise_objection(this);
seq.start(env.agent.sqr);
phase.drop_objection(this);
endtask
endclass
// Top Level Module
module top;
bit clk, rst_n;
always #5 clk = ~clk;
mem_if intf(clk, rst_n);
memory_dut dut (.clk(clk), .rst_n(rst_n), .addr(intf.addr),
.wr_en(intf.wr_en), .wdata(intf.wdata), .rdata(intf.rdata));
initial begin
uvm_config_db#(virtual mem_if)::set(null, "*", "vif", intf);
run_test("mem_test");
end
endmodule