class base_test extends uvm_test;
`uvm_component_utils(base_test)
my_env env;
function void build_phase(uvm_phase phase);
// Create the environment using the factory
env = my_env::type_id::create("env", this);
// Pass standard configurations to the agents
uvm_config_db#(int)::set(this, "env.agt*", "is_active", UVM_ACTIVE);
endfunction
// Catch-all timeout for safety
function void start_of_simulation_phase(uvm_phase phase);
uvm_top.set_timeout(1ms);
endfunction
endclass
class ahb_agent extends uvm_agent;
`uvm_component_utils(ahb_agent)
ahb_driver drv;
ahb_sequencer sqr;
ahb_monitor mon;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 1. Monitor is ALWAYS built
mon = ahb_monitor::type_id::create("mon", this);
// 2. Logic to build Driver/Sequencer
if(get_is_active() == UVM_ACTIVE) begin
drv = ahb_driver::type_id::create("drv", this);
sqr = ahb_sequencer::type_id::create("sqr", this);
end
endfunction
function void connect_phase(uvm_phase phase);
// Only connect if the agent is driving
if(get_is_active() == UVM_ACTIVE) begin
drv.seq_item_port.connect(sqr.seq_item_export);
end
endfunction
endclass
task run_phase(uvm_phase phase);
forever begin
// 1. Wait for physical request (e.g., Slave Select or Valid)
@(posedge vif.req_valid);
// 2. NOW ask the Sequencer: "The DUT is asking, what do I reply?"
seq_item_port.get_next_item(req);
// 3. Drive the response data from the sequence item
vif.resp_data <= req.data;
vif.resp_ready <= 1;
@(posedge vif.clk);
vif.resp_ready <= 0;
// 4. Return control to sequence
seq_item_port.item_done();
end
endtask
seq_item_port.get_next_item(req):
This is a blocking call. The driver sleeps here until a sequence provides a new transaction.
seq_item_port.item_done():
This is a non-blocking signal back to the sequencer. It unblocks the finish_item() call in the sequence.
Drivers do not access DUT signals directly. They use a Virtual Interface (VIF)
class axi_driver extends uvm_driver #(axi_item);
`uvm_component_utils(axi_driver)
virtual axi_if vif;
task run_phase(uvm_phase phase);
// Reset the interface
vif.valid <= 0;
forever begin
// Get the transaction
seq_item_port.get_next_item(req);
// DRIVE logic
@(posedge vif.clk);
vif.addr <= req.addr;
vif.data <= req.data;
vif.valid <= 1;
// Wait for DUT acknowledgment
wait(vif.ready == 1);
@(posedge vif.clk);
vif.valid <= 0;
// Complete handshake
seq_item_port.item_done();
end
endtask
endclass
Modern protocols (like AXI or PCIe) are pipelined. This means a driver might start a new request before the previous response has finished.
The Pipelining Pattern:
Instead of a simple loop, you use fork...join_none to handle the "Data Phase" and "Address Phase" in parallel.
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
// 1. Driving Phase (Blocking)
drive_address_phase(req);
// 2. Data Phase (Non-blocking / Background)
fork
automatic axi_item req_copy = req;
drive_data_phase(req_copy);
join_none
// 3. Immediately ask for next item while data drives
seq_item_port.item_done();
end
Sampling via Virtual Interfaces
class spi_monitor extends uvm_monitor;
`uvm_component_utils(spi_monitor)
virtual spi_if vif;
uvm_analysis_port #(spi_item) mon_ap;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon_ap = new("mon_ap", this);
endfunction
task run_phase(uvm_phase phase);
forever begin
spi_item item = spi_item::type_id::create("item");
// 1. Wait for start of transaction
@(negedge vif.cs_n);
// 2. Sample data bits over 8 clock cycles
for (int i = 0; i < 8; i++) begin
@(posedge vif.sclk);
item.data[7-i] = vif.mosi;
end
// 3. COMPLETE: Reconstructed the object!
// 4. BROADCAST: Send to anyone listening
mon_ap.write(item);
end
endtask
endclass
The mon_ap.write(item)
call is the ONLY way a monitor should communicate with the outside world. It implement a Publisher-Subscriber pattern:
