How to build UVM Environment Part – 2
In short: This is part two of building a full UVM environment for the pipe design. Here we finish the driver so it actually wiggles the pins, add the sequencer, and write several sequences: a single packet, many packets, and a directed case. We also explain the handshake between a sequence and the driver, so you understand what get_next_item and item_done really do. All code follows the IEEE 1800.2 rules.
In part one we understood the pipe design, wrote its interface, built the data_packet transaction, and set up the driver skeleton. Now we make stimulus flow. By the end of this part, a sequence will create packets and the driver will drive them onto the pins.
A simple way to picture it
Think of a coffee shop at the counter. The customer writing an order is the sequence. The person at the till who lines up the orders and hands them to the barista one at a time is the sequencer. The barista who reads one order and makes the drink is the driver. The barista does not start the next drink until the current one is done. That one at a time handoff is exactly the sequence to driver handshake we build here.

The sequencer
The sequencer controls the flow of sequences to the driver. What is striking is how little you write: the whole class is a few lines, because the library does the hard part. You parameterise uvm_sequencer with your transaction type and you are done.
class pipe_sequencer extends uvm_sequencer #(data_packet);
`uvm_component_utils(pipe_sequencer)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclassFinishing the driver
In part one the driver fetched its interface. Now we fill in the run phase so it drives real activity. The pattern is a forever loop with three steps: ask for the next item, drive it onto the pins, then report that it is done. Those three calls are the heart of every UVM driver.
class pipe_driver extends uvm_driver #(data_packet);
`uvm_component_utils(pipe_driver)
virtual pipe_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual pipe_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "virtual interface not set for pipe_driver")
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
data_packet tr;
seq_item_port.get_next_item(tr); // 1. ask the sequencer
drive_item(tr); // 2. drive the pins
seq_item_port.item_done(); // 3. tell it we are done
end
endtask
virtual task drive_item(data_packet tr);
// wait for any requested delay between packets
repeat (tr.delay) @(posedge vif.clk);
@(posedge vif.clk);
vif.cf <= tr.cf;
vif.enable <= tr.enable;
vif.data_in1 <= tr.data_in1;
vif.data_in2 <= tr.data_in2;
@(posedge vif.clk);
vif.enable <= 1'b0; // drop enable after one cycle
endtask
endclassWhat the three handshake calls mean
| Call | Who calls it | What it does |
|---|---|---|
| get_next_item | driver | Blocks until the sequencer has a packet, then returns it. |
| item_done | driver | Tells the sequencer the packet is finished so the next can start. |
| start_item / finish_item | sequence | The sequence side of the same handshake, wrapping randomisation. |
The rule to remember: every get_next_item must be matched by an item_done. If you forget item_done, the driver will hang forever waiting, and the sequence will never send the next packet. This is one of the most common first-testbench bugs.
Sequences: the stimulus recipes
A sequence creates transactions and sends them to the driver through the sequencer. We will write three, from simplest to most useful, so you can see how they grow.
Example 1: a single random packet
The simplest sequence sends one randomised packet. It creates the item, starts the handshake, randomises, and finishes.
class random_sequence extends uvm_sequence #(data_packet);
`uvm_object_utils(random_sequence)
function new(string name = "random_sequence");
super.new(name);
endfunction
virtual task body();
data_packet tr = data_packet::type_id::create("tr");
start_item(tr);
if (!tr.randomize())
`uvm_error("SEQ", "randomize failed")
finish_item(tr);
endtask
endclassExample 2: many packets in a loop
Real tests send many packets. This sequence repeats the handshake a random number of times, so each run has a different length. Notice the count itself is randomised with a constraint, which is a small but real example of controlling test size.
class multiple_random_sequence extends uvm_sequence #(data_packet);
`uvm_object_utils(multiple_random_sequence)
rand int loop;
constraint loop_c { loop inside {[5:10]}; }
function new(string name = "multiple_random_sequence");
super.new(name);
endfunction
virtual task body();
repeat (loop) begin
data_packet tr = data_packet::type_id::create("tr");
start_item(tr);
if (!tr.randomize())
`uvm_error("SEQ", "randomize failed")
finish_item(tr);
end
endtask
endclassExample 3: a directed sequence
Sometimes you need an exact case, not a random one. A directed sequence sets the fields by hand. This is how you reproduce a specific scenario, for example both channels enabled with a fixed control value, which is useful when chasing a known bug.
class directed_sequence extends uvm_sequence #(data_packet);
`uvm_object_utils(directed_sequence)
function new(string name = "directed_sequence");
super.new(name);
endfunction
virtual task body();
data_packet tr = data_packet::type_id::create("tr");
start_item(tr);
// Fixed, repeatable values instead of random ones
tr.cf = 2'b01;
tr.enable = 1'b1;
tr.data_in1 = 16'h00FF;
tr.data_in2 = 16'hFF00;
tr.delay = 0;
finish_item(tr);
endtask
endclassWhich sequence should I use?
| Sequence type | Best for | Trade-off |
|---|---|---|
| Single random | A quick smoke test | Covers very little on its own |
| Multiple random | Broad coverage across a run | Cannot target an exact case |
| Directed | Reproducing a known scenario or bug | You must know the values in advance |
A healthy test plan uses all three: random for breadth, directed for the exact cases you care about, and multi-count to control how long a run lasts.
Connecting the driver to the sequencer
The driver and sequencer talk through a port and an export. The connection is made in the agent, which we build in part three, but here is the single line that links them so you know what is coming.
// Inside the agent connect_phase (built in Part 3)
driver.seq_item_port.connect(sequencer.seq_item_export);Without this line the driver has nobody to ask for items, so get_next_item would block forever. It is the wire that carries every packet.
How the interface handle reaches the driver
The driver fetched its interface with a config DB get. Something must have put it there first. That is done at the top level with a set, usually in the test, passing the real interface down to every component that needs it. We explain this fully in our guide on passing the virtual interface through uvm_config_db, and we wire it up in part four.

Common mistakes at this stage
- Missing item_done. The driver hangs waiting and no more packets flow. Always pair it with get_next_item.
- Not checking randomize. If randomize returns zero and you ignore it, you drive stale values. Always test the return and report an error.
- Driving without a clock edge. Assigning pins outside a clocking event causes races. Drive on the clock, as shown in drive_item.
- Leaving enable high. If you never drop enable, the design keeps acting on old data. Drop it after the packet, as in the example.
Where we are now
You now have a working driver, a sequencer, and three kinds of sequence. Stimulus can flow into the design. In part three we add the monitor to observe the pins, wrap everything in an agent, and build a scoreboard to check the results.
Keep learning
For the ideas behind this part, browse the UVM guides for sequences, the sequencer, and driver communication, the SystemVerilog guides for constraints and randomisation, and the Verification Fundamentals for the wider flow. Preparing for an interview? See the interview questions.
Frequently asked questions
What is the difference between the sequencer and a sequence?
A sequence is a recipe that creates transactions. The sequencer is the component that runs sequences and hands each transaction to the driver one at a time.
What do get_next_item and item_done do?
get_next_item blocks until the sequencer has a packet and returns it. item_done tells the sequencer the packet is finished so the next one can start. Every get_next_item must be matched by an item_done.
When should I use a directed sequence instead of a random one?
Use a directed sequence when you need an exact, repeatable case, such as reproducing a known bug. Use random sequences for broad coverage across many runs.
Why do I check the return value of randomize?
If randomize cannot satisfy the constraints it returns zero and leaves the fields unchanged. Checking the return and reporting an error stops you from silently driving stale values.
Where is the driver connected to the sequencer?
In the agent connect phase, with driver.seq_item_port.connect(sequencer.seq_item_export). We build the agent in part three.
Why does my driver hang forever?
The usual cause is a missing item_done, so the driver waits for a handshake that never completes. Check that every get_next_item has a matching item_done.


Hi Hardik,
if(!uvm_config_db#(string)::get(this, “”, “monitor_intf”, monitor_intf))
here u are getting monitor interface using alias “monitor_intf” right. But I couldn’t see anywhere u set anything to the alias name “monitor_intf”.
I have same doubt in the following line also.
if(!uvm_config_db#(virtual pipe_if)::get(this, “”, “monitor_intf”, vif)).
In top u are using aliases “in_intf” and “out_intf”. What I learned is the same alias need to be used while getting also. Otherwise how they will connect?
Thanks,
Vivek