In short: A UVM sequence is a transient object that generates transaction-level stimulus in its body() task, while the sequencer is a permanent component that arbitrates between competing sequences and hands transactions to the driver over the TLM seq_item_port handshake.
If the driver and monitor are the hands and eyes of a UVM testbench, the sequence and sequencer are the brain that decides what to stimulate and when. Mastering this layer is what separates an engineer who can only run a canned test from one who can shape directed, constrained-random, and layered traffic on demand. This is also the single most common topic in verification interviews at companies using Synopsys VCS, Cadence Xcelium, or Siemens Questa, so it pays to understand the mechanics, not just the boilerplate.
The division of labor: sequence vs. sequencer
The two are often confused because they are always used together, but their roles are opposite. A uvm_sequence extends uvm_sequence_item's cousin in the class hierarchy and is an object (dynamic, created and garbage-collected on the fly). A uvm_sequencer is a component (static, built once during the build phase and living for the whole simulation). The sequence contains the algorithm for producing transactions; the sequencer is a traffic controller that owns the FIFO-like channel to the driver and resolves contention when several sequences want the same driver at once.
Rule of thumb: sequences come and go thousands of times in a run; a sequencer is instantiated exactly once per driver it feeds. If you ever find yourself calling new() on a sequencer in a test, something is wrong.
Inside body(): the heart of stimulus generation
All the real work happens in the body() task. The canonical pattern uses four calls per transaction:
req = tx::type_id::create("req");— factory-build the item so tests can override its type.start_item(req);— block until the sequencer grants this sequence access to the driver.assert(req.randomize() with { ... });— apply constraints after the grant, so late-binding decisions (e.g. address based on prior response) are possible.finish_item(req);— hand the item over and block until the driver callsitem_done().
The `uvm_do and `uvm_do_with macros collapse these into one line, but experienced engineers often expand them manually. The reason is debuggability: a stack trace through a macro-generated block is painful in VCS DVE or Verdi, whereas explicit calls let you set breakpoints on the exact randomize that failed. The macros also hide the response handshake, which matters for pipelined protocols.
A subtle but critical point: randomization happens between start_item and finish_item, never before start_item. Randomizing early throws away reactive information and is a classic review-flagged bug.
Arbitration: when sequences collide
Because many sequences can run on one sequencer concurrently (a virtual sequence launching five parallel traffic generators, say), the sequencer must pick whose item goes next. UVM exposes an arbitration mode set via set_arbitration():
| Mode | Behavior | Typical use |
|---|---|---|
SEQ_ARB_FIFO | First-come-first-served (default) | General traffic |
SEQ_ARB_WEIGHTED | Random, weighted by priority | Mixed-rate masters |
SEQ_ARB_RANDOM | Pure random order | Stress / fuzzing |
SEQ_ARB_STRICT_FIFO | Highest priority, then FIFO | QoS modelling |
SEQ_ARB_USER | Override user_priority_arbitration() | Custom schemes |
Priority is passed as the second argument to start_item(req, 200) (default is 100). You can also grab exclusive access with lock()/unlock() (queued) or grab()/ungrab() (jumps the queue) — essential when a sequence must emit an atomic burst that no other sequence can interleave.
Sequence libraries and layering
Real projects don't hand-write every scenario. A uvm_sequence_library is a container that registers many sequences and replays them in random, random-unique, or sequential order — ideal for regression where you want broad coverage from one test. Layering goes further: a high-level "frame" sequence can call a low-level "byte" sequence on the same sequencer, so you model protocol stacks (e.g. a MIPI or Ethernet framing layer sitting over a PHY layer) without rewriting primitives.
The companion to the sequence is the thing it drives — see our deep dive on the UVM driver and monitor for the pin-level half of the handshake, and our guide to virtual sequences and sequencers for coordinating stimulus across multiple interfaces in one SoC-level test.
Response path and common pitfalls
Drivers can return responses via put_response(), which the sequence reads with get_response(rsp). Forgetting to consume responses while the driver keeps putting them causes a silent memory leak that bloats a Questa regression over millions of transactions. Two other frequent bugs: starting a sequence on the wrong sequencer handle (a null-pointer at run time), and calling randomize() without checking its boolean return, which hides constraint-solver failures that otherwise cost hours in debug.
Want to write a full UVM environment end-to-end, from sequence item to scoreboard, with lab exercises on industry simulators? Explore the Verification track on our CourseTron courses page. Teams needing hands-on mentoring can also look at the structured programs from ChipXpert's VLSI training.
FAQ
What is the difference between finish_item() and item_done()?
finish_item() is called by the sequence and blocks until the driver signals completion; item_done() is called by the driver to release that block. They are two ends of the same handshake.
Can two sequences run on one sequencer at the same time?
Yes. They run concurrently and the sequencer arbitrates between them each time the driver requests an item, using the configured arbitration mode and per-item priorities.
Why randomize after start_item() and not before?
Randomizing after the grant lets the sequence make reactive decisions based on earlier responses and ensures the latest configuration/constraints apply, which is essential for pipelined and reactive protocols.
When should I use grab() instead of lock()?
Use grab() when a sequence must preempt the queue immediately for an atomic burst; use lock() when it can wait its turn but still needs exclusive, uninterrupted access.