Hi Rahul,
The idea in UVM is that a sequencer/driver combination will be tied to a specific design interface (usually encapsulated in an agent), which is why we parameterize our sequencer and driver (and monitor’s analysis port) to a transaction of a specific bus protocol. Then when we write our sequences, they generate transaction items for that specific bus protocol.
But in a fullchip/SoC testbench, we need some mechanism to coordinate between the different interfaces–that’s where the virtual sequencer/sequences come in. You can think of a virtual sequencer as a testbench controller, coordinating the traffic between all the different bus protocols. It’s called “virtual” (a terrible term, by the way, since the keyword “virtual” is used to mean 3 completely different things in SystemVerilog) because it doesn’t generate transactions (sequence items)—it just tells other sequencers what to do.
Typically, you only need one virtual sequencer, but nothing stops you from having as many as you want. Truth be told, you can even run virtual sequences WITHOUT a virtual sequencer if you like, but there are some advantages to using a virtual sequencer. First, virtual sequencers arbitrate between multiple sequences, and this allows you specify sequence priorities or grab exclusive access to specific bus interfaces. Second, we can use a virtual sequencer to hold references to our bus-specific sequencers, which allows us to write our sequences in a generic way so that they don’t need hard-coded hierarchical references or any knowledge of how the testbench is constructed (which makes them portable between test benches). Third, we can use a virtual sequencer’s hierarchical path to query hierarchical information from the config database from inside our sequences.
Now, to answer your first question about generating the stimulus. There is not one correct way to generate the stimulus you’re looking for. It really depends on what you prefer, and it depends on where you put the randomization.
The easiest way to generate stimulus is as follows. First, create two simple sequences that generate a single sequence item. For example,
class seq1 extends uvm_sequence_item#(trans_type1);
`uvm_object_utils(seq1)
// Constructor
task body();
`uvm_do(req)
endtask
endclass
Create a similar class for seq2. (Note, Verification Academy does not recommend the use of sequence macros, but I’m using them here to simplify the code). Then create a virtual sequence that invokes the two sequences in the way you specified above:
class virt_seq extends uvm_sequence;
`uvm_object_utils(virt_seq)
// Constructor
task body();
seq1 s1;
seq2 s2;
s1 = seq1::type_id::create("s1");
s2 = seq1::type_id::create("s2");
// Now randomize your sequences to generate the stimulus you want
`uvm_do_on_with(s1, seqr1, { /* Constraints for "a" transaction */ })
`uvm_do_on_with(s2, seqr2, { /* Constraints for "x" transaction */ })
`uvm_do_on_with(s2, seqr2, { /* Constraints for "y" transaction */ })
`uvm_do_on_with(s1, seqr1, { /* Constraints for "b" transaction */ })
`uvm_do_on_with(s2, seqr2, { /* Constraints for "z" transaction */ })
`uvm_do_on_with(s1, seqr1, { /* Constraints for "c" transaction */ })
endtask
endclass
This virtual sequence will generate the sequence a, x, y, b, z, c. If you want these transactions to be randomly interleaved, then you could place everything inside of a fork … join (the Verilog standard does not guarantee the order of thread execution):
task body();
...
fork
`uvm_do_on_with(s1, seqr1, { /* Constraints for "a" transaction */ })
`uvm_do_on_with(s2, seqr2, { /* Constraints for "x" transaction */ })
`uvm_do_on_with(s2, seqr2, { /* Constraints for "y" transaction */ })
`uvm_do_on_with(s1, seqr1, { /* Constraints for "b" transaction */ })
`uvm_do_on_with(s2, seqr2, { /* Constraints for "z" transaction */ })
`uvm_do_on_with(s1, seqr1, { /* Constraints for "c" transaction */ })
join
endtask
But chances are that this won’t be so very random so you may prefer to put this inside of a randcase or randsequence.
Alternatively, you could define your sequences to generate 3 transactions at a time. Sequence 1 could generate sequentially or randomly a, b, c, and sequence 2 generate x, y, z. But coordinating between these transaction objects becomes a lot tricker because the sequencers are running independently of each other. For that, you would need to some kind of synchronization between the two sequences. You could do this with a uvm_event or uvm_barrier, but that means that your sequences have to be aware of each other. Instead, you could pass a reference to your sequence item to the virtual sequencer (maybe using a uvm_event?) and then start/stop the sequencers based on the order you want the traffic, or you could have your sequences use a global barrier and make decisions at that point. But by far, it’s easiest to use the simple approach shown above.
I hope that helps!
-Doug