TO Be coming

Nov 11, 2026

RTL and VLSI Learning Hub - Main Page

Complete Sitemap 22 Topics

RTL Digital Design
Resource Map

Every VLSI, Verilog, Digital Design, Protocol & career topic in one colourful index. Pick a card, jump straight to the article, and follow along.

150+Articles
22Sections
FreeAlways

Here is the blog site map. Feel free to post your feedback to improve it further.

Insta link to follow — MY Insta Profile — Follow me for more updates

Test Yourself VLSI Quiz Hub Digital Design · UPF · Protocols · STA — Sharpen your skills with fresh quizzes

1. Introduction

2 links
Latest VLSI Topics Click to explore all the newest VLSI articles in one place

3. Digital Design

28 links

4. Interface / Bus Protocols

7 links

5. Verilog Tutorial / Crash Course

12 links

6. RTL Design Code

20 links

7. Clock Domain Crossing (CDC)

3 links

8. RTL Lint

2 links

9. STA (Static Timing Analysis)

2 links

10. Synthesis

4 links

11. IC Fabrication

1 link

12. AHB-AXI Protocol

2 links

13. PCIe Protocol

4 links

14. Solution — Palnitkar Guide

1 link

16. DAA (Design & Analysis of Algorithms)

4 links

17. Scripting / Others

4 links

18. Interview Preparation

6 links

19. Academic / Educational Projects

4 links

20. Verification

1 link

21. Physical Design

4 links

22. General Questions & VLSI Quiz

12 links

23. Non-VLSI Topics

17 links

Enjoyed this map?

Follow for fresh VLSI, Verilog & semiconductor updates every week.

Follow on Instagram

UVM Course : Day 9: Advanced Scoreboards — Two-Import Pattern, Out-of-Order Comparison, and uvm_tlm_analysis_fifo

Day 9: Advanced Scoreboards — Two-Import Pattern, Out-of-Order Comparison, and uvm_tlm_analysis_fifo
Advertisement
Top banner — drop your 728×90 or responsive ad unit here.

Day 9: Advanced Scoreboards — Two-Import Pattern, Out-of-Order Comparison, and uvm_tlm_analysis_fifo

A 30-day SystemVerilog/UVM verification series. The FIFO scoreboard was easy because writes and reads looked different. Real protocols don't. Today the scoreboard gets serious.

The scoreboard from Days 4 and 7 had it easy. Each transaction carried both wr_en and rd_en, so a single write() function could look at the flags and decide which side to update. That works for a FIFO. It falls apart on any real protocol, because:

  • Write and read transactions often arrive on different interfaces, from different monitors.
  • The transaction types are often different classes — a write command versus a read response, not two variants of one item.
  • The DUT can reorder transactions. Two reads issued in order may come back out of order, and the scoreboard must still match them.
  • The comparison can be slow — a reference model, a memory lookup, a set of protocol assertions — and it must not slow the monitor.

Today's four mechanisms solve those four problems. They compose into the standard advanced-scoreboard architecture you will find in every mature verification environment.


1. The Two-Import Pattern

When two different monitors publish to the same scoreboard, the scoreboard cannot just define one write() function. UVM provides the `uvm_analysis_imp_decl macro to generate distinct import types, each with its own write_() method:

`uvm_analysis_imp_decl(_wr)
`uvm_analysis_imp_decl(_rd)

class fifo_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(fifo_scoreboard)

    uvm_analysis_imp_wr #(fifo_tx, fifo_scoreboard) wr_export;
    uvm_analysis_imp_rd #(fifo_tx, fifo_scoreboard) rd_export;

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

    function void write_wr(fifo_tx tr);
        // called only by wr_export
    endfunction

    function void write_rd(fifo_tx tr);
        // called only by rd_export
    endfunction
endclass

In the env, connect each monitor to the correct import:

wr_agent.mon.ap.connect(sb.wr_export);
rd_agent.mon.ap.connect(sb.rd_export);

Now the scoreboard has no ambiguity. It knows which side each transaction came from without inspecting the transaction fields. If a third monitor appears (say, a coverage collector), it gets a third import or a third `uvm_analysis_imp_decl.

