TO Be coming

Oct 29, 2026

UVM Course: Day 1: UVM Architecture — Components, Objects, and a Working FIFO Testbench

Day 1: UVM Architecture — Components, Objects, and a Working FIFO Testbench

Day 1: UVM Architecture — Components, Objects, and a Working FIFO Testbench

A 30-day SystemVerilog/UVM verification series. Today: the skeleton of every UVM testbench, and how to actually build one.

Most UVM tutorials start with a diagram of boxes and arrows. That diagram is correct, but it doesn't answer the question every beginner actually asks: "How do I know what code to write, and in what order?"

This post answers that. We'll build a complete, minimal UVM environment for a synchronous FIFO write port — every file, in the order you'd actually write them — and then run a 10-question quiz to lock it in.


1. The Two Base Classes: Component vs Object

Everything in UVM descends from one of two classes. Getting this distinction right on Day 1 saves you weeks of confusion later.

uvm_component — the structural skeleton. These are created at time zero and live for the entire simulation. They form a hierarchy: test → env → agent → driver/monitor/sequencer. They participate in UVM phases. They have a parent.

uvm_object — the transient data. Transactions, sequences, configuration objects. Created when needed, destroyed after. No parent, no phases. A uvm_sequence_item is a uvm_object.

Aspectuvm_componentuvm_object
LifetimeWhole simulationTransient
HierarchyHas a parent, unique path nameNo hierarchy
PhasesParticipates (build, connect, run…)Does not participate
Constructornew(name, parent)new(name)
Registration macro`uvm_component_utils`uvm_object_utils
Rule of thumb: if it does something for the whole test (drives, monitors, scores), it's a component. If it is something that gets passed around (a transaction, a sequence), it's an object.

2. The DUT We'll Verify

A synchronous FIFO with a write port and a read port, 8 entries deep, 8 bits wide. Nothing fancy — just enough logic to exercise a real UVM environment.

// fifo_sync.sv
module fifo_sync #(parameter DEPTH = 8, WIDTH = 8) (
    input  logic             clk,
    input  logic             rst_n,
    input  logic             wr_en,
    input  logic [WIDTH-1:0] wr_data,
    output logic             full,
    input  logic             rd_en,
    output logic [WIDTH-1:0] rd_data,
    output logic             empty
);
    logic [WIDTH-1:0] mem [0:DEPTH-1];
    logic [$clog2(DEPTH):0] wptr, rptr, count;

    assign full  = (count == DEPTH);
    assign empty = (count == 0);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wptr <= '0; rptr <= '0; count <= '0;
        end else begin
            if (wr_en && !full) begin
                mem[wptr[$clog2(DEPTH)-1:0]] <= wr_data;
                wptr <= wptr + 1;
            end
            if (rd_en && !empty) begin
                rd_data <= mem[rptr[$clog2(DEPTH)-1:0]];
                rptr <= rptr + 1;
            end
            case ({wr_en && !full, rd_en && !empty})
                2'b10: count <= count + 1;
                2'b01: count <= count - 1;
                default: count <= count;
            endcase
        end
    end
endmodule

3. The Creation Order (this is the part nobody tells you)

Write your files in this order. Each one only depends on files you've already written — no forward references, no confusion.

  1. Interface
  2. Transaction
  3. Driver
  4. Monitor
  5. Sequencer
  6. Agent
  7. Scoreboard
  8. Sequence
  9. Environment
  10. Test
  11. Top module

Let's build each one.


4. Interface (fifo_if.sv)

interface fifo_if #(parameter WIDTH = 8) (input logic clk);
    logic             rst_n;
    logic             wr_en;
    logic [WIDTH-1:0] wr_data;
    logic             full;
    logic             rd_en;
    logic [WIDTH-1:0] rd_data;
    logic             empty;

    clocking drv_cb @(posedge clk);
        default input #1step output #1;
        output wr_en, wr_data, rd_en;
        input  full, empty, rd_data;
    endclocking

    clocking mon_cb @(posedge clk);
        default input #1step;
        input wr_en, wr_data, full, rd_en, rd_data, empty;
    endclocking

    modport DRV (clocking drv_cb, input rst_n);
    modport MON (clocking mon_cb, input rst_n);
endinterface

Two clocking blocks, deliberately. The driver drives through drv_cb with a 1ns output skew — this prevents the driver and the DUT from seeing the same clock edge simultaneously, which is the classic source of race conditions in simulation. The monitor samples through mon_cb with #1step input skew, which means it reads signal values just before the edge — the same values the DUT's flip-flops saw.

