Verification Martial Arts: A Verification Methodology Blog

How to use VMM callbacks

Posted by Janick Bergeron on November 19th, 2008

One of the major benefits of using VMM is that it helps create reusable and extensible transactors and verification environments. One of the key extensibility features of VMM is the “callback”.

The following code sample shows a transactor with its core functionality implemented in the main() task.

   class my_xactor extends vmm_xactor;
      ...
      task main();
         ...
         forever begin
            ...
            in_chan.get(in_tr);    // Get input transaction
            ...                    // Execute transaction
            out_chan.put(out_tr);  // Generate output transaction
            ...
         end
      endtask
   endclass

How would you modify this transactor to accommodate DUT or test-specific features such as saving the output transactions in a scoreboard, injecting errors or adding functional coverage? One option would be to modify the transactor itself and change the functionality in the main() task as shown in the code below:

   class my_xactor extends vmm_xactor;
      ...
      task main();
         ...
         forever begin
            ...
            in_chan.get(in_tr);    // Get input transaction
            if ($urandom % 10 < 2) in_tr.checksum = $random;
            ...                    // Execute transaction
            sb.expect(out_tr);
            out_chan.put(out_tr);  // Generate output transaction
            ...
         end
      endtask
   endclass

However, this makes the transactor not reusable for other DUTs or tests. You must avoid making DUT or test-specific modifications to reusable code. This is a problem that is generic to reusable software, not just verification, and the object-oriented world provides a solution to this problem: virtual methods. Virtual methods can be used to provide extension points at relevant point in the functionality of the transactor:

   class my_xactor extends vmm_xactor;
      virtual task pre_exec(in_trans tr);
      endtask
      virtual task post_exec(out_trans tr);
      endtask
      ...
      task main();
         ...
         forever begin
            ...
            in_chan.get(in_tr);    // Get input transaction
            pre_exec(in_tr);
            ...                    // Execute transaction
            post_exec(out_tr);
            out_chan.put(out_tr);  // Generate output transaction
            ...
         end
      endtask
   endclass

VMM guidelines 4-155 through 4-158 provide recommendations for useful extension points through virtual methods. Virtual methods should also have suitable argument allowing the behavior of the transactor to be observed or modified as necessary. It is now possible to provide DUT or test-specific extensions of the transactor without modifying the reusable code:

   class my_dut_xactor extends my_xactor;
      virtual task post_exec(out_trans tr);
         sb.expect(tr);
      endtask
   endclass

   class my_test_xactor extends my_dut_xactor;
      virtual task pre_exec(in_trans tr);
         if ($urandom % 10 < 2) in_tr.checksum = $random;
      endtask
   endclass

There are two problems though. First, each time you extend the transactor, you create a new class type but the verification environment needs to be written using a class type that is known a priori; if the class type changes for every test, the environment itself will need to be changed for every test. Second, to combine orthogonal extensions, you must extend the previously extended class type, creating a linear chain of new class types; combining N different orthogonal extensions leads to potentially having to create and maintain 2N classes.

Object-oriented programming provide several well-known approaches and solutions to common problem called “patterns“. Patterns are not base classes or libraries. They are techniques and examples on how to structure object-oriented code to solve a specific challenge. Different patterns can be used, alone or in combination.

The first problem can be solve using the “abstract factory” pattern. But it introduces several limitations. For example, it requires an argument-free constructor. This has the consequence of requiring separate methods for transactor configuration and connection. VMM was designed to use the simple and well-known pins-and-wire connectivity model–which has been used to connect Verilog modules for many, many years and is well understood by all design and verification engineers. Just like Verilog modules are instantiated with their configuration parameters and port connections specified at the same time, so are VMM transactors. And this requires that they be passed as arguments via the constructor. Second, the factory pattern is used only at the time the transactor is created. This creates a static lifetime for the transactor and its extensions. It is not possible to dynamically introduce or remove a transactor extension (such as error injection) during the execution of a test.

The better pattern to use for transactor extension is the “facade pattern”. It calls for the separation of the virtual methods into a separate class, called the “facade”. The transactor will then call the virtual methods through that facade class.

   class my_xactor_cbs;
      virtual task pre_exec(in_trans tr);
      endtask
      virtual task post_exec(out_trans tr);
      endtask
   endclass

   class my_xactor extends vmm_xactor;
      my_xactor_cbs cbs;
      ...
      task main();
         ...
         forever begin
            ...
            in_chan.get(in_tr);    // Get input transaction
            if (cbs != null) cbs.pre_exec(in_tr);
            ...                    // Execute transaction
            if (cbs != null) cbs.post_exec(out_tr);
            out_chan.put(out_tr);  // Generate output transaction
            ...
         end
      endtask
   endclass

