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.