The way “UVM Hierarchical Sequences” works?

We discussed about “Sequences” in my previous post titled “UVM Sequences and Transactions Application“.  Here, we’ll talk about “Hierarchical Sequences“. Following are the questions most likely pops-up into our mind the moment we think about Hierarchical Sequences.

  1. What are Hierarchical Sequences?
  2. Why do we need them?
  3. How to create Sequence Hierarchy?
  4. How to implement Hierarchical Sequences?

Before getting deep into Hierarchical Sequences, let see some data published by Mentor Graphics during a study conducted by them on Functional Verification (which is shown in the Diagram 1 below):

Source: Wilson Research Group & Mentor Graphics 2012 Functional Verification Study

Diagram 1: Mean time non-FPGA verification engineers spent on verification tasks

Based on the conducted study & the above diagram its very clear that most of the non-FPGA verification engineers time goes into debugging various scenarios generated by the verification environment or the Testbench. Now, since we know that in UVM Verification Environment, Sequences play a key role in stimulus generation and complex scenarios creation. So more we’re able to control & configure the Sequences better it would be from the Testbench stimulus generation and Testbench debugging point of view. So faster & easier the debugging would be, better would be the response time of engineers & a better life for them.

One of successful approach to handle complex things is to add modularity into the process and object management, hence this similar modular approach applied to the Sequences can help immensely to deal with complex sequences management & provides us the following benefits:

  • Re-use of the code
  • Better Maintenance
  • Easy of Debugging
  • Better Control over Scenario Generation

To understand the “Hierarchical Sequences” lets pickup an analogy of the way we start a car. We just perform broadly three or four tasks to make our car moving. Those are –

  • Put the keys into car ignition slot, Press the clutch & start the car
  • Shift the gear &
  • Finally slowly release the clutch and press the accelerator of the car (Obviously holding the steering 🙂 )

But inside the car engine many many operations takes place to make it all happen. In reality, our commands are transformed into sub-commands & then those sub-commands are transformed into smaller tasks and finally those tasks are transformed into lowest level operations.

Similar breakdown operation happens if we talk about “Hierarchical Sequences”. In the most simplistic view, Top-level sequence breaks down into sub-sequences & then sub-sequences are further breaks down into lowest level sequences. The lowest level sequences are also called “Base Sequences”.

In real operation terms, Base Sequences are the Sequences which helps to transmits the Transaction items from the Sequencer to the Driver. Other sequences (i.e. top-level sequence & sub-sequences) are for the management of the Base Sequences in a particular order & configuration required to set up to create a particular scenario. There might be direct parallel participation of individual transaction items along with sub-sequences, which depends upon the verification scenario requirements.

Lets see the same concept shown in the Diagram 2 below:

 

Diagram 2: Sequences & Transactions nested arrangement in Hierarchical Sequence execution

Let me quickly explain the above diagram – red color indicates transaction items while green color indicates sequences. At the top, three sequences i.e. S1, S2 & S3 are shown. Sequence S1 is generating 4 transactions items in order i.e. A, B, C, A.

Another sequence S2 nest the sequence S1 inside it & triggers it when sequence S2 is executed. Sequence S2 generates the order of transaction items & sequence i.e. E, S1, D, E. When Sequence S1 is triggered and executed, it unfolds into generations of transaction items mentioned above.

Finally, the top-level sequence is Sequence S3 which nest the sequence S2 inside it. When S3 is executed it generates transaction F, then triggers & executes Sequence S2 & finally generates transaction item A.

The order of un-rolling nested sequences into transaction items is shown in the bottom part of the diagram. So finally the following order of transaction items takes place:


F followed by E followed by A followed by B followed by A followed by C followed by D followed by E followed by A


Hierarchical Sequences allows previously defined Sequences to be re-used & also provides a way of organizing a long sequence into smaller interrelated sequences.

Now takes few practical example where defining Hierarchical Sequences can be useful:

  1. Imagine a UVM sequence generating 20-25 SIZED Ethernet packets followed by a PAUSE packet followed
    by 30-40 QTAGGED packets. It may be helpful to model this sequence as a hierarchical sequence where the generation of SIZED and QTAGGED packets are defined as flat sequences (generating only transaction items) that are then used in the final hierarchical sequence.
  2. In AMBA AHB protocol, two main fundamental operations are WRITE & READ. So lets call the Base Sequence as AHB_BASE_WRITE and AHB_BASE_READ. Next level of sequences will be on top of these base sequences and may loop for N number of times. The number of loops i.e. N can be controllable from the top. Hence these sequences will encapsulate base sequences. Lets call these sequences as AHB_LOOP_WRITE & AHB_LOOP_READ. Now moving to upside next level, there might be another sequences which generates X number of loops for Read followed by Write operation. Lets call this top-level sequence as WRITE_FOLLOWED_BY_READ & it will instantiate both AHB_LOOP_WRITE & AHB_LOOP_READ inside it. We can see that using the different values for “N ” & “X” we can create as big scenario as we want. At the same time, by using these different sequences in arbitrary order we may create various different scenarios to test DUT.

