A new engineer writes a virtual sequence that reads the DMA's output side through the stream agent, and passes it env.out_agt.sqr. The test stops at the first item with a fatal from the sequence: neither the item's sequencer nor dedicated sequencer has been supplied. They search the bench for the sequencer class to see what is wrong with it, and find none. The bench has no sequencer class at all, only a one-line typedef.
So what does a sequencer actually do, why does the bench not need to write one, and why was this one missing?
A sequencer sits between any number of sequences and exactly one driver. Sequences ask it for a turn; it decides who goes next (arbitration), hands the chosen item to the driver when the driver asks for one, and routes any response back to the sequence that sent the request. Think of the ticket counter at a deli: customers (sequences) take numbers, the counter calls the next one, and the cook (driver) serves whichever order is put in front of them. The counter never cooks.
That job is the same for every protocol. Nothing in it depends on what an APB transfer looks like, only on the item type it passes. So the UVM library provides a complete one, uvm_sequencer #(REQ, RSP), and an agent usually needs nothing more than a name for it.
This is the sequencer earning its place. The poller and the interrupt handler run at the same time, both on the APB bus. Without arbitration their transfers would collide; with it, they interleave one transfer at a time, and the handler's grab() lets it jump the queue when the interrupt arrives.
// snippet: from the course DMA bench
typedef uvm_sequencer #(apb_item) apb_sequencer;
typedef uvm_sequencer #(axis_item) axis_sequencer;The typedef gives the agent a readable type name and fixes the item type, so the compiler rejects an attempt to start a stream sequence on the APB sequencer. The agent creates it with apb_sequencer::type_id::create("sqr", this) like any component: the parameterized library class is already registered with the factory, so overrides by type still work.
Inside the active agent, drv.seq_item_port.connect(sqr.seq_item_export) joins the two. It is a port-to-export connection (a port connects to an export or imp), and it is the only wire the sequencer needs. Through it the driver calls get_next_item, item_done and the other pull methods; the sequencer implements them. If the connection is missing, elaboration does not catch it (the driver's seq_item_port is created with a minimum connection count of 0, so a passive agent's unused driver would not be an error), and the run fails at the driver's first get_next_item, which has nothing behind it. The mechanics of what each call does inside the sequencer are in Inside the sequencer.
The default mode is UVM_SEQ_ARB_FIFO: first come, first served, with priority ignored. UVM_SEQ_ARB_WEIGHTED and the two STRICT modes use the priority passed to start(); the STRICT modes can starve low-priority sequences. lock() and grab() give one sequence exclusive use until unlock()/ungrab(); a grab request goes to the front of the queue, a lock request to the back, and the item already in the driver finishes first. The bench's interrupt handler uses grab, and lock vs grab walks through that exact code.
Arbitration is per item, not per sequence, and that matters for the stream agent. A packet is N items, each arbitrated on its own, so two packet sequences started on in_agt.sqr at the same time interleave their beats: the DUT sees one job with words from two packets, and TLAST lands in the wrong place. The bench never does it, but a senior bench must make it impossible: a sequence that owns a multi-item transfer calls lock() before its first item and unlock() after its last, so its beats go out back to back.
Extend uvm_sequencer only when the sequencer must hold something sequences need to reach. Three cases come up in practice:
Handles for sequences. A register model, the agent's config object or a status object, so a sequence can read them through p_sequencer (which exists only when the sequence uses uvm_declare_p_sequencer). The alternative, passing the handle in a sequence field, is often cleaner; see p_sequencer.
A custom arbitration rule. Override user_priority_arbitration() and set UVM_SEQ_ARB_USER.
A virtual sequencer. A sequencer with no driver that only holds other sequencers' handles. The bench shows you can do without one: dma_vseq takes the handles as fields and is started with start(null).
If none of these apply, a typedef is the right answer, and an empty class apb_sequencer extends uvm_sequencer #(apb_item) adds nothing but a constructor to maintain.
Back to the opening fatal. env.out_agt is passive, so its build_phase never created sqr; the handle was null. start(null) is legal (it is how virtual sequences start), so nothing complained until the first start_item, which found no sequencer anywhere and raised the SEQ fatal. The fix is twofold: start stimulus only on active agents, and have every virtual sequence check its handles before starting, as dma_vseq does. Reading the DMA's output is the monitor's job, not a sequence's.
One more component-side duty: on a reset, sqr.stop_sequences() kills every sequence running on the sequencer and clears its queue. It is a blunt tool, not a graceful stop; Stopping sequences and reset shows when to use it and what it leaves behind.
Run dma_vseq_test with +UVM_VERBOSITY=UVM_MEDIUM to see the arbitration from the picture:
UVM_INFO dma_tb.sv(572) @ 405000: uvm_test_top.env.apb.sqr@@vseq.isr [IRQ] handler has the APB sequencer
UVM_INFO dma_tb.sv(623) @ 575000: reporter@@vseq [VSEQ] job done: STATUS=00000002 after 4 polls
UVM_INFO dma_tb.sv(411) @ 675000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
UVM_ERROR : 0`env.apb.sqr@@vseq.isr` is how a sequence reports: the sequencer it runs on, then @@, then its own name under its parent. The handler is running on the APB agent's sequencer, as its child path shows.
4 polls happened before the handler stopped the poller. Each poll was one APB read, arbitrated on the same sequencer as the handler's transfers.
`reporter@@vseq`: the virtual sequence has no sequencer, so its messages come from the global reporter.
A team wanted their sequences to read the agent's config object, so they wrote class apb_sequencer extends uvm_sequencer #(apb_item) with a cfg field and declared uvm_declare_p_sequencer(apb_sequencer) in their base sequence. Every test failed at the first sequence with a DCLPSQ fatal. The agent still created its sequencer from the old typedef in a shared package, so the object was a plain uvm_sequencer #(apb_item), and the $cast to the new class inside the p_sequencer macro failed.
The fix was making the agent create the new class (or having one definition of apb_sequencer, not two). The lesson is broader: once a sequence depends on p_sequencer, the sequencer type is part of the agent's contract, and every sequence using the macro can run only on that type. Passing cfg to the sequence as a field, set by whoever starts it, avoids the coupling altogether.
env.apb.sqr under the default arbitration. One is started with priority 500, the other with 100. Which goes first?Whichever requested first. UVM_SEQ_ARB_FIFO ignores priority entirely. To make priority matter, set the arbitration to UVM_SEQ_ARB_WEIGHTED or one of the STRICT modes, and beware that the STRICT modes can starve the low-priority sequence.
The typedef fixes REQ to axis_item. axis_pkt_seq is a uvm_sequence #(axis_item), so starting it on the APB sequencer is a type error the compiler catches, and the driver's get_next_item(req) gets an axis_item with no cast. The name is cheap; the type checking is the point.
Yes. Its driver still needs something to decide each response (how long to stall, what data to return), and a responder sequence running on a sequencer is the standard way to make that decision randomizable and overridable. The bench's axis_sink_agent has its own sink_sqr for exactly this; the agent patterns topic builds it.
Next: every component in this topic touched the pins through vif.mon_cb or @(posedge vif.clk). In Sampling, races and X you will see why those two choices matter, through the bug that cost the bench a word per packet.