UVM Sequencer connected reg_sequencer to reg_driver - but a sequencer with nothing feeding it transactions just sits idle. This article writes the piece that actually generates stimulus: a sequence whose body() task creates reg_transaction objects and sends them through the chain this entire chapter has been building.

9 min read | Intermediate Level

What You'll Learn

  • What a sequence is, and how it relates to a sequencer
  • How to write a body() task using start_item() and finish_item()
  • Why this article uses start_item()/finish_item() instead of the `uvm_do macro
  • How to start a sequence on reg_sequencer and watch the full chain run

What Is a Sequence?

A sequence generates the transaction objects a sequencer routes to a driver. Sequences are made of one or more transactions, assembled in whatever pattern a particular test scenario needs - a single write, a long stream of random reads and writes, or something more deliberate. Sequences are the actual stimulus behind a verification plan; everything built in this chapter so far exists to give a sequence somewhere to send its transactions.

Declaring reg_sequence

A sequence extends uvm_sequence, parameterized with the same transaction type reg_sequencer already uses:


class reg_sequence extends uvm_sequence #(reg_transaction);

  // [Recommended] Registers with the factory using uvm_object_utils,
  // not uvm_component_utils, since a sequence is a uvm_object, not a uvm_component
  `uvm_object_utils(reg_sequence)

  function new(string name = "reg_sequence");
    super.new(name);
  endfunction

  // body() comes next
endclass

Notice the constructor takes only a name, with no parent - the same shape reg_transaction's constructor had back in its own article, and for the same reason: a sequence is a uvm_object, not a uvm_component. It doesn't live at a fixed point in the testbench hierarchy the way reg_sequencer does; it's created, runs once, and is done.

Writing body()

The actual stimulus lives entirely inside body(). Every sequence built on uvm_sequence #(reg_transaction) already has a built-in property named req of that exact type - there's no need to declare it yourself:


virtual task body();
  repeat (5) begin
    req = reg_transaction::type_id::create("req");
    start_item(req);
    assert(req.randomize());
    finish_item(req);
  end
endtask

start_item(req) and finish_item(req) are the manual equivalent of what reg_driver's seq_item_port was waiting for all along. start_item(req) blocks until reg_sequencer is ready to accept it - which, on the driver's side, is the exact moment get_next_item() is waiting to unblock. Randomization happens in between the two calls, deliberately as late as possible, so it can take the current state of the test into account rather than committing to values too early. finish_item(req) then hands the item off and waits for reg_driver's item_done() call before returning, completing exactly the handshake the driver and sequencer articles described from their own two sides.

Where is This Used in Practice

The pattern of creating an item, calling start_item(), randomizing, then calling finish_item() is the same in every UVM testbench, regardless of protocol - an AXI sequence, an APB sequence, and this register-block sequence all follow this identical shape. Only the transaction class and its fields change from one protocol to the next.

Why Not the `uvm_do` Macro?

UVM also provides a shortcut, `uvm_do(req), that performs the create/start_item/randomize/finish_item sequence shown above in a single macro call. It isn't used here, and that's a deliberate choice worth explaining rather than skipping past: writing the four steps out individually keeps every part of the handshake visible, which matters when something goes wrong and needs debugging. The macro form is covered in How to Use uvm_do Sequence Macros.

Quick Check

Q: Looking back at reg_transaction's constraint from its own article, what stops assert(req.randomize()) from ever producing a transaction that's neither a write nor a read?

Show Answer

The wr_xor_rd_c constraint, defined directly on reg_transaction, forces wr_en and rd_en to never both be 0 or both be 1. Every call to randomize() on a reg_transaction is bound by that constraint automatically - the sequence doesn't need to repeat or enforce it itself.

Running reg_sequence

A sequence has to be started against a specific sequencer before any of this runs. With reg_sequencer already built, starting reg_sequence on it looks like this:


reg_sequence seq;
seq = reg_sequence::type_id::create("seq");
seq.start(m_sequencer);

start() runs body() to completion before returning. Once this call is made, the entire chain this chapter has been assembling finally runs end to end: reg_sequence creates and randomizes five reg_transaction objects, reg_sequencer passes each one to reg_driver in turn, and reg_driver drives the corresponding pin wiggles onto reg_if, which reg_block actually responds to.

This snippet calls start() directly for clarity, but in a real testbench it's normally called from inside a test's run_phase, with a simulation objection raised beforehand and dropped afterward so the simulation doesn't end while the sequence is still running. Both of those are covered in UVM Test and How to Create and Use a Sequence.

What About p_sequencer?

UVM Sequencer mentioned p_sequencer only by name. Here's the short version: p_sequencer is a type-specific handle to the sequencer a sequence is running on, declared inside a sequence with `uvm_declare_p_sequencer(SEQUENCER_TYPE). It only earns its place when a custom sequencer class has members or methods beyond what plain uvm_sequencer provides - for example, the max_transactions property from the practice exercise in UVM Sequencer.

reg_sequence doesn't need it. Nothing in body() reaches back into anything reg_sequencer-specific; it only ever touches req and the inherited start_item()/finish_item() methods, which are available on every sequence regardless of which sequencer it runs on. p_sequencer becomes worth declaring the moment that stops being true.

Common Mistakes

Mistake #1: Inserting a delay between start_item() and finish_item()

There should be no simulation time consumed between these two calls. Randomization and any other in-between logic should happen instantly, in zero simulated time - introducing a delay here breaks the timing assumptions the sequencer and driver are relying on for the handshake.

Mistake #2: Declaring a local variable instead of using req

Wrong:


reg_transaction tr;
tr = reg_transaction::type_id::create("tr");
start_item(tr);

Right:


req = reg_transaction::type_id::create("req");
start_item(req);

Why? req isn't something you need to declare - uvm_sequence #(reg_transaction) already provides it as a built-in property typed to whatever transaction class the sequence is parameterized with. Declaring a separate variable still works, but it's redundant and diverges from what every other UVM sequence you'll read looks like.

Practice Exercise

Challenge: Force a Write-Only Sequence

Modify reg_sequence's body() so every generated transaction is a write, never a read, without changing reg_transaction's constraint itself.

Hint

randomize() accepts an inline with clause that adds a constraint just for that call, on top of any constraints already defined on the class.

Solution
virtual task body();
  repeat (5) begin
    req = reg_transaction::type_id::create("req");
    start_item(req);
    assert(req.randomize() with { wr_en == 1; });
    finish_item(req);
  end
endtask
  

The inline with clause adds wr_en == 1 on top of reg_transaction's own wr_xor_rd_c constraint, rather than replacing it - since that constraint already forces rd_en to 0 whenever wr_en is 1, every generated transaction becomes a write.

Summary

In this article, you learned:

  • A sequence generates the transactions a sequencer routes to a driver, and body() is where that stimulus is written
  • start_item() and finish_item() implement the handshake the driver and sequencer articles each described from their own side
  • Writing the manual form instead of the `uvm_do macro keeps every step of that handshake visible while you're still learning it
  • p_sequencer is only necessary once a sequence needs to reach a custom sequencer's own members

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5
Cron Job Starts