Jul 23, 2013

Much ado about nothing

Having wasted a few hours debugging a UVM issue with `uvm_do macros and a pre_do task in a parent class, I decided to dig in to starting sequences without using the macro implementations. Some of the main EDA companies recommend this approach and there was a good paper at DVCon discussing the pros and cons of the various UVM macros.

So while several people recommend using the methods directly and avoiding the macros, I didn't find a great deal of clear information that described the correct set of method calls to use for a sequence_item and sequence. The examples in Adam's paper above are somewhat confusing, mixing sequence_items and sequences within the same code in some cases and suffering from some typos in other samples. After I worked out the information below, I took another look at the paper and the examples are correct (ignoring the typos), but the interleaving of the sequence and sequence_item start makes the operations more complex than it needs to be.

Luckily enough the UVM source code is all available and I found the simplest way to resolve this was just to go and look at the implementation. In particular, two files are useful: the macro implementations themselves in src/macros/uvm_sequence_defines.svh and the class definitions for sequences and sequence_item in src/seq/uvm_sequence_base.svh

My first confusion stemmed from the class inheritance.

uvm_transaction
V
uvm_sequence_item
V
uvm_sequence_base
V
uvm_sequence

At first glance, this tends to imply that a uvm_sequence is just another type of uvm_sequence_item and they can be treated as interchangeable. However, it fairly quickly becomes apparent that this is only very superficially true. The reality is that sequences ( uvm_sequence) have a distinct API from sequence items (uvm_sequence_item) and only share a few common features.

In particular, sequences runs on a sequencer, without any arbitration controlling their execution. Multiple sequences can be launched on a sequencer and will execute in parallel. In contrast, sequence_items are subject to arbitration to control access to the sequencer's downstream port. A multi-step arbitration handshake is done, but only for sequence items - not sequences. This is the fundamental difference in the API and the reason for the different methods used to start items and sequences.

Sequences and sequence items can both be launched using the various uvm_do macros, so the common base does help provide this abstraction to a single interface, but it really just hides the multiple APIs that are being used behind the scenes. I agree with the view that it is better to just understand what is going on and use the function calls directly. That way you will tend not to be surprised by the various hooks provided for callbacks within the macro invocations.

From uvm_sequence_defines.svh, the methods used to start a sequence item are:

   `uvm_create(item)
   sequencer.wait_for_grant(prior);                           
   this.pre_do(1);
   item.randomize();
   this.mid_do(item);
   sequencer.send_request(item);
   sequencer.wait_for_item_done();
   this.post_do(item);

A sequence however, is launched with:

     `uvm_create(sub_seq)                             
     sub_seq.randomize();
     sub_seq.pre_start();        
     this.pre_do(0);             
     this.mid_do(sub_seq);       
     sub_seq.body();             
     this.post_do(sub_seq);      
     sub_seq.post_start();

Now, the `uvm_create macro call can be replaced with a direct call to the factory::create method, for both the sequence_item and sequence, something like this for a sequence_item,

    item = custom_sequence_item::type_id::create("item",, get_full_name());

or this for a sequence

    seq = custom_sequence::type_id::create("seq",, get_full_name());

In the sequence_item startup, the various calls after the create can be replaced by two functions that encapsulate the arbitration for the driver port, to actually launch a transaction.

   item = custom_sequence_item::type_id::create("item", , get_full_name());
   start_item(item);                             
   item.randomize();
   finish_item(item);

Breaking up the uvm_do in this way provides more control over the randomization of the item - you can disable constraints, assign values after start_item, rather than trying to insert code in pre_do. One fundamental problem with trying to use pre_do to modify constraints is there isn't a clear indication which call to uvm_do in the sequence body has triggered the callback to pre_do. The only indication in the pre_do API is if it is being called before a sequence_item is started or a sequence [via the is_item flag]. If you have multiple calls to `uvm_do (or start_item) in your sequence body, there isn't a reliable way to differentiate which call has triggered the callback (other than maybe looking at the existence of a member variable, if it has been created or not, but this fails with loops or threads of execution in the body)

Similarly to start a sequence, there is a simpler API that encapsulates the various calls to pre_start, pre_do etc:

    seq = custom_sequence::type_id::create("seq",, get_full_name());
    seq.randomize();
    seq.start(target_sequencer, this);

These 4 calls for a sequence_item and 3 calls for a sequence can then be further enhanced using control over constraints, randomize with {} constructs and also using the prioritization arguments to the tasks, or providing a different target sequencer. The end result is much more flexible than the 18 flavours of `uvm_do macros and easier to work out the flow of execution.

There are comments.