Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
    • What Is UVM?
    • Testbench Architecture and Environment Composition
      • The Four Layers
      • The Agent as the Reuse Unit
      • The Env Owns the Infrastructure
      • Coordinating Agents
      • Active-Passive Mixes and Composition Debug
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Testbench Architecture and Environment Composition
  1. Topics
  2. UVM Verification
  3. UVM Foundations
  4. Testbench Architecture and Environment Composition
  5. Coordinating Agents

Part 1 · Foundations · Intermediate

Coordinating Agents

A DMA job across two agents: from a test task to a virtual sequence, virtual sequencers versus direct handles, and the ordering bug coordination prevents.

On this page

    A DMA job is a small play in four acts: program KEY and LEN over APB, write GO, stream LEN words on the input, wait for the interrupt and clear STATUS. Two agents, one order. A colleague wants to speed up the random test by starting job 2's register writes while job 1 is still streaming, "since they're on different interfaces". The regression goes red with mismatches that look like data corruption. Nothing in the DUT changed. Who is supposed to own the order across agents?

    The idea: a conductor who plays no instrument

    Each agent's sequencer is a queue for one interface; it knows nothing about the others. A scenario that spans interfaces needs something above them, like a conductor who plays no instrument but tells each section when to come in. In UVM that conductor is a virtual sequence: a sequence that sends no items itself, and instead starts ordinary sequences on the agents' sequencers in the right order.

    A virtual sequence needs handles to those sequencers. It can get them from a virtual sequencer, a component that holds the handles and nothing else, or the test can hand them over directly before starting the sequence. Both are common; the virtual sequencer is older and works well with uvm_declare_p_sequencer, while direct handles avoid an extra component. The bench does the simplest version of all: the test itself conducts, in its run_job task.

    The picture: one job, two agents, one order

    Sequence diagram: the job sequence programs KEY and LEN, writes GO, starts the packet on the stream sequencer, waits for the interrupt, then reads and clears STATUS.
    The ordering lives in one place, the job sequence. Neither agent's sequencer knows the other exists.

    Build it: from a test task to a virtual sequence

    What the bench does today

    The base test's run_job task is a virtual sequence in all but name. It starts one-access APB sequences and a packet sequence on two different sequencers, in a fixed order, and waits on the interrupt between jobs.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    // snippet: from the course DMA bench
    class dma_base_test extends uvm_test;
      `uvm_component_utils(dma_base_test)
      dma_env env;
      virtual irq_if irq_vif;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = dma_env::type_id::create("env", this);
        if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq_vif", irq_vif))
          `uvm_fatal("NOVIF", "irq_if not set for the test")
      endfunction
    
      function void end_of_elaboration_phase(uvm_phase phase);
        uvm_root::get().print_topology();
      endfunction
    
      // Register helpers used by every test.
      task reg_write(bit [7:0] addr, bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 1; s.data = data;
        s.start(env.apb.sqr);
      endtask
    
      task reg_read(bit [7:0] addr, output bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 0; s.data = 0;
        s.start(env.apb.sqr);
        data = s.rdata;
      endtask
    
      // One complete job: program the DMA, stream n words, wait for the interrupt,
      // return STATUS and clear its DONE/ERR bits.
      task run_job(dma_job_cfg job, int unsigned n_words, output bit [31:0] status);
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
        reg_write(KEY, job.key);
        reg_write(LEN, job.len);
        reg_write(CTRL, {30'b0, job.irq_en, 1'b1});
        if (!pkt.randomize() with { n == n_words; })
          `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.in_agt.sqr);
        wait (irq_vif.irq === 1'b1);
        reg_read(STATUS, status);
        reg_write(STATUS, 32'h6);                // write 1 to clear DONE and ERR
      endtask
    endclass

    This is a fine starting point, and many real benches stay this way for block-level work. Its limit is reuse: the scenario lives in a test class, so a chip-level test that wants to run "one DMA job" alongside other traffic cannot start it as a sequence.

    The same job as a virtual sequence

    Moving the task into a sequence makes it something any test, or any other virtual sequence, can start. This version takes sequencer handles directly, so it needs no virtual sequencer component.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // snippet: a DMA job as a virtual sequence (handles set by the starter)
    class dma_job_vseq extends uvm_sequence;
      `uvm_object_utils(dma_job_vseq)
      apb_sequencer  apb_sqr;           // set by the test before start()
      axis_sequencer axis_sqr;
      virtual irq_if irq_vif;
      dma_job_cfg    job;
    
      function new(string name = "dma_job_vseq");
        super.new(name);
      endfunction
    
      task body();
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
        write(KEY, job.key);
        write(LEN, job.len);
        write(CTRL, {30'b0, job.irq_en, 1'b1});                    // GO last
        if (!pkt.randomize() with { n == local::job.len; }) `uvm_fatal("RAND", "pkt")
        pkt.start(axis_sqr, this);                                 // child of this sequence
        wait (irq_vif.irq === 1'b1);
        write(STATUS, 32'h6);                                      // clear DONE and ERR
      endtask
    
      task write(bit [7:0] addr, bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 1; s.data = data;
        s.start(apb_sqr, this);
      endtask
    endclass

    The test sets vseq.apb_sqr = env.apb.sqr, vseq.axis_sqr = env.in_agt.sqr and the interrupt handle, then calls vseq.start(null): a virtual sequence runs on no sequencer of its own. With a virtual sequencer instead, the env copies the same handles into it in connect_phase, and the sequence reaches them through p_sequencer after uvm_declare_p_sequencer. Virtual sequences covers both styles in depth.

    Common pitfall

    Copy sequencer handles only from active agents. A passive agent has no sequencer, so env.out_agt.sqr is null, and starting a sequence on it fails far from the cause. Guard each handle copy with a check, or assert at end_of_elaboration that every handle a virtual sequence needs is non-null.

    Watch it run: jobs in order

    dma_rand_test runs three jobs through run_job, one after another. The times on the [JOB] lines show the ordering at work: each job starts only after the previous one's interrupt and STATUS clear (values from the course's run; they differ by simulator and seed):

    Diagram
    UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
    UVM_INFO dma_tb.sv(731) @ 645000: uvm_test_top [JOB] key=0e564b57 len=1 irq_en=1
    UVM_INFO dma_tb.sv(731) @ 885000: uvm_test_top [JOB] key=5a1793f4 len=3 irq_en=1
    UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched

    Job 1 moves 10 words and takes 645 ns; job 2 moves a single word and the next job starts 240 ns later. Every word of every job is compared, because each job's KEY was written while the DMA was idle.

    What breaks: the key the hardware ignored

    Back to the hook. With job 2's register writes overlapped onto job 1's stream, job 2's KEY write arrives while the DMA is busy. The DUT ignores it, exactly as its spec says (The DMA you will verify lists "KEY and LEN ignored while busy" as a testplan row). The scoreboard does not: its write_apb updates its key on every observed KEY write. So the remaining words of job 1 are predicted with job 2's key, and every one of them mismatches. It looks like data corruption in the DUT; it is a coordination bug in the test, exposed by a gap in the scoreboard's model.

    Two fixes, at two layers. The scenario fix is ordering: a job's register writes wait until the previous job has finished, which the job sequence enforces by waiting for the interrupt and clearing STATUS before returning. The model fix is to make the scoreboard as strict as the hardware, ignoring KEY writes while it believes a job is in flight. A strong bench does both, because the second fix is what lets a later test write KEY mid-job on purpose to verify that the DUT ignores it.

    Interview

    "What is a virtual sequence, and do you need a virtual sequencer?"

    A weak answer: "A virtual sequence runs on a virtual sequencer and controls other sequencers." Half right, and it implies the sequencer is mandatory.

    A strong answer: "A virtual sequence coordinates several interfaces: it sends no items itself, it starts ordinary sequences on each agent's sequencer in the right order. It needs handles to those sequencers. A virtual sequencer is one way to provide them, with p_sequencer giving typed access; setting the handles directly before start(null) is another and avoids a component. Either way, guard against passive agents, whose sequencer is null."

    Expect the follow-ups: Why start child sequences with the virtual sequence as parent? (so they inherit its context, priorities and locks) and What calls never appear in a virtual sequence? (start_item and finish_item: it starts sequences, it does not send items).

    Check yourself A virtual sequence forks the APB programming and the stream packet in parallel for a single job. Does the DMA lose data?

    Usually not, and the scoreboard is safe too. Until GO is written the DMA is not busy, so s_tready stays low and the stream driver waits with TVALID high; the input monitor publishes a word only when TVALID and TREADY are both high, which cannot happen before GO, and GO is the last APB write. But that safety comes from the DUT's handshake, not from the sequence. Reorder the APB writes, or verify a variant that buffers input before GO, and the scoreboard can predict with a stale key. Programming first, then streaming, is clearer and matches how software drives the block.

    Check yourself A chip-level test makes the DMA's APB agent passive (the CPU drives APB) and runs dma_job_vseq unchanged. What happens?

    apb_sqr is null, so the first write fails when it starts apb_rw_seq on it. At chip level the register programming has to come from the real CPU (software or a CPU model), and the virtual sequence must be restructured or replaced. This is why handle copies need guards and why virtual sequences should state which agents they require.

    Check yourself Why does dma_job_vseq start its child sequences with this as the parent (s.start(apb_sqr, this))?

    So each child runs in the context of the virtual sequence: it inherits its priority, appears under it in sequence traces, and is allowed through if the virtual sequence holds a lock or grab on that sequencer. Started without a parent, the child would compete as an unrelated sequence and could be blocked by its own parent's lock.

    Next: that last question, the same env with some agents passive, is the subject of Active-passive mixes and composition debug.

    Related topics

    • Active-passive mixes and composition debug

    • Virtual sequences

    • p_sequencer

    Previous

    17 / 280

    Next: Active-Passive Mixes and Composition Debug

    Up next

    Up nextActive-Passive Mixes and Composition Debug

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Four Layers
    • The Agent as the Reuse Unit