VMM Central Verification Martial Arts

How to use VMM callbacks

Posted by janick 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.

Posted in Uncategorized | 1 Comment »

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading ... Loading ...

Where did this blog go?

Posted by janick on November 6th, 2008

Darn. Haven’t posted in over three months!

My appologies to readers: these past few months have been crazy! Two trips to India, one to Japan and many to various parts of North America. Made my 1K status with United in mid-october this year. Lots of customer visits, working on lots of new VMM stuff.

I’ll be back on a regular schedule soon.

Posted in Uncategorized | No Comments »

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading ... Loading ...

Garbage collection

Posted by janick on July 18th, 2008

In SystemVerilog, unlike C, you don’t have to explictly free dynamically allocated class instances. Like most modern programming languages, SystemVerilog includes a garbage collector that frees memory that is no longer needed.

Although there are a few garbage collection processes out there, the lack of a root object (like the sys object in e) almost garantees that Reference Counting is used1.

To illustrate how reference counting works, consider the following code segment:

fork
   while (...) begin
      eth_frame pkt = new;         // Line 1
      scoreboard.push_back(pkt);   // Line 2
      ...
   end                             // Line 3
   forever begin
      eth_frame pkt;
      pkt = rx();
      if (!scoreboard[0].compare(pkt)) …
      scoreboard.pop_front();      // Line 4
   end
join

When the eth_frame instance gets first created on line 1, its reference count is set to “1″. When it is later added to the scoreboard on line 2, its reference count is incremented to “2″ (one for the “pkt” variable, one for the “scoreboard” queue). When the while loop iterates on line 3, the dynamic “pkt” variable disappears, reducing the reference count to 1. When the class instance is finally popped out of the scoreboard on line 4, its reference count is decremented to “0″ and its memory is garbage collected.

This usually works well until you create circular references, i.e. objects that refer to each other.

Circular references are always created whenever you introduce the concept of a “parent” object. Take a RAL model for example, an instance of the vmm_ral_reg class has a reference to its parent vmm_ral_block class instance that gets returned by the vmm_ral_reg::get_block() method. But the vmm_ral_block class instance has a reference to all of the vmm_ral_reg instances it contains, which get returned by the vmm_ral_block::get_registers() method.

That create a circular reference for every register in a block.

That means that a RAL model cannot be garbage collected. Ever.

In the case of a RAL model, that’s not a problem because it corresponds to the DUT and that DUT, being modelled using modules and interfaces, is static in nature and can never be dynamically modified.

But in the case of objects that get created in large numbers but should live only for as long as needed (such as packets or transactions who only need to live while they are processed by the DUT), then you have to be very careful to break that circular reference to enable garbage collection. Otherwise you will end up consuming an ever-increase amount of memory.

1Turns out not to be accurate. See follow-up comment.

Posted in Uncategorized | 5 Comments »

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading ... Loading ...

Size does matter

Posted by janick on July 6th, 2008

The VMM Register Abstract Abstraction layer is documented with a 64-bit data value system. For example, the write() method in the vmm_ral_field class is documented as:

task vmm_ral_field::write(output vmm_rw::status_e status,
                          input  bit [63:0]       value,
                          …);

However, to be able to handle fields (and registers and virtual registers) that are wider than 64 bits, the implementation uses the VMM_RAL_DATA_WIDTH macro to define the maximum size of a field (and register and virtual register):

class vmm_ral_field;
   local bit [`VMM_RAL_DATA_WIDTH-1:0] value;
   …
   task vmm_ral_field::write(output vmm_rw::status_e              status,
                             input  bit [`VMM_RAL_DATA_WIDTH-1:0] value,
                             …);
   …
endclass

Of course, the macro is defined by default to “64″, unless you define it otherwise:

`ifndef VMM_RAL_DATA_WIDTH
   `define VMM_RAL_DATA_WIDTH 64
`endif

Normally, you don’t really need to worry about the value of this macro, as SystemVerilog will silently extend or trim the value assigned to the value argument (or returned via that same argument in the read() method). That works fine if you know a priori the size of the field and use an appropriately sized expression or variable for it.

However, when writing generic code that needs to work with unknown field sizes, you must be careful to declare any temporary or working variable using:

bit [`VMM_RAL_DATA_WIDTH-1:0] tmp;

to avoid inadvertently losing data. You can get the actual size of the field by using the vmm_ral_field::get_n_bits() method. See the pre-defined register tests in $VMM_HOME/sv/RAL/tests for examples.