Why not just define two write() functions? Because both would have the same signature and the same name — the compiler can't distinguish them, and UVM's analysis port calls a single known method. The macro's whole job is to produce a distinct method name per import type.

2. uvm_tlm_analysis_fifo — Decoupling Producer and Consumer

Remember the Day 4 warning: write() is called synchronously by the publisher. If write() does a comparison that takes hundreds of nanoseconds of simulation time, the monitor slows down and starts missing transactions. For a simple scoreboard, that does not matter because comparison is cheap. For a reference model or a slow checker, it matters enormously.

The fix is a buffer between the monitor and the scoreboard: uvm_tlm_analysis_fifo. It has an analysis import on one side and a blocking get on the other. The monitor pushes transactions in; a separate thread in the scoreboard pulls them out at whatever pace it likes. Nothing on the producer side ever blocks.

class fifo_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(fifo_scoreboard)

    uvm_tlm_analysis_fifo #(fifo_tx) wr_fifo;
    uvm_tlm_analysis_fifo #(fifo_tx) rd_fifo;

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

    virtual task run_phase(uvm_phase phase);
        fork
            process_writes();
            process_reads();
        join
    endtask

    task process_writes();
        fifo_tx tr;
        forever begin
            wr_fifo.get(tr);       // blocking pull, in its own thread
            // slow comparison — safe here, no monitor is waiting
            $display("got write: %0h", tr.wdata);
        end
    endtask

    task process_reads();
        fifo_tx tr;
        forever begin
            rd_fifo.get(tr);
            $display("got read:  %0h", tr.rdata);
        end
    endtask
endclass

In the env, connect to the fifo's analysis export rather than the scoreboard directly:

wr_agent.mon.ap.connect(sb.wr_fifo.analysis_export);
rd_agent.mon.ap.connect(sb.rd_fifo.analysis_export);
When to reach for analysis fifo: when the scoreboard must do anything more expensive than push-and-pop — reference models, memory models, protocol assertions, file I/O. When comparison is trivial (as in the FIFO), a direct uvm_analysis_imp is simpler and equally correct.

3. Out-of-Order Comparison

When the DUT can return reads in a different order than they were issued, an in-order shadow queue is wrong. There are two standard answers:

3.1 Sorted comparison by transaction ID

Every read carries a tag or ID. The scoreboard stores the expected result in an associative array keyed by ID. When the observed read arrives, the scoreboard looks up the ID and compares:

fifo_tx expected_by_id [bit [7:0]];

function void write_exp(fifo_tx tr);
    expected_by_id[tr.id] = tr;
endfunction

function void write_obs(fifo_tx tr);
    fifo_tx expected;
    if (!expected_by_id.exists(tr.id)) begin
        `uvm_error("SB", $sformatf("unexpected ID %0h", tr.id))
        return;
    end
    expected = expected_by_id[tr.id];
    expected_by_id.delete(tr.id);
    if (tr.rdata !== expected.rdata)
        `uvm_error("SB", $sformatf("ID %0h mismatch: exp %0h got %0h",
                  tr.id, expected.rdata, tr.rdata))
endfunction

This handles arbitrary reordering and is the most common pattern in bus protocols with transaction IDs (AXI, PCIe, and many custom buses).

3.2 Two analysis fifos, compared in arrival order

If the DUT is in-order but the two sides (expected and observed) arrive at different simulation times, use two uvm_tlm_analysis_fifos and a third thread that pulls one from each and compares:

task process_writes();
    fifo_tx exp_tr;
    forever begin
        wr_fifo.get(exp_tr);
        compare_one(exp_tr);
    end
endtask

