Wednesday, April 23, 2014

UVM Drain Time - The Old Fashioned Way

One of the most useful additions of OVM 2.1 was the objection mechanism. You could raise an objection when starting your main traffic sequence and drop it once it was finished, thus stopping the simulation. Being done with input traffic didn't mean that nothing else would happen from that point as the DUT may have required some additional time to drain any transactions that were still being processed. Setting a drain time added an extra delay from the time that all objections were dropped to the stop of the simulation, making sure that there were no outstanding transactions at that point.

The drain time was usually set in the base test, before the run phase was started. I liked to do it in the end_of_elaboration phase, but any other phase would have worked as well:

class test_base extends ovm_test;
  // ...
  
  function void end_of_elaboration();
    ovm_test_done.set_drain_time(this, 100ns);
  endfunction
  
  // ...
endclass

When UVM was released, it brought another cool addition: multiple run-time phases like reset, main, shutdown, etc. While I must admit I have never really used them myself, they do look very useful for someone doing SoC verification. Sadly, along with them my old friend, the test_done objection, was deprecated in favor of phase specific objections.

The way to do it now is to set the drain time inside the phase task by using the phase argument:

class test_base extends uvm_test;
  // ...
  
  task run_phase(uvm_phase phase);
    phase.phase_done.set_drain_time(this, 5);
  endfunction
  
  // ...
endclass

I don't like this, because I can't just set the drain time in the base test anymore and forget about it. Calling super.run_phase(phase) in subclasses is out of the question for obvious reasons. One could create a dummy method and always call that first; at least this way the value of the drain time would be centralized in one location. There are however problems with this approach. We always have to call this method which means it's very easy to forget to do it. Also, if we want different drain times for each phase we would need multiple such methods. I guess it's back to the drawing board.

While looking around through the UVM base class library (BCL) I noticed that each phase has a singleton associated with it. We can get handles to these singletons in the end_of_elaboration phase and set the drain time there:

class test_base extends uvm_test;
  // ...
  
  function void end_of_elaboration_phase(uvm_phase phase);
    uvm_phase run_phase = uvm_run_phase::get();
    run_phase.phase_done.set_drain_time(this, 5);
  endfunction
  
  // ...
endclass

It seems that we're home free, but we're not done just yet. If we try to do the same for the main phase, after setting the drain time it just doesn't' work. After some more experimenting and asking around I found out that the objects we get returned via uvm_*_phase::get() are different that what is passed to the uvm_*_phase(...) methods as the phase argument. Here's a link to the thread in the UVM forums with the explanation - many thanks to kirloy for clearing it up. Applying what we learned gives us this:

class test_base extends uvm_test;
  // ...
  
  function void end_of_elaboration_phase(uvm_phase phase);
    uvm_phase main_phase = phase.find_by_name("main", 0);
    main_phase.phase_done.set_drain_time(this, 10);
  endfunction
  
  // ...
endclass

Now we're really done. As usual, the code can be found on the blog's repository. Thanks for reading and stay tuned for more!

