The Universal Verification Methodology (UVM) reference implementation has long included the “UBUS” example as the gold standard for teaching layered testbench architecture. The standard UBUS example features a Master Agent, a Slave Agent, a Bus Monitor, a Scoreboard, RTL bus arbitration, and a layered sequence library. While it serves as an excellent tutorial for OOP class inheritance and phase execution, it also exposes the heavy boilerplate and tight coupling inherent to UVM.
In this appendix we rebuild the UBUS architecture using the actor framework. Heavy OOP classes are replaced by immutable structs, and monolithic components by reactive actors. The result preserves the verification functionality in significantly less code and scales horizontally without locking. The implementation faithfully mirrors the same dut_dummy.v RTL arbiter interface that the original UBUS example uses, so the actor framework integrates with the same hardware handshake.
In the legacy UVM example, the ubus_transfer class contains both the data payload and numerous utility functions. In our actor framework, behavior is strictly separated from data. We define the pure data contracts as immutable SystemVerilog structs.
package ubus_pkg;
typedef enum logic [1:0] { NOP=0, READ=1, WRITE=2 } ubus_dir_e;
// The request envelope sent by the Test Thread to the Master Actor
typedef struct {
longint id;
int master_id;
logic [15:0] addr;
ubus_dir_e dir;
logic [7:0] data;
int size;
int transmit_delay;
} UbusReq_s;
// Response a master actor publishes back to its requester
typedef struct {
longint id;
int master_id;
logic [7:0] data;
logic error;
} UbusRsp_s;
// Scoreboard notification published by the bus monitor
typedef struct {
logic [15:0] addr;
logic [7:0] data;
ubus_dir_e dir;
int master_id;
} UbusMonPkt_s;
endpackage
A standard UVM multi-master testbench needs a uvm_sequencer to queue and arbitrate transactions before they reach the uvm_driver. In the actor framework, the queue role is filled by the actor’s mailbox: the UbusMasterActor pops a request from its mailbox, raises sig_request on the RTL arbiter, waits for grant, drives the pin-level transaction, and publishes a response.
class UbusMasterActor extends Actor;
virtual ubus_if vif;
int master_id;
virtual task act(MsgBase msg);
if (msg.getTypeName() == $typename(UbusReq_s)) begin
UbusReq_s req = Msg#(UbusReq_s)::unwrap(msg);
UbusRsp_s rsp;
// Optional pre-transfer gap
if (req.transmit_delay > 0)
repeat(req.transmit_delay) @(vif.cb);
// ---- ARBITRATION PHASE ----
@(vif.cb);
vif.cb.sig_request[master_id] <= 1;
do begin
@(vif.cb);
end while (vif.sig_grant[master_id] !== 1'b1);
vif.cb.sig_request[master_id] <= 0;
// ---- ADDRESS PHASE ----
vif.cb.sig_addr <= req.addr;
// ... drive read/write/size
@(vif.cb);
// ---- DATA PHASE ----
// ... drive data out
@(vif.cb); // Mandatory 1-cycle wait
while (vif.sig_wait !== 0) @(vif.cb);
`PUBLISH(rsp); // Publish response back to topology
end
endtask
endclass
The UVM Slave Agent requires its own sequencer and driver. Our UbusSlaveActor is simply a reactive node that maintains an internal memory array. It autonomously monitors the shared bus, responding only to transactions within its assigned address range, and injecting randomized wait states to throttle the bus.
class UbusSlaveActor extends Actor;
virtual ubus_if vif;
int slave_id;
logic [15:0] min_addr, max_addr;
logic [7:0] mem[logic[15:0]];
virtual task run();
forever begin
do begin
@(vif.cb);
end while (vif.sig_grant[0] !== 1'b1 && vif.sig_grant[1] !== 1'b1);
begin
logic [15:0] captured_addr;
@(vif.cb); // Capture address
captured_addr = vif.cb.sig_addr;
if (captured_addr >= min_addr && captured_addr <= max_addr) begin
// Inject random temporal delay
int wait_cycles = $urandom_range(0, 4);
if (wait_cycles > 0) begin
vif.sig_wait <= 1; // Direct NBA removes 1-cycle delay
repeat(wait_cycles) @(vif.cb);
end
vif.sig_wait <= 0;
if (vif.cb.sig_write) begin
@(vif.cb); // Wait for Data Phase before sampling
mem[captured_addr] = vif.cb.sig_data;
end
// ... complete READ data phase
vif.sig_wait <= 'hz;
end
end
end
endtask
endclass
Because data is immutable and flows via typed message structs, passive observability is trivial. The UbusProtocolMonitorActor sniffs the bus and PUBLISHes structs. The UbusScoreboardActor subscribes to these streams, maintaining a shadow memory to verify data integrity without ever blocking the active execution threads.
class UbusScoreboardActor extends Actor;
logic [7:0] shadow_mem[logic[15:0]];
virtual task act(MsgBase msg);
if (msg.getTypeName() == $typename(UbusMonPkt_s)) begin
UbusMonPkt_s pkt = Msg#(UbusMonPkt_s)::unwrap(msg);
case (pkt.dir)
WRITE: shadow_mem[pkt.addr] = pkt.data;
READ: begin
if (shadow_mem.exists(pkt.addr)) begin
if (shadow_mem[pkt.addr] === pkt.data)
$display("Scoreboard: READ PASS [0x%04h]", pkt.addr);
else
$error("Scoreboard: READ FAIL [0x%04h]", pkt.addr);
end
end
endcase
end
endtask
endclass
In the legacy UVM UBUS environment, wiring two masters and four slaves involves uvm_config_db entries for each agent, plus the connect_phase that hooks each agent’s analysis port up to the scoreboard. In the actor framework, the env is itself an actor that constructs the sub-actors in its own constructor and connects edges with ‘WIRE.
class UbusEnvActor extends Actor;
UbusMasterActor masters[2];
UbusSlaveActor slaves[4];
UbusProtocolMonitorActor bus_monitor;
UbusScoreboardActor scoreboard;
function new(virtual ubus_if vif, string name="UbusEnvActor");
masters[0] = new(vif, 0, "Master[0]");
masters[1] = new(vif, 1, "Master[1]");
slaves[0] = new(vif, 0, 16'h0000, 16'h3FFF, "Slave[0]");
slaves[1] = new(vif, 1, 16'h4000, 16'h7FFF, "Slave[1]");
slaves[2] = new(vif, 2, 16'h8000, 16'hBFFF, "Slave[2]");
slaves[3] = new(vif, 3, 16'hC000, 16'hFFFF, "Slave[3]");
bus_monitor = new(vif, "BusMonitor");
scoreboard = new("Scoreboard");
// Bus monitor publishes UbusMonPkt_s to Scoreboard
`WIRE(bus_monitor, UbusMonPkt_s, scoreboard)
endfunction
// ... start() method
endclass
We replace the monolithic uvm_test base class. We execute the classic 2 Master / 4 Slave test by firing randomized structs concurrently via two test threads, matching the UVM sequence’s behavior without virtual sequencers.
class Ubus2M4STest;
UbusEnvActor env;
task run();
env.start(); // Boot the topology non-blockingly
fork
begin // Master 0 Thread
for (int i = 0; i < 6; i++) begin
logic [15:0] addr = $urandom_range(16'h0000, 16'h7FFF);
`PUBLISH_TO(env.masters[0], UbusReq_s'{/* READ */});
`PUBLISH_TO(env.masters[0], UbusReq_s'{/* WRITE (modify) */});
`PUBLISH_TO(env.masters[0], UbusReq_s'{/* READ (verify) */});
end
end
begin // Master 1 Thread
for (int i = 0; i < 8; i++) begin
logic [15:0] addr = $urandom_range(16'h8000, 16'hFFFF);
`PUBLISH_TO(env.masters[1], UbusReq_s'{/* READ */});
`PUBLISH_TO(env.masters[1], UbusReq_s'{/* WRITE (modify) */});
`PUBLISH_TO(env.masters[1], UbusReq_s'{/* READ (verify) */});
end
end
join
endtask
endclass
The shipped example (ubus_2m_4s_test.sv) factors this stimulus into a first-class Ubus2M4SMasterStimulus actor and makes Ubus2M4STest a thin orchestrator that instantiates one stimulus actor per master; the inline fork form is shown here for brevity.
The previous steps cover the full feature set of the legacy UVM UBUS integrated example: bus arbitration through the RTL arbiter, randomized slave wait-states, passive protocol monitoring, scoreboarding, and the multi-master / multi-slave 2M/4S test scenario.
Table A.1 compares line counts between the legacy UVM components and their actor-framework equivalents. Both columns are reproducible by running wc -l: the UVM side on the in-tree ch5_legacy_uvm_ubus/ tree, the actor side on appA_actor_ubus/.
The four-to-one reduction is the least interesting result here. Line count is a poor proxy for anything that matters — terse code can be unreadable and verbose code can be correct — so the headline ratio is better read as a side effect than as the point. The advantages that actually carry weight are architectural:
• Concurrency without shared state. Actors interact only through asynchronous messages; each keeps its state private and every payload is immutable or passed by value, so there is no shared execution space to guard. The fork-join over shared configuration objects, sequencer state, and global variables that a UVM environment leans on — with the mutex and semaphore discipline it requires, the threads left idling on locks, and the scheduling-dependent races that reproduce on one run and vanish on the next — has nothing to operate on here. That same absence of contention is why the topology scales out rather than degrading past a thread count.
• Data-oriented design. Messages are plain SystemVerilog structs and behavior lives in the actors, so data and behavior are kept apart instead of fused into one class. Each actor owns its own copy of a payload, which is what makes the network data-race-free; and because the structs are contiguous values rather than pointer-chased heap objects, the layout is cache-friendly — the property that lets a future emulator schedule actors across cores without lock contention. Constraints and coverage attach to the behavior, not to the data.
• Declarative, fully visible structure. A producer never names its consumers and a consumer never names its producers; the entire topology lives in the parent’s ‘WIRE statements, with no hidden edges and no run-time uvm_config_db string lookups to chase. There is no build/connect/run phase machine to keep in lock-step either — an actor is alive and listening the moment it is wired — so the phase-ordering and configuration-timing bugs that dominate UVM bring-up cannot arise.
• The same structural shape as the silicon. The testbench is a concurrent network of state machines exchanging events on mailboxes — the shape of the DUT itself, rather than a sequential class hierarchy laid over it. That structural match is why the actor core has a synthesizable form (Appendix E) that carries onto FPGA emulation and silicon, where a dynamic-heap-heavy UVM testbench cannot follow.
• Observability built in. Every message carries its own trace_id and causal ancestry, so cross-actor tracing, recording, and deterministic replay are intrinsic to the substrate rather than bolted on afterwards.
Compactness still earns a place on the list, but for a humbler reason than the ratio suggests: comprehensibility. With the ceremony stripped away, far less stands between a reader and the verification intent, which makes the actor version easier to follow — and to change — than the verbose original. That is a real benefit; it is simply a secondary one.
Converting the UBUS example to the actor framework removes the UVM class-inheritance overhead, the uvm_config_db indirection, the phase-synchronization logic, and most of the boilerplate macros. What remains is an asynchronous reactive network of actors driving the same dut_dummy RTL arbiter the legacy testbench drives — and, as a by-product, in roughly one-quarter the lines of code.