Common mistake: using a single clocking block for both driving and sampling. It works in simple cases but fails once you have multiple drivers or bidirectional signals. Always split them.

5. Transaction (fifo_item.sv)

class fifo_item extends uvm_sequence_item;
    rand bit [7:0] data;
    rand bit       is_write;
         bit       full_at_time;

    `uvm_object_utils_begin(fifo_item)
        `uvm_field_int(data,         UVM_ALL_ON)
        `uvm_field_int(is_write,     UVM_ALL_ON)
        `uvm_field_int(full_at_time, UVM_ALL_ON)
    `uvm_object_utils_end

    function new(string name = "fifo_item");
        super.new(name);
    endfunction
endclass

is_write and data are rand — the sequence randomizes them. full_at_time is not randomized; it's a passive observation the monitor fills in when it sends the item to the scoreboard. Mixing stimulus fields and observation fields in one item is common and fine, as long as you're disciplined about which is which.


6. Driver (fifo_driver.sv)

class fifo_driver extends uvm_driver #(fifo_item);
    `uvm_component_utils(fifo_driver)

    virtual fifo_if.DRV 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 fifo_if.DRV)::get(this, "", "vif", vif))
            `uvm_fatal("NO_VIF", "driver did not get vif")
    endfunction

    task run_phase(uvm_phase phase);
        vif.drv_cb.wr_en   <= 0;
        vif.drv_cb.wr_data <= 0;
        forever begin
            seq_item_port.get_next_item(req);
            vif.drv_cb.wr_en   <= req.is_write;
            vif.drv_cb.wr_data <= req.data;
            @(vif.drv_cb);
            seq_item_port.item_done();
        end
    endtask
endclass

This is the get_next_item / item_done handshake. The driver asks the sequencer for the next item, drives it onto the pins for one clock, then signals completion. If you ever forget item_done(), the sequencer blocks and your simulation hangs with no error message — one of the most common beginner deadlocks.


7. Monitor (fifo_monitor.sv)

class fifo_monitor extends uvm_monitor;
    `uvm_component_utils(fifo_monitor)

    virtual fifo_if.MON vif;
    uvm_analysis_port #(fifo_item) ap;

    function new(string name, uvm_component parent);
        super.new(name, parent);
        ap = new("ap", this);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual fifo_if.MON)::get(this, "", "vif", vif))
            `uvm_fatal("NO_VIF", "monitor did not get vif")
    endfunction

    task run_phase(uvm_phase phase);
        fifo_item tr;
        forever begin
            @(vif.mon_cb);
            if (vif.mon_cb.wr_en || vif.mon_cb.rd_en) begin
                tr = fifo_item::type_id::create("tr");
                tr.data         = vif.mon_cb.wr_data;
                tr.is_write     = vif.mon_cb.wr_en;
                tr.full_at_time = vif.mon_cb.full;
                ap.write(tr);
            end
        end
    endtask
endclass

The monitor is passive — it drives nothing. It samples the interface every clock and publishes a transaction through its uvm_analysis_port. The analysis port is special: it broadcasts to any number of subscribers (scoreboard, coverage) without blocking. The monitor should never wait on a consumer.


8. Sequencer (fifo_sequencer.sv)

class fifo_sequencer extends uvm_sequencer #(fifo_item);
    `uvm_component_utils(fifo_sequencer)

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
endclass

That's the whole file. The base class already provides the arbitration, the FIFO, and the handshake. You only extend uvm_sequencer when you need custom arbitration (like round-robin between multiple sequences) — for now, the empty subclass is correct.


9. Agent (fifo_agent.sv)

