A complete step-by-step walkthrough of building a block-level UVM testbench from scratch — SV top module, sequence item, driver, monitor, agent config, agent, environment config, environment, base test, and the sequence that drives it all.
This post builds a complete block-level UVM testbench for a simple 32-bit APB GPIO DUT. Every component is shown in the order it should be written. By the end you will have a working testbench that you can adapt to any DUT by changing the sequence item fields and the driver/monitor signal assignments.
The SV top module is static — it is not a UVM component. It instantiates the DUT, the SystemVerilog interface, generates the clock, and calls run_test(). The only UVM-specific job it has is placing the virtual interface into config_db before simulation time starts.
module tb_top; import uvm_pkg::*; `include "uvm_macros.svh" `include "gpio_test_pkg.sv" // imports all testbench classes // ① Clock generation logic clk; initial clk = 0; always #5 clk = ~clk; // 100 MHz // ② Interface instantiation apb_if apb_bus(.PCLK(clk), .PRESETn(rst_n)); // ③ DUT instantiation logic rst_n; gpio_dut dut( .PCLK (clk), .PRESETn(rst_n), .PADDR (apb_bus.PADDR), .PWRITE (apb_bus.PWRITE), .PWDATA (apb_bus.PWDATA), .PSELx (apb_bus.PSELx), .PENABLE(apb_bus.PENABLE), .PRDATA (apb_bus.PRDATA), .PREADY (apb_bus.PREADY), .PSLVERR(apb_bus.PSLVERR) ); // ④ Pass virtual interface into config_db BEFORE run_test() initial begin uvm_config_db #(virtual apb_if)::set( null, "uvm_test_top", "apb_vif", apb_bus); rst_n = 0; #20ns; rst_n = 1; run_test(); // ⑤ Start UVM — blocks until all objections dropped end endmodule
null as the first argument means “from the top level” — no component context. The path "uvm_test_top" makes it accessible to the test and all its descendants. If you call run_test() before setting the virtual interface, the test’s build_phase will fail to retrieve it.
The sequence item represents one atomic transaction on the bus. It carries all the information the driver needs to generate one bus transfer, and all the information the monitor needs to report one observed transaction.
class gpio_seq_item extends uvm_sequence_item; `uvm_object_utils_begin(gpio_seq_item) `uvm_field_int(addr, UVM_ALL_ON) `uvm_field_int(data, UVM_ALL_ON) `uvm_field_int(we, UVM_ALL_ON) `uvm_field_int(strobe, UVM_ALL_ON) `uvm_object_utils_end // Randomisable fields rand logic [11:0] addr; // 12-bit APB address rand logic [31:0] data; // 32-bit data (write data or captured read data) rand logic we; // 1=write 0=read rand logic [3:0] strobe; // PSTRB byte enables // Status populated by driver / monitor on completion logic slverr; // PSLVERR from DUT // Constraints constraint valid_addr_c { addr inside {12'h000, 12'h004, 12'h008, 12'h00C, 12'h010}; } constraint strobe_write_c { we -> (strobe != 4'b0000); // writes must have at least one strobe } constraint strobe_read_c { !we -> (strobe == 4'b0000); // reads must have zero strobes } function new(string name = "gpio_seq_item"); super.new(name); endfunction endclass
The driver pulls sequence items from the sequencer using the get_next_item() / item_done() handshake and drives the DUT signals via the virtual interface. The driver’s run_phase runs forever in a loop.
class gpio_driver extends uvm_driver #(gpio_seq_item); `uvm_component_utils(gpio_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("DRV", "No virtual interface!") endfunction task run_phase(uvm_phase phase); gpio_seq_item item; // Drive idle / reset state on interface vif.PSELx <= 0; vif.PENABLE <= 0; @(posedge vif.PRESETn); // wait for reset to deassert forever begin seq_item_port.get_next_item(item); // block until item available drive_transfer(item); // APB Setup + Access phases seq_item_port.item_done(); // signal completion to sequencer end endtask task drive_transfer(gpio_seq_item item); // SETUP phase (1 cycle) @(posedge vif.PCLK); vif.PADDR <= item.addr; vif.PWDATA <= item.data; vif.PWRITE <= item.we; vif.PSTRB <= item.strobe; vif.PSELx <= 1; vif.PENABLE <= 0; // ACCESS phase @(posedge vif.PCLK); vif.PENABLE <= 1; do @(posedge vif.PCLK); while (!vif.PREADY); // wait for PREADY item.slverr = vif.PSLVERR; if (!item.we) item.data = vif.PRDATA; // capture read data vif.PSELx <= 0; vif.PENABLE <= 0; endtask endclass
The monitor passively observes the DUT interface and assembles complete transactions. It never drives any signals. When a transfer completes (PSEL+PENABLE+PREADY=1), it builds a sequence item from the observed signals and broadcasts it via the analysis port.
class gpio_monitor extends uvm_monitor; `uvm_component_utils(gpio_monitor) virtual apb_if vif; uvm_analysis_port #(gpio_seq_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); ap = new("ap", this); if (!uvm_config_db #(virtual apb_if)::get( this, "", "vif", vif)) `uvm_fatal("MON", "No virtual interface!") endfunction task run_phase(uvm_phase phase); gpio_seq_item txn; forever begin // Wait for transfer completion: PSEL & PENABLE & PREADY @(posedge vif.PCLK); if (vif.PSELx && vif.PENABLE && vif.PREADY) begin txn = gpio_seq_item::type_id::create("txn"); txn.addr = vif.PADDR; txn.we = vif.PWRITE; txn.strobe = vif.PSTRB; txn.slverr = vif.PSLVERR; if (vif.PWRITE) txn.data = vif.PWDATA; else txn.data = vif.PRDATA; ap.write(txn); // broadcast to all subscribers end end endtask endclass
class gpio_agent_cfg extends uvm_object; `uvm_object_utils(gpio_agent_cfg) virtual apb_if vif; uvm_active_passive_enum is_active = UVM_ACTIVE; bit has_coverage = 1; function new(string name = "gpio_agent_cfg"); super.new(name); endfunction endclass
typedef uvm_sequencer #(gpio_seq_item) gpio_sequencer; class gpio_agent extends uvm_agent; `uvm_component_utils(gpio_agent) gpio_driver m_drv; gpio_sequencer m_seqr; gpio_monitor m_mon; gpio_agent_cfg m_cfg; uvm_analysis_port #(gpio_seq_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); if (!uvm_config_db #(gpio_agent_cfg)::get( this, "", "cfg", m_cfg)) `uvm_fatal("AGT", "No agent config!") ap = new("ap", this); m_mon = gpio_monitor::type_id::create("m_mon", this); if (m_cfg.is_active == UVM_ACTIVE) begin m_seqr = gpio_sequencer::type_id::create("m_seqr", this); m_drv = gpio_driver::type_id::create("m_drv", this); end uvm_config_db #(virtual apb_if)::set(this, "m_mon", "vif", m_cfg.vif); if (m_cfg.is_active == UVM_ACTIVE) uvm_config_db #(virtual apb_if)::set(this, "m_drv", "vif", m_cfg.vif); endfunction function void connect_phase(uvm_phase phase); m_mon.ap.connect(this.ap); if (m_cfg.is_active == UVM_ACTIVE) m_drv.seq_item_port.connect(m_seqr.seq_item_export); endfunction endclass
The environment config nests the agent config inside it. The test creates both, populates them, assigns the agent config handle into the env config, then puts the env config into config_db. The env retrieves it and distributes the nested agent config to the agent.
class gpio_env_cfg extends uvm_object; `uvm_object_utils(gpio_env_cfg) // Nested agent config — test populates and assigns this gpio_agent_cfg m_agent_cfg; // Enable/disable analysis components bit has_scoreboard = 1; bit has_coverage = 1; function new(string name = "gpio_env_cfg"); super.new(name); endfunction endclass
class gpio_env extends uvm_env; `uvm_component_utils(gpio_env) gpio_agent m_agent; gpio_scoreboard m_sb; gpio_env_cfg m_cfg; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); // ① Get env config from config_db if (!uvm_config_db #(gpio_env_cfg)::get( this, "", "cfg", m_cfg)) `uvm_fatal("ENV", "No env config!") // ② Push agent config down to agent uvm_config_db #(gpio_agent_cfg)::set( this, "m_agent", "cfg", m_cfg.m_agent_cfg); // ③ Create agent (always) and optional analysis components m_agent = gpio_agent::type_id::create("m_agent", this); if (m_cfg.has_scoreboard) m_sb = gpio_scoreboard::type_id::create("m_sb", this); endfunction function void connect_phase(uvm_phase phase); // Connect agent analysis port to scoreboard if (m_cfg.has_scoreboard) m_agent.ap.connect(m_sb.analysis_export); endfunction endclass
The test is the top of the UVM hierarchy. Its build_phase is responsible for creating all config objects, retrieving virtual interfaces from config_db, nesting configs, placing the env config into config_db, and creating the env. Its run_phase starts the root sequence.
class gpio_base_test extends uvm_test; `uvm_component_utils(gpio_base_test) gpio_env m_env; gpio_env_cfg m_env_cfg; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); // ① Create env config m_env_cfg = gpio_env_cfg::type_id::create("m_env_cfg"); // ② Create agent config, get virtual interface from tb_top m_env_cfg.m_agent_cfg = gpio_agent_cfg::type_id::create("m_agent_cfg"); if (!uvm_config_db #(virtual apb_if)::get( this, "", "apb_vif", m_env_cfg.m_agent_cfg.vif)) `uvm_fatal("TEST", "No virtual interface in config_db!") // ③ Configure env and agent defaults m_env_cfg.has_scoreboard = 1; m_env_cfg.m_agent_cfg.is_active = UVM_ACTIVE; // ④ Place env config into config_db for env to retrieve uvm_config_db #(gpio_env_cfg)::set( this, "m_env", "cfg", m_env_cfg); // ⑤ Create env — triggers env's build_phase m_env = gpio_env::type_id::create("m_env", this); endfunction task run_phase(uvm_phase phase); gpio_write_seq seq; phase.raise_objection(this); seq = gpio_write_seq::type_id::create("seq"); seq.start(m_env.m_agent.m_seqr); // start on agent's sequencer phase.drop_objection(this); endtask endclass
The sequence generates the actual stimulus. Its body() task creates sequence items, randomises them, and sends them to the sequencer using the start_item() / finish_item() handshake.
class gpio_write_seq extends uvm_sequence #(gpio_seq_item); `uvm_object_utils(gpio_write_seq) int num_txns = 10; function new(string name = "gpio_write_seq"); super.new(name); endfunction task body(); gpio_seq_item item; repeat(num_txns) begin item = gpio_seq_item::type_id::create("item"); start_item(item); // request access to sequencer if (!item.randomize() with { we == 1; }) `uvm_fatal("SEQ", "Randomisation failed") finish_item(item); // send to driver, block until done end endtask endclass
| What gets wired | Where | How |
|---|---|---|
| Virtual interface → config_db | tb_top initial block | config_db::set(null, "uvm_test_top", "apb_vif", apb_bus) |
| Virtual interface → agent_cfg.vif | test build_phase | config_db::get(this, "", "apb_vif", m_env_cfg.m_agent_cfg.vif) |
| env_cfg → env | test build_phase | config_db::set(this, "m_env", "cfg", m_env_cfg) |
| agent_cfg → agent | env build_phase | config_db::set(this, "m_agent", "cfg", m_cfg.m_agent_cfg) |
| vif → driver | agent build_phase | config_db::set(this, "m_drv", "vif", m_cfg.vif) |
| vif → monitor | agent build_phase | config_db::set(this, "m_mon", "vif", m_cfg.vif) |
| seq_item_port → seq_item_export | 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) |
| agent.ap → scoreboard | env connect_phase | m_agent.ap.connect(m_sb.analysis_export) |
| Item | Key fact |
|---|---|
| tb_top responsibility | DUT + interface instantiation, clock gen, config_db::set(vif), run_test() |
| Virtual interface in config_db | Set with null context before run_test() — path “uvm_test_top” makes it global |
| Sequence item fields | All bus signals needed by driver to drive one transfer + monitor to report one transfer |
| Driver loop | get_next_item() → drive signals → item_done() — forever in run_phase |
| Monitor trigger | PSEL & PENABLE & PREADY all HIGH on rising PCLK = transfer complete |
| Monitor output | ap.write(txn) — broadcasts completed transaction to all subscribers |
| Config nesting order | test creates agent_cfg → assigns into env_cfg → sets env_cfg into config_db → env gets it → pushes agent_cfg to agent |
| Agent config key field | vif handle + is_active — all other fields are optional |
| Sequencer typedef | typedef uvm_sequencer #(my_item) my_sequencer; — no extra code needed |
| Sequence body() | create item → start_item() → randomize() → finish_item() per transfer |
| Test run_phase | raise_objection → seq.start(m_env.m_agent.m_seqr) → drop_objection |
| TLM wired in | agent connect_phase — seq_item_port.connect(seq_item_export) |
| Analysis port wired in | agent connect_phase (forward) + env connect_phase (subscribe scoreboard) |
| File naming convention | One class per .svh file; all collected in a _pkg.sv package |