All of the above is documented in Chapter 16 “Maximum Data Size” of the RAL User’s Guide.

But why not make the size of the field a parameter and do away with the macro altogether?

You mean beside the fact that VCS did not support parameterized class at the time RAL was created? :-)

There are a few reasons…

One could be the potential memory saving by making the data members holding the mirrored field value match the actual size of the field. However, experiments on a customer RAL model with hundreds of thousands of fields (with a VMM_RAL_DATA_WIDTH defined to 512) did not show a significant memory saving (at least with VCS).

The other is that the notion of an absolute maximum field size would still be required to be able to write generic code, independent of their respective sizes. It is impossible to operate on a field through a parameterized class unless its size is known a priori because you must specialize1 a parameterized class whenever you use it. It would thus still be necessary to create a size-generic field base class that can deal with any size of fields (so a function such as vmm_ral_reg::get_fields() can be provided). As a user, you would still need to know about and use the VMM_RAL_DATA_WIDTH macro.

Finally, the automatic value resizing only works for input, output and inout arguments. A ref argument (such as the ones use in the field pre/post_read/write callback methods) require that a variable of the exact same size be used. Again, this would require knowing, a priori, how large a field is before you could write code for it.

So my current line of thinking is that class parameters are fine for generic types, but not that useful for generic sizes. Or am I all wet?

1and that specialization must be defined at compile-time because you can’t use a run-time expression to specialize a class. For example, you could not call get_n_bits() to obtain the size of the field then use the result to specialize a parameterized class to access it.

Posted in Coding Style, RAL, SV | No Comments »

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading ... Loading ...

Message, message on the wall!

Posted by janick on June 19th, 2008

Why does the VMM message interface (the vmm_log class) have a start_msg() and a text() method that must be used in this convoluted way:

if (log.start_msg(vmm_log::DEBUG_TYP, vmm_log::TRACE_SEV)) begin
   log.text("This is a trace message");
   log.end_msg();
end

Why not the much simpler one-liner:

log.message(vmm_log::DEBUG_TYP, vmm_log::TRACE_SEV,
            "This is a trace message");

which would then eliminate the need for the macro:

`vmm_trace(log, "This is a trace message");

Given the examples above, there is absolutely no reason. However, this example illustrates why:

`vmm_trace(log, $psprintf("Read 'h%h from 'h%h with status %s",
                           data, addr, status.name()));

which expands to:

if (log.start_msg(vmm_log::DEBUG_TYP, vmm_log::TRACE_SEV)) begin
   log.text($psprintf("Read 'h%h from 'h%h with status %s",
                      data, addr, status.name()));
   log.end_msg();
end

The $psprintf() (and all other formatting system tasks $sformat(), $format(), $write(), $display(), etc…) may be simple to use but they are very run-time expensive. And if you are not going to display a message, why incur the cost of composing its image?

When using a single procedure call, the value of all of its arguments must be determined before it is called. Thus, using this approach:

log.trace($psprintf("Read 'h%h from 'h%h with status %s",
                    data, addr, status.name());

incurs the cost of creating the message image every single time. And most of the time, this debug message will simply be filtered out (think about the thousands and thousands of regression runs where debug is not enabled!).

On the other hand, checking first if messages of a certain type or severity are going to be filtered out or not and only then composing the image of the message improves your run-time performance.

By how much? Of course, it depends on the number of messages that will eventually get filtered out. But just to give you an idea, I ran this experiment using VCS:

program p;

initial
begin
   int i;
   string msg;
   i = $urandom() % 2; // See footnote1
   if (i == 1) i–;
   repeat (100000) begin
`ifdef GUARD
      if (i)
`endif
      msg = $psprintf(”%s, line %0d: Message #%0d at %t”,
                      `__FILE__, `__LINE__, 0, $time());
   end
end

endprogram

With `GUARD defined, which causes the $psprintf() call to be skipped, I get run-times of approximately 0.025 seconds. With `GUARD undefined, which causes the $psprintf() call to be executed, I get run-times of approximately 0.230 second or 10x slower simulation performance.

Personally, I think the performance gain is worth the little extra bit of code to write. Remember to always optimize the right thing: you’ll write that code once but you’ll run it thousands and thousands of times. So saving a few lines of codes is not always the right decision.

1 I use a convoluted way to set i to 0 to prevent an optimizing compiler from optimizing the entire if statement away.

Posted in Uncategorized | 1 Comment »

1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading ... Loading ...