Using the facade pattern, the environment can be written using the original “my_xactor” class and your extensions can be introduced in the existing transactor instance by assigning to the “cbs” facade instance. Extensions can also be dynamically introduced or removed as the facade instance can be modified at any time.

   class my_dut_cbs extends my_xactor_cbs;
      ...
      virtual task post_exec(out_trans tr);
         sb.expect(tr);
      endtask
   endclass

   class my_test_cbs extends my_dut_cbs;
      ...
      virtual task pre_exec(in_trans tr);
         if ($urandom % 10 < 2) in_tr.checksum = $random;
      endtask
   endclass

   class tb_env extends vmm_env;
      my_xactor xact;
      ...
      virtual function void build();
         ...
         xact = new(...);
         begin
            my_dut_cbs cb = new(...);
            xact.cbs = cb;
         end
         ...
      endfunction
      ...
   endclass

   program test;
   initial begin
      tb_env env = new();
      my_test_cbs err_ext = new(...);
      fork
         #1000 env.xact.cbs = err_ext;
      join_none
      env.run();
   end
   endprogram

The facade pattern solves the new class type issue, but not the exponential number of class extensions. If a “chain of responsibilityobserver” pattern is used in combination with the facade pattern, it is possible to register multiple facade instances with the same transactor, each providing an orthogonal extension.

   class my_dut_cbs extends my_xactor_cbs;
      ...
      virtual task post_exec(out_trans tr);
         sb.expect(tr);
      endtask
   endclass

   class my_test_cbs extends my_xactor_cbs;
      virtual task pre_exec(in_trans tr);
         if ($urandom % 10 < 2) in_tr.checksum = $random;
      endtask
   endclass

   class tb_env extends vmm_env;
      my_xactor xact;
      ...
      virtual function void build();
         ...
         xact = new(...);
         begin
            my_dut_cbs cb = new(...);
            xact.cbs.push_back(cb);
         end
         ...
      endfunction
      ...
   endclass

   program test;
   initial begin
      tb_env env = new();
      my_test_cbs err_ext = new(...);
      fork
         #1000 env.xact.cbs.push_back(err_ext);
      join_none
      env.run();
   end
   endprogram

