Jump to content
c4brian

sequence DUT synchronization

Recommended Posts

I am trying to synchronize when sequences are sent to the DUT. 
I send one transaction to the DUT at the beginning, and then I need to wait for the DUT to send out a transaction (much later) before I continue with my sequence.
 
Right off the top, I was thinking I would add an analysis_imp, and when the DUT sends the transaction, have the agent broadcast it, and my sequence could trigger an event in say, the write function.
 
I don't believe this makes sense though, as I don't believe ports were meant to be created in transient types, like a uvm_sequence.
 
It appears as the only real "connection" the sequence has with the environment is through an agents sequencer. 
 
What would be ideal is if my sequence were simply waiting on an event that is triggered by receiving a transaction in an agent attached to the DUT, but i don't know where that event would live.
 
Here is the body method of my "directed_test" sequence:
 
start_item (txn, -1, a_sequencer);
finish_item(txn);
 
`uvm_info(report_id, "waiting for DUT to send an ARP reply...", UVM_LOW)
 
<------- need something here to wait/sync to the DUT response
 
`uvm_info(report_id, "OK to start next sequence item!", UVM_LOW)
start_item (next_txn, -1, a_sequencer);
finish_item(next_txn);

Share this post


Link to post
Share on other sites

When the DUT sends out a transaction, don't you have some kind of responder (aka slave) sequence that services that? This could be your sync point.

 

I'm new to responder sequences; I watched a tutorial on them just now.

 

My main sequence is sending transactions to the DUT through an agent configured as a "master", hooked up to the DUT Rx interface.  Some time later, the DUT will send out a transaction on it's TX interface.  Are you saying I should connect another "slave" agent to the TX interface with a "responder sequence"?  This makes a little sense, except I'm not sure about a few details.

 

This 2nd agent, after completing the "dummy transaction" (when the DUT sends the transaction I am waiting for), I would need to update the transaction in the responder sequence, and then send it through the original "master" agent (to drive it to the DUT RX interface); in order to do this, it will need steal control of the original agents sequencer for some time.  Which is okay, using .grab, I suppose?

 

Next, even if this works, how does the responder sequence tell the main sequence to proceed?  There is no handle to anything in the main sequence except the main "master" sequencer handle.

 

If this is way off base, please tell me, I've not implemented a responder before.

Share this post


Link to post
Share on other sites

Since you want to coordinate stimulus across two interfaces, a virtual sequence makes the most sense in this case. You'd need a virtual sequencer with handles to both agents' sequencers. The virtual sequence running on this sequencer would dispatch the TX and RX/responder sequences to the appropriate sequencers.

 

I'm assuming you've watched Tom Fitzpatrick's tutorial on responder sequences. Let's say yours would look like this (pseudocode off the top of my head):

class rx_responder_seq extends uvm_sequence_item #(my_item);
  // event we can use to synchronize when the DUT sends out
  // something out of its TX interface
  event item_finished_e;
  
  task body();
    forever begin
      start_item(req);
      req.randomize();
      finish_item(req);
      -> item_finished_e;
    end
  endtask
endclass

You could use the event to synchronize to the moment the DUT's TX item finishes. In your virtual sequence you'd do something like this:

class virtual_seq extends uvm_sequence;
  task body();
    rx_responder_seq resp_seq;
    tx_item txn, next_txn;

    // create responder and fork it
    resp_seq = rx_responder_seq::type_id::create("resp_seq");
    fork
      resp_seq.start(p_sequencer.rx_seqr);
    join_none

    // now you can do your stimulus
    txn = tx_item::type_id::create("txn");
    start_item(txn, -1, p_sequencer.tx_seqr);
    finish_item(txn);

    // wait for the DUT to send an item
    wait @(resp_seq.item_finished_e);

    // do the next TX item
    next_txn = tx_item::type_id::create("next_txn");
    start_item(next_txn, -1, p_sequencer.tx_seqr);
    finish_item(next_txn);

    // ...
  endtask
endclas

You don't need to grab the TX sequencer, because you don't have parallel threads starting items on it. Your RX and TX agents also stay completely separate (as they should be) and the stitching is done one level above (in the virtual sequencer and sequence).

Share this post


Link to post
Share on other sites