task compare_one(fifo_tx exp_tr);
    fifo_tx obs_tr;
    rd_fifo.get(obs_tr);      // blocking; waits for the matching read
    if (obs_tr.rdata !== exp_tr.wdata)
        `uvm_error("SB", $sformatf("mismatch: exp %0h got %0h",
                  exp_tr.wdata, obs_tr.rdata))
endtask
The deadlock trap: if the DUT drops a read (never issues it), rd_fifo.get(obs_tr) blocks forever, the scoreboard never returns, the objection is never dropped, and the test hangs. Either (a) wrap the get in a timeout with uvm_wait_for_nba_region or a fork/join_any with a #timeout branch, or (b) rely on check_phase to catch the leftover expected reads and trust the run to end via objections from the test. For a real project, choose the timeout — the "hang forever" failure mode wastes far more time than a "test timed out" failure.

4. Reference Model Integration

For anything more complex than a FIFO, the expected value is not the input value. It's the input value transformed by the DUT's specification. That transform lives in a reference model — sometimes called a predictor or a golden model.

The reference model is a component that receives the stimulus side and publishes the expected side:

class fifo_ref_model extends uvm_component;
    `uvm_component_utils(fifo_ref_model)

    uvm_analysis_imp #(fifo_tx, fifo_ref_model) in_imp;
    uvm_analysis_port #(fifo_tx) out_ap;

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

    function void write(fifo_tx tr);
        fifo_tx predicted = fifo_tx::type_id::create("predicted");
        // model the DUT's transform — e.g., invert, add one, apply a mask
        predicted.wr_en = tr.wr_en;
        predicted.wdata = ~tr.wdata;
        out_ap.write(predicted);
    endfunction
endclass

The env then wires three things together: the driver's analysis port (or a copy of the stimulus) into the model, the model's output into the scoreboard's expected side, and the monitor's output into the scoreboard's observed side.

// Env connect_phase
drv.ap.connect(model.in_imp);
model.out_ap.connect(sb.wr_export);
mon.ap.connect(sb.rd_export);

The scoreboard does not know the reference model exists. It just compares two streams. That is the point: each stage knows only its input and its output.

Where do expected transactions come from? Two schools. (a) From the sequence: the driver publishes whatever it drove, and the reference model transforms it. This catches driver bugs. (b) From the monitor's observed write side: the monitor publishes writes, and the model transforms them into expected reads. This is simpler but misses driver bugs. For a real project, (a) — it catches more.

5. Putting It All Together

Here is the full advanced scoreboard, in the "expected by ID, reference model output, buffered on both sides" pattern that real projects use:

`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_obs)

class fifo_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(fifo_scoreboard)

    uvm_analysis_imp_exp #(fifo_tx, fifo_scoreboard) exp_export;
    uvm_analysis_imp_obs #(fifo_tx, fifo_scoreboard) obs_export;

    fifo_tx           expected [bit [7:0]];
    int unsigned      num_exp, num_obs, num_mismatch, num_orphan;

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

    function void write_exp(fifo_tx tr);
        if (expected.exists(tr.id)) begin
            `uvm_error("SB", $sformatf("duplicate expected ID %0h", tr.id))
            num_mismatch++;
            return;
        end
        expected[tr.id] = tr;
        num_exp++;
    endfunction

    function void write_obs(fifo_tx tr);
        fifo_tx e;
        if (!expected.exists(tr.id)) begin
            `uvm_error("SB", $sformatf("unexpected observed ID %0h", tr.id))
            num_orphan++;
            return;
        end
        e = expected[tr.id];
        expected.delete(tr.id);
        num_obs++;
        if (tr.rdata !== e.rdata) begin
            num_mismatch++;
            `uvm_error("SB", $sformatf("ID %0h mismatch: exp %0h got %0h",
                      tr.id, e.rdata, tr.rdata))
        end
    endfunction

    function void check_phase(uvm_phase phase);
        if (num_exp == 0 && num_obs == 0)
            `uvm_error("SB", "no traffic observed")
        if (num_mismatch)
            `uvm_error("SB", $sformatf("%0d mismatches", num_mismatch))
        if (num_orphan)
            `uvm_error("SB", $sformatf("%0d orphan observed transactions", num_orphan))
        if (expected.size())
            `uvm_error("SB", $sformatf("%0d expected transactions never observed",
                      expected.size()))
    endfunction
