In this tutorial we illustrate how to use classes that represent data objects in a constrained-random testbench. This tutorial illustrates the following key points:
typedef struct { rand bit [10:0] ID; // 11-bit identifier rand bit RTR; // reply required? bit [1:0] rsvd; // "reserved for expansion" bits rand bit [3:0] DLC; // 4-bit Data Length Code rand byte data[]; // data payload bit [14:0] CRC; // 15-bit checksum } message_t;
In the case of a system where data is sent serially, a packed struct could be convenient - a packed struct means that the data structure can be packed into a single vector, making it easier to use in a system where information is sent serially. However in this case, we want to randomize both the contents and the length of the payload data, and that is more easily done using an unpacked array - hence we have to use an unpacked struct.
Note also the use of typedef - this allows us to create a re-usable name for our struct data type message_t.
To get the full benefits of the SystemVerilog constrained random generation facilities, it is convenient to use a class. This allows us to associate functions with our data (class methods), and also to benefit from other built-in methods such as pre_randomize() and post_randomize(). So for maximum flexibility, our next step is to create a class:
class CAN_Message; rand message_t message; // Class methods go here endclass: CAN_Message
Now we have defined our CAN_message class, we need to add methods to the struct that can modify or inspect it. We would need to add methods to this class for many purposes, including calculating the correct 15-bit CRC. For example, consider this very straightforward method to set or clear the RTR (reply request) bit in a message structure, ensuring that there is no payload data if the RTR bit is set:
class CAN_Message; rand message_t message; // Class methods go here task set_RTR (bit new_value); // Set the RTR bit as requested message.RTR = new_value; if (message.RTR) begin // Messages with the RTR bit set should have no data. message.DLC = 0; clear_data(); // make the data list empty end endtask task clear_data; message.data.delete(); endtask endclass: CAN_Message
Note that this method is itself a part of the CAN_message class, so that it can directly access any fields of the struct, using the .member notation.
The idea of pseudo-random stimulus generation is central to the directed random verification methodology. It's obviously ridiculous to use random numbers for every part of every struct. You need control over the random generation process. SystemVerilog provides this control using constraints.
A constraint is a Boolean expression describing some property of a field. Constraints direct the random generator to choose values that satisfy the properties you specify in your constraints. Within the limits of your constraints, the values are still randomly chosen. The process of choosing values that satisfy the constraints is called solving. The verification tool that does this is called the solver; the solver may be embedded in a simulator or be part of a separate testbench generator program.
For example, the four-bit DLC field in our CAN_message.message struct can hold values in the range 0 to 15, but the CANbus message specifications require its value to be restricted to a maximum of 8. We can express this constraint as the numerical inequality
DLC <= 8
or, perhaps more clearly, using SystemVerilog’s range-membership operator inside
DLC inside {[0:8]}
These are both Boolean expressions and therefore they can be used in a constraint using the constraint keyword. Constraints are class members, just like fields and methods. They can be written either in the original class, or in derived classes. In this example we are modifying the original class definition. The example also shows how you can control the number of elements in a dynamic array by using the dynamic_array.size()method as part of a constraint.
class CAN_Message; //... constraint c1 { message.DLC inside {[0:8]}; } constraint c2 { message.data.size() == message.DLC; } endclass: CAN_Message
The random generator will always attempt to honour your constraints. It is sometimes possible to write conflicting constraints, in which case the generator will fail.
Now that we've completed this class definition, we need to be able to make use of it in the testbench. As a simple example of this process, suppose we want to build a test that needs ten distinct messages to do its work. We would create an unpacked array of 10 CAN_message objects:
CAM_message test_message[10];
We could then initialize the messages with random data like this:
for (int i = 0; i < 10; i++) test_message[i].randomize();
We could also provide additional constraints using the with construct:
test_message[0].randomize() with { message.DCL == 4; };
This is the same as writing a constraint in the CAN_message class like this
constraint c3 { message.DCL == 4; }
Alternatively, we could use the class inheritance mechanism to create a subclass, where the message length is fixed:
class CAN_message_4 extends CAN_message; // ... constraint c1 { message DCL == 4; } // Overload c1 endclass
Suppose that the DUT has a serial input for receiving CAN messages. In order to drive the abstract class data into the DUT, the message struct will need to be serialised. To do this we can write a method in the class.
class CAN_Message; rand message_t message; // Class methods go here // ... task getbits(ref bit data_o, input int delay=1); bit [17:0] header; bit [14:0] tail; header = {message.ID,message.RTR,message.rsvd,message.DLC}; tail = message.CRC; $display("tail=%0b",tail); //step through message and output each bit (from left to right) foreach(header[i]) #delay data_o = header[i]; foreach(message.data[i,j]) #delay data_o = message.data[i][j]; foreach(tail[i]) #delay data_o = tail[i]; endtask //... endclass: CAN_Message
This getbits task updates the output data_o by using a ref argument. An input delay is specified as a simple model of the bit period. Here is an example of calling this function:
module top(); // declaration of CAN_message and message_t omitted... bit data_o; const int bit_interval = 1; CAN_Message test_message[10]; int interval=10; initial message_gen: begin for (int i = 0; i < 10; i++) begin std::randomize(interval) with {interval>0;interval<6;};//random interval $display("interval=%0d",interval); #interval; $display("time = %0t",$time); test_message[i] = new; test_message[i].randomize(); test_message[i].print(); test_message[i].getbits(data_o,bit_interval); #bit_interval $display("time = %0t",$time); end $finish; end:message_gen endmodule : top
So far in this tutorial we have looked at how random variables and constraints in classes are used to create tests. SystemVerilog also provides a number of other constructs that are not covered here, including the ability to create random sequences of tokens.
Having seen how to write tests using SystemVerilog, we shall now consider how we can measure their effectiveness. One way to do this is to measure the functional coverage. This is a user-defined metric of how much of the design has been tested. (SystemVerilog also includes the concurrent cover property statement, which is used to count the number of times a particular sequence or property occurs. For further information see the Assertion-Based verification Tutorial.)
As an example of functional coverage, consider a variable of a user-defined enumerated type:
enum {Red, Green, Blue} Colour;
It would be useful to know whether or not the variable Colour has been set to all the possible values at some point during simulation. To do this you would define a covergroup containing a single coverpoint:
covergroup cg_Colour @(posedge Clock); coverpoint Colour; endgroup
Next you must create an instance of the covergroup. This is like creating a class object:
cg_Colour = new cg_inst;
During simulation, the simulator will count the number of times that Colour takes each of the values, Red, Green and Blue. The value of Colour is sampled on every rising edge of Clock. (You don’t have to specify a sampling event; if you don’t then you must sample the values explicitly, using the covergroup’s sample method: cg_inst.sample()); .
In the example we have just used, the simulator will create three bins for the coverpoint - one for each value of the enumerated type. Suppose we are covering a variable of an integer type:
bit [15:0] i; covergroup cg_Short @(posedge Clock); c : coverpoint i; endgroup
The simulator could potentially create 2^16 bins for the coverpoint. (In fact, there is a default of a maximum of 64 automatically created bins.) It would probably be more useful to define some bins to hold specific values or ranges of values:
covergroup cg_Short @(posedge Clock); coverpoint i { bins zero = { 0 }; bins small = { [1:100] }; bins hunds[3] = { 200,300,400,500,600,700,800,900 }; bins large = { [1000:$] }; bins others[] = default; }; endgroup
This creates one bin, “zero”, for the value of i being 0; one bin, “small”, for all values of i between 1 and 100, inclusive; three bins, for the eight values listed, with the first holding 200 and 300, the next 400 and 500 and the last 600, 700, 800 and 900; one bin for values 1000 and above, and one bin for every other value.
It is often useful to know how often two (or more) variables have specific pairs (triples etc.) of values. This is achieved using cross coverage:
logic [3:0] x, y; covergroup cg_xy @(posedge Clock); X : coverpoint x; Y : coverpoint y; XY : cross X, Y; endgroup
This will create 16 bins for each of the coverpoints X and Y and 256 bins for XY – one for each possible pair of values. Note that SystemVerilog coverpoints only operate on 2 state values: values x or z are excluded.
Coverage of transitions may also be collected. An example where this may be used is for finite state machines. Consider a state machine with three states, Idle, State1 and State2, where the only legal transitions are those to and from Idle. In addition, the state machine should only remain in the Idle state for a maximum of 4 clocks.
enum {Idle, State1, State2} State; covergroup cg_State @(posedge Clock); states : coverpoint State; state_trans : coverpoint State { bins legal[] = ( Idle => State1, State2 ), ( State1, State2 => Idle); bins idle[] = ( Idle [* 2:4] ); bins illegal = default sequence; } endgroup
This would create a separate bin for each legal transition – including remaining in Idle – and one bin for all the illegal transitions.
SystemVerilog also provides the illegal_bins construct, which causes the simulator to stop with an error if an illegal value or transition occurs:
covergroup cg_State @(posedge Clock); ... illegal_bins illegal = default sequence; } endgroup
Options control the behaviour of covergroups and coverpoints. For example, the coverage results for a particular covergroup or coverpoint may be weighted, or a maximum number of automatically created bins could be specified. Options such as these can be set in the covergroup, or procedurally after the covergroup has been instanced.
int i_a, i_b, i_c; covergroup cg @(posedge Clock); option.auto_bin_max = 10; a : coverpoint i_a; b : coverpoint i_b; c : coverpoint i_c { option.auto_bin_max = 20; } endgroup cg cg_inst = new; cg_inst.a.option.weight = 2;
In this example, 10 bins are created for the coverpoints cg_inst.a and cg_inst.b and 20 bins are created for cg_inst.c. cg_inst.a is assigned a weight of 2, whereas the other coverpoints each have a weight of 1 (the default weight).
There are many other options – refer to the SystemVerilog LRM for details of these.
The other functional coverage features that have not been covered in this tutorial are covergroup arguments; wildcard bins and block execution events. For details of these, please refer to the SystemVerilog LRM.