Verification Martial Arts: A Verification Methodology Blog

Archive for May, 2009

How VMM can help controlling transactors easily?

Posted by Fabian Delguste on 29th May 2009

Fabian Delguste / Synopsys Verification Group

Controlling VMM transactors can sometimes be a bit hectic. A typical situation I see is when I have registered a list of transactors for driving some DUT interfaces but only want to start a few of them. Another common situation is when I want to turn off scenario generators and replay transactions directly from a file. Yet another task I often face is registering transactor callbacks without knowing where they are exactly located in the environment.

As you can see, there are many situations where fine-grain functional control of transactors is necessary.

Since VMM 1.1 came out, I have been using a new base class called vmm_xactor_iter that allows accessing any transactor directly by name. In this case all I need to do is to construct a vmm_xactor_iter with regular expression and use the iterator to loop thru all matching transactors.

To understand better how this base class works, I’ll show you a real life example. The scope of this example is to show how to start generators only when vmm_channel playback has not been turned on. As you know vmm_channel can be used to replay transactions directly from files that contain transactions that were recorded in a previous session. This can speed up simulation by turning off constraint solving.

1. string match_xactors = (cfg.mode == tb_cfg::PLAYBACK) ? /Drivers/” : “/./”;

2.

3. `foreach_vmm_xactor(vmm_xactor, “/./”, match_xactors)

4. begin

5.  `vmm_note(log, $psprintf(“Starting %s”, xact.get_instance()));

6.   xact.start_xactor();

7. end

  • In line 1, match_xactors string takes “Drivers*” value when playback mode is selected otherwise it takes “.” when no this mode is not selected. In the first case, transactors named “Drivers” match otherwise all transactors, including generators match
  • In line 3, `foreach_vmm_xactor macro is used to create a vmm_xactor_iter using previous regular expression. This macro can be used to traverse and start all matching objects by using the xact object to access transactors

In case you’d like to have more control over vmm_xactor_iter, it’s possible to use its first() / next() / xactor() methods to traverse matching transactors. Also it’s possible to ensure the regular expression returns at least one transactor. Here is the same example written using these methods.

1. string match_xactors = (cfg.mode == tb_cfg::PLAYBACK) ? “/Drivers/” : “/./”;

3. vmm_xactor_iter iter = new(“/./”, match_xactors);

4. if(iter.xactor()==null)

5. `vmm_fatal(log, $psprintf(“No matching transactors for ‘%s’”, match_xactors));

7. while(iter.xactor()!=null) begin

8.   xact = iter.xactor();

9.   xact.start_xactor();

10.end

Should you need to reclaim the memory allocation required to store all transactors, it’s possible to enable garbage collection by invoking vmm_xactor::kill().

The good news is that vmm_xactor_iter allows me to:

  • Configure the transactor without knowing its hierarchy
  • Provide dynamic access to transactors
  • Reduce code for multiple configurations and callback extensions
  • Use powerful regular expressions for name matching
  • Reuse transactors: no need to modify code when changing the environment content

I hope you find vmm_xactor_iter, and all of the other VMM features, as useful as I do

Posted in Configuration, Structural Components, SystemVerilog, Tutorial | No Comments »

VMM VIP’s on multiple buses

Posted by Adiel Khan on 27th May 2009

image

Adiel Khan, Synopsys CAE

Increasingly, more design-oriented engineers are writing VMM code. Some are trying to map typically good design architecture practices to verification development.

A dangerous mapping is parameterization, from modules to classes.

In my old Verilog testbenches I would develop reusable modules and use #parameters extensively to control the settings of the modules I was instantiating. (It was a sad day when I heard IEEE was deprecating my friend the defparam).

1. module vip #(parameter int data_width = 16,

2. parameter int addr_width = 16)

3. (addr, data);

4. output [addr_width-1:0] addr;

5. inout [data_width-1:0] data;

6.

7. endmodule

This would allow me to instantiate this VIP for many bus variants.

8. vip #(64, 32) vip_inst1(…);

9. vip #(32, 128) vip_inst2(…);