The combination of the facade and chain-of-responsibilityobserver patterns creates what is known as VMM callbacks. VMM provides some pre-defined functionality to support the callbacks: the “vmm_xactor_callbacks” base class, the vmm_xactor::prepend_callback(), vmm_xactor::append_callback() and vmm_xactor::unregister_callback() and the `vmm_callback macro. Using the provided VMM functionality, a reusable transactor would have the following structure:

   class my_xactor_cbs extends vmm_xactor_callback;
      virtual task pre_exec(in_trans tr);
      endtask
      virtual task post_exec(out_trans tr);
      endtask
   endclass

   class my_xactor extends vmm_xactor;
      ...
      task main();
         ...
         forever begin
            ...
            in_chan.get(in_tr);    // Get input transaction
            `vmm_callback(my_xactor_cbs, pre_exec(in_tr));
            ...                    // Execute transaction
            `vmm_callback(my_xactor_cbs, post_exec(out_tr));
            out_chan.put(out_tr);  // Generate output transaction
            ...
         end
      endtask
   endclass

The environment can provide a callback extension to provide DUT-specific functionality that is shared by all tests:

   class my_dut_cbs extends my_xactor_cbs;
      ...
      virtual task post_exec(out_trans tr);
         sb.expect(tr);
      endtask
   endclass

   class tb_env extends vmm_env;
      my_xactor xact;
      ...
      virtual function void build();
         ...
         xact = new(...);
         begin
            my_dut_cbs cb = new(...);
            xact.append_callback(cb);
         end
         ...
      endfunction
      ...
   endclass

And a test can similarly provide additional callback extensions in addition to the DUT-specific extensions already registered in the environment. Each test can extend a transactor differently according to its need:

   class my_test_cbs extends my_xactor_cbs;
      virtual task pre_exec(in_trans tr);
         if ($urandom % 10 < 2) in_tr.checksum = $random;
      endtask
   endclass

   program test;
   initial begin
      tb_env env = new();
      my_test_cbs err_ext = new(...);
      fork
         #1000 env.xact.append_calback(err_ext);
      join_none
      env.run();
   end
   endprogram

You will find additional guidelines and examples on using and implementing callbacks in the VMM book pp. 198-201 and pp 221-225.

Share and Enjoy:
  • Print
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Google Buzz
  • LinkedIn
  • RSS
  • Twitter

4 Responses to “How to use VMM callbacks”

  1. Corey Goss Says:

    Hi Janick,

    Your posting on this topic was very informative and thorough. SystemVerilog is indeed an OOP language and, as such, has it’s limitations when it comes to easily modelling many kinds of functionality needed for performing common verification tasks. Callbacks fall nicely into that category, which is the very reason that VMM is of help here. While OOP raises the level of abstraction beyond what basic Verilog offers, there is a clear need for even further abstraction. This is confirmed by the various verification class libraries such as VMM and OVM that bring further ease-of-use to the complex challenge.

    As you may be aware, Aspect Oriented Programming (AOP) languages raise the level of abstraction even further beyond what class libraries can offer as most of the library features are built right into the languages themself (as well as tons of other ease-of-use features), allowing users to build verification environments in creative ways unimaginable by most skilled OOP verification engineers, and indeed most skilled OOP SW programmers!

    If I use your example of callbacks mentioned above, an AOP implementation of this feature (using the e language) would be:

    unit my_xactor;

    retrieve_forever() is {

    while (TRUE) {

    in_chan.get(in_tr); // Get input transaction
    do_after_get(); // Call to empty method in this base unit
    … // Execute transaction
    do_before_put(); // Call to empty method in this base unit
    out_chan.put(out_tr); // Generate output transaction

    };
    do_after_get() is empty; // define empty method to be extended later
    do_before_put() is empty; // define empty method to be extended later
    };
    };

    At first glance, the code looks quite similar to your example however, notice that there is no need to define a callback class and instantiate it within the xactor (this reduces code by removing the need to instantiate it, build it in the new() function of both the xactor and the testcase and also removes the need to append it.) Note also that there is no need to pass in any variable to the function call in the above code as all variables within the unit are visible to the function and can be accessed. I am not sure if that is the case for your example above.

    Now, the beauty of AOP comes in the re-use. Since the functions are empty, they can be extended in any way desireable to implement the desired behaviour through extensions that can be based on any enumerated/boolean field of the unit/class itself. For example, if I wanted to implement specific functionality for only the BLUE xactors (where BLUE was some enumerated field differentiating this xactor from others):

    extend BLUE my_xactor {
    do_after_get() is also {print in_tr.item;};
    do_before_put() is also { print out_tr.item;};
    };

    All my_xactor’s in the environment that are BLUE will now contain the above functionality. If I wanted to layer additional functionality to only the TENG ATM xactors:

    extend TENG ATM my_xactor {
    do_after_get() is also {out(“TENG ATM xactor input trans payload size: “,in_trans.item.payload.size()};
    do_before_put() is also {out(“TENG ATM xactor output trans payload size: “,out_tr.item.payload.size();};
    };

    In the above situation, all TENG ATM xactors would automatically pick up the extension and the BLUE TENG ATM xactors would pick up the previous extension as well. Essentially, this is like plug and play with your callbacks, customizing each type of xactor to have the specific callback that they need automatically when the xactor is generated. Using the built in generation of the e language, these extensions would allow a user to automatically create a random list of my_xactors, each having their own unique functionality based on the random values generated for each of their internal fields. The below code is all that would be needed to achieve this.

    unit my_env {
    drivers: list of my_xactor is instance; //no need for build(), new() or randomize()
    };

    I hope you can see how the above example greatly reduces the amount of code required for callback implementation while, at the same time, allowing for much greater flexibility and functionality to be added. There is no need for extra instantiations, calls to new(), “poking” different flavours of callbacks into the environment or creating extra objects for each “flavour”. In addition, there is no need to do large scale pre-planning to determine exactly which tasks will be made virtual such that we can override their functionality later.

    In my opinion, AOP is to OOP what OOP is to Verilog … a huge productivity boost. The e language was developed back in 1992 with AOP in mind and has been in use for many years however, I would be very interested in hearing your thoughts on AOP and any comments you might have on whether Synopsys is planning on pushing for AOP to be implemented in SystemVerilog, or whether VCS will be supporting any other AOP languages, such as e, anytime soon.

    Again, thanks for the great detailed post.

    regards,
    Corey

  2. Sharon Rosenberg Says:

    Hi Janick,

    Corey made several good points and I would like to further elaborate on the reasoning and the value for not recommending callbacks in the OVM (both e and SV).
    Reusability and extensibility are very interesting topics and indeed using callbacks provides a solution for some types of extensions.

    Issues with Callbacks:
    ———————-

    There are a few key deficiencies of callbacks that your analysis overlooks.
    a. Callbacks are focused on procedural extensions and do not solve the entire extensibility problem. Many times a user wants to add more fields, coverage points and events to a pre-defined verification IP. Callbacks do not address this issue.

    b. The VIP developer must predict, in advance, the extension points and provide the extensions there. For example, I can develop a BFM for an AHB component following the protocol and assuming arbitration. Some systems that have only one master may decide to remove or modify the arbitration logic. If I predicted this scenario in advance, I can create callbacks around the pieces of logic that need to be extended. But the extension options are endless. Should we recommend a callback prior to every task and function in the system?

    c. What about overriding/removing code? For example, I would like to mute the arbitration logic. Callbacks can be used to extend but more logic is needed for disabling pre-defined logic.

    d. What about ease-of-use? You list some SW design patterns. These were introduced 15 years ago and are still relevant however there is some criticism on the complexity and encrypted style of some of these patterns. OOPSLA is an annual conference covering topics on object-oriented programming systems, languages and applications. Among their claims is instead of inventing or capturing complex patterns for solving problems, why couldn’t we create better tools/languages. Click this link to read more: http://www.sigplan.org/oopsla/oopsla99/2_ap/tech/2d1a_gang.html

    Now let’s see what other solutions are there for the reuse and extensibility.

    AOP and e:
    ———-

    e did exactly what OOPSLA experts recommend – define a solution for the problem. The Aspect oriented e extend capability is a hot thing in SW languages today (read more on aspect java)

    a. Fewer lines of code to create and maintain. As was mentioned so many times before before, less code equals better productivity, right?

    b. Extensibility of existing class is much easier to learn/teach than design patterns

    c. Supports procedural as well as structural extensions

    d. No need to predict, in advance, extension points.

    e. Can disable code using empty extension.

    OVM SV solution:
    —————-

    Factory
    ——-

    As was mentioned OVM supports callbacks. However, for extensions we recommend the factory mechanism. Why?

    a. Supports procedural as well as structural extensions

    b. No need to predict, in advance, extension points. Every function and task is a potential extension point.

    c. Can disable code using an empty extension.

    d. It is not as short and concise as e and AOP, but less lines of code than callbacks.

    e. Structural extensions are easier to understand and debug.

    f. As you claim in your VMM book, you need a factory anyway for data item extensions, so it is better to learn one powerful pattern than a few point solutions.

    g. Most environment reuse starts the same way: A user gets a VIP component and configures/extends/instantiates it according to the local project requirements. As OVM recommends, this is done inside a set of testbench classes, one for each configuration. A family of tests is written using one of the configured testbenches. This leads people to a tree-like topology reuse.

    TLM
    —-

    What if a graph reuse topology is needed (multiple parallel development of the same classes that later need to be combined)?

    Analysis TLM ports can provide procedural extensions. Every object is passed by reference to the TLM and you can manipulate it in various environments that can later be combined.

    Why TLM?

    TLM analysis ports are already in place for other reasons. They can cross language boundaries, they already standardized in OSCI SystemC. Why invent, teach, and ask users to implement a new or additional solution? I heard from quite a few users that they do not know when to use VMM channels or call backs.

    To summarise, use the OVM factory for all it’s benefits. If there is a real concern of orthogonal reuse, use TLM analysis ports.

    Thanks and regards,

    – Sharon (Cadence Design Systems)

  3. janick Says:

    Thank Sharon you for your lengthy comment.

    I did not reply to Corey’s original comment because my blog post is about SystemVerilog, while his comment was about e (a different language) and AOP (a programming model that does not [yet] exists in SystemVerilog. With all due respect to Corey, I considered it interesting but tangential to the topic at hand.

    Callbacks can be used to add more fields, coverage groups (you can’t add coverage points in SV, only groups) or events to an IP. They simply won’t reside in the IP itself but in the callback. Same result, different approach.

    The VMM makes several recommendations on where callbacks should be inserted. How is relying on the structure of the IP any better? The methods must be defined at a suitable granularity to allow the additional functionality to be inserted before or after them. How would you know what granularity is needed? You can’t extend a virtual method in the middle — but you can put a callback there. Or a call to an empty virtual method which amounts to the same thing as having a callback. Then, these methods must be virtual. An easy thing to accomplish, however, it does not happen on its own. And how can you ensure that the implementation of all these virtual methods allow you to modify the behavior as you need? So these implicit extension points need to be planned, just like callbacks.

    Extensibility does not happen by accident — only by design. Cut & paste of the original implementation of a method, to modify or remove a couple of statements in it, is not extension nor re-use. Functionality that could be removed or overridden could be implemented in VMM using a chain-of-responsibility pattern by stopping at the first callback encountered that claims responsibility for the functionality. The default behavior of the `vmm_callback() macro is to execute all registered callbacks — but that is only one of the possible way to go through the callback registry. Again, same result but different approach.

    Callbacks and factories are different approaches to solving the same problem. For a specific problem, a specific solution may yield a little less code than the other. Claims of ease of learning and debugging are easy to make, but harder to substantiate — so I often choose not to make them. The ease-of-use is usually a function of the tool and what you are used to anyway, not something intrinsic.

    For extensions, I’ll continue to recommend callbacks, why?

    a. They can do structural and procedural extensions.

    b. They clearly document the extension points of an IP.

    c. They limit the potential side effect of user extensions.

    d. SystemVerilog does not support multiple inheritance. Combining multiple orthogonal extensions using inheritance leads to an exponential number of class extensions. Different orthogonal callback extensions can be registered in any combination or order without additional work or complexity.

    e. Callback extensions can be dynamically added and removed. It is not necessary to code the dynamic nature of the extensions in the functionality.

    As for crossing language boundaries, it is a technical issue. There is no requirement that both sides have to use the same methods and constructs. As long as they have similar semantics, the adaptation of one side’s methods and constructs to the other side is done at the same time as the data is translated. A vmm_channel in SV can connect to a tlm_fifo on the SC side or a vmm_notify can connect to an analysis port, thereby facilitating language interoperability.

    The transaction-level interface mechanisms in VMM have been successfully used to verify hundreds of designs without proving to be a limitation. OSCI-TLM was designed for a language that supports multi-inheritance and lacks the built-in notions of concurrency and connectivity. SystemVerilog is inheritenly concurrent and has a well-known connectivity model (modules ports to wires). I guess I’ll reverse my earlier statement and claim that the resulting connectivity model is a lot easier to understand and debug…I always chuckle when I see the large number of slides necessary to explain the TLM mechanism in AVM/OVM presentations. I can explain the VMM channel connectivity in one slide. Ultimately though, both accomplish the same result, using a different approach.

    You keep bringing up e and AOP. Many of the OVM features feel like it is trying to be e. Some are really cool and we did not hesitate to acknowledge them. Take the automation macros. When I first saw them, I was really impressed and I was baffled for a while as to how they worked. It took me a couple of days to figure out how to implement them in VMM (it came to me in the shower – go figure!). But I was horrified when I was finally able to look at their implementation in the OVM to discover that they rely on a run-time duplication of the symbol table and that the high-frequency transaction copy/compare/display/pack/unpack methods are interpretive. That must cost quite a bit of memory and run time overhead!

    Trying to force-fit a use model that does not “flow” with the underlying language can be counterproductive. It requires a large number of classes and macros to implement (and document and maintain). For example, e has some form of introspection. Adding it to SV in OVM by duplicating the symbol table at run-time is *really* expensive and I question if the benefits are worth the cost — especially since VMM is able to provide similar benefits without it. For better or for worse, the industry is using SV. So we designed VMM to work well with its underlying capabilities and leveraging its well-known use models with the least number of classes and macros.

    Cheers.

  4. Michael Says:

    Having used callbacks I like the factory concept much more.

    1. Callbacks have limited scope and cannot easily change the base class implementation.

    2. I find myself spending a lot of times coding these callbacks and giving them enough flexibility. There ends up being hundreds of more lines of code that is difficult to read.

    3. Even after the callback we still need to change some methods and classes from the base implementation, especially for directed testcases. We use a different compilation flow where certain testbench files are not read in.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>