class fifo_agent extends uvm_agent;
    `uvm_component_utils(fifo_agent)

    fifo_driver    drv;
    fifo_monitor   mon;
    fifo_sequencer sqr;

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mon = fifo_monitor::type_id::create("mon", this);

        if (get_is_active() == UVM_ACTIVE) begin
            drv = fifo_driver::type_id::create("drv", this);
            sqr = fifo_sequencer::type_id::create("sqr", this);
        end
    endfunction

    function void connect_phase(uvm_phase phase);
        if (get_is_active() == UVM_ACTIVE)
            drv.seq_item_port.connect(sqr.seq_item_export);
    endfunction
endclass

The agent is a container. The get_is_active() check matters: a passive agent (used when you're reusing a VIP but someone else is driving that interface) only creates a monitor — no driver, no sequencer. You'll hit this a lot on real projects.


10. Scoreboard (fifo_scoreboard.sv)

class fifo_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(fifo_scoreboard)

    uvm_analysis_imp #(fifo_item, fifo_scoreboard) exp_imp;

    int unsigned writes_seen, reads_seen;

    function new(string name, uvm_component parent);
        super.new(name, parent);
        exp_imp = new("exp_imp", this);
    endfunction

    function void write(fifo_item tr);
        if (tr.is_write) writes_seen++;
        else             reads_seen++;
        `uvm_info("SB", $sformatf("writes=%0d reads=%0d", writes_seen, reads_seen), UVM_MEDIUM)
    endfunction

    function void check_phase(uvm_phase phase);
        if (writes_seen == 0 && reads_seen == 0)
            `uvm_error("SB", "no traffic observed")
    endfunction
endclass

Today the scoreboard just counts. On Day 7–9 we'll build a real reference model that predicts what the DUT should have done and compares. But the check_phase is the important structural lesson: it runs after run_phase ends, and it's the right place to assert "no unaccounted data" or "no traffic seen at all."


11. Sequence (fifo_seq.sv)

class fifo_write_seq extends uvm_sequence #(fifo_item);
    `uvm_object_utils(fifo_write_seq)

    function new(string name = "fifo_write_seq");
        super.new(name);
    endfunction

    task body();
        fifo_item tr;
        repeat (20) begin
            tr = fifo_item::type_id::create("tr");
            start_item(tr);
            if (!tr.randomize() with { is_write == 1; })
                `uvm_error("SEQ", "randomize failed")
            finish_item(tr);
        end
    endtask
endclass

start_item / finish_item is the sequence-side handshake that pairs with the driver's get_next_item / item_done. Don't confuse it with sequence.start(), which is what a test calls to launch a whole sequence.


12. Environment (fifo_env.sv)

class fifo_env extends uvm_env;
    `uvm_component_utils(fifo_env)

    fifo_agent      agent;
    fifo_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 = fifo_agent::type_id::create("agent", this);
        sb    = fifo_scoreboard::type_id::create("sb", this);
    endfunction

    function void connect_phase(uvm_phase phase);
        agent.mon.ap.connect(sb.exp_imp);
    endfunction
endclass

The env is where the monitor's analysis port gets connected to the scoreboard's analysis import. This is the only place that should know both exist — that separation is what makes each piece reusable.


13. Test (fifo_test.sv)

class fifo_test extends uvm_test;
    `uvm_component_utils(fifo_test)

    fifo_env env;

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = fifo_env::type_id::create("env", this);
    endfunction

    task run_phase(uvm_phase phase);
        fifo_write_seq seq;
        phase.raise_objection(this);
        seq = fifo_write_seq::type_id::create("seq");
        seq.start(env.agent.sqr);
        phase.drop_objection(this);
    endtask
endclass

The raise_objection / drop_objection pair is how you tell UVM "don't end run_phase until my sequence is done." Without it, run_phase returns immediately and the simulation terminates before the sequence runs. (UVM will eventually time out and warn you, after 9200 seconds by default — not a fun way to spend an afternoon.)


14. Top Module (tb_top.sv)

module tb_top;
    logic clk = 0;
    always #5 clk = ~clk;

    fifo_if fif (clk);

    fifo_sync #(.DEPTH(8), .WIDTH(8)) dut (
        .clk     (clk),
        .rst_n   (fif.rst_n),
        .wr_en   (fif.wr_en),
        .wr_data (fif.wr_data),
        .full    (fif.full),
        .rd_en   (fif.rd_en),
        .rd_data (fif.rd_data),
        .empty   (fif.empty)
    );

    initial begin
        fif.rst_n = 0;
        repeat (2) @(posedge clk);
        fif.rst_n = 1;
    end

    initial begin
        uvm_config_db#(virtual fifo_if.DRV)::set(null, "*", "vif", fif);
        uvm_config_db#(virtual fifo_if.MON)::set(null, "*", "vif", fif);
        run_test();
    end
endmodule

The interface is passed to drivers and monitors through uvm_config_db. Classes can't see module-scope signals directly — this is the only clean way to bridge that gap. Note the two ::set calls: one for the DRV modport, one for MON. The driver and monitor each ::get the flavor they need.


15. How to Run It

With a standard UVM install (Questa, VCS, or Xcelium), the compile-and-run flow is roughly:

vlog -sv fifo_sync.sv fifo_if.sv \
     fifo_item.sv fifo_driver.sv fifo_monitor.sv \
     fifo_sequencer.sv fifo_agent.sv fifo_scoreboard.sv \
     fifo_seq.sv fifo_env.sv fifo_test.sv tb_top.sv

vsim -c tb_top +UVM_TESTNAME=fifo_test -do "run -all; quit"

The +UVM_TESTNAME=fifo_test plusarg tells run_test() which test to instantiate. This is why the test must be registered with `uvm_component_utils — the factory uses that registration to create it by name.