Mapping the approach from modules to classes, I could end up with:

1. class pkt_c #( parameter int data_size=16,

2. parameter int addr_size=16)

3. extends vmm_data;

4. rand bit [addr_size-1:0] addr;

5. rand bit [data_size-1:0] data;

6.

7. endclass

8. //specialized class with 64 & 32 sizes

9. pkt_c #(64, 32) pkt1=new();

10. //specialized class with 32 & 128 sizes

11. pkt_c #(32, 128) pkt2=new();


Be warned, in the SystemVerilog testbench centric view of VIP reusability, parameterization of classes leads to a dead-end path. Moving one layer of abstraction up, I really don’t care if it is a 32/64/128 bits wide interfaces. What I want to do is use pkt_c around the verification environment. The simplest case is creating a reusable driver using pkt_c to drive any bus-width interface.

However, if I try to use a generic class instantiation, I will get a specialization with parameters = 16&16. I cannot perform the $cast() to put the right pkt_c type onto the bus.


1. class pkt_driver_c extends vmm_xactor;

2. virtual protected task main();

3. forever begin : GET_OBJ_TO_SEND

4. pkt_c pkt_to_send; //default class instance

5. pkt_c #(64, 32) pkt_created;

6. randomize(pkt_created);// generator code

7. $cast(pkt_to_send, pkt_created); //FAILS !!!!!

If you are using VMM channels, they must similarly be specialized and cannot carry generic parameterized classes:

8. `vmm_channel(pkt_c)

9. class pkt_driver_c extends vmm_xactor;

10. pkt_c_channel in_chan; //Can only carry pkt_c#(16,16)!!!

Or you must upfront select which specialization you want for use with a parameterized channel.

8. class pkt_driver_c extends vmm_xactor;

9. vmm_channel_typed #(pkt_c#(64, 32)) in_chan;


Hence, for the driver to operate on the correct object type, I need to instantiate the exact specialization throughout my entire environment and make the driver itself parameterized. Now you can clearly see instantiating a specific specialization in the driver (or monitor, scoreboard etc) stops the code from being really reusable for other bus_widths.


1. pkt_c #(64, 32) pkt_to_send;

2. pkt_driver_c #(64, 32) driver;


A better approach is one that was described by Janick in the “Size Does Matter” blog of using `define. Let’s expand on this and see how it works for reusable VIPs. Well, the first thing that comes to my mind is that a `define is a global namespace macro with a single value, whereas I am using my VIP with 2 different bus architectures. Therefore, the `define alone is not enough: you also need a local constant to be able to exclude unwanted bits when you have a VIP instantiated for various bus widths.

1. //default define values

2. `define MAX_DATA_SIZE 16

3. `define MAX_ADDR_SIZE 16

4. class pkt_c extends vmm_data;

5. static vmm_log log = new(“Pkt”, “class”);

6. //instance constant to control actual bus sizes

7. const int addr_size;

8. logic [`MAX_ADDR_SIZE:0] addr;

9. logic [`MAX_DATA_SIZE:0] data;

10. // pass a_size as arg to coverage

11. // ensuring valid coverage ranges.

12. covergroup cg (int a_size);

13. coverpoint addr

14. {bins ad_bin[] = {[0:a_size]};}

15. endgroup

16. // sizes specialized at construction for pkts

17. // on buses less than MAX bus widths

