The Brain of Stimulus
What Are Sequences?
The Simple Version: Sequences are like test scripts. They define WHAT
data to send to your design and in what order.
Think of it like: A recipe. It lists the
ingredients (data) and steps (order) to create a test scenario.
A UVM Sequence is a procedural block that defines what data to send to the
DUT. Unlike components, sequences are transient objectsΒthey are created,
execute their body() task, and then disappear.
Sequence Life-Cycle:
- Creation: Instantiate via the factory.
- Start: Called from a test or another sequence using
seq.start(sqr).
- Body: The main execution thread where transactions are generated.
- Cleanup: The sequence object is eventually garbage collected.
The Handshake: Start vs Finish
To send a transaction, a sequence must coordinate with the Sequencer and
Driver.
This is done via the start_item / finish_item pair.
Why not just call randomize()?
UVM uses something called Late Randomization. This means we wait until the very
last moment (when the Driver is ready) to pick random values. This is smart because it
lets us make decisions based on the current state of the design, not the state
from 10 cycles ago.
task body();
repeat(10) begin
// 1. Create the request object
req = my_item::type_id::create("req");
// 2. Wait for Sequencer Grant
start_item(req);
// 3. Late Randomization (Mastering Late Bindings)
if (!req.randomize() with { addr inside {[0:100]}; }) begin
`uvm_error("BODY", "Randomization failed")
end
// 4. Send to Driver & Block until item_done()
finish_item(req);
end
endtask
Macros vs Manual
You will often see the `uvm_do macro family in legacy code. While
concise, they wrap multiple operations (create, wait, randomize, send) into
a single line, which can make debugging difficult.
| Method |
Pros |
Cons |
| `uvm_do |
Very fast to write |
Hidden logic, hard to debug randomization |
| start_item |
Ultimate control, enables late randomization |
More lines of code |
Architectural Decision
Always prefer start_item/finish_item for production testbenches.
The 10% extra typing is worth the 100% extra visibility during debug.
pre_body() & post_body() Hooks
UVM provides two optional hook methods that wrap around your body()
task.
These are commonly asked in interviews and are essential for advanced sequence control.
Execution Order:
pre_body() - Called before body starts
body() - Your main sequence logic
post_body() - Called after body completes
Common Use Cases:
- Objection Handling: Raise objection in
pre_body(), drop in
post_body(). This ensures your sequence keeps simulation alive during
its run.
- Setup/Cleanup: Initialize sequence state or open files before body, release
resources after.
- Logging: Print start/end markers for debug tracing.
class my_sequence extends uvm_sequence #(my_item);
`uvm_object_utils(my_sequence)
// Called BEFORE body()
virtual task pre_body();
if (starting_phase != null)
starting_phase.raise_objection(this, "Sequence started");
endtask
// Main execution
virtual task body();
repeat(10) begin
`uvm_do(req)
end
endtask
// Called AFTER body()
virtual task post_body();
if (starting_phase != null)
starting_phase.drop_objection(this, "Sequence complete");
endtask
endclass
Interview Tip
Q: Why use pre_body/post_body for objections instead of doing
it in body()?
A: Because body() might have nested sequence calls
(`uvm_do_with) that could also raise objections. Using the hooks ensures
balanced raise/drop at the outer level and makes the pattern reusable.
The Complete `uvm_do` Macro Family
Interview Essential: UVM provides a family of macros for sequence execution.
Understanding when to use each is critical.
| Macro |
Creates? |
Randomizes? |
Sends? |
Purpose |
`uvm_do |
|
|
|
Basic: Create + randomize + send |
`uvm_do_with |
|
+ constraints |
|
With inline constraints |
`uvm_do_on |
|
|
(specific sqr) |
Send to a different sequencer |
`uvm_do_on_with |
|
+ constraints |
(specific sqr) |
Constraint + target sequencer |
`uvm_do_pri |
|
|
+ priority |
Control arbitration priority |
`uvm_create |
|
β |
β |
Create only, manual control |
`uvm_send |
β |
β |
|
Send pre-built item |
`uvm_rand_send |
β |
|
|
Randomize + send existing item |
task body();
// 1. Basic: Create, randomize all fields, send
`uvm_do(req)
// 2. With inline constraints
`uvm_do_with(req, { addr inside {[0:255]}; cmd == WRITE; })
// 3. Send to a specific sequencer (multi-agent)
`uvm_do_on(req, p_sequencer.ahb_sqr)
// 4. Both constraint AND specific sequencer
`uvm_do_on_with(req, p_sequencer.dma_sqr, { burst_len == 4; })
// 5. With priority (for arbitration)
`uvm_do_pri(req, 100) // Higher priority wins
// 6. Manual control: Create only
`uvm_create(req)
req.addr = 32'h1000; // Manually set fields
req.data = 32'hDEAD;
`uvm_send(req) // Then send
// 7. Reuse allocated item
`uvm_create(req)
`uvm_rand_send(req) // Randomize and send
endtask
When to Use Which?
- `uvm_do - Quick tests, prototyping
- `uvm_do_with - Most common, constrained stimulus
- `uvm_do_on - Virtual sequences, multi-agent
- `uvm_create + `uvm_send - Complex scenarios, manual field assignment
Production Recommendation
While macros are convenient, prefer
start_item/finish_item for production testbenches. The explicit flow makes
debugging much easier when randomization fails or transactions get stuck.