One more thing to consider: if your DUT's RX interface is unidirectional (i.e. no handshake mechanism), then you don't need any responder sequence. A simple monitor would just suffice. In this case, your virtual sequencer should contain a handle to the TX sequencer and one to the RX monitor. To synchronize, you can use the event from the monitor that signals that an item was collected.

Share this post


Link to post
Share on other sites

I don't currently use an explicit virtual sequencer, but saying that I did, would this allow my main sequence to access an event in the monitor using e.g. p_sequencer.monitor_handle.my_event? (maybe its m?)

Share this post


Link to post
Share on other sites

In response to your former comment ( thank you for the lengthy response ), that setup makes sense, but seems a little clunky for what I am doing.  I saw this because I'm essentially just monitoring the TX interface of the DUT, waiting for this magic transaction to appear.  I'll never turn around and drive that interface (like a responder might); I would prefer if I have an agent on that interface, that it is simply passive. 

 

What I really need is to know that transaction arrived from the DUT, modify it just a little bit, and then send it right back in through the RX interface.  It seems like a no-brainer for a monitor to provide this information to the sequence.  I'm hoping/guessing your recent comment was aimed at that.

Share this post


Link to post
Share on other sites

Since you only have one interface you're driving (the DUTs RX if), you can make your life easier and just pass a handle of the RX monitor (looking at the DUTs TX interface) to your sequence using the config DB:

class my_sequence extends uvm_sequence #(my_item);
  // handle to the monitor
  rx_monitor monitor;

  task body();
    // get the monitor handle
    uvm_config_db #(rx_monitor)::get(p_sequencer, "", "monitor", monitor);

    // drive first TX

    wait (monitor.item_finished_e);

    // drive second TX
  endtask
endclass

Somewhere in your testbench you do the corresponding "set". You can use the sequencer's path for the config DB operations (seeing as how the sequences are running on the sequencer and have a handle to that):

class my_test extends uvm_test;
  function void end_of_elaboration_phase(uvm_phase phase);
    uvm_config_db #(rx_monitor)::set(this, "path.to.tx.seqr", "monitor", path.to.rx.monitor);
  endfunction
endclass

Share this post


Link to post
Share on other sites

@David That would work, but it relies on using a singleton as a global instance, which is usually frowned upon in software development. Using the config DB also means relying on a singleton (the DB itself), but at least using the complete path to set a config setting is more portable for vertical reuse. When relying on the event pool, there is a chance that the same key is used in multiple places (I guess this is what you meant by not selecting the string too casually).

Share this post


Link to post
Share on other sites

So I took the first approach and simply populated the monitor into the database, scoped to the sequencer on which is running an "ARP handler" sequence, which needs access to an event in the monitor.

 

So, this seems to work just fine.

// from test
// Not a typo, I scoped the monitor from one layering agent to the sequencer in another. (Rx vs Tx)
virtual function void end_of_elaboration_phase(uvm_phase phase);
  uvm_config_db #( Eth2Arp_monitor )::set( this , "environment.SysLnp_Rx_layer_agent.Arp2Eth.sequencer" , "monitor" , environment.SysLnp_Tx_layer_agent.Arp2Eth.monitor ) ;
end function
 // worker sequence
class ARP_handler_seq extends uvm_sequence #(arp_transaction);
  `uvm_object_utils(ARP_handler_seq);

 Eth2Arp_monitor Eth2Arp_mon ;

  virtual task body();

    if( !uvm_config_db #( Eth2Arp_monitor )::get( m_sequencer, "" ,"monitor" , Eth2Arp_mon ) )
      `uvm_fatal(report_id , "cannot find resource Eth2Arp_mon" )

    forever begin

      // wait for the response (TX) LL layering agents to detect an ARP packet
      @(Eth2Arp_mon.got_txn_event);

      //do something

    end

  endtask
endclass

You lost me at "singleton".  Apparently the uvm_config_db is a singleton.

 

Concerning the uvm_event and mainly the uvm_event_pool, I have not seen a good tutorial on how they work (they are not discussed in the Mentor Cookbook), and what I get for the money.  I take it I don't have to use the uvm config database using that option?

Share this post


Link to post
Share on other sites

Using the UVM pools is a shortcut you can take, but it might hurt you further down the line. Consider the case where you wouldn't pass the monitor handle to your sequence and use the uvm_event_pool instead. You wouldn't be able to tell by just looking at the class declaration that it has an external dependency. You'd have to read the body() method to see that at some point it's waiting on event from outside. Even in your current setup, it might be cleaner to get a uvm_event handle from the monitor and not the monitor itself, as that would show that you only need the event and nothing more.

