UVM Sequences Tutorial

The "brain" of signal generation. Sequences define the stream of transactions to be executed.

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.

Implementation: Recommended Flow

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:

  1. pre_body() - Called before body starts
  2. body() - Your main sequence logic
  3. 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.
Example: Objection Handling Pattern

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
Examples: Each Macro in Action

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.