18. function new(int a_s=`MAX_ADDR_SIZE);

19. addr_size = a_s;

20. cg = new(addr_size);

21. `vmm_note(log, $psprintf(“\nADDR_TYPE: “,$typename(addr),

22. “\nDATA_TYPE: “,$typename(data),

23. “\nMAX_BUS_SIZE: “, addr_size));

24. endfunction

The code above allows for a default implementation and all the user needs to do is set the `MAX_ADDR_SIZE and `MAX_DATA_SIZE symbols and all the code will be fully reusable across drivers, monitors, subenv, SoC etc.

For situations where two VIP’s of differing bus architectures are used, the compiler symbols need to be set to the biggest bus architecture in the system; smaller bus-widths are set using addr_size. It is not necessary for addr_size variable to be an instance constant or set during construction. By using instance constants, this ensures the bus-widths are not changed at runtime by users. Having the value of addr_size set during construction gives the users the flexibility to setup the object as they want. For pseudo-static objects such as drivers, monitors, subenvs, masters, slaves, scoreboards etc you should check the construction of verification modules for your particular design architecture during the vmm_env::start phase.

N.B not shown above, but assumed, is that the addr_size variable would be used to ensure correct masking occurs when performing do_pack(), do_unpack() compare() etc.

Just to wrap up some loose ends…

I’m not totally discounting the merits of parameterized classes just insuring people look at all the options. For instance you could parameterize everything and then set the SIZE at the vmm_subenv level and map the SIZE parameters to all other objects. At some point you will want to monitor or scoreboard across different bus-widths and then the parameterized class casting will bite you, reducing you to manually mapping the members within the comparison objects. There is a time and place for everything, so probably need another blog showing merits and where to use parameterized classes.

The vmm_data class is not the only place you might need to know the size of the bus, the same `define & instance constant technique can be used throughout your VIP classes.

This blog does not discuss the pros and cons of putting coverage groups in your data object class. I merely used the covergroup in the data-object as a vehicle to demonstrate how you can make your classes more reusable. I think a separate blog about where best to put coverage will clarify the usage models.

All the code snippets can be run with VCS-2009.06 & VMM1.1. Contact me for more complete code examples and bugs or issues you find.

Posted in Coding Style, Configuration, Register Abstraction Model with RAL, Reuse, Structural Components, VMM | 9 Comments »

Did you notice vmm_notify?

Posted by Janick Bergeron on 20th May 2009

jb_blog Janick Bergeron, Synopsys Fellow

In the recent past, we’ve had several customers state that VMM did not have a one-to-many passive transaction-level interface akin to Analysis Ports in some other methodology (whose name escapes me right now :-) .

Not true. Always had. From day one. Since March 2006 to be exact.

The VMM Notification Service (vmm_notify) can be used to connect a transactor (or channels or any other testbench component) to a scoreboard or functional coverage collector or any other passive observer. There can be multiple observers, and they will all see the same transaction stream.

There are pre-defined notification in vmm_xactor and vmm_channel: you might want to look them up as they may already provide all the information you are looking for. But if you need to define your own, here’s how you can do it.

First, you need to configure a new indication:

class monitor extends vmm_xactor;
int OBSERVED;

function new(string name);
super.new(“monitor”, name);
this.OBSERVED = this.notify.configure();
endfunction

endclass

then you simply indicate the notification whenever appropriate, supplying a transaction descriptor as status:

class monitor extends vmm_xactor;
int OBSERVED;


task main();
int n = 0;
super.main();
forever begin
trans tr = new;
tr.data_id = n++;

this.notify.indicate(this.OBSERVED, tr);

end
endtask
endclass

Any number of observers can now monitor the transactions through the indication via the vmm_notify::wait_for() method:

class observer;
function new(string name, vmm_notify ntfy, int id);
fork
forever begin
vmm_data tr;
ntfy.wait_for(id);
tr = ntfy.status(id);
$write(“At %0d on %s: %s\n”, $time, name, tr.psdisplay());
end
join_none
endfunction
endclass

program tb_env;
initial
begin
monitor  mon  = new(“tb_env.mon”);
observer sb1  = new(“[sb1] “, mon.notify, mon.OBSERVED);
observer sb2  = new(“[sb2] “, mon.notify, mon.OBSERVED);

end
endprogram

There is only one problem with using the vmm_notify::wait_for() method: it won’t catch multiple indications with no delays between them. If there can be multiple indications in the same simulation cycle, you must use the vmm_notify_callbacks::indicated() method to observe them. This complicates the coding of the observer slightly but provides a more reliable observability:

typedef class observer;
class subscribe extends vmm_notify_callbacks;
local observer obs;
function new(observer obs);
this.obs = obs;
endfunction
virtual function void indicated(vmm_data status);
this.obs.observe(status);
endfunction

endclass

class observer;
string name;
function new(string name, vmm_notify ntfy, int id);
subscribe cb = new(this);
ntfy.append_callback(id, cb);
this.name = name;
endfunction

function void observe(vmm_data tr);
$write(“At %0d on %s: %s\n”, $time, name, tr.psdisplay());
endfunction
endclass

If you are going to have a lot of different observer classes, this additional code can become a burden. It can be greatly simplified by using a template and agreeing on a method name within the observer class to do the observation:

class subscribe #(type T = int) extends vmm_notify_callbacks;
local T obs;
function new(T obs, vmm_notify ntfy, int id);
this.obs = obs;
ntfy.append_callback(id, this);
endfunction
virtual function void indicated(vmm_data status);
this.obs.observe(status);
endfunction
endclass

class observer;
string name;
function new(string name, vmm_notify ntfy, int id);
subscribe#(observer) cb = new(this, ntfy, id);
this.name = name;
endfunction
function void observe(vmm_data tr);
$write(“At %0d on %s: %s\n”, $time, name, tr.psdisplay());
endfunction
endclass

The parameterized class subscribe needs to be written only once, and can then be used with any class that implements an observe() function:

class yet_another_observer;
string name;
function new(string name, vmm_notify ntfy, int id);
subscribe#(yet_another_observer) cb = new(this, ntfy, id);
this.name = name;
endfunction
function void observe(vmm_data tr);
$write(“At %0d on %s: %s\n”, $time, name, tr.psdisplay());
endfunction
endclass

Look for a pre-defined vmm_notify callback subscription class template in the next VMM release…

Posted in Communication | 3 Comments »

Multi-stream Scenario Generator (MSS)

Posted by Amit Sharma on 15th May 2009

Amit  Sharma, CAE Manager, Synopsys

Multi-stream scenario generation is an important feature recently added to VMM. It targets generation and coordination of stimulus across multiple interfaces. It also allows hierarchical layering of scenarios and the reuse of block level scenarios at system level.

Multi-stream scenarios are described by extending the vmm_ms_scenario class. They can be composed of individual transactions, procedural code, existing single-stream scenarios as well as other multi-stream scenarios. Depending on your requirements, a multi-stream scenario can be either single threaded, multi threaded or reactive.

In a nutshell, you simply need to provide an implementation of the vmm_ms_scenario::execute() task. Whatever the behavior of this method is, it is what defines the multi-stream scenario. This method can execute single/multiple transactions/scenarios. It can execute procedural code. It can read a file. It can do anything SystemVerilog code can do.

Let’s take a look at the example that comes with the VMM 1.1 release (available in sv/examples/std_lib/mss_simple directory). This example has a multi-stream scenario controlling the execution of two independent single stream scenarios: my_atm_scenario and my_cpu_scenario. Let’s see how the multi-stream scenario called my_scenario controls the execution of these two scenarios.

1. class my_scenario extends vmm_ms_scenario;

2.    my_atm_cell_scenario atm_scenario;

3.    my_cpu_scenario cpu_scenario;

4.    atm_cell_channel atm_chan;

5.    cpu_channel cpu_chan;

6.

7.    int MSC = this.define_scenario(“Multistream SCENARIO”, 0);

8.    local bit [7:0] id;

9.

10.   function new(bit [7:0] id);

11.       super.new(null);

12.       atm_scenario = new(id);

13.       cpu_scenario = new();

14.       this.id = id;

15.   endfunction: new

16.

17.   task execute(ref int n);

18.       fork

19.         begin

20.            atm_cell_channel atm_chan;

21.            int unsigned nn = 0;

22.            $cast(atm_chan, this.get_channel(“ATM_SCENARIO_CHANNEL”));

23.             atm_scenario.randomize with {length == 1;};

24.             atm_scenario.apply(atm_chan, nn);

25.             n += nn;

26.         end

27.         begin

28.             cpu_channel cpu_chan;

29.             int unsigned nn = 0;

30.             $cast(cpu_chan,this.get_channel(“CPU_SCENARIO_CHANNEL”));

31.             cpu_scenario.randomize with {length == 1;};

32.             cpu_scenario.apply(cpu_chan, nn);

33.             n += nn;

34.         end

35.       join

36.   endtask: execute

37. endclass: my_scenario

In lines 1-3, my_scenario extends vmm_ms_scenario and declares single stream scenarios. These single-stream scenario members,if prefixed with the rand keyword, would be implicitly randomized before execution.

In lines 17-36, the virtual execute() method is overridden with the user code to implement the scenario. In this example, atm_scenario and cpu_scenario are randomized and executed in parallel threads. If VMM single stream scenarios are used (as in this example), then the apply()method of the scenarios is called to send the transactions of the scenario through the appropriate VMM channel. Thus there is a very close correlation between the procedural “apply()” method of single stream scenarios and the “execute()” method of multi-stream scenarios. If multi-stream sub-scenarios had been instantiated in the above scenario, the “execute()” method of the child scenarios would be invoked instead of the apply() method.

A multi-stream scenario can connect to any channel interface in the testbench environment. In lines 22 and 30, the get_channel() method is used to get the appropriate vmm_channel handle by specifying their logical names “ATM_SCENARIO_CHANNEL” and “CPU_SCENARIO_CHANNEL” . All you need to do is to register the different channels to the multi-stream generator under these logical names.

Now that we have created our multi-stream scenario, let’s see how to use it in our verification environment. The following code snippet shows how channels and scenarios are registered with the multi-stream scenario generator, how they are started and used by other transactors.

1. my_scenario sc0 = new(0);

2. ……

3. atm_cell_channel atm_chan = new(“ATM CELL CHANNEL”, “TEST”);

4. cpu_channel cpu_chan = new(“CPU CELL CHANNEL”, “TEST”);

5. ….

6. ….

7. gen.register_channel(“ATM_SCENARIO_CHANNEL”, atm_chan);

8. gen.register_channel(“CPU_SCENARIO_CHANNEL”, cpu_chan);

9. gen.register_ms_scenario(“SCENARIO_0″, sc0);

10. ….

11. `

12. gen.stop_after_n_scenarios = 10;

13.

14. …

15. gen.start_xactor();

16. gen.notify.wait_for(vmm_ms_scenario_gen::DONE);

17. …

In line 1-4, the multi-stream scenario and channels are created.

In lines 7-8, channels are registered with the generator through the register_channel() method under the logical names “ATM_SCENARIO_CHANNEL” and “CPU_SCENARIO_CHANNEL”. Scenarios will subsequently get the channel handles through the get_channel() method by specifying these names as discussed earlier.

In line 9, the scenario is registered with the generator through the register_ms_scenario() method. Any number of scenarios can be registered and their order of execution can be controlled. The number of scenarios to be executed is controlled through the stop_after_n_scenarios field of the generator.

With these simple steps, you can quickly implement interesting multiple stream scenarios to stress your DUT interfaces. Thanks to this functionality, you can easily reuse existing single stream scenarios and/or other multi-stream scenarios in a hierarchical way. You can instantiate other scenarios and ‘execute’ them. In case of a single stream scenario, its apply() method has to be called after it is randomized and in case of a multi stream scenario, execute() method has to be called after randomization. Any registered multi stream scenario can be obtained by another scenario by its logical name. And thus, a hierarchical scenario library with increasing complexity can be easily created.

Last but not least, multi-stream scenarios can be vertically reused, where coordination between various generators is a must have. VMM allows a multi-stream scenario generator to be registered with another generator and have that top-level generate execute the scenarios of its sub-generators. This allows top level generator to control and synchronize the execution of scenarios of other generators. I’ll address hierarchical multi-stream scenarios in my next blog. Stay tuned.

Posted in Reuse, Stimulus Generation | 18 Comments »

The hidden pitfalls of type name hiding in a derived class

Posted by Wei-Hua Han on 9th May 2009

Weihua Han, CAE, Synopsys

Here I describe one major difference between virtual and non-virtual methods, and how type name hiding can collide with these methods. This is commonly used OOP feature in SystemVerilog.

“type name hiding” here refers to the situation where a user type definition in the derived class uses the same type name defined in base class, i.e, the new type definition in derived class “hides” the type definition in base class.

SystemVerilog LRM (Language Reference Manual) does not forbid hiding base class type definition in a derived class.  Here’s a typical example:

  1. class company_xactor_c;
  2.         typedef enum { SOFT, HARD } reset_e;
  3.         function void do_reset (reset_e rst);
  4.                 if(rst==SOFT)
  5.                         $display("Do SOFT reset");
  6.                 else
  7.                         $display("Do HARD reset");
  8.         endfunction
  9. endclass
  10. class pci_prj_xactor_c extend company_xactor_c;
  11.                 typedef enum { SOFT, HARD, PCI } reset_e; 
  12.                 function void do_reset(reset_e rst);
  13.                         if(rst==SOFT)..
  14.                         else if(rst==HARD)…
  15.                         else ….
  16.                 endfunction
  17. endclass

Here we intend to have a new “reset_e” definition in PCI project by adding a new PCI specific label called “PCI”.  Next, we also rewrite the “do_reset” method which now comes with a specific “reset” behavior for PCI project.  Then, all transactors derived from “pci_prj_xactor_c” for this PCI project can use this “PCI” label to define PCI specific behaviors.

It seems everything works fine until now. But we will see problems crop up when virtual function (polymorphism) comes into the picture. 

Below is a simplified but realistic scenario.

As you know, VMM allows you to extend the base class library. It’s not uncommon for you to add some specific methods and to create your own base class library, such as:

  1. class company_xactor extends vmm_xactor;
  2.         virtual function void tb_start(int nth_run,
  3. reset_e  reset_e_list[$]);
  4.         …
  5.         endfunction
  6. endclass

Here a company-wide “tb_start” method is added to the company-wide transactor class. And “reset_e” here is derived from vmm_xactor. We may also have a project specific transactor base class like:

  1. class project_xactor extends company_xactor; 
  2.                 typedef enum int { 
  3.                          SOFT_RST, 
  4.                          PROTOCOL_RST, 
  5.                          FIRM_RST, 
  6.                          HARD_RST, 
  7.                          PON, 
  8.                          PCIE 
  9.                 } reset_e; 
  10.     virtual function void tb_start(int nth_run,  reset_e  reset_e_list[$]); 
  11.             … 
  12.             endfunction
  13. endclass

And we have a new “reset_e” definition with some new project specific labels.  “tb_start” is also refined for the project.

Although this may seem fine, there is a fairly serious error in this code: the virtual function in SystemVerilog class follows similar common OOP polymorphism requirement.

These OOP requirements are described in SystemVerilog LRM. Here is an excerpt:

“Virtual methods provide prototypes for the methods that later override them, i.e., all of the information generally found on the first line of a method declaration: the encapsulation criteria, the type and number of arguments, and the return type if it is needed. Later, when subclasses override virtual methods, they shall follow the prototype exactly by having matching return types and matching argument names, types, and directions. It is not necessary to have matching default expressions, but the presence of a default shall match. ”

In above code, “reset_e” definition in derived project_xactor class actually defines a new type, i.e, project_xactor::reset_e, which is a different one as the “reset_e” defined in vmm_xactor.  Now the prototypes of tb_start in company_xactor is
      void company_xactor::tb_reset(int nth_run, vmm_xactor::reset_e reset_e_list[$]).
And in project_xactor it is:
      void project_xactor::tb_reset(int nth_run, project_xactor::reset_e reset_e_list[$]).
These are different.  They are not in compliance with the LRM. Sure, a user can omit to make “tb_start” virtual.  Technically, this will work fine.  However, in this case, all the benefits of using polymorphism will be lost.

Although type name hiding is allowed, we need to be careful in how it is used, since polymorphism is quite commonly employed in VMM and in most testbench environments. This is a very important aspect of reuse and extendibility.

In my next blog post, I will discuss more about what is overriding, what is hiding, what are overridden and what are hiden in SystemVerilog.

Posted in SystemVerilog, VMM | 1 Comment »