HomeProblemsTheoryUVM
Beginner3 min readChapter 2

UVM Testbench Architecture

Understand the layered testbench architecture — from test to environment to agent, and how the components connect.

UVM Testbench Architecture

A UVM testbench follows a layered architecture that separates concerns cleanly. Each layer has a specific responsibility, making the environment modular and reusable.


The Big Picture

text
┌──────────────────────────────────────────────┐ │ uvm_test │ ← Top-level test │ ┌────────────────────────────────────────┐ │ │ │ uvm_env │ │ ← Environment │ │ ┌──────────────┐ ┌───────────────┐ │ │ │ │ │ uvm_agent │ │ Scoreboard │ │ │ │ │ │ ┌──────────┐ │ │ │ │ │ │ │ │ │ Sequencer │ │ └───────────────┘ │ │ │ │ │ │ Driver │ │ │ │ │ │ │ │ Monitor │ │ │ │ │ │ │ └──────────┘ │ │ │ │ │ └──────────────┘ │ │ │ └────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ ┌───────▼───────┐ │ DUT (RTL) │ └───────────────┘

Layer-by-Layer Breakdown

1. Test Layer (uvm_test)

The test is the top-level component. It creates the environment, configures it, and starts stimulus.

systemverilog
class my_test extends uvm_test; `uvm_component_utils(my_test) my_env env; function new(string name, uvm_component parent); super.new(name, parent); endfunction // Build phase: create sub-components function void build_phase(uvm_phase phase); super.build_phase(phase); env = my_env::type_id::create("env", this); endfunction // Run phase: start stimulus task run_phase(uvm_phase phase); my_sequence seq; phase.raise_objection(this); seq = my_sequence::type_id::create("seq"); seq.start(env.agent.sequencer); phase.drop_objection(this); endtask endclass

2. Environment Layer (uvm_env)

The environment is a container that instantiates agents, scoreboards, and coverage collectors.

systemverilog
class my_env extends uvm_env; `uvm_component_utils(my_env) my_agent agent; my_scoreboard sb; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); agent = my_agent::type_id::create("agent", this); sb = my_scoreboard::type_id::create("sb", this); endfunction function void connect_phase(uvm_phase phase); // Wire the monitor's output to the scoreboard's input agent.monitor.ap.connect(sb.analysis_export); endfunction endclass

3. Agent Layer (uvm_agent)

The agent bundles three components for a single interface:

ComponentRole
SequencerManages sequence items (transactions)
DriverConverts transactions into pin-level signals
MonitorPassively observes the interface
systemverilog
class my_agent extends uvm_agent; `uvm_component_utils(my_agent) my_sequencer sequencer; my_driver driver; my_monitor monitor; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); monitor = my_monitor::type_id::create("monitor", this); // Only create driver & sequencer in ACTIVE mode if (get_is_active() == UVM_ACTIVE) begin sequencer = my_sequencer::type_id::create("sequencer", this); driver = my_driver::type_id::create("driver", this); end endfunction function void connect_phase(uvm_phase phase); if (get_is_active() == UVM_ACTIVE) driver.seq_item_port.connect(sequencer.seq_item_export); endfunction endclass

Active vs Passive Agents

ModeContainsPurpose
ActiveSequencer + Driver + MonitorDrives and observes the DUT
PassiveMonitor onlyObserves only (no stimulus)

Use passive agents when you need to monitor a bus you don't control (e.g., a slave-side interface).


How Data Flows

text
Test │ starts Sequence ──▶ Sequencer ──▶ Driver ──▶ DUT Monitor (observes) Scoreboard (checks)
  1. The test creates a sequence and starts it on the sequencer.
  2. The sequencer passes items to the driver.
  3. The driver converts transactions to pin-level signals on the DUT.
  4. The monitor observes the DUT's response.
  5. The scoreboard compares expected vs actual.

Key Takeaways

  • The architecture is hierarchical: test → env → agent → driver/monitor/sequencer.
  • Each component has a single responsibility.
  • Reusability: write an SPI agent once, use it in any project that has SPI.
  • Scalability: need two AXI interfaces? Just instantiate two AXI agents.