A team verifies a DMA subsystem with four channels, three power states, two memory types and an interrupt controller that can coalesce. Their virtual sequences cover each feature well. Then the SoC integration plan asks for "every channel pair in parallel, in every power state, with and without coalescing, from both memories". That is hundreds of combinations, each with its own legal ordering rules, and the virtual sequence that tries to encode them becomes a 2,000-line randcase nobody dares to touch.
This lesson is about choosing the right tool for stimulus as it grows: sequence libraries, layered virtual sequences, and portable stimulus (the Accellera Portable Test and Stimulus Standard, PSS). It is honest about the last one, which depends on a commercial tool to be useful.
A sequence library is a cookbook: a set of recipes for one cook (one sequencer), and a rule for picking which to cook next. A layered virtual sequence is a menu planner: it composes recipes across several cooks into a meal, in an order a person wrote down. Portable stimulus is a kitchen scheduler that knows the dependencies: you tell it that dessert needs the oven, the oven needs preheating and only two burners exist, and it plans many different legal meals for you, including ones you would not have thought of.
The difference that matters is who writes the scenario. With libraries and virtual sequences, an engineer writes each scenario's structure, and randomisation varies the details. With PSS, the engineer writes the rules (actions, what each needs and produces, which resources they lock), and a tool's solver generates the scenarios, then emits code for a UVM bench, for a C test on an embedded processor, or for both.
A uvm_sequence_library holds registered sequences of one item type and, when started, runs a chosen number of them in random, random-cyclic or user-defined order. It is cheap breadth on one interface: a library of APB register sequences, or of stream packet shapes. Its limit is that it knows nothing across interfaces and nothing about ordering rules. The mechanics, including the selection-mode trap, are in Sub-sequences and sequence libraries.
Virtual sequences scale further when they are built in layers, each layer a small sequence with one job: atomic sequences that do one thing on one interface (program a job, handle an interrupt), scenario sequences that compose atomics with the ordering rules of one feature, and a top sequence that picks scenarios from a configuration. A test is then a configuration plus a top sequence, not new code:
// snippet: a top-level virtual sequence choosing scenarios by weight
class dma_soc_top_vseq extends uvm_sequence;
`uvm_object_utils(dma_soc_top_vseq)
dma_soc_cfg cfg; // weights and counts come from the test's config
function new(string name = "dma_soc_top_vseq");
super.new(name);
endfunction
task body();
repeat (cfg.n_scenarios) begin
randcase
cfg.w_single: run_scenario(dma_single_job_vseq::type_id::create("single"));
cfg.w_parallel: run_scenario(dma_parallel_jobs_vseq::type_id::create("parallel"));
cfg.w_lowpower: run_scenario(dma_job_across_sleep_vseq::type_id::create("lowpower"));
endcase
end
endtask
task run_scenario(dma_scenario_vseq s);
s.cfg = cfg;
if (!s.randomize()) `uvm_fatal("RAND", {"randomize failed: ", s.get_name()})
s.start(null, this); // virtual sequence: no sequencer of its own
`uvm_info("SCEN", {"done: ", s.get_name()}, UVM_MEDIUM)
endtask
endclassThis works well into the hundreds of scenarios. It strains when the combinations of scenarios carry rules: channel 2 may not start while channel 0 holds the shared buffer, a sleep request must wait for outstanding jobs, an interrupt coalescing window must span two completions. Each rule becomes code in some scenario, and scenarios that combine features duplicate each other's rules. Coverage of which combinations ran is another hand-written covergroup.
PSS was released by Accellera in 2018 and has been revised since. It defines one semantic model with two input formats, a domain-specific language and a C++ class library. A model is made of components holding actions; actions declare the flow objects they consume and produce (buffers, streams, states), the resources they lock or share (a channel, a bus), constraints on their fields, and exec blocks that say what code to emit for a target. An activity composes actions with sequence, parallel, select, schedule and repeat. A sketch of the DMA in the DSL, simplified:
// PSS DSL sketch (simplified): the DMA moves a buffer and locks one of four channels
buffer data_buf_s {
rand bit[31:0] addr;
rand bit[15:0] words;
}
resource channel_r {}
component dma_c {
pool [4] channel_r chan_p;
bind chan_p *;
action xfer_a {
input data_buf_s src;
output data_buf_s dst;
lock channel_r chan;
rand bit[15:0] len;
constraint { len in [1..16]; dst.words == len; }
exec body SV = """
start_dma_job({{chan.instance_id}}, {{len}});
""";
}
}
component pss_top {
dma_c dma;
mem_c mem; // has an action that outputs data_buf_s
pool data_buf_s buf_p;
bind buf_p *;
action two_parallel_xfers_a {
activity {
parallel {
do dma_c::xfer_a;
do dma_c::xfer_a;
}
}
}
}Inference. Each xfer_a needs an input buffer, and the activity never says where it comes from. The tool infers an action from mem_c that produces one. That is the scheduler adding "preheat the oven" by itself.
Resources. Two parallel transfers each lock a channel; the pool has four, so the tool picks two different channels. With a pool of one, it would refuse to schedule them in parallel.
Graph-based stimulus. The tool sees the model as a graph of legal action sequences and can walk paths not yet covered, instead of hoping random selection reaches them. Scenario coverage is a property of the model, not a separate covergroup.
Exec blocks are templates. The {{...}} fields are filled in per generated scenario; the same action can have an SV exec for the UVM bench and a C exec for the embedded processor.
The hand-off to UVM is through a realization layer: each atomic action maps to an existing UVM sequence (here, start_dma_job would start the bench's job sequence on the right agents). The generated code calls those sequences in the solved order; the UVM env keeps its agents, monitors, scoreboards and coverage, which still judge every transfer. PSS replaces the top of the stimulus stack, never the checking.
Situation | Best fit | Why |
|---|---|---|
One block, one or two interfaces | Sequences and a library | Few cross-interface rules; PSS would be overhead. |
Subsystem with a handful of interacting features | Layered virtual sequences | Rules fit in scenario sequences; everything stays in SystemVerilog and any simulator runs it. |
SoC with many IPs, shared resources and many configurations | PSS on top of the UVM bench | Combinations and resource rules are modelled once; the tool generates and covers them. |
Same scenarios needed in simulation, emulation and on silicon | PSS with C targets | One model, retargeted; the UVM bench is one target among several. |
A common first-year failure with generated stimulus looks like this. A generated SoC scenario fails after 18 ms: one DMA transfer's data mismatches. The generated SystemVerilog is 9,000 lines of calls with solver-chosen values, and the failing transfer cannot be traced back to an action in the model. Two days go into reading generated code.
The teams that recover make two changes. Every atomic action logs itself: the realization sequence prints the action name, its instance and its solved fields with a stable message ID at start and end, so the log reads as a scenario. And every atomic action is an existing UVM sequence with its own directed test, so when a generated scenario fails, the question is only which combination broke, never whether the building block works. A third, quieter fix closes a drift: the model's constraint said len in [1..16] while the UVM sequence it mapped to also allowed len == 0, so the two definitions of "legal" disagreed; constraints on an action are checked against the sequence's own constraints in review.
What combinations or resource rules the block has that virtual sequences cannot express, whether the scenarios must be reused in emulation or on silicon, and whether a tool license and a realization layer are budgeted. For one block with two interfaces the usual answer is that constrained-random sequences and a small virtual sequence are cheaper and run on any simulator.
So a failing generated scenario can only mean a combination problem, not a broken building block, and so the action's behaviour and constraints are defined and checked once, in the bench, rather than re-implemented in generated code.
randcase weights from a config object. What do you lose compared with a PSS tool's graph-based selection?Random weights reach common combinations quickly and rare ones slowly or never, and nothing records which combinations ran unless you write that coverage yourself. A graph-based tool knows the space of legal scenarios and can target the paths not yet covered.
Next: stimulus and checking are only as reusable as the agents underneath them. The last lesson turns an agent into VIP that other projects can adopt: VIP and IP Reuse at Scale.