endclass

Notice the four checks in check_phase: traffic existed, no mismatches, no orphans, and no leftovers. Those four cover the standard failure modes of any scoreboard. If any of them is missing, a real bug can slip through silently.


6. Common Advanced Scoreboard Bugs

SymptomCause
Scoreboard crashes because write() is defined twiceForgot `uvm_analysis_imp_decl, or used the wrong suffix in the export type
Simulation slows down after adding a checkerSlow work done inside write() instead of in a pulled thread via uvm_tlm_analysis_fifo
Test hangs at endScoreboard thread blocked on get for a transaction that never came
Scoreboard passes a broken DUTOnly checks mismatches, not orphans or leftovers; a dropped transaction looks like success
Out-of-order DUT always reports mismatchIn-order shadow queue used on a reordering DUT
Reference model checks pass even when the driver is wrongExpected data came from the monitor instead of from the driver

7. What Day 10 Will Add

Tomorrow: Functional Coverage. A scoreboard tells you the DUT did the right thing on the transactions it saw. Coverage tells you which transactions it didn't see. That is the difference between "the test passed" and "the test passed and it exercised what I meant it to." You will write covergroups, coverpoints, and crosses, and learn how to tie them to a verification plan so closure is measurable.


8. Quiz

Ten questions on the two-import pattern, uvm_tlm_analysis_fifo, out-of-order comparison, and reference models. Options shuffled per load.

Question 1 of 10
Loading…
    0 / 10
    Advertisement
    Bottom banner — drop your 728×90 or responsive ad unit here.

    Coming Up Next: Day 10 — Functional Coverage

    • Covergroups and coverpoints — how to declare what you meant to exercise, not just what happened to run.
    • Cross coverage — the joint conditions that catch real bugs, and why single coverpoints give false confidence.
    • Sampling — where and when coverage is sampled, and why sampling at the wrong time silently reports zero hits.
    • Tying to a verification plan — mapping each coverpoint to a line in the plan so coverage closure means something concrete.
    ← Back to the 30-Day UVM Course
    See all 30 days, week by week.

    Series: 30 days of SystemVerilog/UVM verification. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 put it in motion. Day 4 wired the plumbing. Day 5 installed the knobs. Day 6 made it modular. Day 7 made it yours. Day 8 made it coordinated. Day 9 made it rigorous.

    Nov 8, 2026

    UVM Course :30-Day SystemVerilog/UVM Verification Series — All Posts

    30-Day SystemVerilog/UVM Verification Series — All Posts

    30-Day SystemVerilog/UVM Verification Series

    A project-based learning path from a working testbench skeleton to a complete, coverage-closed verification environment.

    Every day builds on the previous one. The FIFO testbench from Day 1 keeps growing throughout the series. Each post follows the same structure: Example → Minor Details → Advantages → Disadvantages/Pitfalls → Implementation Details → 10-Question Quiz.

    ✅ 7 of 30 days published  ·  ⏳ 23 to go
    How to use this page: Replace each href="#" with the actual URL of that day's post on your blog. The cards are already styled and linked — you just drop in the URLs.

    Week 1 — Foundations

    Day 1 UVM Architecture — Components, Objects, and the FIFO Testbench Component vs object, the full FIFO testbench skeleton, and the exact order to write files in. Day 2 UVM Phases — The Simulation Lifecycle Full phase order, why build is top-down and connect is bottom-up, objections, cleanup phases, and timeout patterns. Day 3 Sequences, Sequencer, Driver — The Stimulus Handshake start_item/finish_item vs get_next_item/item_done, why randomize goes after start_item, and deadlock patterns. Day 4 Transactions and TLM — How Data Moves Between Components Analysis ports vs point-to-point, port→import direction, reference vs copy semantics, and building a real scoreboard. Day 5 uvm_config_db and the Factory set/get path rules, type_id::create() vs new(), and type and instance overrides for per-test configuration. Day 6 Agents and Environment Layering Active vs passive agents, get_is_active(), env composition, and reusable VIP patterns. Day 7 Week 1 Review + Mini-Project Assemble a working FIFO verification environment with write sequence, read sequence, and scoreboard comparison.

    Week 2 — Checking and Coverage

    Day 8 Monitors in Depth Passive observation, #1step clocking-block sampling, publishing to multiple subscribers, monitor reuse. Day 9 Scoreboards and Reference Models Shadow models, expected-vs-observed comparison, queue-based architectures, ordering checks. Day 10 Functional Coverage covergroup, coverpoint, cross, bins, sample(), and coverage-driven verification basics. Day 11 Coverage Closure Coverage holes, illegal bins, ignore_bins, bin weighting, coverage reports, and when coverage is "enough." Day 12 Assertions (SVA) Immediate vs concurrent assertions, assert property, bind constructs, and using assertions inside monitors. Day 13 Assertion Patterns Handshake assertions, overflow/underflow checks, liveness vs safety properties in practice. Day 14 Week 2 Review + Mini-Project Add coverage and assertions to the FIFO environment; close a specific coverage hole end to end.

    Week 3 — Advanced UVM

    Day 15 Virtual Sequences Coordinating stimulus across multiple agents, virtual sequencers, and when to reach for them. Day 16 Sequence Layering Composing small sequences into complex scenarios, do macros, sequence libraries, reusable scenarios. Day 17 Arbitration and Grab/Lock Sequencer arbitration policies, grab() vs lock(), priority-based stimulus, and avoiding starvation. Day 18 uvm_config_db Deep Dive Hierarchical set/get, wildcards, config objects vs scalar values, and common path bugs. Day 19 Factory Overrides in Practice Running the same testbench against DUT or golden model, swapping monitors per test, override timing pitfalls. Day 20 Testbench Reuse and VIP Passive agents for integration, port-level VIP vs protocol VIP, and adapter patterns. Day 21 Week 3 Review + Mini-Project Multi-agent environment with a virtual sequence coordinating a FIFO plus a separate read-back check.

    Week 4 — Methodology and Scale

    Day 22 Register Abstraction Layer (RAL) uvm_reg, uvm_reg_block, uvm_reg_map, and frontdoor vs backdoor access. Day 23 RAL in Practice Register model integration, uvm_reg_adapter, auto-prediction, and register tests. Day 24 Memory Models uvm_mem, sparse memory, memory-aware sequences, and memory access policies. Day 25 Reporting and Messaging uvm_info/warning/error/fatal, verbosity levels, custom report servers, and log formatting. Day 26 Phasing Edge Cases Runtime sub-phase interactions, phase jumping, phase timeouts, and run_phase vs sub-phases. Day 27 Debugging UVM Factory debug, config_db tracing, print_topology(), isolating hangs, common deadlock patterns. Day 28 Regression and Test Management Test selection, +UVM_TESTNAME strategies, regression result parsing, and coverage across regression.

    Week 5 — Capstone

    Day 29 Capstone Part 1 — Verification Plan and Environment Design a verification plan for a multi-port FIFO with back-pressure; build the UVM environment from scratch. Day 30 Capstone Part 2 — Coverage Closure and Regression Sequence library, functional coverage closure, assertions, RAL integration, regression run, and final review.

    30 days of SystemVerilog/UVM verification.

    UVM Course : Day 8: Virtual Sequences and Virtual Sequencers — Coordinating Multiple UVM Agents

    Day 8: Virtual Sequences and Virtual Sequencers — Coordinating Multiple UVM Agents
    Advertisement
    Top banner — drop your 728×90 or responsive ad unit here.

    Day 8: Virtual Sequences and Virtual Sequencers — Coordinating Multiple UVM Agents

    A 30-day SystemVerilog/UVM verification series. Week 1 built a working testbench. Week 2 makes it smart. Today the testbench starts driving behaviour, not just stimulus.

    Yesterday you had a working FIFO environment with two agents. Each could run a sequence. But they ran them independently — the write sequence did not know the read sequence existed, and there was no way to say "write three items, then read two, then verify." That is the gap virtual sequences fill.

    A virtual sequence is a sequence that does not drive any interface itself. Its job is to start sub-sequences on the real sequencers of the real agents, in whatever order, at whatever time, the test dictates. That is how real testbenches produce coordinated stimulus across a bus, a memory, and an interrupt line — behaviour, not just data.


    1. Why a Plain Sequence Isn't Enough

    A sequence's body() calls start_item() and finish_item(). Those calls go through one sequencer, which forwards to one driver. That is perfect for "send a write to the write agent," and useless for "send a write to the write agent, wait for it to land, then send a read to the read agent."

    You could, of course, write two sequences and start them from the test with a delay between the start() calls. That works exactly once. It has no way to react to a full condition, no way to wait on a signal from the read side, and no way to be reused in another test without copy-pasting the delay.

    A virtual sequence fixes this by removing the sequence from the interface altogether. The virtual sequence:

    • Has no seq_item_port of its own — it does not talk to any driver.
    • Holds a handle to a virtual sequencer, which in turn holds handles to the real sequencers.
    • Starts sub-sequences on the real sequencers, waiting between them however it likes.

    2. The Virtual Sequencer

    A virtual sequencer is a plain uvm_sequencer. Its only job is to hold the handles to the real sequencers so that a virtual sequence can find them.

    class fifo_vseqr extends uvm_sequencer;
        `uvm_component_utils(fifo_vseqr)
        fifo_sequencer wr_sqr;
        fifo_sequencer rd_sqr;
        function new(string name, uvm_component parent);
            super.new(name, parent);
        endfunction
    endclass

    That is the entire class. There is nothing to override, nothing to build in build_phase. The env is what fills in the handles, and it can only do so after both agents exist — so in connect_phase:

    class fifo_env extends uvm_env;
        `uvm_component_utils(fifo_env)
        fifo_agent      wr_agent;
        fifo_agent      rd_agent;
        fifo_scoreboard sb;
        fifo_vseqr      vseqr;
        function new(string name, uvm_component parent);
            super.new(name, parent);
        endfunction
        virtual function void build_phase(uvm_phase phase);
            super.build_phase(phase);
            wr_agent = fifo_agent::type_id::create("wr_agent", this);
            rd_agent = fifo_agent::type_id::create("rd_agent", this);
            sb       = fifo_scoreboard::type_id::create("sb", this);
            vseqr    = fifo_vseqr::type_id::create("vseqr", this);
        endfunction
        virtual function void connect_phase(uvm_phase phase);
            super.connect_phase(phase);
            // Observation path
            wr_agent.mon.ap.connect(sb.exp_imp);
            rd_agent.mon.ap.connect(sb.exp_imp);
            // Virtual sequencer handles — only for active agents
            if (wr_agent.get_is_active() == UVM_ACTIVE)
                vseqr.wr_sqr = wr_agent.sqr;
            if (rd_agent.get_is_active() == UVM_ACTIVE)
                vseqr.rd_sqr = rd_agent.sqr;
        endfunction
    endclass
    Why the get_is_active() guard? In passive mode the agent has no sequencer, so wr_agent.sqr is null. Assigning it unconditionally leaves the virtual sequencer holding a null handle, which then crashes the first virtual sequence that tries to start on it. Guarding means a virtual sequence can safely null-check p_sequencer.wr_sqr and skip that sub-sequence.

    3. `uvm_declare_p_sequencer — The Bridge

    A virtual sequence needs to reach the virtual sequencer's fields. UVM provides the macro `uvm_declare_p_sequencer to do exactly that: it declares a typed p_sequencer handle inside the sequence and casts the running sequencer to that type when start() is called.

    class fifo_write_then_read_seq extends uvm_sequence #(fifo_tx);
        `uvm_object_utils(fifo_write_then_read_seq)
        `uvm_declare_p_sequencer(fifo_vseqr)
        fifo_write_seq wseq;
        fifo_read_seq  rseq;
        function new(string name = "fifo_write_then_read_seq");
            super.new(name);
        endfunction
        virtual task body();
            // Write phase
            if (p_sequencer.wr_sqr != null) begin
                wseq = fifo_write_seq::type_id::create("wseq");
                wseq.num_items = 4;
                wseq.start(p_sequencer.wr_sqr);
            end
            // Read phase
            if (p_sequencer.rd_sqr != null) begin
                rseq = fifo_read_seq::type_id::create("rseq");
                rseq.num_items = 4;
                rseq.start(p_sequencer.rd_sqr);
            end
        endtask
    endclass
    A note about "wait for drain." A sequence does not normally have a clock handle. The clean way to wait is either (a) put the drain delay into a uvm_event that the monitor signals when it publishes a matching transaction, or (b) let the scoreboard's check_phase assert that every write eventually has a matching read. The safest pattern in a beginner testbench is to make the read sequence longer than the write sequence — drain happens automatically as the read sequence keeps pulling. Do not put repeat (4) @(posedge clk) with a hardcoded clock handle into a sequence; it breaks reusability.

    4. The Sub-Sequences

    Virtual sequences start sub-sequences. Those are normal uvm_sequence classes, each bound to one sequencer. Here is a concrete write sequence:

    class fifo_write_seq extends uvm_sequence #(fifo_tx);
        `uvm_object_utils(fifo_write_seq)
        rand int unsigned num_items = 10;
        function new(string name = "fifo_write_seq");
            super.new(name);
        endfunction
        virtual task body();
            repeat (num_items) begin
                `uvm_do_with(req, { req.wr_en == 1; req.rd_en == 0; })
            end
        endtask
    endclass
    class fifo_read_seq extends uvm_sequence #(fifo_tx);
        `uvm_object_utils(fifo_read_seq)
        rand int unsigned num_items = 10;
        function new(string name = "fifo_read_seq");
            super.new(name);
        endfunction
        virtual task body();
            repeat (num_items) begin
                `uvm_do_with(req, { req.wr_en == 0; req.rd_en == 1; })
            end
        endtask
    endclass

    Those are the sequences you already had in Week 1. The virtual sequence is what makes them compose.


    5. The Test Starts the Virtual Sequence

    Nothing about the test's structure changes. The base test still creates the env, still raises and drops objection in run_phase, and still calls start(). The only difference is which sequencer it starts on and which sequence class it creates.

    class fifo_base_test extends uvm_test;
        `uvm_component_utils(fifo_base_test)
        fifo_env env;
        function new(string name, uvm_component parent);
            super.new(name, parent);
        endfunction
        virtual function void build_phase(uvm_phase phase);
            super.build_phase(phase);
            env = fifo_env::type_id::create("env", this);
        endfunction
        virtual task run_phase(uvm_phase phase);
            fifo_write_then_read_seq vseq;
            phase.raise_objection(this);
            vseq = fifo_write_then_read_seq::type_id::create("vseq");
            vseq.start(env.vseqr);   // start on the virtual sequencer
            phase.drop_objection(this);
        endtask
    endclass

    Note vseq.start(env.vseqr). That is the only line that differs from Day 7.


    6. When You Don't Need a Virtual Sequencer

    Virtual sequencers earn their keep when a test must coordinate two or more active agents. They are overhead when:

    • Only one agent is active. Start the sequence directly on that agent's sequencer. No virtual layer.
    • The test is not coordinating stimulus — for example, a scoreboard-only test that just runs the passive monitors against a preloaded DUT.
    • The DUT has only one interface. A virtual sequence adds indirection with no payoff.
    Rule of thumb: if a sequence's body() would call start() on more than one sequencer, make it virtual. If it would call start() on exactly one, keep it a normal sequence.

    7. Common Virtual Sequence Bugs

    SymptomCause
    Null-handle crash inside body()Virtual sequencer field never assigned in env's connect_phase
    p_sequencer is nullstart() was called on the wrong sequencer type, so the cast inside `uvm_declare_p_sequencer returned null
    Sub-sequence runs but nothing drivesSub-sequence started on a passive agent's (nonexistent) sequencer
    Read sub-sequence hangs foreverWrite sub-sequence didn't produce data, so the FIFO stays empty and the read waits
    Test ends before anything happensObjection dropped before vseq.start() returned
    Simulation runs foreverVirtual sequence forgot to raise or drop an objection, or the sub-sequence deadlocks on a missing handshake

    8. What Day 9 Will Add

    Tomorrow: Advanced Scoreboards. The FIFO scoreboard worked because write and read were distinguishable by the enable bit. Real protocols don't give you that. You will learn the two-import pattern, out-of-order comparison, and uvm_tlm_analysis_fifo — the buffering component that decouples the monitor from the scoreboard so a slow comparison never blocks a fast DUT.


    9. Quiz

    Sixteen questions on virtual sequences, virtual sequencers, p_sequencer, and running the same testbench on Synopsys VCS, Siemens Questa, and Cadence Xcelium. Options shuffled per load. Submit reveals all answers with explanations.

    Question 1 of 16
    Loading…
      0 / 16

      10. Run This Day on Synopsys VCS, Siemens Questa, or Cadence Xcelium

      The virtual sequence code from today — uvm_declare_p_sequencer, p_sequencer handles, sub-sequence start() calls — is identical on every major simulator, because UVM is an IEEE 1800.2 standard class library that Synopsys VCS, Siemens EDA's Questa, and Cadence's Xcelium all ship. Only the compile and run flow differs:

      SimulatorCompileRun
      Synopsys VCSvcs -full64 -sverilog -ntb_opts uvm-1.2 fifo_tb_top.sv./simv +UVM_TESTNAME=fifo_base_test
      Siemens Questavlog -sv -L mtiUvm +incdir+$UVM_HOME/src $UVM_HOME/src/uvm_pkg.sv fifo_tb_top.svvsim -L mtiUvm fifo_tb_top +UVM_TESTNAME=fifo_base_test
      Cadence Xceliumxrun -uvm -sv fifo_tb_top.sv +UVM_TESTNAME=fifo_base_test (single-step compile and run)
      Portable means portable. A virtual sequence that runs on the Synopsys VCS simulator runs unchanged on Questa and Xcelium. If behaviour differs between tools, suspect a race or an uninitialized signal in the testbench — not the UVM library. Use the native debuggers to step through the sequencer–driver handshake: Verdi with VCS, Questa Visualizer with the Questa simulator, or SimVision with Cadence Xcelium.
      Advertisement
      Bottom banner — drop your 728×90 or responsive ad unit here.

      Coming Up Next: Day 9 — Advanced Scoreboards

      • Two-import pattern — separate wr_exp and rd_exp imports for scoreboards where writes and reads are not distinguishable from the transaction alone.
      • Out-of-order comparison — how to compare when the DUT reorders reads, using uvm_tlm_analysis_fifo to buffer expected vs observed.
      • Analysis fifo — the standard component for decoupling a slow scoreboard from a fast monitor.
      • Reference model integration — chaining a model between the monitor and the scoreboard to produce expected transactions from observed ones.
      ← Back to the 30-Day UVM Course
      See all 30 days, week by week.

      Series: 30 days of SystemVerilog/UVM verification. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 put it in motion. Day 4 wired the plumbing. Day 5 installed the knobs. Day 6 made it modular. Day 7 made it yours. Day 8 made it coordinated.

      SiteMap