14 comments:

  1. Hi Tudor, thank you for the post.

    I have another solution ... don't use drain time! I find that the drain time is only needed when the _scoreboard_ isn't objecting, or at least not objecting correctly. Furthermore, setting the correct drain time is a painful process: you set it to a value that is too low, then you increase it a bit (being careful to not be too generous), it's now working great, until you run the testcase that needs some more, then you increase it again, etc. You will side-step this headache entirely if you don't use drain time and put objections in the right spots.

    ReplyDelete
    Replies
    1. Hi,

      I get what you mean, but the whole objection mechanism is a bit broken in UVM. Some technologists (especially the ones from Mentor Graphics) recommend only setting objections in the top level sequence started by the test. This way you know you finished driving your stimulus and add a conservative drain time to make sure that all traffic has been responded to by the DUT. The reason they say this is because objections from components have to propagate up the hierarchy and this wastes CPU cycles to simulate (they even had numbers to back it up). If you can afford the performance hit, then I agree, setting objections is much better. From what I know they've addressed this problem in UVM 1.2 (though they created new ones) and objections don't need to propagate along the whole hierarchy anymore, making the whole objection mechanism faster.

      Delete
    2. In my experience, objections certainly aren't broken but are in fact essential ingredients to a healthy test bench. There is only a performance hit when objections are abused. (You know you are over-using objections if a component has an objection count over 1, or if an objection is raised and lowered many times in the same time-step.) Proper use of objections will not only determine exactly when to end the simulation, but will greatly help debugging deadlocks, enable self-adjusting test cases and provide a foundation for the elegant UVM heartbeat monitor.

      Mentor has a case, but they shouldn't scare away sensible use of the methodology.

      Delete
  2. Hi Tudor,

    In this post you mentioned "I can't just set the drain time in the base test anymore and forget about it. Calling super.run_phase(phase) in subclasses is out of the question for obvious reasons."

    Can you specify what those obvious reasons are? I was thinking that as long as run_phase() task call in base test is virtual, the sub-tests could still call super.run_phase() without any issue.

    ReplyDelete
    Replies
    1. Hi Sailaja,

      The person who develops the testbench isn't always the same person that writes tests. I would rather have my setup in such a way that everything is configured and test writers just have to start sequences. Making them call super.run_phase() inside their tests means they have to concern themselves with some testbench issues and I would rather avoid that. In OVM this was possible (because there were no phase specific objections), but in UVM I had to do the call to super.run_phase() thing this until I found this approach.

      Coming back to the second half of your question (assuming we don't set the drain time like in this post), with a test that directly inherits from the base test this isn't a problem, because you can just call super.run_phase() as the base test doesn't run any stimulus. The problem I always had was when I wanted to apply the same setting to a test that inherits from a test that inherits from the base class (a grandchild class). This means calling super.run_phas() in the grandchild class will execute all of the stimulus of the child class (the one that directly inherits from the base test) as well and this isn't what you might want.

      Delete
  3. If you don't like using a string to pass the phase name, it's possible to use 'find(...)' instead of 'find_by_name(...)':

    uvm_phase main_phase = phase.find(uvm_main_phase::get());

    ReplyDelete
  4. I noticed that I passed this as the first argument to set_drain_time(...). This means that only objections that are raised by the test itself or any of its sub-components will trigger this drain time when they are subsequently dropped.

    If you're raising objections from sequences (via the starting_phase variable), then this won't work. This is because sequences aren't components, so they aren't technically children of the test. To set the drain time globally, we need to set it for uvm_root by calling set_drain_time(null, ...)

    ReplyDelete
  5. Great post! One thing is not clear though, near the end of the post you have 2 code snippets to set the drain time. The first does it for the run_phase, the second for the main_phase. And you wrote "If we try to do the same for the main phase, after setting the drain time it just doesn't' work."

    Did you mean that the first method (using ::get() ) does work for the run_phase but not for the main_phase? I don't understand why that would be the case. Or maybe I misunderstood that sentence and you were maybe meaning using ::get() to set the drain time is not working for any of the phases?

    ReplyDelete
    Replies
    1. Calling uvm_main_phase::get() and trying to set the drain time on its result doesn't work. The get() returns a different object than the one that gets passed to main_phase(uvm_phase phase) as the phase argument.

      Delete
    2. Setting the drain time works for the other phases too, but we have to get a handle to the phase by calling phase.find_by_name("main", 0). It's kind of inconsistent of the implementation. I think they may have changed it again in UVM 1.2 as this forum thread shows: http://forums.accellera.org/topic/5531-uvm12-can-report-that-run-is-not-a-task-based-phase/

      Delete
    3. I mainly wanted to verify that "Calling uvm_main_phase::get() and trying to set the drain time on its result doesn't work." holds in general, i.e. that rule holds for any phase and wherever you call it. So

      class test_base extends uvm_test;
      // ...

      function void end_of_elaboration_phase(uvm_phase phase);
      uvm_phase run_phase = uvm_run_phase::get();
      run_phase.phase_done.set_drain_time(this, 5);
      endfunction

      // ...
      endclass

      also doesn't work. If first interpreted your post as if this particular one was working but not if that would have been main_phase.

      It is clear now that for any phase I need to set the drain time on the object passed as phase argument (so in that phase itself) or using the find_by_name if I want to set it for a phase that I am curently not in, right?


      So from the

      Delete
    4. For some reason, setting a drain time on the result of uvm_run_phase::get() does work in UVM 1.1d, even though it returns a different object than the one passed to run_phase(...). It doesn't work for uvm_main_phase.get(), though. I've no idea why.

      They made some changes in UVM 1.2, so it might not work for the former anymore either.

      You're better off using find_by_name(...), because that always works.

      Delete
    5. You can also use phase.find(uvm_main_phase::get(), 0). This is better because you avoid using strings. The second argument has to be 0. For the run phase, a value of 1 (i.e. stay in scope) also works. Again, no idea why, as this is something that concerns the implementation.

      I kind of find the whole thing rather confusing and not so well documented (from a user point of view I mean).

      Delete
    6. "I kind of find the whole thing rather confusing and not so well documented (from a user point of view I mean)."

      I agree ;-) As I find with more things in UVM. Luckely there are sites like this that give more insight. I would actually prefer a UVM 1.3 with better documentation, a split in 'user' API and 'developer' API, and deprecation of 'old mechanisms' over a bunch of new features.

      Delete