Share this post


Link to post
Share on other sites

Well now that you mention it, my sequence not also needs the event, but a copy of the transaction received by the monitor; unless the uvm_event can carry object data with it, I think the only way this will work is by passing in the monitor handle, which copied the received transaction prior to firing the event (so it's available to any listeners).

Share this post


Link to post
Share on other sites

So I took the first approach and simply populated the monitor into the database, scoped to the sequencer on which is running an "ARP handler" sequence, which needs access to an event in the monitor.

 

So, this seems to work just fine.

 

I don't think this is a good solution. Firstly, it is complicated for a simple thing. Secondly, the sequence has to know details about the environment and dot-point into it in a way that can't be checked by the compiler. And thirdly, it isn't clear what is going on because a simple thing is drowning in uvm.

 
Why not avoid direct use of uvm and invent your own interfacing? Define an interface to your dut in the top level environment, and implement your test and sequences by means of that specific top level env.
 
For example, if the nature of your dut is such that you have to wait for an ARP reply before you can continue with something else, then add an event to your top level env and signal ARP reply with it. If there is a need to communicate data, then pass them with the event. Your sequence will go something like this:
 
task body();
   DoSomething();
   m_tb.m_arpReply.Await();
   DoSomethingElse();
endtask
 
The actual event source is now hidden from the sequence. The event could be triggered directly by a monitor somewhere, or it could be triggered by a write to an analysis port, or whatever, and these implementation details could change at some point, but the sequence would continue to work. Also, the compiler is now able to check that something called m_arpReply actually exists on the top level env and is used in a reasonable way.
 
Another thing is that, in your solution, you block directly on an SV event. What if the ARP reply does not arrive? By using your own interfacing, you can easily add timeout and common error handling. Sequences can then be kept terse and simple, and you don't have to be an uvm expert in order to understand what they are doing.
 
 
Erling

Share this post


Link to post
Share on other sites

Erling,

 

I am interested in hearing more about your approach.  I get the idea of what you are getting at, but I am a little confused of some of the implementation details, and also your verbiage.

m_tb.m_arpReply.Await();

What is exactly is m_tb, and how does the sequence have access to it? 

Is m_arpReply a monitor? 

Await is a task running on this monitor?

 

If there is an event declared in the environment, I don't see how neither an instantiated monitor can trigger, nor how a virtual or worker sequence gets access to said event.

 

How do you pass data "with" an event?  In my monitor currently, I simply store a local copy of the transaction before triggering the event, and the listener can pull the copy directly.

Share this post


Link to post
Share on other sites

Erling,

 

I am interested in hearing more about your approach.  I get the idea of what you are getting at, but I am a little confused of some of the implementation details, and also your verbiage.

m_tb.m_arpReply.Await();

What is exactly is m_tb, and how does the sequence have access to it? 

Is m_arpReply a monitor? 

Await is a task running on this monitor?

 

In the sequence example, m_tb is a reference to the test bench, m_arpReply is an event (not necessarily an uvm_event), and Await is a member function of the event to wait for it to trigger. The Await function could be equipped with a timeout parameter and error handling if needed.

 
The idea here is that a test (and a virtual sequence) is implemented by means of a testbench, much in the same way a test script can be implemented by means of a testbench in the lab. In other words, the test is implemented by means of something specific, and not just a generic uvm_env top level instance.
 
A few helper classes can be defined in a utility package to make this reusable, for example:
 
virtual class Testbench extends uvm_env;
  // common functionality for all test benches
endclass
 
A generic test can then be defined as:
 
virtual class Test #(type TB = Testbench) extends uvm_test;
  protected TB m_tb;
  // here: common functionality for all tests
endclass
 
A virtual sequence is test-like, ie it also has a reference to the testbench, for example:
 
virtual class VirtualSequence #(type TB=Testbench) extends uvm_sequence_base;
  protected TB m_tb;
  // here: common functionality for all virtual sequences
endclass
 
As an example, consider a testbench for the Monty Python machine that goes ping. If we do something with this machine, then we must wait for the machine to go ping before we can do something else, and thus it must be possible to wait for the ping. First we write up a definition of the ping machine testbench:
 
class PingTb extends Testbench;
  Event m_ping;
  // ping machine test environment goes here
endclass
 
Given this testbench definition, base classes for all virtual sequences for the PingTb can be defined, for example:
 
virtual class PingSeq extends VirtualSequence #(PingTb);
  task WaitForPing();
    m_tb.m_ping.Await();
  endtask
  // + other common functionality for all ping sequences
endclass
 
A base class for all ping machine tests would go like this:
 
virtual class PingTest extends Test #(PingTb);
  // common functionality for all tests for the ping machine
endclass
 
An actual sequence could be:
 
class MyPingSeq extends PingSeq;
  task body();
    DoSomething();
    WaitForPing();
    DoSomethingElse();
  endtask
endclass
 
Common functionality provided by the Testbench, Test and VirtualSequence base classes could be helpers to run sequences easily, and thus an actual test for the ping machine could go like this:
 
class MyPingTest extends PingTest;
  task Main();
    RunSequence(MyPingSeq::Create());
  endtask
endclass
 
Now, if you compare MyPingTest with the uvm way, you may find that MyPingTest is something that can be discussed with colleagues that aren't uvm experts, without being met with a staring glare. The uvm details are mostly hidden in base classes. Objections, for example, can be taken care of by the Test base.
 

 

If there is an event declared in the environment, I don't see how neither an instantiated monitor can trigger, nor how a virtual or worker sequence gets access to said event.

 

The event can be connected (by the test bench environment in the connect phase) to a source event in a monitor (or whatever), much in the same way ports are connected. The connection is a simple assignment of the source event to the event in the test bench interface.

 

 

How do you pass data "with" an event?  In my monitor currently, I simply store a local copy of the transaction before triggering the event, and the listener can pull the copy directly.

 

We don't actually pass data with events. Instead we have a parameterized Notification class combining an event with a data member of any type, and methods to send and receive. The uvm_event can do something similar, but it wasn't parameterized when we needed it, and still has other shortcomings. However, the uvm_event can be used as a starting point, it demoes how to pass data with an event.

 

 

Erling

Share this post


Link to post
Share on other sites

First, thank you for the detailed response.

 

So, in summary, you provide a handle to the environment inside your virtual sequence.  Is this accurate?

This would give the virtual sequence direct access to every environment resource, including API classes, etc.

class PingTb extends Testbench;

  Event m_ping;
  // ping machine test environment goes here
endclass

I see you capitalized "Event"; is this a class, or a system-verilog event?  I assume it's a class because later you call a class method: m_ping.Await().

class MyPingTest extends PingTest;
  task Main();
    RunSequence(MyPingSeq::Create());
  endtask
endclass

Why not just use the uvm_run_phase? 

 

What starts Main()?

Share this post


Link to post
Share on other sites

So, in summary, you provide a handle to the environment inside your virtual sequence.  Is this accurate?

 

Yes. We assign a testbench reference automatically to establish a "implemented by means of relationship" between the virtual sequence and the testbench. We do not assign sequencers to virtual sequences, but use automated sequence to sequencer mapping.

 

I see you capitalized "Event"; is this a class, or a system-verilog event?  I assume it's a class because later you call a class method: m_ping.Await().

 

Yes, it's our own class (to provide timeout and common error handling to deal with things that are supposed to happen, but didn't).

 

Why not just use the uvm_run_phase?

 

Because a test in uvm is subject to kill. We want a test to be more than a renamed uvm_component. It is the one and only component that knows the overall purpose of a particular simulation run. It is special, and should not need to fight uvm in order to stay alive, among other things. The Test base silences uvm_objections first thing, and provides an interface to all other components for quick phase progress control.

 

What starts Main()?

 

Main() is called by the run() method of the common Test base class for all tests (for any testbench), after initialization of the base class. A test will end orderly when Main returns (unless regular components have disabled phase progress, this will have the Test base echo names of busy components on the transcript, and wait for them to enable phase progress again, but not longer than a configurable timeout, after which the Test base will print full names of all stalled components, and then exit abortively). This may sound complicated, but is written once and for all, and hidden for actual tests.

 
I sorry already for this being off topic (and off forum, probably) several posts ago. Send me a personal message should you have other questions on this matter. 
 
 
Erling

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×