Before getting into the example code implementations for Hierarchical Sequences, lets talk about few important things to know about root sequence & sub-sequences:

  • Not always but most of the time, the top-level sequences (as named above) acts as Root Sequences. There might be some case where a sequence might starts another sequence as a root sequence. So fundamentally, there is a root sequence and sub-sequences. Primary difference is that sub-sequence do have a Parent Sequence but root sequence do not have a parent sequence.
  • Having a parent sequence impacts the way a running sequence is treated by a Sequencer. For example, the grabbed sequencer accepts the items from the sub-sequences if the currently running sequence is the direct parent or distant parent of the sub-sequences.
  • The default sequence of the sequencer always starts as a root sequence.
  • A root sequence starts its own execution thread while, mostly, a sub-sequence starts in its parent sequence’s execution thread. But this is not always a mandatory condition. For example –
    • Lets assume a sequence S1 starts another sequence S2 in its execution thread as root sequence, in that case, sequence S2 will not get privileges currently granted to the sequence S1 (e.g. the interaction with sequencer is currently grabbed by the S1) and will have to wait for S2 to complete before continuing further.
    • On the other side, if a sequence S1 starts two parallel sub-sequences called S2 & S3 (by using fork..join statement) in its execution thread, then sub-sequences S2 & S3 will get all the privileges currently granted to the parent sequence S1.

Its time now to run through an Example UVM Code to understand about Hierarchical Sequence setup, implementation & execution:


////////////// Transaction Item /////////////
class packet extends uvm_sequence_item;
 `uvm_object_utils(packet)
 
 // Constructor
 function new (string name);
 super.new(name);
 endfunction: new
 
 // Packet properties
 rand byte payload;
 
endclass: packet

/////////// Sequencer Declaration /////////////////
typedef uvm_sequencer #(packet) my_sequencer;

////////// Flat Sub-Sequence Declaration ///////////
class flat_sub_seq extends uvm_sequence #(packet);
 `uvm_object_utils(packet)
 
 // Sub-Sequence Properties
  rand byte payload;
  rand int count;
  constraint c {payload < 10;}
 
 // Constructor
 function new (string name);
  super.new(name);
 endfunction: new
 
 // Body Task
 virtual task body();

  packet pkt;

 `uvm_do_with(pkt, {payload == default_payload;})
  repeat (count)
 `uvm_do(pkt)

 endtask: body

 endclass: flat_sub_seq
 
////////// Hierarchical Top-level Sequence //////////
class hier_top_seq extends uvm_sequence #(packet);
 `uvm_object_utils(hier_top_seq)
 
 // Constructor
 function new (string name);
 super.new(name);
 endfunction: new
 
 // Body Task
 virtual task body();
  
  packet pkt;
  flat_sub_seq fss;
 
 `uvm_do(pkt)
 `uvm_do(fss)
 `uvm_do_with(fss, {payload == 6 && count == 5;})

 endtask: body
 
endclass: hier_top_seq
 
//// Test To Start Hierarchical Top-level Sequence ////
class test_hier_seq extends uvm_test;
 `uvm_component_utils(test_hier_seq)
 
 // Constructor
 function new (string name, uvm_component parent);
  super.new(name, parent);
 endfunction: new
 
 // Build Function (Build Lower Level Components)
 function void build_phase (uvm_phase phase);
  super.build_phase(phase);
  ..
  ..
 endfunction: build_phase
 
 // Run Task
 virtual task run_phase(uvm_phase phase);
 
 // Create Top-level Sequence
  hier_top_seq hts;
  hts = hier_top_seq::type_id::create("hts", this);
 
  phase.raise_objection(this, "Starting hier_top_seq as Root Sequence");
  hts.start(env.agnt.sqnr);
  phase.drop_objection(this, "Droping hier_top_seq as Root Sequence");
 
 endtask: run_phase
 
endclass: test_hier_seq

In the above example code, we’d a transaction item called “packet” with a Data Member inside it i.e. payload which is declared random (rand) in nature.

Next, there is a sequence “flat_sub_seq” which is flat in nature and is not calling any sequence inside it. flat_sub_seq has defined data members which are random in nature e.g. count & its value is decided when the sequence flat_sub_seq is created and randomized inside its parent sequence i.e. “hier_top_seq“. Flat sequences is shown to utilize the UVM macros `uvm_do and `uvm_do_with to generate packet transactions with randomized values based on static constraints & dynamic constraints i.e. in-line constraints.

Next is the top level sequence that is called “hier_top_seq“. This sequence is calling another sequence i.e. flat_sub_seq inside it and thats why it is Parent Sequence of flat_sub_seq which is called as Child Sequence. Child sequence once started can have privileges on the resources assigned to the parent sequence e.g. grabbed sequencer. Using `uvm_do & `uvm_do_with inside the body() task different scenarios can be created.

Finally, the top level sequence or parent sequence i.e hier_top_seq is started on the sequencer i.e. env.agnt.sqnr using start() task as Root Sequence inside the test called “test_hier_seq“. This test is finished using the raise & drop objection mechanism.


In summary, Hierarchical Sequence approach provides the capabilities to create complex scenarios using UVM Verification Environment. This approach provides re-usability, modularity, ease of debugging & better stimulus code management (one of the biggest part of the UVM Testbench).

With that I would like to conclude my this post here about Hierarchical Sequences. If you found it useful and helpful in any possible way, I’m happy to write it down. Please feel free to share your suggestions and/or comments. Again, thank you for your time, Bye!