16. Quiz

Ten questions. One at a time in a real interactive system — here they're all shown, but the answers are close enough to the correct option that you have to actually know the material.

Q1. What is the core difference between uvm_component and uvm_object?
  • A) Components can be randomized; objects cannot
  • B) Components are quasi-static structural nodes in a hierarchy; objects are transient data
  • C) Objects participate in UVM phases; components do not
  • D) Components cannot be overridden by the factory
Correct: B. Components form the testbench skeleton and live for the whole simulation. Objects are created, used, and destroyed on demand.
Q2. How many parameters does the constructor of uvm_component take?
  • A) One — just the name
  • B) Two — name and parent
  • C) Three — name, parent, and type
  • D) Determined by the factory at runtime
Correct: B. The parent argument is what establishes hierarchy. uvm_object takes only name.
Q3. Which macro registers a uvm_object-derived class with the factory?
  • A) `uvm_component_utils
  • B) `uvm_object_utils
  • C) `uvm_analysis_imp_decl
  • D) `uvm_info
Correct: B. Using the component macro on an object compiles but fails at factory creation time.
Q4. You set +UVM_TESTNAME=my_test, but my_test was never registered with the factory. What happens?
  • A) UVM falls back to the default test
  • B) A warning is printed and simulation continues
  • C) A fatal error is reported and simulation ends
  • D) An empty test class is auto-generated
Correct: C. run_test() cannot create an unregistered type, so it reports a fatal error.
Q5. A standard UVM agent typically contains which three components?
  • A) Test, env, scoreboard
  • B) Sequencer, driver, monitor
  • C) Config database, factory, phasing
  • D) Sequence, item, port
Correct: B. Sequencer routes stimulus, driver drives pins, monitor observes outputs. A passive agent contains only the monitor.
Q6. Which of these is a transient object?
  • A) uvm_env
  • B) uvm_driver
  • C) uvm_sequence_item
  • D) uvm_test
Correct: C. A sequence item is a data packet: created with the sequence, destroyed after the driver consumes it.
Q7. What does calling run_test() in a top-level initial block actually do?
  • A) Compiles all test files
  • B) Starts the UVM phase mechanism and instantiates the test named by +UVM_TESTNAME
  • C) Generates the clock and reset signals
  • D) Performs a syntax check and exits
Correct: B. run_test() is the UVM entry point — it creates the test via the factory and starts phasing.
Q8. Which statement about component hierarchical paths is correct?
  • A) Every component has a unique path like env.agent.driver
  • B) Path names are fixed at compile time and cannot change
  • C) Objects also have hierarchical paths
  • D) Path names are only used for clock generation
Correct: A. Path names are used for debugging and for targeted config_db lookups. Objects have no path.
Q9. A class extends uvm_component but forgets the `uvm_component_utils macro. What is the consequence?
  • A) Compilation error
  • B) It can still be created with new(), but the factory cannot create it by type name
  • C) The factory auto-registers it on first use
  • D) Simulation runs faster
Correct: B. The registration macro is what makes ::type_id::create() work. Without it, create() by name fails.
Q10. Which component is responsible for passively observing DUT output and broadcasting transactions to the scoreboard?
  • A) Driver
  • B) Sequencer
  • C) Monitor
  • D) Sequence
Correct: C. The monitor is passive — it never drives. It samples the interface and publishes through an analysis port.

What's Next

Day 2 will go deep on UVM phases — the build/connect/run/check/report order, why build_phase is top-down but connect_phase is bottom-up, and how objections actually control simulation end. The FIFO testbench from today will keep growing through the rest of the series.

Series: 30 days of SystemVerilog/UVM verification, project-based.

No comments:

Post a Comment

SiteMap