In this blog, I describe the necessary steps one has to take while writing a sequence to make sure it can be reusable. Personally, I feel writing sequences is the most challenging part in verifying any IP. Careful planning is required to write sequences without which we end up writing one sequence for every scenario from scratch. This makes sequences hard to maintain and debug.
Here’s where you can find more information on our Verification IP.
As we know, sequences are made up of several data items, which together form an interesting scenario. Sequences can be hierarchical thereby creating more complex scenarios. In its simplest form, a sequence should be a derivative of the uvm_sequence base class by specifying request and response item type parameter and implement body task with the specific scenario you want to execute.
class usb_simple_sequence extends uvm_sequence #(usb_transfer);
rand int unsigned sequence_length; constraint reasonable_seq_len { sequence_length < 10 }; //Constructor function new(string name=”usb_simple_bulk_sequence”); super.new(name); endfunction //Register with factory `uvm_object_utils(usb_simple_bulk_sequence) //the body() task is the actual logic of the sequence virtual task body(); repeat(sequence_length) `uvm_do_with(req, { //Setting the device_id to 2 req.device_id == 8’d2; //Setting transfer type to BULK req.type == usb_transfer::BULK_TRANSFER; }) endtask : body endclass |
In the above sequence we are trying to send usb bulk transfer to a device whose id is 2. Test writers can invoke this by just assigning this sequence to the default sequence of the sequencer in the top-level test.
class usb_simple_bulk_test extends uvm_test;
… virtual function void build_phase(uvm_phase phase ); … uvm_config_db#(uvm_object_wrapper)::set(this, "sequencer_obj. main_phase","default_sequence", usb_simple_sequence::type_id::get()); … endfunction : build_phase endclass
|
So far, things look simple and straight forward. To make sure the sequence is reusable for more complex scenarios, we have to follow a few more guidelines.
task pre_start()
if(starting_phase != null) starting_phase.raise_objection(this); endtask : pre_start task post_start() if(starting_phase != null) starting_phase.drop_objection(this); endtask : post_start |
Note that starting_phase is defined only for the sequence which is started as the default sequence for a particular phase. If you have started it explicitly by calling the sequence’s start method then it is the user’s responsibility to set the starting_phase.
class usb_simple_bulk_test extends uvm_test;
usb_simple_sequence seq; … virtual function void main_phase(uvm_phase phase ); … //User need to set the starting_phase as sequence start method is explicitly called to invoke the sequence seq.starting_phase = phase; seq.start(); … endfunction : main_phase endclass |
class usb_simple_sequence extends uvm_sequence #(usb_transfer);
rand int unsigned sequence_length; constraint reasonable_seq_len { sequence_length < 10 }; … virtual task body(); usb_transfer::type_enum local_type; bit[7:0] local_device_id; //Get the values for the variables in case toplevel //test/sequence sets it. uvm_config_db#(int unsigned)::get(null, get_full_name(), “sequence_length”, sequence_length); uvm_config_db#(usb_transfer::type_enum)::get(null, get_full_name(), “local_type”, local_type); uvm_config_db#(bit[7:0])::get(null, get_full_name(),? “local_device_id”, local_device_id); repeat(sequence_length) `uvm_do_with(req, { req.device_id == local_device_id; req.type == local_type; }) endtask : body endclass |
With the above modifications we have given control to the top-level test or sequence to modify the device_id, sequence_length and type. A few things to note here: the parameter type and string (third argument) used in uvm_config_db#()::set should be matching the type being used in uvm_config_db#()::get. Make sure to ‘set’ and ‘get’ with exact datatype. Otherwise value will not get set properly, and debugging will become a nightmare.
One problem with the above sequence is: if there are any constraints in the usb_transfer class on device_id or type, then this will restrict the top-level test or sequence to make sure it is within the constraint.
For example if there is a constraint on the device_id in the usb_transfer class, constraining it to be below 10 then top-level test or sequence should constraint it, within this range. If the top-level test or sequence sets it to a value like 15 (which is over 10) then you will see a constraint failure during runtime.
Sometimes the top-level test or sequence may need to take full control, and may not want to enable the constraints which are defined inside the lower level sequences or data items. One example where this is required is negative testing:- the host wants to make sure devices are not responding to the transfer with a device_id greater than 10 and so wants to send a transfer with device_id 15. So to give full control to the top-level test or sequence, we can modify the body task as shown below:
virtual task body();
usb_transfer::type_enum local_type; bit[7:0] local_device_id; int status_seq_len = 0; int status_type = 0; int status_device_id = 0; status_seq_len = uvm_config_db#(int unsigned)::get(null, get_full_name(), “sequence_length”, sequence_length); status_type = uvm_config_db#(usb_transfer::type_enum)::get(null, get_full_name(),“local_type”,local_type); status_device_id = uvm_config_db#(bit[7:0])::get(null, get_full_name(), “local_device_id”,local_device_id); //If status of uvm_config_db::get is true then try to use the values // set by toplevel test or sequence instead of the random value. if(status_device_id || status_type) begin `uvm_create(req) req.randomize(); if(status_type) begin //Using the value set by top level test or sequence //instead of the random value. req.type = local_type; end if(status_device_id) begin //Using the value set by top level test or sequence //instead of the random value. req.device_id = local_device_id; end end repeat(sequence_length) `uvm_send(req) endtask : body |
It is always good to be cautious while using `uvm_do_with as it will add the constraints on top of any existing constraints in a lower level sequence or sequence item.
Also note that if you have more variables to ‘set’ and ‘get’ then I recommend you create the object and set the values in the created object, and then set this object using uvm_config_db from the top-level test/sequence (instead of setting each and every variable inside this object explicitly). This way we can improve runtime performance by not searching each and every variable (when we execute uvm_config_db::get) , and instead get all variables in one shot using the object.
virtual task body();
usb_simple_sequence local_obj; int status = 0; status = uvm_config_db#usb_simple_sequence)::get(null, get_full_name(),“local_obj”,local_obj); //If status of uvm_config_db::get is true then try to use //the values set in the object we received. if(status) begin `uvm_create(req) this.sequence_length = local_obj.sequence_length; //Copy the entire req object inside the object which we //received from uvm_config_db to the local req. req.copy (local_obj.req); end else begin //If we did not get the object from top level sequence/test //then create one and randomize it. `uvm_create(req) req.randomize(); end repeat(sequence_length) `uvm_send(req) endtask : body |
class usb_complex_sequence extends uvm_sequence #(usb_transfer);
//Object of simple sequence used for sending bulk transfer usb_simple_sequence simp_seq_bulk; //Object of simple sequence used for sending interrupt transfer usb_simple_sequence simp_seq_int; … virtual task body(); //Variable for getting device_id for bulk transfer bit[7:0] local_device_id_bulk; //Variable for getting device_id for interrupt transfer bit[7:0] local_device_id_int; //Variable for getting sequence length for bulk int unsigned local_seq_len_bulk; //Variable for getting sequence length for interrupt int unsigned local_seq_len_int; //Get the values for the variables in case top level //test/sequence sets it. uvm_config_db#(int unsigned)::get(null, get_full_name(), “local_seq_len_bulk”,local_seq_len_bulk); uvm_config_db#(int unsigned)::get(null, get_full_name(), “local_seq_len_int”,local_seq_len_int); uvm_config_db#(bit[7:0])::get(null, get_full_name(), “local_device_id_bulk”,local_device_id_bulk); uvm_config_db#(bit[7:0])::get(null, get_full_name(), “local_device_id_int”,local_device_id_int); //Set the values for the variables to the lowerlevel //sequence/sequence item, which we got from //above uvm_config_db::get. //Setting the values for bulk sequence uvm_config_db#(int unsigned)::set(null, {get_full_name(),”.”, ”simp_seq_bulk”}, “sequence_length”,local_seq_len_bulk); uvm_config_db#(usb_transfer::type_enum)::set(null, {get_full_name(), “.”,“simp_seq_bulk”} , “local_type”,usb_transfer::BULK_TRANSFER); uvm_config_db#(bit[7:0])::set(null, {get_full_name(), “.”, ”simp_seq_bulk”}, “local_device_id”,local_device_id_bulk); //Setting the values for interrupt sequence uvm_config_db#(int unsigned)::set(null, {get_full_name(),”.”, ”simp_seq_int”}, “sequence_length”,local_ seq_len_int); uvm_config_db#(usb_transfer::type_enum)::set(null, {get_full_name(), “.”,“simp_seq_int”} , “local_type”,usb_transfer::INT_TRANSFER); uvm_config_db#(bit[7:0])::set(null,{get_full_name(),“.”, ”simp_seq_bulk”},“local_device_id”,local_device_id_int); `uvm_do(simp_seq_bulk) simp_seq_bulk.get_response(); `uvm_send(simp_seq_int) simp_seq_int.get_response(); endtask : body endclass |
Note that in the above sequence, we get the values using uvm_config_db::get from the top level test or sequence, and then we set it to a lower level sequence again using uvm_config_db::set. This is important without this if we try to use `uvm_do_with and pass the values inside the constraint block then this will be applied as an additional constraint instead of setting these values.
I came across these guidelines and learned them, at times the hard way. I sure hope these will come in handy when you use sequences that come pre-packed with VIPs to build more complex scenarios, and also when you wish to write your own sequences from scratch. If you come across more such guidelines or rules of thumb for writing re-usable, maintainable and debuggable sequences, please share them with me.
Authored by Hari Balisetty, Broadcom
Here’s where you can find more information on our Verification IP