Debugging Tip: Look At The Bubbles

Debugging Tip: Look At The Bubbles

Posted by

When we say “debugging” the first instinct is to associate it with something like a scoreboard or a monitor but quite often a fair amount of time will be spent in debugging the sequences.
The code inside the sequences can easily become huge and complicated. If you have sequences running on multiple threads, fighting for the same interface, it will not be long until you hear a voice inside your head asking “Who the f&%# is driving this on the bus?”

In this post I want to show you how I am handling such scenario.


Let’s say that we have a DUT with an APB interface through which we can access some control and status registers, send TX traffic and receive RX traffic.

DUT with APB interface
DUT with APB interface

In my random test I usually think about some sequences (scenarios) which I can run in parallel even if they will try to access the same APB interface. The trick is to find scenarios which do no affect each other.

So my random test will have at least these threads running in parallel:

  • configure TX path and start sending data
  • configure RX path and start receiving data
  • read status registers
  • access unmapped locations
  • handle interrupts (e.g. clear them)

You can imagine that such parallel threads will create chaos on the APB bus very difficult to debug by looking on the waveforms or even in the logs.

APB interface waveforms
APB interface waveforms

The salvation comes from an UVM feature called transaction. A transaction is actually a class (uvm_transaction) which you can record and view it on the waveforms.
So instead of the chaotic toggling of the APB signals we can now see this:

View UVM transactions instead of signals
View UVM transactions instead of signals

Let’s see how we can change our UVM SystemVerilog code in order to be able to see these transactions on the waveforms.

Record The APB Sequence Item

The first step is to the specify what information we want to see in the “bubble” representing one APB sequence item.
There are two ways of doing this: overriding do_record() function or using `uvm_field_* macro. The code below shows an implementation of the first option:

class cfs_apb_item_drv extends uvm_sequence_item;
   //direction
   rand cfs_apb_direction direction;

   //address
   rand cfs_apb_address address;
   ...
   virtual function void do_record(uvm_recorder recorder);
      super.do_record(recorder);
      recorder.record_string("direction", direction.name());
      recorder.record_field("address", address, `CFS_APB_MAX_ADDR_WIDTH);
      //add all the relevant fields from the APB item class
      ...
   endfunction
endclass

Next we must modify the APB driver code to record the transaction.
We do this using four functions: enable_transaction(), record(), begin_tr() and end_tr().

class cfs_driver extends uvm_driver#(cfs_apb_item_drv);
   virtual task run_phase(uvm_phase phase);
      forever begin
         cfs_apb_item_drv item;
         seq_item_port.get_next_item(item);

         //the next line enables transaction recording and, in the data browser or
         //of simulator will place a transaction called APB_ITEM under the APB driver
         item.enable_recording(get_tr_stream("APB_ITEM"));

         //the next line will set the begin time of this current transaction
         void'(begin_tr(.tr(item), .stream_name("APB_ITEM")));
         
         //even if it is not necessary, call record() function here.
         //this will populate the "bubble" with the information specified in do_record()
         //and it will be displayed even if the test ends in the middle of the transaction.
         //This record() function is also called inside end_tr() function
         item.record();

         drive_apb_item(item);
         
         //the next line will set the end time of this current transaction
         end_tr(item);

         seq_item_port.item_done();
      end
   endtask;
endclass


Finally, we have to enable UVM to collect this transaction information. This is controlled by a field called recording_detail of the uvm_component. There are several ways through which we can set this to a non UVM_NONE value.

One way is to set it from the test via the UVM configuration database, like so:

class cfs_dut_test_random extends uvm_test;
  ...
  virtual function void build_phase(uvm_phase phase);
    ...
    uvm_config_db#(int)::set(this, "uvm_test_top.env.apb_agent.driver", "recording_detail", UVM_FULL);
    ...
  endfunction
  ...
endclass

An other way is to use the command line arguments (UVM_FULL is equal to 400):

+uvm_set_config_int=uvm_test_top.env.apb_agent.driver,recording_detail,400

With these simple modifications of our code we can get rid of the APB signals and replace them with our APB_ITEM transaction.

Transaction of the APB item
Transaction of the APB item

Record The Sequences

We take a similar approach for recording the sequences as transactions.
First we specify what kind of information we want to see in the bubble:

class cfs_apb_sequence_tx_traffic extends uvm_sequence;
   //endianness
   rand cfs_apb_endianness endianness;

   //number of bytes to send
   rand int unsigned num_of_bytes;
   ...
   virtual function void do_record(uvm_recorder recorder);
      super.do_record(recorder);
      recorder.record_string("endianness", endianness.name());
      recorder.record_field("number of bytes", num_of_bytes, 32);
      //add all the relevant fields from the sequence
      ...
   endfunction
endclass

Last thing is to add in the body of the sequence the calls for the uvm_transaction functions:

class cfs_apb_sequence_tx_traffic extends uvm_sequence;
   ...
   virtual task body();
      enable_recording(p_sequencer.get_tr_stream("TX_TRAFFIC"));

      p_sequencer.begin_tr(.tr(this), .stream_name("TX_TRAFFIC"));
      record();

      //do the regular stuff in the sequence body
      ...
     
      p_sequencer.end_tr(.tr(this));
   endtask
endclass

Now you can easily identify on the waveforms the lifetime of a particular sequence.

Lifetime of a sequence and all the APB items driven during this period
Lifetime of a sequence and all the APB items driven during this period

Determine Bubble’s Parent-Child Relationship

In most of the cases you will end up with some layered sequences. When viewing all the sequences on the waveforms it is a good thing to know what sequence called other sequences – parent-child relationship.
In UVM every uvm_object created gets an unique number called inst_id. To easily identify parent-child relationship we can record the ID of the child and the ID of the parent:

class cfs_apb_sequence_tx_traffic extends uvm_sequence;
   ...
   virtual function void do_record(uvm_recorder recorder);
      uvm_sequence_base parent = get_parent_sequence();
      super.do_record(recorder);

      if(parent != null) begin
         recorder.record_field("parent_inst_id", parent.get_inst_id(), 32, UVM_DEC);
      end
      recorder.record_field("inst_id", get_inst_id(), 32, UVM_DEC);
   endfunction
endclass

We must to a similar thing for the APB sequence item:

class cfs_apb_item_drv extends uvm_sequence_item;
   ...
   virtual function void do_record(uvm_recorder recorder);
      uvm_sequence_base parent = get_parent_sequence();
      super.do_record(recorder);
      if(parent != null) begin
         recorder.record_field("parent_inst_id", parent.get_inst_id(), 32, UVM_DEC);
      end
   endfunction
endclass

Now it is much easier to see on the waveforms who called who:

Parent-child relationship in transactions representing uvm_sequence
Parent-child relationship in transactions representing uvm_sequence


With this little thing we get a quantum leap in debugging efficiency 🙂

Extra Tip: Record Where The Sequence Was Started In Your Code

In some cases knowing the child-parent relationship between the sequences is not enough.
For example, one parent sequence can call some sequences multiple times in its body() task.
So it would be very useful to know in what file and at what line number a sequence was started.
We can do this very easily by taking advantage of some UVM macros:

class cfs_apb_sequence_tx_traffic extends uvm_sequence;
   //name of the file from which this sequence was called
   protected string file_name;

   //line number inside the file
   protected int unsigned line_number;

   function void set_call_id(string file_name, int unsigned line_number);
       string trim_file_name = file_name;
       this.line_number = line_number;

       for(int i = trim_file_name.len() - 1; i >= 0; i--) begin
          if(trim_file_name[i] == "/") begin
             trim_file_name= trim_file_name.substr(i + 1, trim_file_name.len() - 1);
             break;
          end
       end
       this.file_name = trim_file_name;
   endfunction

   virtual function void do_record(uvm_recorder recorder);
      uvm_sequence_base parent = get_parent_sequence();
      super.do_record(recorder);
      ...
      recorder.record_string("file", file_name);
      recorder.record_field("line", line_number, 32, UVM_DEC);
      ...
   endfunction
endclass

Now, wherever we start the sequence we add just one line:

seq.set_call_id(`uvm_file, `uvm_line);
seq.start(sequencer);

In this way we can see in the waveforms where a particular sequence was started:

Information about sequence start location in the SystemVerilog file
Information about sequence start location in the SystemVerilog file


There’s your second quantum leap in debugging efficiency 🙂

If you want to find out more about debugging techniques, check out a previous post of mine: Debugging Tip: Always Remember The Cause

Hope you found this useful!

Cristian Slav

9 Comments

  • tudor.timi@verificationgentleman.com' Tudor Timi says:

    There are also begin/end_child_tr() functions for modeling parent/child relationships between recorded transactions.

    • Hi Tudor,

      I know about the functions.
      I tried to use them but I saw no effect in the waves window of my current simulator. (e.g. I was expecting for it to group child transactions under their parent)

  • ciupitudan@gmail.com' Daniel says:

    Really nice technique but in my experience this comes with a simulation speed cost. So I would recommend adding a switch that enables the recording only when you truly need it.

    • Hi Daniel,

      you are right.
      I usually place this code under an “`ifdef DEBUG_MODE” and define DEBUG_MODE in the terminal when I invoke the simulator (e.g. irun +define+DEBUG_MODE … )

  • syed.taahir.active99@gmail.com' Taahir says:

    Hi Cristian,

    You can please show one full working example like in EDA playground which will be helpful as a golden reference.

    Thanks
    Taahir

    • Hi Taahir,
      As far as I know you can not see transactions in the wave viewer from EDA playground.
      Therefore you will not be able to see anything in an example run there.

      Cristi

      • syed.taahir.active99@gmail.com' Taahir says:

        Oh okay Cristi, or else can you please add link for one example working one so that , I have a reference.

        Thanks
        Taahir

        • In order to have a running example I would need a running agent.
          From my point of view everything should be ok if you copy-paste the code from my article to your environment.

          However, if you add a running agent in EDA playground I can add the necessary code so you can record transactions.

  • tudor.timi@verificationgentleman.com' Tudor Timi says:

    After playing around with recording, I’d recommend using the ‘begin/end_tr()’ functions of uvm_component instead of directly calling transaction.begin_tr() directly. Reasons:

    1. You want to ground your transaction stream to a specific component (a monitor, a driver, etc.). This is something you’re anyway doing in your current code by prefixing the stream name with ‘get_full_name()’ in the ‘trans.enable_recording(…)’ call.

    2. You can benefit from the ‘recording_detail’ variable that each component has, which can be controlled via the command line or simulator TCL commands. This means you can enable/disable recording without having to change the code or add your own mechanisms (i.e. an ‘enable_recording’ config variable or anything of the sort).

    3. You want to create the stream of transactions before actually publishing any transactions to it. This is more GUI-friendly, as some simulators have issues working with transactions streams that don’t yet exist. For example, you want to save your waves to a file and load it every time you start a GUI simulation. If the stream wasn’t created yet, then it might not get added to the waves or something else might go wrong (e.g. formatting settings won’t get taken into account). By creating the stream before pushing any transactions to it you are effectively signalling your intent to perform transaction recording.

Leave a Reply

Your email address will not be published.