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.
systemverilogclass 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.
systemverilogclass 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:
| Component | Role |
|---|---|
| Sequencer | Manages sequence items (transactions) |
| Driver | Converts transactions into pin-level signals |
| Monitor | Passively observes the interface |
systemverilogclass 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
| Mode | Contains | Purpose |
|---|---|---|
| Active | Sequencer + Driver + Monitor | Drives and observes the DUT |
| Passive | Monitor only | Observes 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
textTest │ starts ▼ Sequence ──▶ Sequencer ──▶ Driver ──▶ DUT │ Monitor (observes) │ Scoreboard (checks)
- The test creates a sequence and starts it on the sequencer.
- The sequencer passes items to the driver.
- The driver converts transactions to pin-level signals on the DUT.
- The monitor observes the DUT's response.
- 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.