Every component of a production-ready UVM testbench assembled end-to-end — the GPIO DUT, APB interface, sequence item, driver, monitor, agent, scoreboard, coverage collector, register model, environment, virtual sequence, and tests. All 12 pieces wired together with every connection shown.
The DUT is a 32-bit GPIO block with four APB-mapped registers: DATA (0x00), DIR (0x04), IEN (0x08), ISR (0x0C). The interface wraps all APB signals and the GPIO data bus.
// ── APB interface ───────────────────────────────────────── interface apb_if(input logic PCLK, PRESETn); logic [11:0] PADDR; logic PSELx, PENABLE, PWRITE, PREADY, PSLVERR; logic [31:0] PWDATA, PRDATA; logic [3:0] PSTRB; logic [2:0] PPROT; endinterface
class apb_seq_item extends uvm_sequence_item; `uvm_object_utils(apb_seq_item) // Stimulus fields rand logic [11:0] addr; rand logic read_not_write; rand logic [31:0] write_data; rand logic [3:0] byte_en; rand logic [2:0] pprot; // Response fields (populated by driver) logic [31:0] read_data; logic error; constraint valid_addr_c { addr inside {12'h000, 12'h004, 12'h008, 12'h00C}; } constraint strobe_c { if (!read_not_write) byte_en != 4'b0000; } function new(string name = "apb_seq_item"); super.new(name); endfunction function void do_copy(uvm_object rhs); apb_seq_item rhs_; $cast(rhs_, rhs); super.do_copy(rhs); addr = rhs_.addr; read_not_write = rhs_.read_not_write; write_data = rhs_.write_data; byte_en = rhs_.byte_en; read_data = rhs_.read_data; error = rhs_.error; endfunction function bit do_compare(uvm_object rhs, uvm_comparer c); apb_seq_item rhs_; $cast(rhs_, rhs); return (addr == rhs_.addr && read_not_write == rhs_.read_not_write && byte_en == rhs_.byte_en && (read_not_write ? read_data == rhs_.read_data : write_data == rhs_.write_data)); endfunction function string convert2string(); return $sformatf("[%s @0x%03h data=0x%08h be=%04b err=%0b]", read_not_write ? "RD" : "WR", addr, read_not_write ? read_data : write_data, byte_en, error); endfunction endclass
class apb_driver extends uvm_driver #(apb_seq_item); `uvm_component_utils(apb_driver) virtual apb_if vif; function new(string n, uvm_component p); super.new(n,p); 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("DRV","No vif") endfunction task run_phase(uvm_phase phase); apb_seq_item req; vif.PSELx <= 0; vif.PENABLE <= 0; @(posedge vif.PRESETn); forever begin seq_item_port.get_next_item(req); drive_transfer(req); seq_item_port.item_done(); end endtask task drive_transfer(apb_seq_item req); @(posedge vif.PCLK); vif.PADDR <= req.addr; vif.PWDATA <= req.write_data; vif.PWRITE <= ~req.read_not_write; vif.PSTRB <= req.byte_en; vif.PPROT <= req.pprot; vif.PSELx <= 1; vif.PENABLE <= 0; @(posedge vif.PCLK); vif.PENABLE <= 1; do @(posedge vif.PCLK); while (!vif.PREADY); req.error = vif.PSLVERR; if (req.read_not_write) req.read_data = vif.PRDATA; vif.PSELx <= 0; vif.PENABLE <= 0; endtask endclass
class apb_monitor extends uvm_monitor; `uvm_component_utils(apb_monitor) virtual apb_if vif; uvm_analysis_port #(apb_seq_item) ap; function new(string n, uvm_component p); super.new(n,p); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); ap = new("ap",this); if (!uvm_config_db #(virtual apb_if)::get( this,"","vif",vif)) `uvm_fatal("MON","No vif") endfunction task run_phase(uvm_phase phase); @(posedge vif.PRESETn); forever collect_transfer(); endtask task collect_transfer(); apb_seq_item txn; @(posedge vif.PCLK); if (vif.PSELx && vif.PENABLE && vif.PREADY) begin txn = apb_seq_item::type_id::create("txn"); txn.addr = vif.PADDR; txn.read_not_write = ~vif.PWRITE; txn.byte_en = vif.PSTRB; txn.error = vif.PSLVERR; if (vif.PWRITE) txn.write_data = vif.PWDATA; else txn.read_data = vif.PRDATA; ap.write(txn); end endtask endclass
// ── Agent config ────────────────────────────────────────── class apb_agent_cfg extends uvm_object; `uvm_object_utils(apb_agent_cfg) virtual apb_if vif; uvm_active_passive_enum is_active = UVM_ACTIVE; bit has_coverage = 1; function new(string n="apb_agent_cfg"); super.new(n); endfunction endclass // ── Agent ───────────────────────────────────────────────── class apb_agent extends uvm_agent; `uvm_component_utils(apb_agent) apb_driver m_drv; apb_monitor m_mon; uvm_sequencer #(apb_seq_item) m_seqr; apb_agent_cfg m_cfg; uvm_analysis_port #(apb_seq_item) ap; // forwarded from monitor function new(string n, uvm_component p); super.new(n,p); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db #(apb_agent_cfg)::get( this,"","cfg",m_cfg)) `uvm_fatal("AGT","No cfg") ap = new("ap",this); m_mon = apb_monitor::type_id::create("m_mon",this); uvm_config_db #(virtual apb_if)::set( this,"*","vif",m_cfg.vif); if (m_cfg.is_active == UVM_ACTIVE) begin m_seqr = uvm_sequencer #(apb_seq_item)::type_id:: create("m_seqr",this); m_drv = apb_driver::type_id::create("m_drv",this); end endfunction function void connect_phase(uvm_phase phase); m_mon.ap.connect(this.ap); // forward monitor ap through agent boundary if (m_cfg.is_active == UVM_ACTIVE) m_drv.seq_item_port.connect(m_seqr.seq_item_export); endfunction endclass
`uvm_analysis_imp_decl(_expected) `uvm_analysis_imp_decl(_actual) class gpio_scoreboard extends uvm_scoreboard; `uvm_component_utils(gpio_scoreboard) uvm_analysis_imp_expected #(apb_seq_item, gpio_scoreboard) expected_export; uvm_analysis_imp_actual #(apb_seq_item, gpio_scoreboard) actual_export; tlm_analysis_fifo #(apb_seq_item) m_exp_fifo, m_act_fifo; int m_matches = 0, m_mismatches = 0; function new(string n, uvm_component p); super.new(n,p); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); expected_export = new("expected_export",this); actual_export = new("actual_export", this); m_exp_fifo = new("m_exp_fifo",this); m_act_fifo = new("m_act_fifo",this); endfunction function void write_expected(apb_seq_item t); m_exp_fifo.write(t); endfunction function void write_actual(apb_seq_item t); m_act_fifo.write(t); endfunction task run_phase(uvm_phase phase); apb_seq_item exp_t, act_t; forever begin m_exp_fifo.get(exp_t); m_act_fifo.get(act_t); if (exp_t.compare(act_t)) m_matches++; else begin m_mismatches++; `uvm_error("SB", $sformatf( "MISMATCH\n exp: %s\n act: %s", exp_t.convert2string(), act_t.convert2string())) end end endtask function void phase_ready_to_end(uvm_phase phase); if (phase.get_name() != "run") return; if (m_exp_fifo.size() > 0 || m_act_fifo.size() > 0) begin phase.raise_objection(this,"SB draining"); fork begin wait(m_exp_fifo.size()==0 && m_act_fifo.size()==0); phase.drop_objection(this); end join_none end endfunction function void report_phase(uvm_phase phase); `uvm_info("SB", $sformatf( "Result: %0d matches, %0d mismatches — %s", m_matches, m_mismatches, m_mismatches==0 ? "PASSED" : "FAILED"), UVM_LOW) endfunction endclass
class apb_cov_collector extends uvm_subscriber #(apb_seq_item); `uvm_component_utils(apb_cov_collector) apb_seq_item txn; covergroup apb_xfer_cg; cp_rw: coverpoint txn.read_not_write { bins rd={1}; bins wr={0}; } cp_addr: coverpoint txn.addr { bins data_reg={12'h000}; bins dir_reg={12'h004}; bins ien_reg={12'h008}; bins isr_reg={12'h00C}; } cp_err: coverpoint txn.error; cx_rw_addr: cross cp_rw, cp_addr; endgroup function new(string n, uvm_component p); super.new(n,p); apb_xfer_cg = new(); endfunction function void write(apb_seq_item t); txn = t; apb_xfer_cg.sample(); endfunction function void report_phase(uvm_phase phase); `uvm_info("COV", $sformatf( "APB coverage: %.1f%%", apb_xfer_cg.get_coverage()), UVM_LOW) endfunction endclass
The full register model is detailed in UVM-24 and UVM-25. Below is the adapter and the key build snippet for reference in the env wiring:
// ── APB adapter (full implementation in UVM-25) ─────────── class reg2apb_adapter extends uvm_reg_adapter; `uvm_object_utils(reg2apb_adapter) function new(string n="reg2apb_adapter"); super.new(n); supports_byte_enable = 0; provides_responses = 0; endfunction virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw); apb_seq_item apb = apb_seq_item::type_id::create("apb"); apb.read_not_write = (rw.kind == UVM_READ); apb.addr = rw.addr[11:0]; apb.write_data = rw.data; apb.byte_en = 4'b1111; return apb; endfunction virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw); apb_seq_item apb; $cast(apb, bus_item); rw.kind = apb.read_not_write ? UVM_READ : UVM_WRITE; rw.addr = apb.addr; rw.data = apb.read_data; rw.status = apb.error ? UVM_NOT_OK : UVM_IS_OK; endfunction endclass
class gpio_env_cfg extends uvm_object; `uvm_object_utils(gpio_env_cfg) apb_agent_cfg m_apb_cfg; gpio_reg_block gpio_rm; bit has_scoreboard = 1; bit has_coverage = 1; function new(string n="gpio_env_cfg"); super.new(n); endfunction endclass class gpio_env extends uvm_env; `uvm_component_utils(gpio_env) apb_agent m_agent; gpio_scoreboard m_sb; apb_cov_collector m_cov; gpio_predictor m_predictor; uvm_reg_predictor #(apb_seq_item) m_reg_predictor; reg2apb_adapter m_adapter; gpio_env_cfg m_cfg; function new(string n, uvm_component p); super.new(n,p); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db #(gpio_env_cfg)::get( this,"","cfg",m_cfg)) `uvm_fatal("ENV","No cfg") uvm_config_db #(apb_agent_cfg)::set( this,"m_agent","cfg",m_cfg.m_apb_cfg); uvm_config_db #(gpio_reg_block)::set( this,"*","gpio_rm",m_cfg.gpio_rm); m_agent = apb_agent::type_id::create("m_agent", this); m_adapter = reg2apb_adapter::type_id::create("m_adapter"); m_reg_predictor = uvm_reg_predictor #(apb_seq_item)::type_id:: create("m_reg_predictor",this); if (m_cfg.has_scoreboard) begin m_sb = gpio_scoreboard::type_id::create("m_sb", this); m_predictor = gpio_predictor::type_id::create("m_predictor",this); end if (m_cfg.has_coverage) m_cov = apb_cov_collector::type_id::create("m_cov", this); endfunction function void connect_phase(uvm_phase phase); // ── RAL stimulus path ───────────────────────────────── m_cfg.gpio_rm.APB_map.set_sequencer( m_agent.m_seqr, m_adapter); // ── RAL mirror path ─────────────────────────────────── m_reg_predictor.map = m_cfg.gpio_rm.APB_map; m_reg_predictor.adapter = m_adapter; m_agent.ap.connect(m_reg_predictor.bus_in); // ── Analysis: scoreboard and coverage ───────────────── if (m_cfg.has_scoreboard) begin m_predictor.expected_ap.connect(m_sb.expected_export); m_agent.ap.connect(m_sb.actual_export); m_agent.ap.connect(m_predictor.analysis_export); end if (m_cfg.has_coverage) m_agent.ap.connect(m_cov.analysis_export); endfunction endclass
// ── Base register sequence ──────────────────────────────── class gpio_base_reg_seq extends uvm_sequence #(uvm_sequence_item); `uvm_object_utils(gpio_base_reg_seq) gpio_reg_block rm; uvm_status_e status; function new(string n="gpio_base_reg_seq"); super.new(n); endfunction virtual task body(); if (!uvm_config_db #(gpio_reg_block)::get( m_sequencer,"","gpio_rm",rm)) `uvm_fatal("SEQ","No reg model") endtask endclass // ── Register initialisation sequence ────────────────────── class gpio_reg_init_seq extends gpio_base_reg_seq; `uvm_object_utils(gpio_reg_init_seq) function new(string n="gpio_reg_init_seq"); super.new(n); endfunction task body(); super.body(); rm.dir_reg.write(status, 32'hFFFF_0000, .parent(this)); rm.ien_reg.write(status, 32'h0000_000F, .parent(this)); rm.data_reg.write(status, 32'h0000_0000, .parent(this)); endtask endclass // ── Randomised read-write sequence ──────────────────────── class gpio_rand_seq extends gpio_base_reg_seq; `uvm_object_utils(gpio_rand_seq) rand int num_ops = 20; function new(string n="gpio_rand_seq"); super.new(n); endfunction task body(); super.body(); repeat(num_ops) begin uvm_reg_data_t data; if ($random % 2) rm.data_reg.write(status, $urandom, .parent(this)); else rm.data_reg.read(status, data, .parent(this)); end endtask endclass
// ── Base test: builds config tree ───────────────────────── class gpio_base_test extends uvm_test; `uvm_component_utils(gpio_base_test) gpio_env m_env; gpio_env_cfg m_cfg; function new(string n, uvm_component p); super.new(n,p); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); m_cfg = gpio_env_cfg::type_id::create("m_cfg"); m_cfg.m_apb_cfg = apb_agent_cfg::type_id::create("m_apb_cfg"); m_cfg.m_apb_cfg.is_active = UVM_ACTIVE; if (!uvm_config_db #(virtual apb_if)::get( this,"","vif",m_cfg.m_apb_cfg.vif)) `uvm_fatal("TEST","No vif") m_cfg.gpio_rm = gpio_reg_block::type_id::create("gpio_rm"); m_cfg.gpio_rm.build(); uvm_config_db #(gpio_env_cfg)::set( this,"m_env","cfg",m_cfg); m_env = gpio_env::type_id::create("m_env",this); endfunction endclass // ── Derived test: register init + random stimulus ───────── class gpio_rand_test extends gpio_base_test; `uvm_component_utils(gpio_rand_test) function new(string n, uvm_component p); super.new(n,p); endfunction task run_phase(uvm_phase phase); gpio_reg_init_seq init_seq; gpio_rand_seq rand_seq; phase.raise_objection(this); init_seq = gpio_reg_init_seq::type_id::create("init_seq"); init_seq.start(m_env.m_agent.m_seqr); rand_seq = gpio_rand_seq::type_id::create("rand_seq"); rand_seq.randomize() with { num_ops == 50; }; rand_seq.start(m_env.m_agent.m_seqr); phase.drop_objection(this); endtask endclass // ── Built-in register test ──────────────────────────────── class gpio_reg_hw_reset_test extends gpio_base_test; `uvm_component_utils(gpio_reg_hw_reset_test) function new(string n, uvm_component p); super.new(n,p); endfunction task run_phase(uvm_phase phase); uvm_reg_hw_reset_seq hw_reset_seq; phase.raise_objection(this); hw_reset_seq = uvm_reg_hw_reset_seq::type_id::create("hw_reset_seq"); hw_reset_seq.model = m_cfg.gpio_rm; hw_reset_seq.start(m_env.m_agent.m_seqr); phase.drop_objection(this); endtask endclass
module gpio_tb_top; import uvm_pkg::*; `include "uvm_macros.svh" import gpio_test_pkg::*; // ← must import test package for factory logic PCLK, PRESETn; // Clock: 10ns period initial PCLK = 0; always #5ns PCLK = ~PCLK; // Reset: deassert after 80ns initial begin PRESETn = 0; #80ns; PRESETn = 1; end apb_if APB(.PCLK(PCLK), .PRESETn(PRESETn)); gpio_dut dut( .PCLK(PCLK), .PRESETn(PRESETn), .PADDR(APB.PADDR), .PWDATA(APB.PWDATA), .PRDATA(APB.PRDATA), .PWRITE(APB.PWRITE), .PSELx(APB.PSELx), .PENABLE(APB.PENABLE), .PREADY(APB.PREADY), .PSLVERR(APB.PSLVERR), .PSTRB(APB.PSTRB) ); initial begin uvm_config_db #(virtual apb_if)::set( null, "uvm_test_top", "vif", APB); run_test(); end endmodule
| Connection | Where made | Code |
|---|---|---|
| vif → driver + monitor | agent build_phase | uvm_config_db::set(this, "*", "vif", m_cfg.vif) |
| driver ↔ sequencer | agent connect_phase | m_drv.seq_item_port.connect(m_seqr.seq_item_export) |
| monitor.ap → agent.ap | agent connect_phase | m_mon.ap.connect(this.ap) |
| RAL map → sequencer | env connect_phase | gpio_rm.APB_map.set_sequencer(m_agent.m_seqr, m_adapter) |
| agent.ap → RAL predictor | env connect_phase | m_agent.ap.connect(m_reg_predictor.bus_in) |
| agent.ap → scoreboard actual | env connect_phase | m_agent.ap.connect(m_sb.actual_export) |
| agent.ap → gpio predictor | env connect_phase | m_agent.ap.connect(m_predictor.analysis_export) |
| predictor.expected_ap → scoreboard expected | env connect_phase | m_predictor.expected_ap.connect(m_sb.expected_export) |
| agent.ap → coverage | env connect_phase | m_agent.ap.connect(m_cov.analysis_export) |
| reg_predictor.map + adapter | env connect_phase | m_reg_predictor.map = APB_map; m_reg_predictor.adapter = m_adapter |
| vif → config_db | tb_top initial | uvm_config_db::set(null, "uvm_test_top", "vif", APB) |
| Test package in factory | tb_top | import gpio_test_pkg::* |