SystemVerilog TestBench
We need to have an environment known as a testbench to run any kind of simulation on the design.
What is the purpose of a testbench ?
A testbench allows us to verify the functionality of a design through simulations. It is a container where the design is placed and driven with different input stimulus.
- Generate different types of input stimulus
- Drive the design inputs with the generated stimulus
- Allow the design to process input and provide an output
- Check the output with expected behavior to find functional defects
- If a functional bug is found, then change the design to fix the bug
- Perform the above steps until there are no more functional defects
Components of a testbench
The example shown in Introduction is not modular, scalable, flexible or even re-usable because of the way DUT is connected, and how signals are driven. Let’s take a look at a simple testbench and try to understand about the various components that facilitate data transfer from and to the DUT.
| Component | Description |
|---|---|
| Generator | Generates different input stimulus to be driven to DUT |
| Interface | Contains design signals that can be driven or monitored |
| Driver | Drives the generated stimulus to the design |
| Monitor | Monitor the design input-output ports to capture design activity |
| Scoreboard | Checks output from the design with expected behavior |
| Environment | Contains all the verification components mentioned above |
| Test | Contains the environment that can be tweaked with different configuration settings |
What is DUT ?
DUT stands for Design Under Test and is the hardware design written in Verilog or VHDL. DUT is a term typically used in post validation of the silicon once the chip is fabricated. In pre validation, it is also called as Design Under Verification, DUV in short.
// All verification components are placed in this top testbench module
module tb_top;
// Declare variables that need to be connected to the design instance
// These variables are assigned some values that in turn gets transferred to
// the design as inputs because they are connected with the ports in the design
reg clk;
wire en;
wire wr;
wire data;
// Instantiate the design module and connect the variables declared above
// with the ports in the design
design myDsn ( .clk (clk),
.en (en),
.wr (wr),
. ...
.rdata);
// Develop rest of the testbench and write stimulus that can be driven to the design
endmodule
What is an interface ?
If the design contained hundreds of port signals it would be cumbersome to connect, maintain and re-use those signals. Instead, we can place all the design input-output ports into a container which becomes an interface to the DUT. The design can then be driven with values through this interface.
What is a driver ?
The driver is the verification component that does the pin-wiggling of the DUT, through a task defined in the interface. When the driver has to drive some input values to the design, it simply has to call this pre-defined task in the interface, without actually knowing the timing relation between these signals. The timing information is defined within the task provided by the interface. This is the level of abstraction required to make testbenches more flexible and scalable. In the future, if the interface changed, then the new driver can call the same task and drive signals in a different way.
How does the driver know what to drive ?
The generator is a verification component that can create valid data transactions and send them to the driver. The driver can then simply drive the data provided to it by the generator through the interface. Data transactions are implemented as class objects shown by the blue squares in the image above. It is the job of the driver to get the data object and translate it into something the DUT can understand.
Why is a monitor required?
Until now, how data is driven to the DUT was discussed. But that’s only half way through, because our primary aim is to verify the design. The DUT processes input data and sends the result to the output pins. The monitor picks up the processed data, converts it into a data object and sends it to the scoreboard.
What is the purpose of a scoreboard?
The
Why is an environment required ?
It makes the verification more flexible and scalable because more components can be plugged into the same environment for a future project.
What does the test do ?
Exactly. The test will instantiate an object of the environment and configure it the way the test wants to. Remember that we will most probably have thousands of tests and it is not feasbile to make direct changes to the environment for each test. Instead we want certain knobs/parameters in the environment that can be tweaked for each test. That way, the test will have a higher control over stimulus generation and will be more effective.
Here, we have talked about how a simple testbench looks like. In real projects, there’ll be many such components plugged in to do various tasks at higher levels of abstraction. If we had to verify a simple digital counter with maximum 50 lines of RTL code, yea, this would suffice. But, when complexity increases, there will be a need to deal with more abstraction.
What are abstraction levels ?
In the Preface, you saw that we toggled the design using individual signals.
#5 resetn <= 0;
#20 resetn <= 1;
Instead, if you put these two signals in a task and call it “apply_reset” task, you have just created a component that can be re-used and hides the details of what signals and what time intervals it is being asserted. This is a feature we would like to have when developing the testbench - to hide away details - so that the test writer need not bother about the how and instead focus on when and why these tasks should be put to use. A test writer finally uses tasks, configures environment and writes code to test the design.
module tb_top;
bit resetn;
task apply_reset ();
#5 resetn <= 0;
#20 resetn <= 1;
endtask
initial begin
apply_reset();
end
endmodule
SystemVerilog Strings
What is a SystemVerilog string ?
The string data-type is an ordered collection of characters. The length of a string variable is the number of characters in the collection which can have dynamic length and vary during the course of a simulation. A string variable does not represent a string in the same way as a string literal. No truncation occurs when using the string variable.
Syntax
string variable_name [= initial_value];
variable_name is a valid identifier and the optional initial_value can be a string literal, the value "" for an empty string, or a string data type expression. If an initial value is not specified at the time of declaration, then the variable defaults to “”, an empty string literal.
SystemVerilog String Example
module tb;
// Declare a string variable called "dialog" to store string literals
// Initialize the variable to "Hello!"
string dialog = "Hello!";
initial begin
// Display the string using %s string format
$display ("%s", dialog);
// Iterate through the string variable to identify individual characters and print
foreach (dialog[i]) begin
$display ("%s", dialog[i]);
end
end
endmodule
Simulation Log
ncsim> run
Hello!
H
e
l
l
o
!
ncsim: *W,RNQUIE: Simulation is complete.
How are strings represented in Verilog ?
A single ASCII character requires 8-bits (1 byte) and to store a string we would need as many bytes as there are number of characters in the string.
reg [16*8-1:0] my_string; // Can store 16 characters
my_string = "How are you"; // 5 zeros are padded from MSB, and 11 char are stored
my_string = "How are you doing?"; // 19 characters; my_string will get "w are you doing?"
Example
module tb;
string firstname = "Joey";
string lastname = "Tribbiani";
initial begin
// String Equality : Check if firstname equals or not equals lastname
if (firstname == lastname)
$display ("firstname=%s is EQUAL to lastname=%s", firstname, lastname);
if (firstname != lastname)
$display ("firstname=%s is NOT EQUAL to lastname=%s", firstname, lastname);
// String comparison : Check if length of firstname < or > length of lastname
if (firstname < lastname) $display ("firstname=%s is LESS THAN lastname=%s", firstname, lastname); if (firstname > lastname)
$display ("firstname=%s is GREATER THAN lastname=%s", firstname, lastname);
// String concatenation : Join first and last names into a single string
$display ("Full Name = %s", {firstname, " ", lastname});
// String Replication
$display ("%s", {3{firstname}});
// String Indexing : Get the ASCII character at index number 2 of both first and last names
$display ("firstname[2]=%s lastname[2]=%s", firstname[2], lastname[2]);
end
endmodule
Simulation Log
ncsim> run
firstname=Joey is NOT EQUAL to lastname=Tribbiani
firstname=Joey is LESS THAN lastname=Tribbiani
Full Name = Joey Tribbiani
JoeyJoeyJoey
firstname[2]=e lastname[2]=i
ncsim: *W,RNQUIE: Simulation is complete.
Basic String Methods
SystemVerilog also includes a number of special methods to work with strings, which use built-in method notation.
| Usage | Definition | Comments |
|---|---|---|
| str.len() | function int len() | Returns the number of characters in the string |
| str.putc() | function void putc (int i, byte c); | Replaces the ith character in the string with the given character |
| str.getc() | function byte getc (int i); | Returns the ASCII code of the ith character in str |
| str.tolower() | function string tolower(); | Returns a string with characters in str converted to lowercase |
| str.compare(s) | function int compare (string s); | Compares str and s, as in the ANSI C stcmp function |
| str.icompare(s) | function int icompare (string s); | Compares str and s, like the ANSI C strcmp function |
| str.substr (i, j) | function string substr (int i, int j); | Returns a new string that is a substring formed by characters in position i through j of str |
Example
module tb;
string str = "Hello World!";
initial begin
string tmp;
// Print length of string "str"
$display ("str.len() = %0d", str.len());
// Assign to tmp variable and put char "d" at index 3
tmp = str;
tmp.putc (3,"d");
$display ("str.putc(3, d) = %s", tmp);
// Get the character at index 2
$display ("str.getc(2) = %s (%0d)", str.getc(2), str.getc(2));
// Convert all characters to lower case
$display ("str.tolower() = %s", str.tolower());
// Comparison
tmp = "Hello World!";
$display ("[tmp,str are same] str.compare(tmp) = %0d", str.compare(tmp));
tmp = "How are you ?";
$display ("[tmp,str are diff] str.compare(tmp) = %0d", str.compare(tmp));
// Ignore case comparison
tmp = "hello world!";
$display ("[tmp is in lowercase] str.compare(tmp) = %0d", str.compare(tmp));
tmp = "Hello World!";
$display ("[tmp,str are same] str.compare(tmp) = %0d", str.compare(tmp));
// Extract new string from i to j
$display ("str.substr(4,8) = %s", str.substr (4,8));
end
endmodule
Simulation Log
str.len() = 12
str.putc(3, d) = Heldo World!
str.getc(2) = l (108)
str.tolower() = hello world!
[tmp,str are same] str.compare(tmp) = 0
[tmp,str are diff] str.compare(tmp) = -1
[tmp is in lowercase] str.compare(tmp) = -1
[tmp,str are same] str.compare(tmp) = 0
str.substr(4,8) = o Wor
String Conversion Methods
| str.atoi() | function integer atoi(); | Returns the integer corresponding to the ASCII decimal representation in str |
|---|---|---|
| str.atohex() | function integer atohex(); | Interprets the string as hexadecimal |
| str.atooct() | function integer atooct(); | Interprets the string as octal |
| str.atobin() | function integer atobin(); | Interprets the string as binary |
| str.atoreal() | function real atoreal(); | Returns the real number corresponding to the ASCII decimal representation in str |
| str.itoa(i) | function void itoa (integer i); | Stores the ASCII decimal representation of i into str |
| str.hextoa(i) | function void hextoa (integer i); | Stores the ASCII hexadecimal representation of i into str |
| str.octtoa(i) | function void octtoa (integer i); | Stores the ASCII octal representation of i into str |
| str.bintoa(i) | function void bintoa (integer i); | Stores the ASCII binary representation of i into str |
| str.realtoa(r) | function void realtoa (real r); | Stores the ASCII real representation of r into str |
SystemVerilog Arrays
SystemVerilog offers much flexibility in building complicated data structures through the different types of arrays.
- Static Arrays
- Dynamic Arrays
- Associative Arrays
- Queues
Static Arrays
A static array is one whose size is known before compilation time. In the example shown below, a static array of 8-bit wide is declared, assigned some value and iterated over to print its value.
module tb;
bit [7:0] m_data; // A vector or 1D packed array
initial begin
// 1. Assign a value to the vector
m_data = 8'hA2;
// 2. Iterate through each bit of the vector and print value
for (int i = 0; i < $size(m_data); i++) begin
$display ("m_data[%0d] = %b", i, m_data[i]);
end
end
endmodule
Static arrays are further categorized into packed and unpacked arrays.
bit [2:0][7:0] m_data; // Packed
bit [15:0] m_mem [10:0]; // Unpacked
Unpacked arrays may be fixed-size arrays, dynamic arrays, associative arrays or queues.
Dynamic Arrays
A dynamic array is one whose size is not known during compilation, but instead is defined and expanded as needed during runtime. A dynamic array is easily recognized by its empty square brackets [ ].
int m_mem []; // Dynamic array, size unknown but it holds integer values
Associative Arrays
An associative array is one where the content is stored with a certain key. This is easily recognized by the presence of a data type inside its square brackets [ ]. The key is represented inside the square brackets.
int m_data [int]; // Key is of type int, and data is also of type int
int m_name [string]; // Key is of type string, and data is of type int
m_name ["Rachel"] = 30;
m_name ["Orange"] = 2;
m_data [32'h123] = 3333;
Queues
A queue is a data type where data can be either pushed into the queue or popped from the array. It is easily recognized by the $ symbol inside square brackets [ ].
int m_queue [$]; // Unbound queue, no size
m_queue.push_back(23); // Push into the queue
int data = m_queue.pop_front(); // Pop from the queue
SystemVerilog Packed Arrays
There are two types of arrays in SystemVerilog - packed and unpacked arrays.
A packed array is used to refer to dimensions declared before the variable name.
bit [3:0] data; // Packed array or vector
logic queue [9:0]; // Unpacked array
A packed array is guaranteed to be represented as a contiguous set of bits. They can be made of only the single bit data types like bit, logic, and other recursively packed arrays.
Single Dimensional Packed Arrays
A one-dimensional packed array is also called as a vector.
module tb;
bit [7:0] m_data; // A vector or 1D packed array
initial begin
// 1. Assign a value to the vector
m_data = 8'hA2;
// 2. Iterate through each bit of the vector and print value
for (int i = 0; i < $size(m_data); i++) begin
$display ("m_data[%0d] = %b", i, m_data[i]);
end
end
endmodule
Simulation Log
ncsim> run
m_data[0] = 0
m_data[1] = 1
m_data[2] = 0
m_data[3] = 0
m_data[4] = 0
m_data[5] = 1
m_data[6] = 0
m_data[7] = 1
ncsim: *W,RNQUIE: Simulation is complete.
Multidimensional Packed Arrays
A multidimensional packed array is still a set of contiguous bits but are also segmented into smaller groups.
Example #1
The code shown below declares a 2D packed array that occupies 32-bits or 4 bytes and iterates through the segments and prints its value.
module tb;
bit [3:0][7:0] m_data; // A MDA, 4 bytes
initial begin
// 1. Assign a value to the MDA
m_data = 32'hface_cafe;
$display ("m_data = 0x%0h", m_data);
// 2. Iterate through each segment of the MDA and print value
for (int i = 0; i < $size(m_data); i++) begin
$display ("m_data[%0d] = %b (0x%0h)", i, m_data[i], m_data[i]);
end
end
endmodule
Simulation Log
ncsim> run
m_data = 0xfacecafe
m_data[0] = 11111110 (0xfe)
m_data[1] = 11001010 (0xca)
m_data[2] = 11001110 (0xce)
m_data[3] = 11111010 (0xfa)
ncsim: *W,RNQUIE: Simulation is complete.
Example #2
Let us see a 3D packed array now.
module tb;
bit [2:0][3:0][7:0] m_data; // An MDA, 12 bytes
initial begin
// 1. Assign a value to the MDA
m_data[0] = 32'hface_cafe;
m_data[1] = 32'h1234_5678;
m_data[2] = 32'hc0de_fade;
// m_data gets a packed value
$display ("m_data = 0x%0h", m_data);
// 2. Iterate through each segment of the MDA and print value
foreach (m_data[i]) begin
$display ("m_data[%0d] = 0x%0h", i, m_data[i]);
foreach (m_data[i][j]) begin
$display ("m_data[%0d][%0d] = 0x%0h", i, j, m_data[i][j]);
end
end
end
endmodule
Simulation Log
ncsim> run
m_data = 0xc0defade12345678facecafe
m_data[2] = 0xc0defade
m_data[2][3] = 0xc0
m_data[2][2] = 0xde
m_data[2][1] = 0xfa
m_data[2][0] = 0xde
m_data[1] = 0x12345678
m_data[1][3] = 0x12
m_data[1][2] = 0x34
m_data[1][1] = 0x56
m_data[1][0] = 0x78
m_data[0] = 0xfacecafe
m_data[0][3] = 0xfa
m_data[0][2] = 0xce
m_data[0][1] = 0xca
m_data[0][0] = 0xfe
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Unpacked Arrays
An unpacked array is used to refer to dimensions declared after the variable name.
Unpacked arrays may be fixed-size arrays, dynamic arrays, associativek arrays or [queues.
Single Dimensional Unpacked Array
module tb;
byte stack [8]; // depth = 8, 1 byte wide variable
initial begin
// Assign random values to each slot of the stack
foreach (stack[i]) begin
stack[i] = $random;
$display ("Assign 0x%0h to index %0d", stack[i], i);
end
// Print contents of the stack
$display ("stack = %p", stack);
end
endmodule
Simulation Log
Assign 0x24 to index 0
Assign 0x81 to index 1
Assign 0x9 to index 2
Assign 0x63 to index 3
Assign 0xd to index 4
Assign 0x8d to index 5
Assign 0x65 to index 6
Assign 0x12 to index 7
stack = '{'h24, 'h81, 'h9, 'h63, 'hd, 'h8d, 'h65, 'h12}
Multidimensional Unpacked Array
module tb;
byte stack [2][4]; // 2 rows, 4 cols
initial begin
// Assign random values to each slot of the stack
foreach (stack[i])
foreach (stack[i][j]) begin
stack[i][j] = $random;
$display ("stack[%0d][%0d] = 0x%0h", i, j, stack[i][j]);
end
// Print contents of the stack
$display ("stack = %p", stack);
end
endmodule
Simulation Log
ncsim> run
stack[0][0] = 0x24
stack[0][1] = 0x81
stack[0][2] = 0x9
stack[0][3] = 0x63
stack[1][0] = 0xd
stack[1][1] = 0x8d
stack[1][2] = 0x65
stack[1][3] = 0x12
stack = '{'{'h24, 'h81, 'h9, 'h63}, '{'hd, 'h8d, 'h65, 'h12}}
ncsim: *W,RNQUIE: Simulation is complete.
Packed + Unpacked Array
The example shown below illustrates a multidimensional packed + unpacked array.
module tb;
bit [3:0][7:0] stack [2][4]; // 2 rows, 4 cols
initial begin
// Assign random values to each slot of the stack
foreach (stack[i])
foreach (stack[i][j]) begin
stack[i][j] = $random;
$display ("stack[%0d][%0d] = 0x%0h", i, j, stack[i][j]);
end
// Print contents of the stack
$display ("stack = %p", stack);
// Print content of a given index
$display("stack[0][0][2] = 0x%0h", stack[0][0][2]);
end
endmodule
Simulation Log
ncsim> run
stack[0][0] = 0x12153524
stack[0][1] = 0xc0895e81
stack[0][2] = 0x8484d609
stack[0][3] = 0xb1f05663
stack[1][0] = 0x6b97b0d
stack[1][1] = 0x46df998d
stack[1][2] = 0xb2c28465
stack[1][3] = 0x89375212
stack = '{'{'h12153524, 'hc0895e81, 'h8484d609, 'hb1f05663}, '{'h6b97b0d, 'h46df998d, 'hb2c28465, 'h89375212}}
stack[0][0][2] = 0x15
ncsim: *W,RNQUIE: Simulation is complete.
In a multidimensional declaration, the dimensions declared before the name vary more faster than the dimensions following the name.
bit [1:4] m_var [1:5] // 1:4 varies faster than 1:5
bit m_var2 [1:5] [1:3] // 1:3 varies faster than 1:5
bit [1:3] [1:7] m_var3; // 1:7 varies faster than 1:3
bit [1:3] [1:2] m_var4 [1:7] [0:2] // 1:2 varies most rapidly, followed by 1:3, then 0:2 and then 1:7
SystemVerilog Dynamic Array
A dynamic array is an unpacked array whose size can be set or changed at run time, and hence is quite different from a static array where the size is pre-determined during declaration of the array. The default size of a dynamic array is zero until it is set by the new() constructor.
Syntax
A dynamic array dimensions are specified by the empty square brackets [ ].
[data_type] [identifier_name] [];
bit [7:0] stack []; // A dynamic array of 8-bit vector
string names []; // A dynamic array that can contain strings
The new() function is used to allocate a size for the array and initialize its elements if required.
Dynamic Array Example
module tb;
// Create a dynamic array that can hold elements of type int
int array [];
initial begin
// Create a size for the dynamic array -> size here is 5
// so that it can hold 5 values
array = new [5];
// Initialize the array with five values
array = '{31, 67, 10, 4, 99};
// Loop through the array and print their values
foreach (array[i])
$display ("array[%0d] = %0d", i, array[i]);
end
endmodule
Simulation Log
ncsim> run
array[0] = 31
array[1] = 67
array[2] = 10
array[3] = 4
array[4] = 99
ncsim: *W,RNQUIE: Simulation is complete.
Dynamic Array Methods
| Function | Description |
|---|---|
| function int size (); | Returns the current size of the array, 0 if array has not been created |
| function void delete (); | Empties the array resulting in a zero-sized array |
module tb;
// Create a dynamic array that can hold elements of type string
string fruits [];
initial begin
// Create a size for the dynamic array -> size here is 5
// so that it can hold 5 values
fruits = new [3];
// Initialize the array with five values
fruits = '{"apple", "orange", "mango"};
// Print size of the dynamic array
$display ("fruits.size() = %0d", fruits.size());
// Empty the dynamic array by deleting all items
fruits.delete();
$display ("fruits.size() = %0d", fruits.size());
end
endmodule
Simulation Log
ncsim> run
fruits.size() = 3
fruits.size() = 0
ncsim: *W,RNQUIE: Simulation is complete.
How to add new items to a dynamic array ?
Many times we may need to add new elements to an existing dynamic array without losing its original contents. Since the new() operator is used to allocate a particular size for the array, we also have to copy the old array contents into the new one after creation.
int array [];
array = new [10];
// This creates one more slot in the array, while keeping old contents
array = new [array.size() + 1] (array);
Copying dynamic array example
module tb;
// Create two dynamic arrays of type int
int array [];
int id [];
initial begin
// Allocate 5 memory locations to "array" and initialize with values
array = new [5];
array = '{1, 2, 3, 4, 5};
// Point "id" to "array"
id = array;
// Display contents of "id"
$display ("id = %p", id);
// Grow size by 1 and copy existing elements to the new dyn.Array "id"
id = new [id.size() + 1] (id);
// Assign value 6 to the newly added location [index 5]
id [id.size() - 1] = 6;
// Display contents of new "id"
$display ("New id = %p", id);
// Display size of both arrays
$display ("array.size() = %0d, id.size() = %0d", array.size(), id.size());
end
endmodule
Simulation Log
run -all;
ncsim> run
id = '{1, 2, 3, 4, 5}
New id = '{1, 2, 3, 4, 5, 6}
array.size() = 5, id.size() = 6
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Associative Array
When size of a collection is unknown or the data space is sparse, an associative array is a better option. Associative arrays do not have any storage allocated until it is used, and the index expression is not restricted to integral expressions, but can be of any type.
An associative array implements a look-up table of the elements of its declared type. The data type to be used as an index serves as the lookup key and imposes an ordering.
Syntax
// Value Array_Name [ key ];
data_type array_identifier [ index_type ];
Initialization Example
module tb;
int array1 [int]; // An integer array with integer index
int array2 [string]; // An integer array with string index
string array3 [string]; // A string array with string index
initial begin
// Initialize each dynamic array with some values
array1 = '{ 1 : 22,
6 : 34 };
array2 = '{ "Ross" : 100,
"Joey" : 60 };
array3 = '{ "Apples" : "Oranges",
"Pears" : "44" };
// Print each array
$display ("array1 = %p", array1);
$display ("array2 = %p", array2);
$display ("array3 = %p", array3);
end
endmodule
Simulation Log
ncsim> run
array1 = '{1:22, 6:34}
array2 = '{"Joey":60, "Ross":100}
array3 = '{"Apples":"Oranges", "Pears":"44"}
ncsim: *W,RNQUIE: Simulation is complete.
Associative Array Methods
| Function | Description |
|---|---|
| function int num (); | Returns the number of entries in the associative array |
| function int size (); | Also returns the number of entries, if empty 0 is returned |
| function void delete ( [input index] ); | index when specified deletes the entry at that index, else the whole array is deleted |
| function int exists (input index); | Checks whether an element exists at specified index; returns 1 if it does, else 0 |
| function int first (ref index); | Assigns to the given index variable the value of the first index; returns 0 for empty array |
| function int last (ref index); | Assigns to given index variable the value of the last index; returns 0 for empty array |
| function int next (ref index); | Finds the smallest index whose value is greater than the given index |
| function int prev (ref index); | Finds the largest index whose value is smaller than the given index |
Associative Array Methods Example
module tb;
int fruits_l0 [string];
initial begin
fruits_l0 = '{ "apple" : 4,
"orange" : 10,
"plum" : 9,
"guava" : 1 };
// size() : Print the number of items in the given dynamic array
$display ("fruits_l0.size() = %0d", fruits_l0.size());
// num() : Another function to print number of items in given array
$display ("fruits_l0.num() = %0d", fruits_l0.num());
// exists() : Check if a particular key exists in this dynamic array
if (fruits_l0.exists ("orange"))
$display ("Found %0d orange !", fruits_l0["orange"]);
if (!fruits_l0.exists ("apricots"))
$display ("Sorry, season for apricots is over ...");
// Note: String indices are taken in alphabetical order
// first() : Get the first element in the array
begin
string f;
// This function returns true if it succeeded and first key is stored
// in the provided string "f"
if (fruits_l0.first (f))
$display ("fruits_l0.first [%s] = %0d", f, fruits_l0[f]);
end
// last() : Get the last element in the array
begin
string f;
if (fruits_l0.last (f))
$display ("fruits_l0.last [%s] = %0d", f, fruits_l0[f]);
end
// prev() : Get the previous element in the array
begin
string f = "orange";
if (fruits_l0.prev (f))
$display ("fruits_l0.prev [%s] = %0d", f, fruits_l0[f]);
end
// next() : Get the next item in the array
begin
string f = "orange";
if (fruits_l0.next (f))
$display ("fruits_l0.next [%s] = %0d", f, fruits_l0[f]);
end
end
endmodule
Simulation Log
ncsim> run
fruits_l0.size() = 4
fruits_l0.num() = 4
Found 10 orange !
Sorry, season for apricots is over ...
fruits_l0.first [apple] = 4
fruits_l0.last [plum] = 9
fruits_l0.prev [guava] = 1
fruits_l0.next [plum] = 9
ncsim: *W,RNQUIE: Simulation is complete.
Dynamic array of Associative arrays
module tb;
// Create an associative array with key of type string and value of type int
// for each index in a dynamic array
int fruits [] [string];
initial begin
// Create a dynamic array with size 2
fruits = new [2];
// Initialize the associative array inside each dynamic array index
fruits [0] = '{ "apple" : 1, "grape" : 2 };
fruits [1] = '{ "melon" : 3, "cherry" : 4 };
// Iterate through each index of dynamic array
foreach (fruits[i])
// Iterate through each key of the current index in dynamic array
foreach (fruits[i][fruit])
$display ("fruits[%0d][%s] = %0d", i, fruit, fruits[i][fruit]);
end
endmodule
Simulation Log
ncsim> run
fruits[0][apple] = 1
fruits[0][grape] = 2
fruits[1][cherry] = 4
fruits[1][melon] = 3
ncsim: *W,RNQUIE: Simulation is complete.
Dynamic array within each index of an Associative array
// Create a new typedef that represents a dynamic array
typedef int int_da [];
module tb;
// Create an associative array where key is a string
// and value is a dynamic array
int_da fruits [string];
initial begin
// For key "apple", create a dynamic array that can hold 2 items
fruits ["apple"] = new [2];
// Initialize the dynamic array with some values
fruits ["apple"] = '{ 4, 5};
// Iterate through each key, where key represented by str1
foreach (fruits[str1])
// Iterate through each item inside the current dynamic array ie.fruits[str1]
foreach (fruits[str1][i])
$display ("fruits[%s][%0d] = %0d", str1, i, fruits[str1][i]);
end
endmodule
Simulation Log
ncsim> run
fruits[apple][0] = 4
fruits[apple][1] = 5
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Array Manipulation
There are many built-in methods in SystemVerilog to help in array searching and ordering.
Array manipulation methods simply iterate through the array elements and each element is used to evaluate the expression specified by the with clause. The iterator argument specifies a local variable that can be used within the with expression to refer to the current element in the iteration. If an argument is not provided, item is the name used by default.
!img
Array Locator Methods
The with clause and expresison is mandatory for some of these methods and for some others its optional.
Mandatory ‘with’ clause
These methods are used to filter out certain elements from an existing array based on a given expression. All such elements that satisfy the given expression is put into an array and returned. Hence the with clause is mandatory for the following methods.
| Method name | Description |
|---|---|
| find() | Returns all elements satisfying the given expression |
| find_index() | Returns the indices of all elements satisfying the given expression |
| find_first() | Returns the first element satisfying the given expression |
| find_first_index() | Returns the index of the first element satisfying the given expression |
| find_last() | Returns the last element satisfying the given expression |
| find_last_index() | Returns the index of the last element satisfying the given expression |
Example
module tb;
int array[9] = '{4, 7, 2, 5, 7, 1, 6, 3, 1};
int res[$];
initial begin
res = array.find(x) with (x > 3);
$display ("find(x) : %p", res);
res = array.find_index with (item == 4);
$display ("find_index : res[%0d] = 4", res[0]);
res = array.find_first with (item < 5 & item >= 3);
$display ("find_first : %p", res);
res = array.find_first_index(x) with (x > 5);
$display ("find_first_index: %p", res);
res = array.find_last with (item <= 7 & item > 3);
$display ("find_last : %p", res);
res = array.find_last_index(x) with (x < 3);
$display ("find_last_index : %p", res);
end
endmodule
Simulation Log
ncsim> run
find(x) : '{4, 7, 5, 7, 6}
find_index : res[0] = 4
find_first : '{4}
find_first_index: '{1}
find_last : '{6}
find_last_index : '{8}
ncsim: *W,RNQUIE: Simulation is complete.
Optional ‘with’ clause
| Methods | Description |
|---|---|
| min() | Returns the element with minimum value or whose expression evaluates to a minimum |
| max() | Returns the element with maximum value or whose expression evaluates to a maximum |
| unique() | Returns all elements with unique values or whose expression evaluates to a unique value |
| unique_index() | Returns the indices of all elements with unique values or whose expression evaluates to a unique value |
Example
module tb;
int array[9] = '{4, 7, 2, 5, 7, 1, 6, 3, 1};
int res[$];
initial begin
res = array.min();
$display ("min : %p", res);
res = array.max();
$display ("max : %p", res);
res = array.unique();
$display ("unique : %p", res);
res = array.unique(x) with (x < 3);
$display ("unique : %p", res);
res = array.unique_index;
$display ("unique_index : %p", res);
end
endmodule
Simulation Log
ncsim> run
min : '{1}
max : '{7}
unique : '{4, 7, 2, 5, 1, 6, 3}
unique : '{4, 2}
unique_index : '{0, 1, 2, 3, 5, 6, 7}
ncsim: *W,RNQUIE: Simulation is complete.
Array Ordering Methods
These methods operate and alter the array directly.
| Method | Description |
|---|---|
| reverse() | Reverses the order of elements in the array |
| sort() | Sorts the array in ascending order, optionally using with clause |
| rsort() | Sorts the array in descending order, optionally using with clause |
| shuffle() | Randomizes the order of the elements in the array. with clause is not allowed here. |
Example
module tb;
int array[9] = '{4, 7, 2, 5, 7, 1, 6, 3, 1};
initial begin
array.reverse();
$display ("reverse : %p", array);
array.sort();
$display ("sort : %p", array);
array.rsort();
$display ("rsort : %p", array);
for (int i = 0; i < 5; i++) begin
array.shuffle();
$display ("shuffle Iter:%0d = %p", i, array);
end
end
endmodule
Simulation Log
ncsim> run
reverse : '{1, 3, 6, 1, 7, 5, 2, 7, 4}
sort : '{1, 1, 2, 3, 4, 5, 6, 7, 7}
rsort : '{7, 7, 6, 5, 4, 3, 2, 1, 1}
shuffle Iter:0 = '{6, 7, 1, 7, 3, 2, 1, 4, 5}
shuffle Iter:1 = '{5, 1, 3, 4, 2, 7, 1, 7, 6}
shuffle Iter:2 = '{3, 1, 7, 4, 6, 7, 1, 2, 5}
shuffle Iter:3 = '{6, 4, 7, 3, 1, 7, 5, 2, 1}
shuffle Iter:4 = '{3, 6, 2, 5, 4, 7, 7, 1, 1}
ncsim: *W,RNQUIE: Simulation is complete.
Using array ordering on classes
class Register;
string name;
rand bit [3:0] rank;
rand bit [3:0] pages;
function new (string name);
this.name = name;
endfunction
function void print();
$display("name=%s rank=%0d pages=%0d", name, rank, pages);
endfunction
endclass
module tb;
Register rt[4];
string name_arr[4] = '{"alexa", "siri", "google home", "cortana"};
initial begin
$display ("
-------- Initial Values --------");
foreach (rt[i]) begin
rt[i] = new (name_arr[i]);
rt[i].randomize();
rt[i].print();
end
$display ("
--------- Sort by name ------------");
rt.sort(x) with (x.name);
foreach (rt[i]) rt[i].print();
$display ("
--------- Sort by rank, pages -----------");
rt.sort(x) with ( {x.rank, x.pages});
foreach (rt[i]) rt[i].print();
end
endmodule
Simulation Log
ncsim> run
-------- Initial Values --------
name=alexa rank=12 pages=13
name=siri rank=6 pages=12
name=google home rank=12 pages=13
name=cortana rank=7 pages=11
--------- Sort by name ------------
name=alexa rank=12 pages=13
name=cortana rank=7 pages=11
name=google home rank=12 pages=13
name=siri rank=6 pages=12
--------- Sort by rank, pages -----------
name=siri rank=6 pages=12
name=cortana rank=7 pages=11
name=alexa rank=12 pages=13
name=google home rank=12 pages=13
ncsim: *W,RNQUIE: Simulation is complete.
Array Reduction Methods
| Method | Description |
|---|---|
| sum() | Returns the sum of all array elements |
| product() | Returns the product of all array elements |
| and() | Returns the bitwise AND (&) of all array elements |
| or() | Returns the bitwise OR (|) of all array elements |
| xor() | Returns the bitwise XOR (^) of all array elements |
module tb;
int array[4] = '{1, 2, 3, 4};
int res[$];
initial begin
$display ("sum = %0d", array.sum());
$display ("product = %0d", array.product());
$display ("and = 0x%0h", array.and());
$display ("or = 0x%0h", array.or());
$display ("xor = 0x%0h", array.xor());
end
endmodule
Simulation Log
ncsim> run
sum = 10
product = 24
and = 0x0
or = 0x7
xor = 0x4
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Queue
A SystemVerilog queue is a First In First Out scheme which can have a variable size to store elements of the same data type.
It is similar to a one-dimensional unpacked array that grows and shrinks automatically. They can also be manipulated by indexing, concatenation and slicing operators. Queues can be passed to tasks/functions as ref or non-ref arguments.
Syntax and Usage
A queue is distinguished by it’s specification of the size using $ operator.
[data_type] [name_of_queue] [$];
string name_list [$]; // A queue of string elements
bit [3:0] data [$]; // A queue of 4-bit elements
logic [7:0] elements [$:127]; // A bounded queue of 8-bits with maximum size of 128 slots
int q1 [$] = { 1, 2, 3, 4, 5 }; // Integer queue, initialize elements
int q2 [$]; // Integer queue, empty
int tmp; // Temporary variable to store values
tmp = q1 [0]; // Get first item of q1 (index 0) and store in tmp
tmp = q1 [$]; // Get last item of q1 (index 4) and store in tmp
q2 = q1; // Copy all elements in q1 into q2
q1 = {}; // Empty the queue (delete all items)
q2[2] = 15; // Replace element at index 2 with 15
q2.insert (2, 15); // Inserts value 15 to index# 2
q2 = { q2, 22 }; // Append 22 to q2
q2 = { 99, q2 }; // Put 99 as the first element of q2
q2 = q2 [1:$]; // Delete first item
q2 = q2 [0:$-1]; // Delete last item
q2 = q2 [1:$-1]; // Delete first and last item
SystemVerilog Queue Example
module tb;
// Create a queue that can store "string" values
string fruits[$] = { "orange", "apple", "kiwi" };
initial begin
// Iterate and access each queue element
foreach (fruits[i])
$display ("fruits[%0d] = %s", i, fruits[i]);
// Display elements in a queue
$display ("fruits = %p", fruits);
// Delete all elements in the queue
fruits = {};
$display ("After deletion, fruits = %p", fruits);
end
endmodule
Simulation Log
ncsim> run
fruits[0] = orange
fruits[1] = apple
fruits[2] = kiwi
fruits = '{"orange", "apple", "kiwi"}
After deletion, fruits = '{}
ncsim: *W,RNQUIE: Simulation is complete.
What are queue slice expressions ?
A slice expression selects a subset of the existing variable. Queue elements can be selected using slice expressions as shown in the example below.
Some simulators provide different results, hence using queue methods is recommended.
module tb;
// Create a queue that can store "string" values
string fruits[$] = { "orange", "apple", "lemon", "kiwi" };
initial begin
// Select a subset of the queue
$display ("citrus fruits = %p", fruits[1:2]);
// Get elements from index 1 to end of queue
$display ("fruits = %p", fruits[1:$]);
// Add element to the end of queue
fruits[$+1] = "pineapple";
$display ("fruits = %p", fruits);
// Delete first element
$display ("Remove orange, fruits = %p", fruits[1:$]);
end
endmodule
Simulation Log
Compiler version J-2014.12-SP1-1; Runtime version J-2014.12-SP1-1; May 15 16:21 2018
citrus fruits = '{"apple", "lemon"}
fruits = '{"apple", "lemon", "kiwi"}
fruits = '{"orange", "apple", "lemon", "kiwi", "pineapple"}
Remove orange, fruits = '{"apple", "lemon", "kiwi", "pineapple"}
V C S S i m u l a t i o n R e p o r t
Queue Methods Example
In addition to array operators, queues provide several built-in methods.
| Function | Description |
|---|---|
| function int size (); | Returns the number of items in the queue, 0 if empty |
| function void insert (input integer index, input element_t item); | Inserts the given item at the specified index position |
| function void delete ( [input integer index] ); | Deletes the element at the specified index, and if not provided all elements will be deleted |
| function element_t pop_front (); | Removes and returns the first element of the queue |
| function element_t pop_back (); | Removes and returns the last element of the queue |
| function void push_front (input element_t item); | Inserts the given element at the front of the queue |
| function void push_back (input element_t item); | Inserts the given element at the end of the queue |
module tb;
string fruits[$] = {"apple", "pear", "mango", "banana"};
initial begin
// size() - Gets size of the given queue
$display ("Number of fruits=%0d fruits=%p", fruits.size(), fruits);
// insert() - Insert an element to the given index
fruits.insert (1, "peach");
$display ("Insert peach, size=%0d fruits=%p", fruits.size(), fruits);
// delete() - Delete element at given index
fruits.delete (3);
$display ("Delete mango, size=%0d fruits=%p", fruits.size(), fruits);
// pop_front() - Pop out element at the front
$display ("Pop %s, size=%0d fruits=%p", fruits.pop_front(), fruits.size(), fruits);
// push_front() - Push a new element to front of the queue
fruits.push_front("apricot");
$display ("Push apricot, size=%0d fruits=%p", fruits.size(), fruits);
// pop_back() - Pop out element from the back
$display ("Pop %s, size=%0d fruits=%p", fruits.pop_back(), fruits.size(), fruits);
// push_back() - Push element to the back
fruits.push_back("plum");
$display ("Push plum, size=%0d fruits=%p", fruits.size(), fruits);
end
endmodule
Simulation Log
ncsim> run
Number of fruits=4 fruits='{"apple", "pear", "mango", "banana"}
Insert peach, size=5 fruits='{"apple", "peach", "pear", "mango", "banana"}
Delete mango, size=4 fruits='{"apple", "peach", "pear", "banana"}
Pop apple, size=3 fruits='{"peach", "pear", "banana"}
Push apricot, size=4 fruits='{"apricot", "peach", "pear", "banana"}
Pop banana, size=3 fruits='{"apricot", "peach", "pear"}
Push plum, size=4 fruits='{"apricot", "peach", "pear", "plum"}
ncsim: *W,RNQUIE: Simulation is complete.
How to create a queue of classes in SystemVerilog ?
// Define a class with a single string member called "name"
class Fruit;
string name;
function new (string name="Unknown");
this.name = name;
endfunction
endclass
module tb;
// Create a queue that can hold values of data type "Fruit"
Fruit list [$];
initial begin
// Create a new class object and call it "Apple"
// and push into the queue
Fruit f = new ("Apple");
list.push_back (f);
// Create another class object and call it "Banana" and
// push into the queue
f = new ("Banana");
list.push_back (f);
// Iterate through queue and access each class object
foreach (list[i])
$display ("list[%0d] = %s", i, list[i].name);
// Simply print the whole queue, note that class handles are printed
// and not class object contents
$display ("list = %p", list);
end
endmodule
Simulation Log
ncsim> run
list[0] = Apple
list[1] = Banana
list = '{$unit_0x4ccdf83b::Fruit@2_1, $unit_0x4ccdf83b::Fruit@4_1}
ncsim: *W,RNQUIE: Simulation is complete.
How to create a queue of dynamic arrays in SystemVerilog ?
// Declare a dynamic array to store strings as a datatype
typedef string str_da [];
module tb;
// This is a queue of dynamic arrays
str_da list [$];
initial begin
// Initialize separate dynamic arrays with some values
str_da marvel = '{"Spiderman", "Hulk", "Captain America", "Iron Man"};
str_da dcWorld = '{"Batman", "Superman" };
// Push the previously created dynamic arrays to queue
list.push_back (marvel);
list.push_back (dcWorld);
// Iterate through the queue and access dynamic array elements
foreach (list[i])
foreach (list[i][j])
$display ("list[%0d][%0d] = %s", i, j, list[i][j]);
// Simply print the queue
$display ("list = %p", list);
end
endmodule
Simulation Log
ncsim> run
list[0][0] = Spiderman
list[0][1] = Hulk
list[0][2] = Captain America
list[0][3] = Iron Man
list[1][0] = Batman
list[1][1] = Superman
list = '{'{"Spiderman", "Hulk", "Captain America", "Iron Man"}, '{"Batman", "Superman"}}
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Structure
A structure can contain elements of different data types which can be referenced as a whole or individually by their names. This is quite different from arrays where the elements are of the same data-type.
// Normal arrays -> a collection of variables of same data type
int array [10]; // all elements are of int type
bit [7:0] mem [256]; // all elements are of bit type
// Structures -> a collection of variables of different data types
struct {
byte val1;
int val2;
string val3;
} struct_name;
Syntax
struct {
[list of variables]
} struct_name;
Unpacked Structures
A structure is unpacked by default and can be defined using the struct keyword and a list of member declarations can be provided within the curly brackets followed by the name of the structure.
Structure Example
module tb;
// Create a structure called "st_fruit"
// which to store the fruit's name, count and expiry date in days.
// Note: this structure declaration can also be placed outside the module
struct {
string fruit;
int count;
byte expiry;
} st_fruit;
initial begin
// st_fruit is a structure variable, so let's initialize it
st_fruit = '{"apple", 4, 15};
// Display the structure variable
$display ("st_fruit = %p", st_fruit);
// Change fruit to pineapple, and expiry to 7
st_fruit.fruit = "pineapple";
st_fruit.expiry = 7;
$display ("st_fruit = %p", st_fruit);
end
endmodule
Simulation Log
ncsim> run
st_fruit = '{fruit:"apple", count:4, expiry:'hf}
st_fruit = '{fruit:"pineapple", count:4, expiry:'h7}
ncsim: *W,RNQUIE: Simulation is complete.
What is the need to typedef a structure ?
Only one variable was created in the example above, but if there’s a need to create multiple structure variables with the same constituents, it’ll be better to create a user defined data type of the structure by typedef. Then st_fruit will become a data-type which can then be used to create variables of that type.
module tb;
// Create a structure called "st_fruit"
// which to store the fruit's name, count and expiry date in days.
// Note: this structure declaration can also be placed outside the module
typedef struct {
string fruit;
int count;
byte expiry;
} st_fruit;
initial begin
// st_fruit is a data type, so we need to declare a variable of this data type
st_fruit fruit1 = '{"apple", 4, 15};
st_fruit fruit2;
// Display the structure variable
$display ("fruit1 = %p fruit2 = %p", fruit1, fruit2);
// Assign one structure variable to another and print
// Note that contents of this variable is copied into the other
fruit2 = fruit1;
$display ("fruit1 = %p fruit2 = %p", fruit1, fruit2);
// Change fruit1 to see if fruit2 is affected
fruit1.fruit = "orange";
$display ("fruit1 = %p fruit2 = %p", fruit1, fruit2);
end
endmodule
Simulation Log
ncsim> run
fruit1 = '{fruit:"apple", count:4, expiry:'hf} fruit2 = '{fruit:"", count:0, expiry:'h0}
fruit1 = '{fruit:"apple", count:4, expiry:'hf} fruit2 = '{fruit:"apple", count:4, expiry:'hf}
fruit1 = '{fruit:"orange", count:4, expiry:'hf} fruit2 = '{fruit:"apple", count:4, expiry:'hf}
ncsim: *W,RNQUIE: Simulation is complete.
Packed Structures
A packed structure is a mechanism for subdividing a vector into fields that can be accessed as members and are packed together in memory without gaps. The first member in the structure is the most significant and subsequent members follow in decreasing order of significance.
A structure is declared packed using the packed keyword which by default is unsigned.
Example
// Create a "packed" structure data type which is similar to creating
// bit [7:0] ctrl_reg;
// ctrl_reg [0] represents en
// ctrl_reg [3:1] represents cfg
// ctrl_reg [7:4] represents mode
typedef struct packed {
bit [3:0] mode;
bit [2:0] cfg;
bit en;
} st_ctrl;
module tb;
st_ctrl ctrl_reg;
initial begin
// Initialize packed structure variable
ctrl_reg = '{4'ha, 3'h5, 1};
$display ("ctrl_reg = %p", ctrl_reg);
// Change packed structure member to something else
ctrl_reg.mode = 4'h3;
$display ("ctrl_reg = %p", ctrl_reg);
// Assign a packed value to the structure variable
ctrl_reg = 8'hfa;
$display ("ctrl_reg = %p", ctrl_reg);
end
endmodule
Simulation Log
ncsim> run
ctrl_reg = '{mode:'ha, cfg:'h5, en:'h1}
ctrl_reg = '{mode:'h3, cfg:'h5, en:'h1}
ctrl_reg = '{mode:'hf, cfg:'h5, en:'h0}
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog typedef
In complex testbenches some variable declarations might have a longer data-type specification or require to be used in multiple places in the testbench.
In such cases we can use a typedef to give a user-defined name to an existing data type. The new data-type can then be used throughout the code and hence avoids the need to edit in multiple places if required.
// Normal declaration may turn out to be quite long
unsigned shortint my_data;
enum {RED, YELLOW, GREEN} e_light;
bit [7:0] my_byte;
// Declare an alias for this long definition
typedef unsigned shortint u_shorti;
typedef enum {RED, YELLOW, GREEN} e_light;
typedef bit [7:0] ubyte;
// Use these new data-types to create variables
u_shorti my_data;
e_light light1;
ubyte my_byte;
Syntax
typedef data_type type_name [range];
Example
module tb;
typedef shortint unsigned u_shorti;
typedef enum {RED, YELLOW, GREEN} e_light;
typedef bit [7:0] ubyte;
initial begin
u_shorti data = 32'hface_cafe;
e_light light = GREEN;
ubyte cnt = 8'hFF;
$display ("light=%s data=0x%0h cnt=%0d", light.name(), data, cnt);
end
endmodule
Simulation Log
ncsim> run
light=GREEN data=0xcafe cnt=255
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Loops
What are loops ?
A loop is a piece of code that keeps executing over and over. A conditional statement is typically included in a loop so that it can terminate once the condition becomes true. If the loop runs forever, then the simulation will hang indefinitely.
Different types of looping constructs in SystemVerilog are given in the table below.
| forever | Runs the given set of statements forever |
|---|---|
| repeat | Repeats the given set of statements for a given number of times |
| while | Repeats the given set of statments as long as given condition is true |
| for | Similar to while loop, but more condense and popular form |
| do while | Repeats the given set of statements atleast once, and then loops as long as condition is true |
| foreach | Used mainly to iterate through all elements in an array |
forever
This is an infinite loop, just like while (1). Note that your simulation will hang unless you include a time delay inside the forever block to advance simulation time.
module tb;
// This initial block has a forever loop which will "run forever"
// Hence this block will never finish in simulation
initial begin
forever begin
#5 $display ("Hello World !");
end
end
// Because the other initial block will run forever, our simulation will hang!
// To avoid that, we will explicity terminate simulation after 50ns using $finish
initial
#50 $finish;
endmodule
Note that simulation would have continued indefinitely if $finish was not called.
Simulation Log
ncsim> run
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
Simulation complete via $finish(1) at time 50 NS + 0
repeat
Used to repeat statements in a block a certain number of times. The example shown below will display the message 5 times and continues with rest of the code.
module tb;
// This initial block will execute a repeat statement that will run 5 times and exit
initial begin
// Repeat everything within begin end 5 times and exit "repeat" block
repeat(5) begin
$display ("Hello World !");
end
end
endmodule
Simulation Log
ncsim> run
Hello World !
Hello World !
Hello World !
Hello World !
Hello World !
ncsim: *W,RNQUIE: Simulation is complete.
while
You already know this if you know verilog/C. It’ll repeat the block as long as the condition is true. Counter is initially zero and increments until it reaches 10.
module tb;
bit clk;
always #10 clk = ~clk;
initial begin
bit [3:0] counter;
$display ("Counter = %0d", counter); // Counter = 0
while (counter < 10) begin
@(posedge clk);
counter++;
$display ("Counter = %0d", counter); // Counter increments
end
$display ("Counter = %0d", counter); // Counter = 10
$finish;
end
endmodule
Simulation Log
ncsim> run
Counter = 0
Counter = 1
Counter = 2
Counter = 3
Counter = 4
Counter = 5
Counter = 6
Counter = 7
Counter = 8
Counter = 9
Counter = 10
Counter = 10
Simulation complete via $finish(1) at time 190 NS + 0
for
Similar to verilog/C, this allows you to mention starting value, condition and incremental expression all on the same line.
module tb;
bit clk;
always #10 clk = ~clk;
initial begin
bit [3:0] counter;
$display ("Counter = %0d", counter); // Counter = 0
for (counter = 2; counter < 14; counter = counter + 2) begin
@(posedge clk);
$display ("Counter = %0d", counter); // Counter increments
end
$display ("Counter = %0d", counter); // Counter = 14
$finish;
end
endmodule
Simulation Log
ncsim> run
Counter = 0
Counter = 2
Counter = 4
Counter = 6
Counter = 8
Counter = 10
Counter = 12
Counter = 14
Simulation complete via $finish(1) at time 110 NS + 0
do while
This executes the code first and then checks for the condition to see if the code should be executed again.
module tb;
bit clk;
always #10 clk = ~clk;
initial begin
bit [3:0] counter;
$display ("Counter = %0d", counter); // Counter = 0
do begin
@ (posedge clk);
counter ++;
$display ("Counter = %0d", counter); // Counter increments
end while (counter < 5);
$display ("Counter = %0d", counter); // Counter = 14
$finish;
end
endmodule
Simulation Log
ncsim> run
Counter = 0
Counter = 1
Counter = 2
Counter = 3
Counter = 4
Counter = 5
Counter = 5
Simulation complete via $finish(1) at time 90 NS + 0
foreach
This is best suited to loop through array variables, because you don’t have to find the array size, set up a variable to start from 0 until array_size-1 and increment it on every iteration.
module tb_top;
bit [7:0] array [8]; // Create a fixed size array
initial begin
// Assign a value to each location in the array
foreach (array [index]) begin
array[index] = index;
end
// Iterate through each location and print the value of current location
foreach (array [index]) begin
$display ("array[%0d] = 0x%0d", index, array[index]);
end
end
endmodule
Simulation Log
ncsim> run
array[0] = 0x0
array[1] = 0x1
array[2] = 0x2
array[3] = 0x3
array[4] = 0x4
array[5] = 0x5
array[6] = 0x6
array[7] = 0x7
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog while and do-while loop
Both while and do while are looping constructs that execute the given set of statements as long as the given condition is true.
A while loop first checks if the condition is true and then executes the statements if it is true. If the condition turns out to be false, the loop ends right there.
A do while loop first executes the statements once, and then checks for the condition to be true. If the condition is true, the set of statements are executed until the condition turns out to be false. If the condition is false, the loop ends right there.
So the difference between the two is that a do while loop executes the set of statements atleast once.
Syntax
while (<condition>) begin
// Multiple statements
end
do begin
// Multiple statements
end while (<condition>);
Example #1 - while loop
module tb;
initial begin
int cnt = 0;
while (cnt < 5) begin
$display("cnt = %0d", cnt);
cnt++;
end
end
endmodule
Simulation Log
ncsim> run
cnt = 0
cnt = 1
cnt = 2
cnt = 3
cnt = 4
ncsim: *W,RNQUIE: Simulation is complete.
Example #2
module tb;
initial begin
int cnt;
while (cnt != 0) begin
$display ("cnt = %0d", cnt);
cnt++;
end
end
endmodule
Simulation Log
ncsim> run
ncsim: *W,RNQUIE: Simulation is complete.
Example #3 - do while loop
module tb;
initial begin
int cnt = 0;
do begin
$display("cnt = %0d", cnt);
cnt++;
end while (cnt < 5);
end
endmodule
Simulation Log
ncsim> run
cnt = 0
cnt = 1
cnt = 2
cnt = 3
cnt = 4
ncsim: *W,RNQUIE: Simulation is complete.
Example #3 - do while loop
module tb;
initial begin
int cnt = 0;
do begin
$display("cnt = %0d", cnt);
cnt++;
end while (cnt == 0);
end
endmodule
Simulation Log
ncsim> run
cnt = 0
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog foreach loop
SystemVerilog arrays are data structures that allow storage of many values in a single variable. A foreach loop is only used to iterate over such arrays and is the easiest and simplest way to do so.
Syntax
The foreach loop iterates through each index starting from 0. If there are multiple statements within the foreach loop, they have to be enclosed with begin and end keywords like all other procedural blocks.
foreach(<variable>[<iterator>])
// Single statement
foreach(<variable>[<iterator>]) begin
// Multiple statements
end
Example #1: Single dimensional Arrays
module tb;
int array[5] = '{1, 2, 3, 4, 5};
int sum;
initial begin
// Here, "i" is the iterator and can be named as anything you like
// Iterate through each element from index 0 to end using a foreach
// loop.
foreach (array[i])
$display ("array[%0d] = %0d", i, array[i]);
// Multiple statements in foreach loop requires begin end
// Here, we are calculating the sum of all numbers in the array
// And because there are 2 statements within foreach there should
// be a begin-end
foreach (array[l_index]) begin
sum += array[l_index];
$display ("array[%0d] = %0d, sum = %0d", l_index, array[l_index], sum);
end
end
endmodule
Simulation Log
Note that foreach is just a shorter version to the following for loop:
for (int i = 0; i < $size(array); i++) begin
// Statements inside the for loop
end
Example #2: Multidimensional Arrays
module tb;
int md_array [5][2] = '{'{1,2}, '{3,4}, '{5,6}, '{7,8}, '{9,10}};
initial begin
// First iterate through the first dimension using "i"
foreach (md_array[i])
// For each element in first dimension "i", iterate through the
// second dimension using "j"
foreach (md_array[i][j])
$display("md_array[%0d][%0d] = %0d", i, j, md_array[i][j]);
end
endmodule
SystemVerilog for loop
A for loop in SystemVerilog repeats a given set of statements multiple times until the given expression is not satisfied. Like all other procedural blocks, the for loop requires multiple statements within it to be enclosed by begin and end keywords.
Syntax
For loop controls execution of its statements using a three step approach:
- Initialize the variables that affect how many times the loop is run
- Before executing the loop, check to see if the condition is true
- The modifier is executed at the end of each iteration, and jumps to step 2.
for ( [initialization]; <condition>; [modifier])
// Single statement
for ( [initialization]; <condition>; [modifier]) begin
// Multiple statements
end
Example #1 - Array Iteration
In this example, we will iterate through a string array and print out its contents. The array array is initialized with 5 different names of fruits.
The for loop initialization declares a local variable called i that represents index of any element in the array. The conditional expression checks that i is less than size of the array. The modifier increments the value of i so that every iteration of the for loop operates on a different index.
module tb;
string array [5] = '{"apple", "orange", "pear", "blueberry", "lemon"};
initial begin
for (int i = 0; i < $size(array); i++)
$display ("array[%0d] = %s", i, array[i]);
end
endmodule
Simulation Log
ncsim> run
array[0] = apple
array[1] = orange
array[2] = pear
array[3] = blueberry
array[4] = lemon
ncsim: *W,RNQUIE: Simulation is complete.
Example #2 - Multiple Initializations
There can be multiple initializations done in the first part of a for loop. In the code shown below, variables i and j are both initialized as soon as the for loop is entered. To keep the example interesting, the index of each string pointed to by j is replaced by 0.
module tb;
string array [5] = '{"apple", "orange", "pear", "blueberry", "lemon"};
initial begin
for (int i = 0, j = 2; i < $size(array); i++) begin
array[i][j] = "0";
$display ("array[%0d] = %s, %0dth index replaced by 0", i, array[i], j);
end
end
endmodule
Simulation Log
ncsim> run
array[0] = ap0le, 2th index replaced by 0
array[1] = or0nge, 2th index replaced by 0
array[2] = pe0r, 2th index replaced by 0
array[3] = bl0eberry, 2th index replaced by 0
array[4] = le0on, 2th index replaced by 0
ncsim: *W,RNQUIE: Simulation is complete.
Example #3 - Adding multiple modifiers
In the code shown below, j is decremented after each iteration of the for loop along with incrementing i.
module tb;
string array [5] = '{"apple", "orange", "pear", "blueberry", "lemon"};
initial begin
for (int i = 0, j = array[i].len() - 1; i < $size(array); i++, j--) begin
array[i][j] = "0";
$display ("array[%0d] = %s, %0dth index replaced by 0", i, array[i], j);
end
end
endmodule
Simulation Log
ncsim> run
array[0] = appl0, 4th index replaced by 0
array[1] = ora0ge, 3th index replaced by 0
array[2] = pe0r, 2th index replaced by 0
array[3] = b0ueberry, 1th index replaced by 0
array[4] = 0emon, 0th index replaced by 0
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog forever loop
A forever loop runs forever, or for infinite time.
Syntax
forever
// Single statement
forever begin
// Multiple statements
end
A forever loop is similar to the code shown below in Verilog. Both run for infinite simulation time, and is important to have a delay element inside them.
An always or forever block without a delay element will hang in simulation !
always
// Single statement
always begin
// Multiple statements
end
In SystemVerilog, an always block cannot be placed inside classes and other SystemVerilog procedural blocks. Instead we can use a forever loop to achieve the same effect.
The pseudo code shown below mimics the functionality of a monitor in testbench that is once started and allowed to run as long as there is activity on the bus it monitors.
class Monitor;
virtual task run();
forever begin
@(posedge vif.clk);
if (vif.write & vif.sel)
// Capture write data
if (!vif.write & vif.sel)
// Capture read data
end
endtask
endclass
module tb;
Monitor mon;
// Start the monitor task and allow it to continue as
// long as there is activity on the bus
initial begin
fork
mon.run();
join_none
end
endmodule
SystemVerilog repeat
A given set of statements can be executed N number of times with a repeat construct.
Syntax
repeat (<number>)
// Single Statement
repeat (<number>) begin
// Multiple Statements
end
Example #1
module tb;
initial begin
repeat (5) begin
$display ("Repeat this statement");
end
end
endmodule
Simulation Log
ncsim> run
Repeat this statement
Repeat this statement
Repeat this statement
Repeat this statement
Repeat this statement
ncsim: *W,RNQUIE: Simulation is complete.
A repeat loop can also be implemented using a for loop but is more verbose. If the variable i is not required to be referenced inside the loop, a repeat loop would be more suitable.
for (int i = 0; i < number; i++) begin
// Code
end
In the code shown below, we have a repeat loop to wait for a given number of clock cycles.
module tb;
bit clk;
always #10 clk = ~clk;
initial begin
bit [2:0] num = $random;
$display ("[%0t] Repeat loop is going to start with num = %0d", $time, num);
repeat (num) @(posedge clk);
$display ("[%0t] Repeat loop has finished", $time);
$finish;
end
endmodule
In this example, the clock period is 20 ns, and the first posedge of clock happens at 10 ns. Next 3 posedge of clock happens at 30ns, 50ns and 70ns after which the initial block ends. So, this repeat loop successfully waits until 4 posedge of clocks are over.
Simulation Log
ncsim> run
[0] Repeat loop is going to start with num = 4
[70] Repeat loop has finished
Simulation complete via $finish(1) at time 70 NS + 0
SystemVerilog ‘break’ and ‘continue’
break
module tb;
initial begin
// This for loop increments i from 0 to 9 and exit
for (int i = 0 ; i < 10; i++) begin
$display ("Iteration [%0d]", i);
// Let's create a condition such that the
// for loop exits when i becomes 7
if (i == 7)
break;
end
end
endmodule
Simulation Log
ncsim> run
Iteration [0]
Iteration [1]
Iteration [2]
Iteration [3]
Iteration [4]
Iteration [5]
Iteration [6]
Iteration [7]
ncsim: *W,RNQUIE: Simulation is complete.
continue
module tb;
initial begin
// This for loop increments i from 0 to 9 and exit
for (int i = 0 ; i < 10; i++) begin
// Let's create a condition such that the
// for loop
if (i == 7)
continue;
$display ("Iteration [%0d]", i);
end
end
endmodule
Simulation Log
ncsim> run
Iteration [0]
Iteration [1]
Iteration [2]
Iteration [3]
Iteration [4]
Iteration [5]
Iteration [6]
Iteration [8]
Iteration [9]
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog ‘unique’ and ‘priority’ if-else
The conditional if else statement is used to make a decision about whether a statement is executed.
SystemVerilog introduced the following if else constructs for violation checks.
- unique-if
- unique0-if
- priority-if
unique-if, unique0-if
unique-if evaluates conditions in any order and does the following :
- report an error when none of the
ifconditions match unless there is an explicitelse. - report an erorr when there is more than 1 match found in the
if elseconditions
Unlike unique-if, unique0-if does not report a violation if none of the conditions match
No else block for unique-if
module tb;
int x = 4;
initial begin
// This if else if construct is declared to be "unique"
// Error is not reported here because there is a "else"
// clause in the end which will be triggered when none of
// the conditions match
unique if (x == 3)
$display ("x is %0d", x);
else if (x == 5)
$display ("x is %0d", x);
else
$display ("x is neither 3 nor 5");
// When none of the conditions become true and there
// is no "else" clause, then an error is reported
unique if (x == 3)
$display ("x is %0d", x);
else if (x == 5)
$display ("x is %0d", x);
end
endmodule
Simulation Log
ncsim> run
x is neither 3 nor 5
ncsim: *W,NOCOND: Unique if violation: Every if clause was false.
File: ./testbench.sv, line = 18, pos = 13
Scope: tb
Time: 0 FS + 1
ncsim: *W,RNQUIE: Simulation is complete.
Multiple matches in unique-if
module tb;
int x = 4;
initial begin
// This if else if construct is declared to be "unique"
// When multiple if blocks match, then error is reported
unique if (x == 4)
$display ("1. x is %0d", x);
else if (x == 4)
$display ("2. x is %0d", x);
else
$display ("x is not 4");
end
endmodule
Simulation Log
ncsim> run
1. x is 4
ncsim: *W,MCONDE: Unique if violation: Multiple true if clauses at {line=8:pos=15 and line=10:pos=13}.
File: ./testbench.sv, line = 8, pos = 15
Scope: tb
Time: 0 FS + 1
ncsim: *W,RNQUIE: Simulation is complete.
priority-if
priority-if evaluates all conditions in sequential order and a violation is reported when:
- None of the conditions are true or if there’s no
elseclause to the finalifconstruct
No else clause in priority-if
module tb;
int x = 4;
initial begin
// This if else if construct is declared to be "unique"
// Error is not reported here because there is a "else"
// clause in the end which will be triggered when none of
// the conditions match
priority if (x == 3)
$display ("x is %0d", x);
else if (x == 5)
$display ("x is %0d", x);
else
$display ("x is neither 3 nor 5");
// When none of the conditions become true and there
// is no "else" clause, then an error is reported
priority if (x == 3)
$display ("x is %0d", x);
else if (x == 5)
$display ("x is %0d", x);
end
endmodule
Simulation Log
ncsim> run
x is neither 3 nor 5
ncsim: *W,NOCOND: Priority if violation: Every if clause was false.
File: ./testbench.sv, line = 18, pos = 15
Scope: tb
Time: 0 FS + 1
ncsim: *W,RNQUIE: Simulation is complete.
Exit after first match in priority-if
module tb;
int x = 4;
initial begin
// Exits if-else block once the first match is found
priority if (x == 4)
$display ("x is %0d", x);
else if (x != 5)
$display ("x is %0d", x);
end
endmodule
Simulation Log
ncsim> run
x is 4
ncsim: *W,RNQUIE: Simulation is complete.
A SystemVerilog case statement checks whether an expression matches one of a number of expressions and branches appropriately. The behavior is the same as in Verilog.
All case statements can be qualified by unique or unique0 keywords to perform violation checks like we saw in if-else-if construct.
unique and unique0 ensure that there is no overlapping case items and hence can be evaluated in parallel. If there are overlapping case items, then a violation is reported.
- If more than one case item is found to match the given expression, then a violation is reported and the first matching expression is executed
- If no case item is found to match the given expression, then a violation is reported only for
unqiue
unique : No items match for given expression
module tb;
bit [1:0] abc;
initial begin
abc = 1;
// None of the case items match the value in "abc"
// A violation is reported here
unique case (abc)
0 : $display ("Found to be 0");
2 : $display ("Found to be 2");
endcase
end
endmodule
Simulation Log
ncsim> run
ncsim: *W,NOCOND: Unique case violation: Every case item expression was false.
File: ./testbench.sv, line = 9, pos = 14
Scope: tb
Time: 0 FS + 1
ncsim: *W,RNQUIE: Simulation is complete.
unique : More than one case item matches
module tb;
bit [1:0] abc;
initial begin
abc = 0;
// Multiple case items match the value in "abc"
// A violation is reported here
unique case (abc)
0 : $display ("Found to be 0");
0 : $display ("Again found to be 0");
2 : $display ("Found to be 2");
endcase
end
endmodule
Simulation Log
ncsim> run
Found to be 0
ncsim: *W,MCONDE: Unique case violation: Multiple matching case item expressions at {line=10:pos=6 and line=11:pos=6}.
File: ./testbench.sv, line = 9, pos = 14
Scope: tb
Time: 0 FS + 1
ncsim: *W,RNQUIE: Simulation is complete.
priority case
module tb;
bit [1:0] abc;
initial begin
abc = 0;
// First match is executed
priority case (abc)
0 : $display ("Found to be 0");
0 : $display ("Again found to be 0");
2 : $display ("Found to be 2");
endcase
end
endmodule
Simulation Log
ncsim> run
Found to be 0
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Event
An event is a static object handle to synchronize between two or more concurrently active processes. One process will trigger the event, and another process waits for the event.
-
Can be assigned or compared to other event variables
-
- Can be assigned to
null - When assigned to another event, both variables point to same synchronization object
- Can be assigned to
-
Can be passed to queues, functions and tasks
event over; // a new event is created called over
event over_again = over; // over_again becomes an alias to over
event empty = null; // event variable with no synchronization object
How to trigger and wait for an event?
- Named events can be triggered using
->or->>operator - Processes can wait for an event using
@operator or.triggered
Example
module tb;
// Create an event variable that processes can use to trigger and wait
event event_a;
// Thread1: Triggers the event using "->" operator
initial begin
#20 ->event_a;
$display ("[%0t] Thread1: triggered event_a", $time);
end
// Thread2: Waits for the event using "@" operator
initial begin
$display ("[%0t] Thread2: waiting for trigger ", $time);
@(event_a);
$display ("[%0t] Thread2: received event_a trigger ", $time);
end
// Thread3: Waits for the event using ".triggered"
initial begin
$display ("[%0t] Thread3: waiting for trigger ", $time);
wait(event_a.triggered);
$display ("[%0t] Thread3: received event_a trigger", $time);
end
endmodule
Simulation Log
ncsim> run
[0] Thread2: waiting for trigger
[0] Thread3: waiting for trigger
[20] Thread1: triggered event_a
[20] Thread2: received event_a trigger
[20] Thread3: received event_a trigger
ncsim: *W,RNQUIE: Simulation is complete.
What is the difference between @ and .triggered ?
An event’s triggered state persists throughout the time step, until simulation advances. Hence if both wait for the event and trigger of the event happens at the same time there will be a race condition and the triggered property helps to avoid that.
A process that waits on the triggered state always unblocks, regardless of the order of wait and trigger.
module tb;
// Create an event variable that processes can use to trigger and wait
event event_a;
// Thread1: Triggers the event using "->" operator at 20ns
initial begin
#20 ->event_a;
$display ("[%0t] Thread1: triggered event_a", $time);
end
// Thread2: Starts waiting for the event using "@" operator at 20ns
initial begin
$display ("[%0t] Thread2: waiting for trigger ", $time);
#20 @(event_a);
$display ("[%0t] Thread2: received event_a trigger ", $time);
end
// Thread3: Starts waiting for the event using ".triggered" at 20ns
initial begin
$display ("[%0t] Thread3: waiting for trigger ", $time);
#20 wait(event_a.triggered);
$display ("[%0t] Thread3: received event_a trigger", $time);
end
endmodule
Note that Thread2 never received a trigger, because of the race condition between @ and -> operations.
Simulation Log
ncsim> run
[0] Thread2: waiting for trigger
[0] Thread3: waiting for trigger
[20] Thread1: triggered event_a
[20] Thread3: received event_a trigger
ncsim: *W,RNQUIE: Simulation is complete.
wait_order
Waits for events to be triggered in the given order, and issues an error if any event executes out of order.
module tb;
// Declare three events that can be triggered separately
event a, b, c;
// This block triggers each event one by one
initial begin
#10 -> a;
#10 -> b;
#10 -> c;
end
// This block waits until each event is triggered in the given order
initial begin
wait_order (a,b,c)
$display ("Events were executed in the correct order");
else
$display ("Events were NOT executed in the correct order !");
end
endmodule
Simulation Log
Compiler version J-2014.12-SP1-1; Runtime version J-2014.12-SP1-1;
Events were executed in the correct order
V C S S i m u l a t i o n R e p o r t
Merging Events
When one event variable is assigned to another, all processes waiting for the first event to trigger will wait until the second variable is triggered.
module tb;
// Create event variables
event event_a, event_b;
initial begin
fork
// Thread1: waits for event_a to be triggered
begin
wait(event_a.triggered);
$display ("[%0t] Thread1: Wait for event_a is over", $time);
end
// Thread2: waits for event_b to be triggered
begin
wait(event_b.triggered);
$display ("[%0t] Thread2: Wait for event_b is over", $time);
end
// Thread3: triggers event_a at 20ns
#20 ->event_a;
// Thread4: triggers event_b at 30ns
#30 ->event_b;
// Thread5: Assigns event_b to event_a at 10ns
begin
// Comment code below and try again to see Thread2 finish later
#10 event_b = event_a;
end
join
end
endmodule
Simulation Log
ncsim> run
[20] Thread1: Wait for event_a is over
[20] Thread2: Wait for event_b is over
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Functions
SystemVerilog functions have the same characteristics as the ones in Verilog.
Functions
The primary purpose of a function is to return a value that can be used in an expression and cannot consume simulation time.
- A function cannot have time controlled statements like
@,#,fork join, orwait - A function cannot start a task since tasks are allowed to consume simulation time
ANSI-C style declaration
module tb;
// There are two ways to call the function:
initial begin
// 1. Call function and assign value to a variable, and then use variable
int s = sum(3, 4);
$display ("sum(3,4) = %0d", s);
// 2. Call function and directly use value returned
$display ("sum(5,9) = %0d", sum(5,9));
$display ("mul(3,1) = %0d", mul(3,1));
end
// This function returns value of type "byte", and accepts two
// arguments "x" and "y". A return variable of the same name as
// function is implicitly declared and hence "sum" can be directly
// assigned without having to declare a separate return variable
function byte sum (int x, int y);
sum = x + y;
endfunction
// Instead of assigning to "mul", the computed value can be returned
// using "return" keyword
function byte mul (int x, y);
return x * y;
endfunction
endmodule
Simulation Log
ncsim> run
sum(3,4) = 7
sum(5,9) = 14
mul(3,1) = 3
ncsim: *W,RNQUIE: Simulation is complete.
Using declarations and directions
Although ANSI-C style declaration was later introduced in Verilog, the old style declaration of port directions are still valid. SystemVerilog functions can have arguments declared as input and output ports as shown in the example below.
module tb;
initial begin
int res, s;
s = sum(5,9);
$display ("s = %0d", sum(5,9));
$display ("sum(5,9) = %0d", sum(5,9));
$display ("mul(3,1) = %0d", mul(3,1,res));
$display ("res = %0d", res);
end
// Function has an 8-bit return value and accepts two inputs
// and provides the result through its output port and return val
function bit [7:0] sum;
input int x, y;
output sum;
sum = x + y;
endfunction
// Same as above but ports are given inline
function byte mul (input int x, y, output int res);
res = x*y + 1;
return x * y;
endfunction
endmodule
Simulation Log
ncsim> run
s = 14
sum(5,9) = 14
mul(3,1) = 3
res = 4
ncsim: *W,RNQUIE: Simulation is complete.
How to pass arguments by value ?
Pass by value is the default mechanism to pass arguments to subroutines. Each argument is copied into the subroutine area and any changes made to this local variable in the subroutine area is not visible outside the subroutine.
module tb;
initial begin
int a, res;
// 1. Lets pick a random value from 1 to 10 and assign to "a"
a = $urandom_range(1, 10);
$display ("Before calling fn: a=%0d res=%0d", a, res);
// Function is called with "pass by value" which is the default mode
res = fn(a);
// Even if value of a is changed inside the function, it is not reflected here
$display ("After calling fn: a=%0d res=%0d", a, res);
end
// This function accepts arguments in "pass by value" mode
// and hence copies whatever arguments it gets into this local
// variable called "a".
function int fn(int a);
// Any change to this local variable is not
// reflected in the main variable declared above within the
// initial block
a = a + 5;
// Return some computed value
return a * 10;
endfunction
endmodule
Note from the log shown below that the value of a within the initial block did not get changed even though the local variable defined inside the function was assigned a different value.
Simulation Log
ncsim> run
Before calling fn: a=2 res=0
After calling fn: a=2 res=70
ncsim: *W,RNQUIE: Simulation is complete.
How to pass arguments by reference ?
Arguments that are passed by reference are not copied into the subroutine area, instead a reference to the original argument is passed to the subroutine. The argument declaration is preceded by the ref keyword. Any changes made to the variable inside the subroutine will be reflected in the original variable outside the subroutine.
// Use "ref" to make this function accept arguments by reference
// Also make the function automatic
function automatic int fn(ref int a);
// Any change to this local variable will be
// reflected in the main variable declared within the
// initial block
a = a + 5;
// Return some computed value
return a * 10;
endfunction
Its illegal to use argument passing by reference for subroutines with a lifetime of static
Simulation Log
ncsim> run
Before calling fn: a=2 res=0
After calling fn: a=7 res=70
ncsim: *W,RNQUIE: Simulation is complete.
Verilog Task
A function is meant to do some processing on the input and return a single value, whereas a task is more general and can calculate multiple result values and return them using output and inout type arguments. Tasks can contain simulation time consuming elements such as @, posedge and others.
Syntax
A task need not have a set of arguments in the port list, in which case it can be kept empty.
// Style 1
task [name];
input [port_list];
inout [port_list];
output [port_list];
begin
[statements]
end
endtask
// Style 2
task [name] (input [port_list], inout [port_list], output [port_list]);
begin
[statements]
end
endtask
// Empty port list
task [name] ();
begin
[statements]
end
endtask
Static Task
If a task is static, then all its member variables will be shared across different invocations of the same task that has been launched to run concurrently
task sum (input [7:0] a, b, output [7:0] c);
begin
c = a + b;
end
endtask
// or
task sum;
input [7:0] a, b;
output [7:0] c;
begin
c = a + b;
end
endtask
initial begin
reg [7:0] x, y , z;
sum (x, y, z);
end
The task-enabling arguments (x, y, z) correspond to the arguments (a, b, c) defined by the task. Since a and b are inputs, values of x and y will be placed in a and b respectively. Because c is declared as an output and connected with z during invocation, the sum will automatically be passed to the variable z from c.
Automatic Task
The keyword automatic will make the task reentrant, otherwise it will be static by default. All items inside automatic tasks are allocated dynamically for each invocation and not shared between invocations of the same task running concurrently. Note that automatic task items cannot be accessed by hierarchical references.
For illustration, consider the static task display which is called from different initial blocks that run concurrently. In this case, the integer variable declared within the task is shared among all invocations of the task and hence thev displayed value should increment for each invocation.
module tb;
initial display();
initial display();
initial display();
initial display();
// This is a static task
task display();
integer i = 0;
i = i + 1;
$display("i=%0d", i);
endtask
endmodule
Simulation Log
xcelium> run
i=1
i=2
i=3
i=4
xmsim: *W,RNQUIE: Simulation is complete.
If the task is made automatic, each invocation of the task is allocated a different space in simulation memory and behaves differently.
module tb;
initial display();
initial display();
initial display();
initial display();
// Note that the task is now automatic
task automatic display();
integer i = 0;
i = i + 1;
$display("i=%0d", i);
endtask
endmodule
Simulation Log
xcelium> run
i=1
i=1
i=1
i=1
xmsim: *W,RNQUIE: Simulation is complete.
Global tasks
Tasks that are declared outside all modules are called global tasks as they have a global scope and can be called within any module.
// This task is outside all modules
task display();
$display("Hello World !");
endtask
module des;
initial begin
display();
end
endmodule
Simulation Log
xcelium> run
Hello World !
xmsim: *W,RNQUIE: Simulation is complete.
If the task was declared within the module des, it would have to be called in reference to the module instance name.
module tb;
des u0();
initial begin
u0.display(); // Task is not visible in the module 'tb'
end
endmodule
module des;
initial begin
display(); // Task definition is local to the module
end
task display();
$display("Hello World");
endtask
endmodule
Simulation Log
xcelium> run
Hello World
Hello World
xmsim: *W,RNQUIE: Simulation is complete.
Difference between function and task
Although Verilog functions and tasks serve similar purposes, there are a few notable differences between them.
| Function | Task |
|---|---|
| Cannot have time-controlling statements/delay, and hence executes in the same simulation time unit | Can contain time-controlling statements/delay and may only complete at some other time |
| Cannot enable a task, because of the above rule | Can enable other tasks and functions |
| Should have atleast one input argument and cannot have output or inout arguments | Can have zero or more arguments of any type |
| Can return only a single value | Cannot return a value, but can achieve the same effect using output arguments |
When a function attempts to call a task or contain a time consuming statement, the compiler reports an error.
module tb;
reg signal;
initial wait_for_1(signal);
function wait_for_1(reg signal);
#10;
endfunction
endmodule
Simulation Log
#10;
|
xmvlog: *E,BADFCN (testbench.sv,7|4): illegal time/event control statement within a function or final block or analog initial block [10.3.4(IEEE)].
Disable Task
Tasks can be disabled using the disable keyword.
module tb;
initial display();
initial begin
// After 50 time units, disable a particular named
// block T_DISPLAY inside the task called 'display'
#50 disable display.T_DISPLAY;
end
task display();
begin : T_DISPLAY
$display("[%0t] T_Task started", $time);
#100;
$display("[%0t] T_Task ended", $time);
end
begin : S_DISPLAY
#10;
$display("[%0t] S_Task started", $time);
#20;
$display("[%0t] S_Task ended", $time);
end
endtask
endmodule
When display task was launched by the first initial block, T_DISPLAY started and got disabled when time reached 50 units. Immediately the next block S_DISPLAY started and ran to completion by 80 units.
Simulation Log
xcelium> run
[0] T_Task started
[60] S_Task started
[80] S_Task ended
xmsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Testbench Example 2
This is another example of a SystemVerilog testbench using OOP concepts like inheritance, polymorphism to build a functional testbench for a simple design.
Design
module switch
# (parameter ADDR_WIDTH = 8,
parameter DATA_WIDTH = 16,
parameter ADDR_DIV = 8'h3F
)
( input clk,
input rstn,
input vld,
input [ADDR_WIDTH-1:0] addr,
input [DATA_WIDTH-1:0] data,
output reg [ADDR_WIDTH-1:0] addr_a,
output reg [DATA_WIDTH-1:0] data_a,
output reg [ADDR_WIDTH-1:0] addr_b,
output reg [DATA_WIDTH-1:0] data_b
);
always @ (posedge clk) begin
if (!rstn) begin
addr_a <= 0;
data_a <= 0;
addr_b <= 0;
data_b <= 0; end else begin if (vld) begin if (addr >= 0 & addr <= ADDR_DIV) begin
addr_a <= addr;
data_a <= data;
addr_b <= 0;
data_b <= 0;
end else begin
addr_a <= 0;
data_a <= 0;
addr_b <= addr;
data_b <= data;
end
end
end
end
endmodule
Transaction Object
// This is the base transaction object that will be used
// in the environment to initiate new transactions and
// capture transactions at DUT interface
class switch_item;
rand bit [7:0] addr;
rand bit [15:0] data;
bit [7:0] addr_a;
bit [15:0] data_a;
bit [7:0] addr_b;
bit [15:0] data_b;
// This function allows us to print contents of the data
// packet so that it is easier to track in a logfile
function void print (string tag="");
$display ("T=%0t %s addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h addr_b=0x%0h data_b=0x%0h",
$time, tag, addr, data, addr_a, data_a, addr_b, data_b);
endfunction
endclass
Generator
// The generator class is used to generate a random
// number of transactions with random addresses and data
// that can be driven to the design
class generator;
mailbox drv_mbx;
event drv_done;
int num = 20;
task run();
for (int i = 0; i < num; i++) begin
switch_item item = new;
item.randomize();
$display ("T=%0t [Generator] Loop:%0d/%0d create next item", $time, i+1, num);
drv_mbx.put(item);
@(drv_done);
end
$display ("T=%0t [Generator] Done generation of %0d items", $time, num);
endtask
endclass
Driver
// The driver is responsible for driving transactions to the DUT
// All it does is to get a transaction from the mailbox if it is
// available and drive it out into the DUT interface.
class driver;
virtual switch_if vif;
event drv_done;
mailbox drv_mbx;
task run();
$display ("T=%0t [Driver] starting ...", $time);
@ (posedge vif.clk);
// Try to get a new transaction every time and then assign
// packet contents to the interface. But do this only if the
// design is ready to accept new transactions
forever begin
switch_item item;
$display ("T=%0t [Driver] waiting for item ...", $time);
drv_mbx.get(item);
item.print("Driver");
vif.vld <= 1;
vif.addr <= item.addr;
vif.data <= item.data;
// When transfer is over, raise the done event
@ (posedge vif.clk);
vif.vld <= 0; ->drv_done;
end
endtask
endclass
Monitor
// The monitor has a virtual interface handle with which
// it can monitor the events happening on the interface.
// It sees new transactions and then captures information
// into a packet and sends it to the scoreboard
// using another mailbox.
class monitor;
virtual switch_if vif;
mailbox scb_mbx;
semaphore sema4;
function new ();
sema4 = new(1);
endfunction
task run();
$display ("T=%0t [Monitor] starting ...", $time);
// To get a pipeline effect of transfers, fork two threads
// where each thread uses a semaphore for the address phase
fork
sample_port("Thread0");
sample_port("Thread1");
join
endtask
task sample_port(string tag="");
// This task monitors the interface for a complete
// transaction and pushes into the mailbox when the
// transaction is complete
forever begin
@(posedge vif.clk);
if (vif.rstn & vif.vld) begin
switch_item item = new;
sema4.get();
item.addr = vif.addr;
item.data = vif.data;
$display("T=%0t [Monitor] %s First part over",
$time, tag);
@(posedge vif.clk);
sema4.put();
item.addr_a = vif.addr_a;
item.data_a = vif.data_a;
item.addr_b = vif.addr_b;
item.data_b = vif.data_b;
$display("T=%0t [Monitor] %s Second part over",
$time, tag);
scb_mbx.put(item);
item.print({"Monitor_", tag});
end
end
endtask
endclass
Scoreboard
// The scoreboard is responsible to check data integrity. Since
// the design routes packets based on an address range, the
// scoreboard checks that the packet's address is within valid
// range.
class scoreboard;
mailbox scb_mbx;
task run();
forever begin
switch_item item;
scb_mbx.get(item);
if (item.addr inside {[0:'h3f]}) begin
if (item.addr_a != item.addr | item.data_a != item.data)
$display ("T=%0t [Scoreboard] ERROR! Mismatch addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h", $time, item.addr, item.data, item.addr_a, item.data_a);
else
$display ("T=%0t [Scoreboard] PASS! Mismatch addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h", $time, item.addr, item.data, item.addr_a, item.data_a);
end else begin
if (item.addr_b != item.addr | item.data_b != item.data)
$display ("T=%0t [Scoreboard] ERROR! Mismatch addr=0x%0h data=0x%0h addr_b=0x%0h data_b=0x%0h", $time, item.addr, item.data, item.addr_b, item.data_b);
else
$display ("T=%0t [Scoreboard] PASS! Mismatch addr=0x%0h data=0x%0h addr_b=0x%0h data_b=0x%0h", $time, item.addr, item.data, item.addr_b, item.data_b);
end
end
endtask
endclass
Environment
// The environment is a container object simply to hold
// all verification components together. This environment can
// then be reused later and all components in it would be
// automatically connected and available for use
class env;
driver d0; // Driver handle
monitor m0; // Monitor handle
generator g0; // Generator Handle
scoreboard s0; // Scoreboard handle
mailbox drv_mbx; // Connect GEN -> DRV
mailbox scb_mbx; // Connect MON -> SCB
event drv_done; // Indicates when driver is done
virtual switch_if vif; // Virtual interface handle
function new();
d0 = new;
m0 = new;
g0 = new;
s0 = new;
drv_mbx = new();
scb_mbx = new();
d0.drv_mbx = drv_mbx;
g0.drv_mbx = drv_mbx;
m0.scb_mbx = scb_mbx;
s0.scb_mbx = scb_mbx;
d0.drv_done = drv_done;
g0.drv_done = drv_done;
endfunction
virtual task run();
d0.vif = vif;
m0.vif = vif;
fork
d0.run();
m0.run();
g0.run();
s0.run();
join_any
endtask
endclass
Test
// Test class instantiates the environment and starts it.
class test;
env e0;
function new();
e0 = new;
endfunction
task run();
e0.run();
endtask
endclass
Interface
// Design interface used to monitor activity and capture/drive
// transactions
interface switch_if (input bit clk);
logic rstn;
logic vld;
logic [7:0] addr;
logic [15:0] data;
logic [7:0] addr_a;
logic [15:0] data_a;
logic [7:0] addr_b;
logic [15:0] data_b;
endinterface
Testbench Top
// Top level testbench module to instantiate design, interface
// start clocks and run the test
module tb;
reg clk;
always #10 clk =~ clk;
switch_if _if (clk);
switch u0 ( .clk(clk),
.rstn(_if.rstn),
.addr(_if.addr),
.data(_if.data),
.vld (_if.vld),
.addr_a(_if.addr_a),
.data_a(_if.data_a),
.addr_b(_if.addr_b),
.data_b(_if.data_b));
test t0;
initial begin
{clk, _if.rstn} <= 0;
// Apply reset and start stimulus
#20 _if.rstn <= 1;
t0 = new;
t0.e0.vif = _if;
t0.run();
// Because multiple components and clock are running
// in the background, we need to call $finish explicitly
#50 $finish;
end
// System tasks to dump VCD waveform file
initial begin
$dumpvars;
$dumpfile ("dump.vcd");
end
endmodule
SystemVerilog Threads
What are SystemVerilog threads or processes ?
A thread or process is any piece of code that gets executed as a separate entity. In verilog, each of the initial and always blocks are spawned off as separate threads that start to run in parallel from zero time. A fork join block also creates different threads that run in parallel.
What are different fork - join styles ?
We have three different styles of fork join in SystemVerilog.
| fork join | Finishes when all child threads are over |
|---|---|
| fork join_any | Finishes when any child thread gets over |
| fork join_none | Finishes soon after child threads are spawned |
Where is this used in the testbench ?
Components in a verification environment may require the ability to run multiple tasks concurrently. For example, one process may wait for something to happen while another process continues to perform some other task. They are all spawned off as separate threads via fork ... join. For example, a checker can spawn different tasks in parallel to capture and validate data originating from different parts of the testbench.
What is the limitation of a Verilog fork join ?
Code after a fork ... join is executed only when all the threads spawned off inside the fork-join has finished. So, the checker will have to wait until all the threads spawned in the fork-join to finish, before it can proceed.
fork join Example
SystemVerilog fork join waits until all forked processes are complete.
module tb_top;
initial begin
#1 $display ("[%0t ns] Start fork ...", $time);
// Main Process: Fork these processes in parallel and wait untill all
// of them finish
fork
// Thread1 : Print this statement after 5ns from start of fork
#5 $display ("[%0t ns] Thread1: Orange is named after orange", $time);
// Thread2 : Print these two statements after the given delay from start of fork
begin
#2 $display ("[%0t ns] Thread2: Apple keeps the doctor away", $time);
#4 $display ("[%0t ns] Thread2: But not anymore", $time);
end
// Thread3 : Print this statement after 10ns from start of fork
#10 $display ("[%0t ns] Thread3: Banana is a good fruit", $time);
join
// Main Process: Continue with rest of statements once fork-join is over
$display ("[%0t ns] After Fork-Join", $time);
end
endmodule
Simulation Log
fork join_any Example
SystemVerilog fork join_any waits until any one of the forked processes is complete.
module tb_top;
initial begin
#1 $display ("[%0t ns] Start fork ...", $time);
// Main Process: Fork these processes in parallel and wait until
// any one of them finish
fork
// Thread1 : Print this statement after 5ns from start of fork
#5 $display ("[%0t ns] Thread1: Orange is named after orange", $time);
// Thread2 : Print these two statements after the given delay from start of fork
begin
#2 $display ("[%0t ns] Thread2: Apple keeps the doctor away", $time);
#4 $display ("[%0t ns] Thread2: But not anymore", $time);
end
// Thread3 : Print this statement after 10ns from start of fork
#10 $display ("[%0t ns] Thread3: Banana is a good fruit", $time);
join_any
// Main Process: Continue with rest of statements once fork-join is exited
$display ("[%0t ns] After Fork-Join", $time);
end
endmodule
Simulation Log
ncsim> run
[1 ns] Start fork ...
[3 ns] Thread2: Apple keeps the doctor away
[6 ns] Thread1: Orange is named after orange
[6 ns] After Fork-Join
[7 ns] Thread2: But not anymore
[11 ns] Thread3: Banana is a good fruit
ncsim: *W,RNQUIE: Simulation is complete.
fork join_none Example
SystemVerilog fork join_none does not wait and immediately exits the block allowing forked processes to run in background. The main thread resumes execution of statements that follow after the fork join_none block.
module tb_top;
initial begin
#1 $display ("[%0t ns] Start fork ...", $time);
// Main Process: Fork these processes in parallel and exits immediately
fork
// Thread1 : Print this statement after 5ns from start of fork
#5 $display ("[%0t ns] Thread1: Orange is named after orange", $time);
// Thread2 : Print these two statements after the given delay from start of fork
begin
#2 $display ("[%0t ns] Thread2: Apple keeps the doctor away", $time);
#4 $display ("[%0t ns] Thread2: But not anymore", $time);
end
// Thread3 : Print this statement after 10ns from start of fork
#10 $display ("[%0t ns] Thread3: Banana is a good fruit", $time);
join_none
// Main Process: Continue with rest of statements once fork-join is exited
$display ("[%0t ns] After Fork-Join", $time);
end
endmodule
Simulation Log
ncsim> run
[1 ns] Start fork ...
[1 ns] After Fork-Join
[3 ns] Thread2: Apple keeps the doctor away
[6 ns] Thread1: Orange is named after orange
[7 ns] Thread2: But not anymore
[11 ns] Thread3: Banana is a good fruit
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog fork join
SystemVerilog provides support for parallel or concurrent threads through fork join construct. Multiple procedural blocks can be spawned off at the same time using fork and join. There are variations to fork join that allow the main thread to continue executing rest of the statements based on when child threads finish.
Syntax
fork
// Thread 1
// Thread 2
// ...
// Thread 3
join
fork join example
In the example shown below, three threads are forked using fork join. The main thread stays suspended until all the threads spawned by the fork is completed. Any block of code within begin and end are considered as a separate thread, and in this case it is Thread2.
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
// Thread 1
#30 $display ("[%0t] Thread1 finished", $time);
// Thread 2
begin
#5 $display ("[%0t] Thread2 ...", $time);
#10 $display ("[%0t] Thread2 finished", $time);
end
// Thread 3
#20 $display ("[%0t] Thread3 finished", $time);
join
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
endmodule
The main thread forks all three threads at time 0ns. Thread2 is a block of procedural code and finishes only when it executes all the statements inside begin and end. Thread2 takes 15 ns to finish, and because it started at 0ns, it finishes at 15ns and is the first thread to finish. Thread1 takes the most simulation time to finish and does so at 30ns, while Thread3 finishes earlier at 20ns.
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[5] Thread2 ...
[15] Thread2 finished
[20] Thread3 finished
[30] Thread1 finished
[30] Main Thread: Fork join has finished
ncsim: *W,RNQUIE: Simulation is complete.
Nested fork join
fork join can be nested in other fork join also.
Example #1
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
join
print (10, "Thread2");
join
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that this task has to be automatic
task automatic print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[10] Thread2
[20] Thread1_0
[30] Thread1_1
[30] Main Thread: Fork join has finished
ncsim: *W,RNQUIE: Simulation is complete.
Example #2
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
fork // Thread 1
#50 $display ("[%0t] Thread1_0 ...", $time);
#70 $display ("[%0t] Thread1_1 ...", $time);
begin
#10 $display ("[%0t] Thread1_2 ...", $time);
#100 $display ("[%0t] Thread1_2 finished", $time);
end
join
// Thread 2
begin
#5 $display ("[%0t] Thread2 ...", $time);
#10 $display ("[%0t] Thread2 finished", $time);
end
// Thread 3
#20 $display ("[%0t] Thread3 finished", $time);
join
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
endmodule
See that the main thread stays suspended until all the nested forks are over by 110ns.
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[5] Thread2 ...
[10] Thread1_2 ...
[15] Thread2 finished
[20] Thread3 finished
[50] Thread1_0 ...
[70] Thread1_1 ...
[110] Thread1_2 finished
[110] Main Thread: Fork join has finished
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog fork join_any
In a simple SystemVerilog fork join, the main thread waits until all the child threads have finished execution. This means the fork will hang the simulation if any of the child threads run forever and never complete. SystemVerilog also provides a variation to the original with a fork and join_any.
A fork and join_any will allow the main thread to resume execution of further statements that lie after the fork, if any one of the child threads complete. If five threads are launched, the main thread will resume execution only when any one of the five threads finish execution. Once the main thread resumes operation, the remaining four threads that were launched will continue to run in the background.
Syntax
fork
// Thread 1
// Thread 2
// ...
// Thread N
join_any
fork join_any Example
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
print (10, "Thread2");
join_any
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that this task needs to be automatic
task automatic print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[10] Thread2
[10] Main Thread: Fork join has finished
[20] Thread1_0
[30] Thread1_1
ncsim: *W,RNQUIE: Simulation is complete.
Nested fork join_any
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
join_any
print (10, "Thread2");
join_any
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that this task has to be automatic
task automatic print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[10] Thread2
[10] Main Thread: Fork join has finished
[20] Thread1_0
[30] Thread1_1
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit
SystemVerilog fork join_none
There is a third type of fork join in SystemVerilog which is fork and join_none.
A fork and join_none will allow the main thread to resume execution of further statements that lie after the fork regardless of whether the forked threads finish. If five threads are launched, the main thread will resume execution immediately while all the five threads remain running in the background.
Syntax
fork
// Thread 1
// Thread 2
// ...
// Thread N
join_none
fork join_none Example
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
print (10, "Thread2");
join_none
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that we need automatic task
task automatic print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[0] Main Thread: Fork join has finished
[10] Thread2
[20] Thread1_0
[30] Thread1_1
ncsim: *W,RNQUIE: Simulation is complete.
Nested fork join_none
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
begin
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
join_none
$display("[%0t] Nested fork has finished", $time);
end
print (10, "Thread2");
join_none
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that we need automatic task
task automatic print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[0] Main Thread: Fork join has finished
[0] Nested fork has finished
[10] Thread2
[20] Thread1_0
[30] Thread1_1
ncsim: *W,RNQUIE: Simulation is complete.
Why do we need automatic task ?
Without automatic keyword, the same display task with different string tags will produce the same display message. This is because multiple threads call the same task and share the same variable in tool simulation memory. In order for different threads to initiate different copies of the same task, automatic keyword has to be used.
module tb;
initial begin
$display ("[%0t] Main Thread: Fork join going to start", $time);
fork
print (20, "Thread1_0");
print (30, "Thread1_1");
print (10, "Thread2");
join_none
$display ("[%0t] Main Thread: Fork join has finished", $time);
end
// Note that this is not an automatic task, its static
task print (int _time, string t_name);
#(_time) $display ("[%0t] %s", $time, t_name);
endtask
endmodule
Simulation Log
ncsim> run
[0] Main Thread: Fork join going to start
[0] Main Thread: Fork join has finished
[10] Thread2
[20] Thread2
[30] Thread2
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog disable fork join
In the previous article, different ways to launch parallel threads was discussed. Now we’ll see how to disable forked off threads.
All active threads that have been kicked off from a fork join block can be killed by calling disable fork.
Why disable a fork ?
The following things happen at the start of simulation for the given example:
- Main thread executes
initialblock and finds afork join_anyblock - It will launch three threads in parallel, and wait for any one of them to finish
- Thread1 finishes first because of least delay
- Execution of main thread is resumed
Thread2 and Thread3 are still running even though the main thread has come out of fork join_any block.
module tb_top;
initial begin
// Fork off 3 sub-threads in parallel and the currently executing main thread
// will finish when any of the 3 sub-threads have finished.
fork
// Thread1 : Will finish first at time 40ns
#40 $display ("[%0t ns] Show #40 $display statement", $time);
// Thread2 : Will finish at time 70ns
begin
#20 $display ("[%0t ns] Show #20 $display statement", $time);
#50 $display ("[%0t ns] Show #50 $display statement", $time);
end
// Thread3 : Will finish at time 60ns
#60 $display ("[%0t ns] TIMEOUT", $time);
join_any
// Display as soon as the fork is done
$display ("[%0tns] Fork join is done", $time);
end
endmodule
Simulation Log
ncsim> run
[20 ns] Show #20 $display statement
[40 ns] Show #40 $display statement
[40ns] Fork join is done
[60 ns] TIMEOUT
[70 ns] Show #50 $display statement
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit
What happens when fork is disabled ?
The same example is taken from above, and disable fork is added towards the end.
Note that Thread2 and Thread3 got killed because of disable fork
module tb_top;
initial begin
// Fork off 3 sub-threads in parallel and the currently executing main thread
// will finish when any of the 3 sub-threads have finished.
fork
// Thread1 : Will finish first at time 40ns
#40 $display ("[%0t ns] Show #40 $display statement", $time);
// Thread2 : Will finish at time 70ns
begin
#20 $display ("[%0t ns] Show #20 $display statement", $time);
#50 $display ("[%0t ns] Show #50 $display statement", $time);
end
// Thread3 : Will finish at time 60ns
#60 $display ("[%0t ns] TIMEOUT", $time);
join_any
// Display as soon as the fork is done
$display ("[%0tns] Fork join is done, let's disable fork", $time);
disable fork;
end
endmodule
Simulation Log
ncsim> run
[20 ns] Show #20 $display statement
[40 ns] Show #40 $display statement
[40ns] Fork join is done, let's disable fork
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit
SystemVerilog wait fork
wait fork allows the main process to wait until all forked processes are over. This is useful in cases where the main process has to spawn multiple threads, and perform some function before waiting for all threads to finish.
Example
We’ll use the same example seen in the previous article where 3 threads are kicked off in parallel and the main process waits for one of them to finish. After the main thread resumes, let’s wait until all forked processes are done.
module tb_top;
initial begin
// Fork off 3 sub-threads in parallel and the currently executing main thread
// will finish when any of the 3 sub-threads have finished.
fork
// Thread1 : Will finish first at time 40ns
#40 $display ("[%0t ns] Show #40 $display statement", $time);
// Thread2 : Will finish at time 70ns
begin
#20 $display ("[%0t ns] Show #20 $display statement", $time);
#50 $display ("[%0t ns] Show #50 $display statement", $time);
end
// Thread3 : Will finish at time 60ns
#60 $display ("[%0t ns] TIMEOUT", $time);
join_any
// Display as soon as the fork is done
$display ("[%0t ns] Fork join is done, wait fork to end", $time);
// Wait until all forked processes are over and display
wait fork;
$display ("[%0t ns] Fork join is over", $time);
end
endmodule
Simulation Log
ncsim> run
[20 ns] Show #20 $display statement
[40 ns] Show #40 $display statement
[40 ns] Fork join is done, wait fork to end
[60 ns] TIMEOUT
[70 ns] Show #50 $display statement
[70 ns] Fork join is over
ncsim: *W,RNQUIE: Simulation is complete.
Does wait fork wait until all processes are over ?
To see how it behaves in this case, let’s fork two more processes and wait for the fork to finish.
module tb_top;
initial begin
// Fork off 3 sub-threads in parallel and the currently executing main thread
// will finish when any of the 3 sub-threads have finished.
fork
// Thread1 : Will finish first at time 40ns
#40 $display ("[%0t ns] Show #40 $display statement", $time);
// Thread2 : Will finish at time 70ns
begin
#20 $display ("[%0t ns] Show #20 $display statement", $time);
#50 $display ("[%0t ns] Show #50 $display statement", $time);
end
// Thread3 : Will finish at time 60ns
#60 $display ("[%0t ns] TIMEOUT", $time);
join_any
// Display as soon as the fork is done
$display ("[%0t ns] Fork join is done, wait fork to end", $time);
// Fork two more processes
fork
#10 $display ("[%0t ns] Wait for 10", $time);
#20 $display ("[%0t ns] Wait for 20", $time);
join_any
// Wait until ALL forked processes are over
wait fork;
$display ("[%0t ns] Fork join is over", $time);
end
endmodule
Simulation Log
ncsim> run
[20 ns] Show #20 $display statement
[40 ns] Show #40 $display statement
[40 ns] Fork join is done, wait fork to end
[50 ns] Wait for 10
[60 ns] TIMEOUT
[60 ns] Wait for 20
[70 ns] Show #50 $display statement
[70 ns] Fork join is over
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Interprocess Communication
Components in a testbench often need to communicate with each other to exchange data and check output values of the design. A few mechanisms that allow components or threads to affect the control flow of data are shown in the table below.
| Events | Different threads synchronize with each other via event handles in a testbench |
|---|---|
| Semaphores | Different threads might need to access the same resource; they take turns by using a semaphore |
| Mailbox | Threads/Components need to exchange data with each other; data is put in a mailbox and sent |
What are Events ?
An event is a way to synchronize two or more different processes. One process waits for the event to happen while another process triggers the event. When the event is triggered, the process waiting for the event will resume execution.
1. Create an event using 2. Trigger an event using -> operator
->eventA; // Any process that has access to "eventA" can trigger the event
3. Wait for event to happen****4. Pass events as arguments to functions
module tb_top;
event eventA; // Declare an event handle called "eventA"
initial begin
fork
waitForTrigger (eventA); // Task waits for eventA to happen
#5 ->eventA; // Triggers eventA
join
end
// The event is passed as an argument to this task. It simply waits for the event
// to be triggered
task waitForTrigger (event eventA);
$display ("[%0t] Waiting for EventA to be triggered", $time);
wait (eventA.triggered);
$display ("[%0t] EventA has triggered", $time);
endtask
endmodule
Simulation Log
What’s a semaphore ?
Let’s say you wanted to rent a room in the library for a few hours. The admin desk will give you a key to use the room for the time you have requested access. After you are done with your work, you will return the key to the admin, which will then be given to someone else who wants to use the same room. This way two people will not be allowed to use the room at the same time. The key is a semaphore in this context.
A semaphore is used to control access to a resource and is known as a mutex (mutually exclusive) because only one entity can have the semaphore at a time.
module tb_top;
semaphore key; // Create a semaphore handle called "key"
initial begin
key = new (1); // Create only a single key; multiple keys are also possible
fork
personA (); // personA tries to get the room and puts it back after work
personB (); // personB also tries to get the room and puts it back after work
#25 personA (); // personA tries to get the room a second time
join_none
end
task getRoom (bit [1:0] id);
$display ("[%0t] Trying to get a room for id[%0d] ...", $time, id);
key.get (1);
$display ("[%0t] Room Key retrieved for id[%0d]", $time, id);
endtask
task putRoom (bit [1:0] id);
$display ("[%0t] Leaving room id[%0d] ...", $time, id);
key.put (1);
$display ("[%0t] Room Key put back id[%0d]", $time, id);
endtask
// This person tries to get the room immediately and puts
// it back 20 time units later
task personA ();
getRoom (1);
#20 putRoom (1);
endtask
// This person tries to get the room after 5 time units and puts it back after
// 10 time units
task personB ();
#5 getRoom (2);
#10 putRoom (2);
endtask
endmodule
Simulation Log
Note the following about semaphores.
- A semaphore object key is declared and created using
new ()function. Argument tonew ()defines the number of keys. - You get the key by using the
get ()keyword which will wait until a key is available (blocking) - You put the key back using the
put ()keyword
A mailbox is like a dedicated channel established to send data between two components.
For example, a mailbox can be created and the handles be passed to a data generator and a driver. The generator can push the data object into the mailbox and the driver will be able to retrieve the packet and drive the signals onto the bus.
// Data packet in this environment
class transaction;
rand bit [7:0] data;
function display ();
$display ("[%0t] Data = 0x%0h", $time, data);
endfunction
endclass
// Generator class - Generate a transaction object and put into mailbox
class generator;
mailbox mbx;
function new (mailbox mbx);
this.mbx = mbx;
endfunction
task genData ();
transaction trns = new ();
trns.randomize ();
trns.display ();
$display ("[%0t] [Generator] Going to put data packet into mailbox", $time);
mbx.put (trns);
$display ("[%0t] [Generator] Data put into mailbox", $time);
endtask
endclass
// Driver class - Get the transaction object from Generator
class driver;
mailbox mbx;
function new (mailbox mbx);
this.mbx = mbx;
endfunction
task drvData ();
transaction drvTrns = new ();
$display ("[%0t] [Driver] Waiting for available data", $time);
mbx.get (drvTrns);
$display ("[%0t] [Driver] Data received from Mailbox", $time);
drvTrns.display ();
endtask
endclass
// Top Level environment that will connect Gen and Drv with a mailbox
module tb_top;
mailbox mbx;
generator Gen;
driver Drv;
initial begin
mbx = new ();
Gen = new (mbx);
Drv = new (mbx);
fork
#10 Gen.genData ();
Drv.drvData ();
join_none
end
endmodule
Simulation Log
[0] [Driver] Waiting for available data
[10] Data = 0x9d
[10] [Generator] Put data packet into mailbox
[10] [Generator] Data put into mailbox
[10] [Driver] Data received from Mailbox
[10] Data = 0x9d
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Semaphore
Semaphore is just like a bucket with a fixed number of keys. Processes that use a semaphore must first get a key from the bucket before they can continue to execute. Other proceses must wait until keys are available in the bucket for them to use. In a sense, they are best used for mutual exclusion, access control to shared resources and basic synchronization.
Syntax
semaphore [identifier_name];
Note that semaphore is a built-in class and hence it should be used just like any other class object. It has a few methods with which we can allocate the number of keys for that semaphore object, get and put keys into the bucket.
Methods
| Name | Description |
|---|---|
| function new (int keyCount = 0); | Specifies number of keys initially allocated to the semaphore bucket |
| function void put (int keyCount = 1); | Specifies the number of keys being returned to the semaphore |
| task get (int keyCount = 1); | Specifies the number of keys to obtain from the semaphore |
| function int try_get (int keyCount = 1); | Specifies the required number of keys to obtain from the semaphore |
Example
module tb_top;
semaphore key;
initial begin
key = new (1);
fork
personA ();
personB ();
#25 personA ();
join_none
end
task getRoom (bit [1:0] id);
$display ("[%0t] Trying to get a room for id[%0d] ...", $time, id);
key.get (1);
$display ("[%0t] Room Key retrieved for id[%0d]", $time, id);
endtask
task putRoom (bit [1:0] id);
$display ("[%0t] Leaving room id[%0d] ...", $time, id);
key.put (1);
$display ("[%0t] Room Key put back id[%0d]", $time, id);
endtask
task personA ();
getRoom (1);
#20 putRoom (1);
endtask
task personB ();
#5 getRoom (2);
#10 putRoom (2);
endtask
endmodule
Simulation Log
[0] Trying to get a room for id[1] ...
[0] Room Key retrieved for id[1]
[5] Trying to get a room for id[2] ...
[20] Leaving room id[1] ...
[20] Room Key put back id[1]
[20] Room Key retrieved for id[2]
[25] Trying to get a room for id[1] ...
[30] Leaving room id[2] ...
[30] Room Key put back id[2]
[30] Room Key retrieved for id[1]
[50] Leaving room id[1] ...
[50] Room Key put back id[1]
SystemVerilog Mailbox
A SystemVerilog mailbox is a way to allow different processes to exchange data between each other. It is similar to a real postbox where letters can be put into the box and a person can retrieve those letters later on.
SystemVerilog mailboxes are created as having either a bounded or unbounded queue size. A bounded mailbox can only store a limited amount of data, and if a process attempts to store more messages into a full mailbox, it will be suspended until there’s enough room in the mailbox. However, an unbounded mailbox has unlimited size.
There are two types:
- Generic Mailbox that can accept items of any data type
- Parameterized Mailbox that can accept items of only a specific data type
SystemVerilog Mailbox vs Queue
Although a SystemVerilog mailbox essentially behaves like a queue, it is quite different from the queue data type. A simple queue can only push and pop items from either the front or the back. However, a mailbox is a built-in class that uses semaphores to have atomic control the push and pop from the queue. Moreover, you cannot access a given index within the mailbox queue, but only retrieve items in FIFO order.
Where is a mailbox used ?
A SystemVerilog mailbox is typically used when there are multiple threads running in parallel and want to share data for which a certain level of determinism is required.
Generic Mailbox Example
Two processes are concurrently active in the example shown below, where one initial block puts data into the mailbox and another initial block gets data from the mailbox.
module tb;
// Create a new mailbox that can hold utmost 2 items
mailbox mbx = new(2);
// Block1: This block keeps putting items into the mailbox
// The rate of items being put into the mailbox is 1 every ns
initial begin
for (int i=0; i < 5; i++) begin
#1 mbx.put (i);
$display ("[%0t] Thread0: Put item #%0d, size=%0d", $time, i, mbx.num());
end
end
// Block2: This block keeps getting items from the mailbox
// The rate of items received from the mailbox is 2 every ns
initial begin
forever begin
int idx;
#2 mbx.get (idx);
$display ("[%0t] Thread1: Got item #%0d, size=%0d", $time, idx, mbx.num());
end
end
endmodule
Note that there is a race between the two threads where the first thread can push into the mailbox and the second thread can pop from the mailbox on the same delta cycle. Hence the value displayed using num() is valid only until the next get or put is executed on the mailbox and may depend on the start and finish times of the other methods.
Simulation Log
ncsim> run
[1] Thread0: Put item #0, size=1
[2] Thread1: Got item #0, size=0
[2] Thread0: Put item #1, size=1
[3] Thread0: Put item #2, size=2
[4] Thread1: Got item #1, size=1
[4] Thread0: Put item #3, size=2
[6] Thread1: Got item #2, size=2
[6] Thread0: Put item #4, size=2
[8] Thread1: Got item #3, size=1
[10] Thread1: Got item #4, size=0
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Mailbox Functions and Methods
| Function | Description |
|---|---|
| function new (int bound = 0); | Returns a mailbox handle, bound > 0 represents size of mailbox queue |
| function int num (); | Returns the number of messages currently in the mailbox |
| task put (singular message); | Blocking method that stores a message in the mailbox in FIFO order; message is any singular expression |
| function int try_put (singular message); | Non-blocking method that stores a message if the mailbox is not full, returns a postive integer if successful else 0 |
| task get (ref singular message); | Blocking method until it can retrieve one message from the mailbox, if empty blocks the process |
| function int try_get (ref singular message); | Non-blocking method which tries to get one message from the mailbox, returns 0 if empty |
| task peek (ref singular message); | Copies one message from the mailbox without removing the message from the mailbox queue. |
| function int try_peek (ref singular message); | Tries to copy one message from the mailbox without removing the message from queue |
Parameterized mailboxes
By default, a SystemVerilog mailbox is typeless and hence can send and receive objects of mixed data-types. Although this is a good feature, it can result in type mismatches during simulation time and result in errors. To constrain the mailbox to accept and send objects of a fixed data-type, it can be parameterized to that particular data-type.
Example
In the example shown below, we first create an alias for mailboxes that can send and receive strings using the typedef construct. Although this step is optional, it is a good practice to avoid type mismatches between different components from coding errors. Consider that comp1 sends a few strings to comp2 via this mailbox. Naturally both classes need to have a mailbox handle which needs to be connected together and this is best done at the top level or the module where these two classes are instantiated.
// Create alias for parameterized "string" type mailbox
typedef mailbox #(string) s_mbox;
// Define a component to send messages
class comp1;
// Create a mailbox handle to put items
s_mbox names;
// Define a task to put items into the mailbox
task send ();
for (int i = 0; i < 3; i++) begin
string s = $sformatf ("name_%0d", i);
#1 $display ("[%0t] Comp1: Put %s", $time, s);
names.put(s);
end
endtask
endclass
// Define a second component to receive messages
class comp2;
// Create a mailbox handle to receive items
s_mbox list;
// Create a loop that continuously gets an item from
// the mailbox
task receive ();
forever begin
string s;
list.get(s);
$display ("[%0t] Comp2: Got %s", $time, s);
end
endtask
endclass
// Connect both mailbox handles at a higher level
module tb;
// Declare a global mailbox and create both components
s_mbox m_mbx = new();
comp1 m_comp1 = new();
comp2 m_comp2 = new();
initial begin
// Assign both mailbox handles in components with the
// global mailbox
m_comp1.names = m_mbx;
m_comp2.list = m_mbx;
// Start both components, where comp1 keeps sending
// and comp2 keeps receiving
fork
m_comp1.send();
m_comp2.receive();
join
end
endmodule
Simulation Log
ncsim> run
[1] Comp1: Put name_0
[1] Comp2: Got name_0
[2] Comp1: Put name_1
[2] Comp2: Got name_1
[3] Comp1: Put name_2
[3] Comp2: Got name_2
ncsim: *W,RNQUIE: Simulation is complete.
Matching different type mailboxes
Let’s see what would have happened if the SystemVerilog mailboxes were parameterized to different data-types. Consider comp1 to have string type and comp2 to have a byte type mailbox.
class comp2;
mailbox #(byte) list;
...
endclass
module tb;
s_mbox m_mbx;
...
initial begin
m_comp1.names = m_mbx;
m_comp2.list = m_mbx;
end
endmodule
This would result in a compile time error, and allow us to revisit the testbench code to correct the type mismatch.
Simulation Log
m_comp2.list = m_mbx;
|
ncvlog: *E,TYCMPAT (testbench.sv,34|25): assignment operator type check failed (expecting datatype compatible with 'mailbox' but found '$unit::s_mbox (mailbox)' instead).
SystemVerilog Interface
What is an Interface ?
An Interface is a way to encapsulate signals into a block. All related signals are grouped together to form an interface block so that the same interface can be re-used for other projects. Also it becomes easier to connect with the DUT and other verification components.
Example
APB bus protocol signals are put together in the given interface. Note that signals are declared within interface and endinterface.
interface apb_if (input pclk);
logic [31:0] paddr;
logic [31:0] pwdata;
logic [31:0] prdata;
logic penable;
logic pwrite;
logic psel;
endinterface
Why are signals declared logic ?
logic is a new data type that lets you drive signals of this type via assign statements and in a procedural block. Remember that in verilog, you could drive a reg only in procedural block and a wire only in assign statement. But this is only one reason.
Signals connected to the DUT should support 4-states so that X/Z values can be caught. If these signals were bit then the X/Z would have shown up as 0, and you would have missed that DUT had a X/Z value.
How to define port directions ?
Interface signals can be used within various verification components as well as the DUT, and hence modport is used to define signal directions. Different modport definitions can be passed to different components that allows us to define different input-output directions for each component.
interface myBus (input clk);
logic [7:0] data;
logic enable;
// From TestBench perspective, 'data' is input and 'write' is output
modport TB (input data, clk, output enable);
// From DUT perspective, 'data' is output and 'enable' is input
modport DUT (output data, input enable, clk);
endinterface
How to connect an interface with DUT ?
An interface object should be created in the top testbench module where DUT is instantiated, and passed to DUT. It is essential to ensure that the correct modport is assigned to DUT.
module dut (myBus busIf);
always @ (posedge busIf.clk)
if (busIf.enable)
busIf.data <= busIf.data+1;
else
busIf.data <= 0;
endmodule
// Filename : tb_top.sv
module tb_top;
bit clk;
// Create a clock
always #10 clk = ~clk;
// Create an interface object
myBus busIf (clk);
// Instantiate the DUT; pass modport DUT of busIf
dut dut0 (busIf.DUT);
// Testbench code : let's wiggle enable
initial begin
busIf.enable <= 0;
#10 busIf.enable <= 1;
#40 busIf.enable <= 0;
#20 busIf.enable <= 1;
#100 $finish;
end
endmodule
What are the advantages ?
Interfaces can contain tasks, functions, parameters, variables, functional coverage, and assertions. This enables us to monitor and record the transactions via the interface within this block. It also becomes easier to connect to design regardless of the number of ports it has since that information is encapsulated in an interface.
//Before interface
dut dut0 (.data (data),
.enable (enable),
// all other signals
);
// With interface - higher level of abstraction possible
dut dut0 (busIf.DUT);
How to parameterize an interface ?
The same way you would do for a module.
interface myBus #(parameter D_WIDTH=31) (input clk);
logic [D_WIDTH-1:0] data;
logic enable;
endinterface
What are clocking blocks ?
Signals that are specified inside a clocking block will be sampled/driven with respect to that clock. There can be mulitple clocking blocks in an interface. Note that this is for testbench related signals. You want to control when the TB drives and samples signals from DUT. Solves some part of the race condition, but not entirely. You can also parameterize the skew values.
interface my_int (input bit clk);
// Rest of interface code
clocking cb_clk @(posedge clk);
default input #3ns output #2ns;
input enable;
output data;
endclocking
endinterface
In the above example, we have specified that by default, input should be sampled 3ns before posedge of clk, and output should be driven 2ns after posedge of clk.
How to use a clocking block ?
// To wait for posedge of clock
@busIf.cb_clk;
// To use clocking block signals
busIf.cb_clk.enable = 1;
As you can see, you don’t have to wait for the posedge of clk, before you assign 1 to enable. This way, you are assured that enable will be driven 2ns after the next posedge clk.
SystemVerilog Interface Intro
A SystemVerilog interface allows us to group a number of signals together and represent them as a single port. All these signals can be declared and maintained at a single place and be easily maintained. Signals within an interface are accessed by the interface instance handle.
Syntax
Interface blocks are defined and described within interface and endinterface keywords. It can be instantiated like a module with or without ports.
interface [name] ([port_list]);
[list_of_signals]
endinterface
Interfaces can also have functions, tasks, variables, and parameters making it more like a class template. It also has the ability to define policies of directional information for different module ports via the modport construct along with testbench synchronization capabilities with clocking blocks. It can also have assertions, coverage recording and other protocol checking elements. Last but not the least, it can also contain initial and always procedures and continuous assign statements.
A module cannot be instantiated in an interface ! But an interface can be instantiated within a module.
SystemVerilog is now popular as a HDL and let’s see two cases where an interface is used with the same design in both Verilog and SystemVerilog. To keep things simple in this introductory example, we’ll just create a simple interface.
Interface with a Verilog Design
Let us see how an interface can be used in the testbench and connected to a standard Verilog design with a portlist. The code shown below is a design of an up-down counter in Verilog. This module accepts a parameter to decide the width of the counter. It also accepts an input load value load that is loaded into the counter only when load_en is 1.
The counter starts counting down when the input down is 1 and otherwise it counts upwards. The rollover output indicates when the counter either transitions from a max_value to 0 or a 0 to max_value.
module counter_ud
#(parameter WIDTH = 4)
(
input clk,
input rstn,
input wire [WIDTH-1:0] load,
input load_en,
input down,
output rollover,
output reg [WIDTH-1:0] count
);
always @ (posedge clk or negedge rstn) begin
if (!rstn)
count <= 0;
else
if (load_en)
count <= load;
else begin
if (down)
count <= count - 1;
else
count <= count + 1;
end
end
assign rollover = &count;
endmodule
An interface called cnt_if is declared below with a parameterizable value as the width of the counter signal. This task also has a task init() to assign values
interface cnt_if #(parameter WIDTH = 4) (input bit clk);
logic rstn;
logic load_en;
logic [WIDTH-1:0] load;
logic [WIDTH-1:0] count;
logic down;
logic rollover;
endinterface
module tb;
reg clk;
// TB Clock Generator used to provide the design
// with a clock -> here half_period = 10ns => 50 MHz
always #10 clk = ~clk;
cnt_if cnt_if0 (clk);
counter_ud c0 ( .clk (cnt_if0.clk),
.rstn (cnt_if0.rstn),
.load (cnt_if0.load),
.load_en (cnt_if0.load_en),
.down (cnt_if0.down),
.rollover (cnt_if0.rollover),
.count (cnt_if0.count));
initial begin
bit load_en, down;
bit [3:0] load;
$monitor("[%0t] down=%0b load_en=%0b load=0x%0h count=0x%0h rollover=%0b",
$time, cnt_if0.down, cnt_if0.load_en, cnt_if0.load, cnt_if0.count, cnt_if0.rollover);
// Initialize testbench variables
clk <= 0;
cnt_if0.rstn <= 0;
cnt_if0.load_en <= 0;
cnt_if0.load <= 0;
cnt_if0.down <= 0;
// Drive design out of reset after 5 clocks
repeat (5) @(posedge clk);
cnt_if0.rstn <= 1; // Drive stimulus -> repeat 5 times
for (int i = 0; i < 5; i++) begin
// Drive inputs after some random delay
int delay = $urandom_range (1,30);
#(delay);
// Randomize input values to be driven
std::randomize(load, load_en, down);
// Assign tb values to interface signals
cnt_if0.load <= load;
cnt_if0.load_en <= load_en;
cnt_if0.down <= down;
end
// Wait for 5 clocks and finish simulation
repeat(5) @ (posedge clk);
$finish;
end
endmodule
Simulation Log
ncsim> run
[0] down=0 load_en=0 load=0x0 count=0x0 rollover=0
[96] down=1 load_en=1 load=0x1 count=0x0 rollover=0
[102] down=0 load_en=0 load=0x9 count=0x0 rollover=0
[108] down=1 load_en=1 load=0x1 count=0x0 rollover=0
[110] down=1 load_en=1 load=0x1 count=0x1 rollover=0
[114] down=1 load_en=0 load=0xc count=0x1 rollover=0
[120] down=1 load_en=0 load=0x7 count=0x1 rollover=0
[130] down=1 load_en=0 load=0x7 count=0x0 rollover=0
[150] down=1 load_en=0 load=0x7 count=0xf rollover=1
[170] down=1 load_en=0 load=0x7 count=0xe rollover=0
[190] down=1 load_en=0 load=0x7 count=0xd rollover=0
Simulation complete via $finish(1) at time 210 NS + 0
Interface with a SystemVerilog design
Let us now see how an interface can be used in the testbench and be connected to a SystemVerilog design module. SystemVerilog allows a module to accept an interface as the portlist instead of individual signals. In the design example shown below, we have substituted the portlist of counter_ud with an interface handle which is used to define design functionality.
`timescale 1ns/1ns
// This module accepts an interface object as the port list
module counter_ud #(parameter WIDTH = 4) (cnt_if _if);
always @ (posedge _if.clk or negedge _if.rstn) begin
if (!_if.rstn)
_if.count <= 0;
else
if (_if.load_en)
_if.count <= _if.load;
else begin
if (_if.down)
_if.count <= _if.count - 1;
else
_if.count <= _if.count + 1;
end
end
assign _if.rollover = &_if.count;
endmodule
The design instance is passed an interface handle called cnt_if and is used to drive inputs to the design from the testbench. The same interface handle can be used to monitor outputs from the design if required.
// Interface definition is the same as before
module tb;
reg clk;
// TB Clock Generator used to provide the design
// with a clock -> here half_period = 10ns => 50 MHz
always #10 clk = ~clk;
cnt_if cnt_if0 (clk);
// Note that here we just have to pass the interface handle
// to the design instead of connecting each individual signal
counter_ud c0 (cnt_if0);
// Stimulus remains the same as before
Simulation Log
ncsim> run
[0] down=0 load_en=0 load=0x0 count=0x0 rollover=0
[96] down=1 load_en=1 load=0x1 count=0x0 rollover=0
[102] down=0 load_en=0 load=0x9 count=0x0 rollover=0
[108] down=1 load_en=1 load=0x1 count=0x0 rollover=0
[110] down=1 load_en=1 load=0x1 count=0x1 rollover=0
[114] down=1 load_en=0 load=0xc count=0x1 rollover=0
[120] down=1 load_en=0 load=0x7 count=0x1 rollover=0
[130] down=1 load_en=0 load=0x7 count=0x0 rollover=0
[150] down=1 load_en=0 load=0x7 count=0xf rollover=1
[170] down=1 load_en=0 load=0x7 count=0xe rollover=0
[190] down=1 load_en=0 load=0x7 count=0xd rollover=0
Simulation complete via $finish(1) at time 210 NS + 0
What makes it different from Verilog ?
Verilog connects between different modules through its module ports. For large designs, this method of connection can become more time consuming and repetitious. Some of these ports may include signals related to bus protocols like AXI/AHB, clock and reset pins, signals to and from RAM/memory and to other peripheral devices.
Using Verilog Ports
This is the traditional way of port connection in Verilog.
module d_slave ( input clk,
reset,
enable,
// Many more input signals
output gnt,
irq,
// Many more output signals);
// Some design functionality
endmodule
module d_top ( [top_level_ports] );
reg [`NUM_SLAVES-1:0] clk; // Assume `NUM_SLAVES is a macro set to 2
reg [`NUM_SLAVES-1:0] tb_reset;
// Other declarations
d_slave slave_0 ( .clk (d_clk[0]), // These connections have to be
.reset (d_reset[0]) // repeated for all other slave instances
...
.gnt (d_gnt[0]),
... );
d_slave slave_1 ( ... );
d_slave slave_2 ( ... );
endmodule
Let us consider a scenario where there are twelve slaves in the design shown above. If there is a change made at the d_slave module ports, then the change has to be reflected in all the twelve slave instance connections in d_top as well.
Disadvantages
Some cons of using Verilog port method for connection are :
- Tedious to trace, debug and maintain
- Too easy to make or break design functionality
- Changes in design requirements may require modifications in multiple modules
- Duplication needed in multiple modules, communication protocols, and other places
Using SystemVerilog Interface
Note that the module d_top simply uses the interface to connect with the slave instances instead of repetitively declaring connection to each signal of the slave block as shown before.
interface slave_if (input logic clk, reset);
reg clk;
reg reset;
reg enable;
reg gnt;
// Declarations for other signals follow
endinterface
module d_slave (slave_if s_if);
// Design functionality
always (s_if.enable & s_if.gnt) begin // interface signals are accessed by the handle "s_if"
// Some behavior
end
endmodule
module d_top (input clk, reset);
// Create an instance of the slave interface
slave_if slave_if_inst ( .clk (clk),
.reset (reset));
d_slave slave_0 (.s_if (slave_if_inst));
d_slave slave_1 (.s_if (slave_if_inst));
d_slave slave_2 (.s_if (slave_if_inst));
endmodule
Now, if there is a change to one of the signals in the slave interface, it is automatically applied to all the instances. In SystemVerilog, the module portlist can also have a port with an interface type instead of the usual input, output and inout.
Interface Array
In the example below an interface named myInterface with an empty port list is created and instantiated within the top level testbench module. It is also fine to omit the parenthesis for an empty port list and instead truncate the statement with a semicolon
// interface myInterface;
interface myInterface ();
reg gnt;
reg ack;
reg [7:0] irq;
...
endinterface
module tb;
// Single interface handle
myInterface if0 ();
// An array of interfaces
myInterface wb_if [3:0] ();
// Rest of the testbench
endmodule
A single interface called if0 can be instantiated and signals within this interface should be accessed by referencing this handle. This can then be used to drive and sample signals going to the DUT.
We can also have an array of interfaces. Here this array is referred by the name wb_if which has 4 instances of the interface.
module myDesign ( myInterface dut_if,
input logic clk);
always @(posedge clk)
if (dut_if.ack)
dut_if.gnt <= 1;
endmodule
module tb;
reg clk;
// Single interface handle connection
myInterface if0;
myDesign top (if0, clk);
// Or connect by name
// myDesign top (.dut_if(if0), .clk(clk));
// Multiple design instances connected to the appropriate
// interface handle
myDesign md0 (wb_if[0], clk);
myDesign md1 (wb_if[1], clk);
myDesign md2 (wb_if[2], clk);
myDesign md3 (wb_if[3], clk);
endmodule
When an interface is referenced as a port, the variables and nets in it are assumed to have ref and inout access respectively. If same identifiers are used as interface instance name and port name in the design, then implicit port connections can also be used.
module tb;
reg clk;
myInterface dut_if();
// Can use implicit port connection when all port signals have same name
myDesign top (.*);
endmodule
SystemVerilog Interface Bundles
Introduction covered the need for an interface, how to instantiate and connect the interface with a design. There are two ways in which the design can be written:
- By using an existing interface name to specifically use only that interface
- By using a generic interface handle to which any interface can be passed
Obviously, the generic method works best when interface definitions are updated to newer versions with a different name, and needs to support older designs that use it.
Example using a named bundle
In this case, the design references the actual interface name for access to its signals. The example below shows that both design modules myDesign and yourDesign declares a port in the port list called if0 of type myInterface to access signals.
module myDesign ( myInterface if0,
input logic clk);
always @ (posedge clk)
if (if0.ack)
if0.gnt <= 1;
...
endmodule
module yourDesign ( myInterface if0,
input logic clk);
...
endmodule
module tb;
logic clk = 0;
myInterface _if;
myDesign md0 (_if, clk);
yourDesign yd0 (_if, clk);
endmodule
Example using a generic bundle
In this case, the design uses the interface keyword as a placeholder for the actual interface type. The example below shows that both the design modules myDesign and yourDesign uses the placeholder handle to reference signals. The actual interface is then passed during design module instantiation. This generic interface reference can only be declared using ANSI style of port declaration syntax and is illegal otherwise.
module myDesign ( interface a,
input logic clk);
always @ (posedge clk)
if (if0.ack)
if0.gnt <= 1;
...
endmodule
module yourDesign ( interface b,
input logic clk);
...
endmodule
module tb;
logic clk = 0;
myInterface _if;
myDesign md0 ( .*, .a(_if)); // use partial implicit port connections
yourDesign yd0 ( .*, .b(_if));
endmodule
SystemVerilog Modport
Modport lists with directions are defined in an interface to impose certain restrictions on interface access within a module. The keyword modport indicates that the directions are declared as if inside the module.
Syntax
modport [identifier] (
input [port_list],
output [port_list]
);
Shown below is the definition of an interface myInterface which has a few signals and two modport declarations. The modport dut0 essentially states that the signals ack and sel are inputs and gnt and irq0 are outputs to whatever module uses this particular modport.
Similarly, another modport called dut1 is declared which states that gnt and irq0 are inputs and the other two are outputs for any module that uses modport dut1.
interface myInterface;
logic ack;
logic gnt;
logic sel;
logic irq0;
// ack and sel are inputs to the dut0, while gnt and irq0 are outputs
modport dut0 (
input ack, sel,
output gnt, irq0
);
// ack and sel are outputs from dut1, while gnt and irq0 are inputs
modport dut1 (
input gnt, irq0,
output ack, sel
);
endinterface
Example of named port bundle
In this style, the design will take the required correct modport definition from the interface object as mentioned in its port list. The testbench only needs to provide the whole interface object to the design.
module dut0 ( myinterface.dut0 _if);
...
endmodule
module dut1 ( myInterface.dut1 _if);
...
endmodule
module tb;
myInterface _if;
dut0 d0 ( .* );
dut1 d1 ( .* );
endmodule
Example of connecting port bundle
In this style, the design simply accepts whatever directional information is given to it. Hence testbench is responsible to provide the correct modport values to the design.
module dut0 ( myinterface _if);
...
endmodule
module dut1 ( myInterface _if);
...
endmodule
module tb;
myInterface _if;
dut0 d0 ( ._if (_if.dut0));
dut1 d1 ( ._if (_if.dut1));
endmodule
What is the need for a modport ?
Nets declared within a simple interface is inout by default and hence any module connected to the same net, can either drive values or take values from it. In simple words, there are no restrictions on direction of value propagation. You could end up with an X on the net because both the testbench and the design are driving two different values to the same interface net. Special care should be taken by the testbench writer to ensure that such a situation does not happen. This can be inherently avoided by the use of modports.
Example of connecting to generic interface
A module can also have a generic interface as the portlist. The generic handle can accept any modport passed to it from the hierarchy above.
module dut0 ( interface _if);
...
endmodule
module dut1 ( interface _if);
...
endmodule
module tb;
myInterface _if;
dut0 d0 ( ._if (_if.dut0));
dut1 d1 ( ._if (_if.dut1));
endmodule
Design Example
Lets consider two modules master and slave connected by a very simple bus structure. Assume that the bus is capable of sending an address and data which the slave is expected to capture and update the information in its internal registers. So the master always has to initiate the transfer and the slave is capable of indicating to the master whether it is ready to accept the data by its sready signal.
Interface
Shown below is an interface definition that is shared between the master and slave modules.
interface ms_if (input clk);
logic sready; // Indicates if slave is ready to accept data
logic rstn; // Active low reset
logic [1:0] addr; // Address
logic [7:0] data; // Data
modport slave ( input addr, data, rstn, clk,
output sready);
modport master ( output addr, data,
input clk, sready, rstn);
endinterface
Design
Assume that the master simply iterates the address from 0 to 3 and sends data equal to the address multiplied by 4. The master should only send when the slave is ready to accept and is indicated by the sready signal.
// This module accepts an interface with modport "master"
// Master sends transactions in a pipelined format
// CLK 1 2 3 4 5 6
// ADDR A0 A1 A2 A3 A0 A1
// DATA D0 D1 D2 D3 D4
module master ( ms_if.master mif);
always @ (posedge mif.clk) begin
// If reset is applied, set addr and data to default values
if (! mif.rstn) begin
mif.addr <= 0;
mif.data <= 0;
// Else increment addr, and assign data accordingly if slave is ready
end else begin
// Send new addr and data only if slave is ready
if (mif.sready) begin
mif.addr <= mif.addr + 1;
mif.data <= (mif.addr * 4);
// Else maintain current addr and data
end else begin
mif.addr <= mif.addr;
mif.data <= mif.data;
end
end
end
endmodule
Assume that the slave accepts data for every addr and assigns them to internal registers. When the address wraps from 3 to 0, the slave requires 1 additional clock to become ready.
module slave (ms_if.slave sif);
reg [7:0] reg_a;
reg [7:0] reg_b;
reg reg_c;
reg [3:0] reg_d;
reg dly;
reg [3:0] addr_dly;
always @ (posedge sif.clk) begin
if (! sif.rstn) begin
addr_dly <= 0;
end else begin
addr_dly <= sif.addr;
end
end
always @ (posedge sif.clk) begin
if (! sif.rstn) begin
reg_a <= 0;
reg_b <= 0;
reg_c <= 0;
reg_d <= 0;
end else begin
case (addr_dly)
0 : reg_a <= sif.data;
1 : reg_b <= sif.data;
2 : reg_c <= sif.data;
3 : reg_d <= sif.data;
endcase
end
end
assign sif.sready = ~(sif.addr[1] & sif.addr[0]) | ~dly;
always @ (posedge sif.clk) begin
if (! sif.rstn)
dly <= 1;
else
dly <= sif.sready;
end
endmodule
The two design modules are tied together at a top level.
module d_top (ms_if tif);
// Pass the "master" modport to master
master m0 (tif.master);
// Pass the "slave" modport to slave
slave s0 (tif.slave);
endmodule
Testbench
The testbench will pass the interface handle to the design, which will then assign master and slave modports to its sub-modules.
module tb;
reg clk;
always #10 clk = ~clk;
ms_if if0 (clk);
d_top d0 (if0);
// Let the stimulus run for 20 clocks and stop
initial begin
clk <= 0;
if0.rstn <= 0;
repeat (5) @ (posedge clk);
if0.rstn <= 1;
repeat (20) @ (posedge clk);
$finish;
end
endmodule
Remember that the master initiates bus transactions and the slave captures data and stores it in its internal registers reg_* for the corresponding address.
SystemVerilog Clocking Blocks
Module ports and interfaces by default do not specify any timing requirements or synchronization schemes between signals. A clocking block defined between clocking and endcocking does exactly that. It is a collection of signals synchronous with a particular clock and helps to specify the timing requirements between the clock and the signals.
This would allow test writers to focus more on transactions rather than worry about when a signal will interact with respect to a clock. A testbench can have many clocking blocks, but only one block per clock.
Syntax
[default] clocking [identifier_name] @ [event_or_identifier]
default input #[delay_or_edge] output #[delay_or_edge]
[list of signals]
endclocking
The delay_value represents a skew of how many time units away from the clock event a signal is to be sampled or driven. If a default skew is not specified, then all input signals will be sampled #1step and output signlas driven 0ns after the specified event.
clocking ckb @ (posedge clk);
default input #1step output negedge;
input ...;
output ...;
endclocking
clocking ck1 @ (posedge clk);
default input #5ns output #2ns;
input data, valid, ready = top.ele.ready;
output negedge grant;
input #1step addr;
endclocking
Note the following:
- A clocking block called ck1 is created which will be active on the positive edge of clk
- By default, all input signals within the clocking block will be sampled 5ns before and all output signals within the clocking block will be driven 2ns after the positive edge of the clock clk
- data, valid and ready are declared as inputs to the block and hence will be sampled 5ns before the posedge of clk
- grant is an output signal to the block with its own time requirement. Here grant will be driven at the negedge of clk instead of the default posedge.
Use within an interface
Simply put, a clocking block encapsulates a bunch of signals that share a common clock. Hence declaring a clocking block inside an interface can help save the amount of code required to connect to the testbench and may help save time during development.
SystemVerilog Clocking Blocks Part II
Clocking blocks allow inputs to be sampled and outputs to be driven at a specified clock event. If an input skew is mentioned for a clocking block, then all input signals within that block will be sampled at skew time units before the clock event. If an output skew is mentioned for a clocking block, then all output signals in that block will be driven skew time units after the corresponding clock event.
What are input and output skews ?
A skew is specified as a constant expression or as a parameter. If only a number is used, then the skew is interpreted to follow the active timescale in the given scope.
clocking cb @(clk);
input #1ps req;
output #2 gnt;
input #1 output #3 sig;
endclocking
In the example given above, we have declared a clocking block of the name cb to describe when signals belonging to this block has to be sampled. Signal req is specified to have a skew of 1ps and will be sampled 1 ps before the clock edge clk. The output signal gnt has an output skew of 2 time units and hence will follow the timescale followed in the current scope. If we have a timescale of 1ns/1ps then #2 represents 2 ns and hence will be driven 2 ns after the clock edge. The last signal sig is of inout type and will be sampled 1 ns before the clock edge and driven 3 ns after the clock edge.
An input skew of 1step indicates that the signal should be sampled at the end of the previous time step, or in other words, immediately before the positive clock edge.
clocking cb @(posedge clk);
input #1step req;
endclocking
Inputs with explicit #0 skew will be sampled at the same time as their corresponding clocking event, but in the Observed region to avoid race conditions. Similarly, outputs with no skew or explicit #0 will be driven at the same time as the clocking event, in the Re-NBA region.
Example
Consider a simple design with inputs clk and req and drives an output signal gnt. To keep things simple, lets just provide grant as soon as a request is received.
module des (input req, clk, output reg gnt);
always @ (posedge clk)
if (req)
gnt <= 1;
else
gnt <= 0;
endmodule
To deal with the design port signals, let’s create a simple interface called _if.
interface _if (input bit clk);
logic gnt;
logic req;
clocking cb @(posedge clk);
input #1ns gnt;
output #5 req;
endclocking
endinterface
The next step is to drive inputs to the design so that it gives back the grant signal.
module tb;
bit clk;
// Create a clock and initialize input signal
always #10 clk = ~clk;
initial begin
clk <= 0;
if0.cb.req <= 0;
end
// Instantiate the interface
_if if0 (.clk (clk));
// Instantiate the design
des d0 ( .clk (clk),
.req (if0.req),
.gnt (if0.gnt));
// Drive stimulus
initial begin
for (int i = 0; i < 10; i++) begin
bit[3:0] delay = $random;
repeat (delay) @(posedge if0.clk);
if0.cb.req <= ~ if0.cb.req;
end
#20 $finish;
end
endmodule
It can be seen from simulation output window that req is driven #5ns after the clock edge.
Output skew
To get a clear picture of the output skew, lets tweak the interface to have three different clocking blocks each with a different output skew. Then let us drive req with each of the clocking blocks to see the difference.
interface _if (input bit clk);
logic gnt;
logic req;
clocking cb_0 @(posedge clk);
output #0 req;
endclocking
clocking cb_1 @(posedge clk);
output #2 req;
endclocking
clocking cb_2 @(posedge clk);
output #5 req;
endclocking
endinterface
In our testbench, we’ll use a for loop to iterate through each stimulus and use a different clocking block for each iteration.
module tb;
// ... part of code same as before
// Drive stimulus
initial begin
for (int i = 0; i < 3; i++) begin
repeat (2) @(if0.cb_0);
case (i)
0 : if0.cb_0.req <= 1;
1 : if0.cb_1.req <= 1;
2 : if0.cb_2.req <= 1;
endcase
repeat (2) @ (if0.cb_0);
if0.req <= 0;
end
#20 $finish;
end
endmodule
Input skew
To understand input skew, we’ll change the DUT to simply provide a random value every #1ns just for our purpose.
module des (output reg[3:0] gnt);
always #1 gnt <= $random;
endmodule
The interface block will have different clocking block declarations like before each with a different input skew.
interface _if (input bit clk);
logic [3:0] gnt;
clocking cb_0 @(posedge clk);
input #0 gnt;
endclocking
clocking cb_1 @(posedge clk);
input #1step gnt;
endclocking
clocking cb_2 @(posedge clk);
input #1 gnt;
endclocking
clocking cb_3 @(posedge clk);
input #2 gnt;
endclocking
endinterface
In the testbench, we’ll fork 4 different threads at time 0ns where each thread waits for the positive edge of the clock and samples the output from DUT.
module tb;
bit clk;
always #5 clk = ~clk;
initial clk <= 0;
_if if0 (.clk (clk));
des d0 (.gnt (if0.gnt));
initial begin
fork
begin
@(if0.cb_0);
$display ("cb_0.gnt = 0x%0h", if0.cb_0.gnt);
end
begin
@(if0.cb_1);
$display ("cb_1.gnt = 0x%0h", if0.cb_1.gnt);
end
begin
@(if0.cb_2);
$display ("cb_2.gnt = 0x%0h", if0.cb_2.gnt);
end
begin
@(if0.cb_3);
$display ("cb_3.gnt = 0x%0h", if0.cb_3.gnt);
end
join
#10 $finish;
end
endmodule
The output waveform is shown below and it can be seen that the design drives a random value every #1ns.
It’s important to note that the testbench code which sampled through cb_1 clocking block managed to get the value 0x3 while cb_0 got 0xd. Note that these values may be different for other simulators since they can take a different randomization seed value.
Simulation Log
ncsim> run
cb_3.gnt = 0x9
cb_2.gnt = 0x3
cb_1.gnt = 0x3
cb_0.gnt = 0xd
Simulation complete via $finish(1) at time 15 NS + 0
SystemVerilog Class
What are classes ?
class is a user-defined datatype, an OOP construct, that can be used to encapsulate data (property) and tasks/functions (methods) which operate on the data. Here’s an example:
class myPacket;
bit [2:0] header;
bit encode;
bit [2:0] mode;
bit [7:0] data;
bit stop;
function new (bit [2:0] header = 3'h1, bit [2:0] mode = 5);
this.header = header;
this.encode = 0;
this.mode = mode;
this.stop = 1;
endfunction
function display ();
$display ("Header = 0x%0h, Encode = %0b, Mode = 0x%0h, Stop = %0b",
this.header, this.encode, this.mode, this.stop);
endfunction
endclass
There are a few key things to note in the example above:
function new ()is called the constructor and is automatically called upon object creation.thiskeyword is used to refer to the current class. Normally used within a class to refer to its own properties/methods.- *display ()* is a function, and rightly so, because displaying values does not consume simulation time.
function new ()has default values to the arguments, and hence line 6 (below) will create a packet object with values [3’h1, 0, 2’h3, 1]
How can I access signals within a class ?
To do that, you have to create an object of the class, which can be used as a handle to its properties and methods.
module tb_top;
myPacket pkt0, pkt1;
initial begin
pkt0 = new (3'h2, 2'h3);
pkt0.display ();
pkt1 = new ();
pkt1.display ();
end
endmodule
Simulation Log
Header = 0x2, Encode = 0, Mode = 0x3, Stop = 1
Header = 0x1, Encode = 0, Mode = 0x5, Stop = 1
How do I create an array of classes ?
An array of classes can be created in a way similar to how you create an int type array.
module tb_top;
myPacket pkt0 [3];
initial begin
for(int i = 0; i < $size (pkt0); i++) begin
pkt0[i] = new ();
pkt0[i].display ();
end
end
endmodule
Since each myPacket object had no arguments in the constructor new(), default values are applied.
Simulation Log
Header = 0x1, Encode = 0, Mode = 0x5, Stop = 1
Header = 0x1, Encode = 0, Mode = 0x5, Stop = 1
Header = 0x1, Encode = 0, Mode = 0x5, Stop = 1
What is inheritance ?
Let’s say you wanted to have a class with all the properties/methods of myPacket and be able to add more stuff in it without changing myPacket, the best way to do so is by inheritance. In the example below, networkPacket inherits the properties/methods of myPacket using the extend keyword. To call the functions of base class (myPacket), use super keyword.
class networkPkt extends myPacket;
bit parity;
bit [1:0] crc;
function new ();
super.new ();
this.parity = 1;
this.crc = 3;
endfunction
function display ();
super.display();
$display ("Parity = %0b, CRC = 0x%0h", this.parity, this.crc);
endfunction
endclass
What is an abstract/virtual class ?
If you create an abstract class using the virtual keyword, then you cannot create an object of the class. This is useful if you don’t want others to create an object of the class and instead force users to keep the abstract class as the base and extend it to create child classes for their purpose.
// Creation of base class object is invalid
virtual class Base;
bit [7:0] data;
bit enable;
endclass
// Creation of child class object is valid
class Child extends Base;
// User definition
endclass
SystemVerilog Class Handle
What is a class handle ?
A class variable such as pkt below is only a name by which that object is known. It can hold the handle to an object of class Packet, but until assigned with something it is always null. At this point, the class object does not exist yet.
Class Handle Example
// Create a new class with a single member called
// count that stores integer values
class Packet;
int count;
endclass
module tb;
// Create a "handle" for the class Packet that can point to an
// object of the class type Packet
// Note: This "handle" now points to NULL
Packet pkt;
initial begin
if (pkt == null)
$display ("Packet handle 'pkt' is null");
// Display the class member using the "handle"
// Expect a run-time error because pkt is not an object
// yet, and is still pointing to NULL. So pkt is not
// aware that it should hold a member
$display ("count = %0d", pkt.count);
end
endmodule
Simulation Log
ncsim> run
Packet handle 'pkt' is null
count = ncsim: *E,TRNULLID: NULL pointer dereference.
File: ./testbench.sv, line = 18, pos = 33
Scope: tb
Time: 0 FS + 0
./testbench.sv:18 $display ("count = %0d", pkt.count);
ncsim> exit
What is a class object ?
An instance of that class is created only when it’s new() function is invoked. To reference that particular object again, we need to assign it’s handle to a variable of type Packet.
Class Object Example
// Create a new class with a single member called
// count that stores integer values
class Packet;
int count;
endclass
module tb;
// Create a "handle" for the class Packet that can point to an
// object of the class type Packet
// Note: This "handle" now points to NULL
Packet pkt;
initial begin
if (pkt == null)
$display ("Packet handle 'pkt' is null");
// Call the new() function of this class
pkt = new();
if (pkt == null)
$display ("What's wrong, pkt is still null ?");
else
$display ("Packet handle 'pkt' is now pointing to an object, and not NULL");
// Display the class member using the "handle"
$display ("count = %0d", pkt.count);
end
endmodule
Simulation Log
ncsim> run
Packet handle 'pkt' is null
Packet handle 'pkt' is now pointing to an object, and not NULL
count = 0
ncsim: *W,RNQUIE: Simulation is complete.
What happens when both handles point to same object ?
If we assign pkt to a new variable called pkt2, the new variable will also point to the contents in pkt.
// Create a new class with a single member called
// count that stores integer values
class Packet;
int count;
endclass
module tb;
// Create two "handles" for the class Packet
// Note: These "handles" now point to NULL
Packet pkt, pkt2;
initial begin
// Call the new() function of this class and
// assign the member some value
pkt = new();
pkt.count = 16'habcd;
// Display the class member using the "pkt" handle
$display ("[pkt] count = 0x%0h", pkt.count);
// Make pkt2 handle to point to pkt and print member variable
pkt2 = pkt;
$display ("[pkt2] count = 0x%0h", pkt2.count);
end
endmodule
Simulation Log
ncsim> run
[pkt] count = 0xabcd
[pkt2] count = 0xabcd
ncsim: *W,RNQUIE: Simulation is complete.
Now we have two handles, pkt and pkt2 pointing to the same instance of the class type Packet. This is because we did not create a new instance for pkt2 and instead only assigned a handle to the instance pointed to by pkt.
SystemVerilog Class Constructor
A constructor is simply a method to create a new object of a particular class data-type.
Constructors
C/C++ requires complex memory allocation techniques and improper de-allocation could lead to memory leaks and other behavioral issues. SystemVerilog, although not a programming language, is capable of simple construction of objects and automatic garbage collection.
When class constructor is explicity defined
// Define a class called "Packet" with a 32-bit variable to store address
// Initialize "addr" to 32'hfade_cafe in the new function, also called constructor
class Packet;
bit [31:0] addr;
function new ();
addr = 32'hfade_cafe;
endfunction
endclass
module tb;
// Create a class handle called "pkt" and instantiate the class object
initial begin
// The class's constructor new() fn is called when the object is instantiated
Packet pkt = new;
// Display the class variable - Because constructor was called during
// instantiation, this variable is expected to have 32'hfade_cafe;
$display ("addr=0x%0h", pkt.addr);
end
endmodule
In the example above, variable declaration creates an object of class Packet and will automatically call the new() function within the class. The new() function is called a class constructor and is a way to initialize the class variables with some value. Note that it does not have a return type and is non-blocking.
Simulation Log
ncsim> run
addr=0xfadecafe
ncsim: *W,RNQUIE: Simulation is complete.
When class constructor is implicitly called
If the class does not have a new() function explicitly coded, an implicit new method will be automatically provided. In this case, addr is initialized to zero since it is of type bit for which the default value is zero.
// Define a simple class with a variable called "addr"
// Note that the new() function is not defined here
class Packet;
bit [31:0] addr;
endclass
module tb;
// When the class object is instantiated, then the constructor is
// implicitly defined by the tool and called
initial begin
Packet pkt = new;
$display ("addr=0x%0h", pkt.addr);
end
endmodule
Simulation Log
ncsim> run
addr=0x0
ncsim: *W,RNQUIE: Simulation is complete.
Behavior of inherited classes
The new method of the derived class will first call its parent class constructor using super.new(). Once the base class constructor has completed, each property defined in the derived class will be initialized to a default value after which rest of the code within the new method will be executed.
// Define a simple class and initialize the class member "data" in new() function
class baseClass;
bit [15:0] data;
function new ();
data = 16'hface;
endfunction
endclass
// Define a child class extended from the above class with more members
// The constructor new() function accepts a value as argument, and by default is 2
class subClass extends baseClass;
bit [3:0] id;
bit [2:0] mode = 3;
function new (int val = 2);
// The new() function in child class calls the new function in
// the base class using the "super" keyword
super.new ();
// Assign the value obtained through the argument to the class member
id = val;
endfunction
endclass
module tb;
initial begin
// Create two handles for the child class
subClass sc1, sc2;
// Instantiate the child class and display member variable values
sc1 = new ();
$display ("data=0x%0h id=%0d mode=%0d", sc1.data, sc1.id, sc1.mode);
// Pass a value as argument to the new function in this case and print
sc2 = new (4);
$display ("data=0x%0h id=%0d mode=%0d", sc2.data, sc2.id, sc2.mode);
end
endmodule
In the above example, when a subClass object is created, it will first call the new() function of the subClass. From there it branches off to the new() method of the baseClass when invoked by super keyword. data will be initialized next and the control comes back to the subClass. The new method will be done after id is initialized to 4.
Simulation Log
ncsim> run
data=0xface id=2 mode=3
data=0xface id=4 mode=3
ncsim: *W,RNQUIE: Simulation is complete.
When the new function is declared as static or virtual
A constructor can be declared as local or protected, but not as static or virtual. We’ll see more on this in a later session.
class ABC;
string fruit;
// Note that the constructor is defined as "virtual" which is not allowed
// in SystemVerilog. Class constructor cannot be "static" either
virtual function new ();
fruit = "Apple";
endfunction
endclass
// Instantiate the class object and print its contents
module tb;
initial begin
ABC abc = new();
$display ("fruit = %s", abc.fruit);
end
endmodule
Simulation Log
virtual function new ();
|
ncvlog: *E,BADQAL (testbench.sv,6|21): Lifetime or qualifier(s) 'virtual' not allowed before function new declaration.
Typed constructors
The difference here is that you can call the new() function of a subclass but assign it to the handle of a base class in a single statement. This is done by referencing the new() function of the subclass using scope operator :: as shown below.
class C;
endclass
class D extends C;
endclass
module tb;
initial begin
C c = D::new;
end
endmodule
Variable c of base class C now references a newly constructed object of type D. This achieves the same effect as the code given below.
module tb;
initial begin
D d = new;
C c = d;
end
endmodule
SystemVerilog ’this’ keyword
The this keyword is used to refer to class properties, parameters and methods of the current instance. It can only be used within non-static methods, constraints and covergroups. this is basically a pre-defined object handle that refers to the object that was used to invoke the method in which this is used.
Example
A very common way of using this is within the initialization block.
class Packet;
bit [31:0] addr;
function new (bit [31:0] addr);
// addr = addr; // Which addr should get assigned ?
this.addr = addr; // addr variable in Packet class should be
// assigned with local variable addr in new()
endfunction
endclass
Unless there is ambiguity in assignment, use of this keyword is not generally needed for specifying access to class members in methods.
SystemVerilog super Keyword
The super keyword is used from within a sub-class to refer to properties and methods of the base class. It is mandatory to use the super keyword to access properties and methods if they have been overridden by the sub-class.
Example
The super keyword can only be used within a class scope that derives from a base class. The code shown below will have compilation errors because extPacket is not a child of Packet. Note that new method is implicitly defined for every class definition, and hence we do not need a new defintion in the base class Packet.
class Packet;
int addr;
function display ();
$display ("[Base] addr=0x%0h", addr);
endfunction
endclass
class extPacket; // "extends" keyword missing -> not a child class
function new ();
super.new ();
endfunction
endclass
module tb;
Packet p;
extPacket ep;
initial begin
ep = new();
p = new();
p.display();
end
endmodule
Simulation Log
super.new ();
|
ncvlog: *E,CLSSPX (testbench.sv,12|8): 'super' can only be used within a class scope that derives from a base class.
Now let us see the output when extPacket is a derivative of class Packet.
class extPacket extends Packet; // extPacket is a child class of Packet
function new ();
super.new ();
endfunction
endclass
You can see from the simulation result below that there were no compilation errors.
Simulation Log
ncsim> run
[Base] addr=0x0
ncsim: *W,RNQUIE: Simulation is complete.
Accessing base class methods
In the example shown below, display method of the base class is called from the display method of the child class using super keyword.
class Packet;
int addr;
function display ();
$display ("[Base] addr=0x%0h", addr);
endfunction
endclass
class extPacket extends Packet;
function display();
super.display(); // Call base class display method
$display ("[Child] addr=0x%0h", addr);
endfunction
function new ();
super.new ();
endfunction
endclass
module tb;
Packet p;
extPacket ep;
initial begin
ep = new();
p = new();
ep.display();
end
endmodule
Simulation Log
ncsim> run
[Base] addr=0x0
[Child] addr=0x0
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Typedef Class
Sometimes the compiler errors out because of a class variable being used before the declaration of the class itself. For example, if two classes need a handle to each other, the classic puzzle of whether chicken or egg came first pops up. This is because the compiler processes the first class where it finds a reference to the second class being that which hasn’t been declared yet.
class ABC;
DEF def; // Error: DEF has not been declared yet
endclass
class DEF;
ABC abc;
endclass
Compilation Error
file: typdef-class.sv
DEF def;
|
ncvlog: *E,NOIPRT (typedef-class.sv,2|5): Unrecognized declaration 'DEF' could be an unsupported keyword, a spelling mistake or missing instance port list '()' [SystemVerilog].
In such cases you have to provide a forward declaration for the second class using typedef keyword. When the compiler see a typedef class, it will know that a definition for the class will be found later in the same file.
Usage
typedef class DEF; // Inform compiler that DEF might be
// used before actual class definition
class ABC;
DEF def; // Okay: Compiler knows that DEF
// declaration will come later
endclass
class DEF;
ABC abc;
endclass
By using a typedef DEF is declared to be of type class which is later proved to be the same. It is not necessary to specify that DEF is of type class in the typedef statement.
typedef DEF; // Legal
class ABC;
DEF def;
endclass
Using typedef with parameterized classes
A typedef can also be used on classes with a parameterized port list as shown below.
typedef XYZ;
module top;
XYZ #(8'h3f, real) xyz0; // positional parameter override
XYZ #(.ADDR(8'h60), .T(real)) xyz1; // named parameter override
endmodule
class XYZ #(parameter ADDR = 8'h00, type T = int);
endclass
SystemVerilog Inheritance
Inheritance is a concept in OOP that allows us to extend a class to create another class and have access to all the properties and methods of the original parent class from the handle of a new class object. The idea behind this scheme is to allow developers add in new properties and methods into the new class while still maintaining access to the original class members. This allows us to make modifications without touching the base class at all.
Example
ExtPacket is extended and hence is a child class of Packet. Being a child class, it inherits properties and methods from its parent. If there exists a function with the same name in both the parent and child class, then its invocation will depend on the type of the object handle used to call that function. In the example below, both Packet and ExtPacket have a function called display(). When this function is called by a child class handle, the child class display() function will be executed. If this function is called by a parent class handle, then the parent class display() function will be executed.
class Packet;
int addr;
function new (int addr);
this.addr = addr;
endfunction
function display ();
$display ("[Base] addr=0x%0h", addr);
endfunction
endclass
// A subclass called 'ExtPacket' is derived from the base class 'Packet' using
// 'extends' keyword which makes 'EthPacket' a child of the parent class 'Packet'
// The child class inherits all variables and methods from the parent class
class ExtPacket extends Packet;
// This is a new variable only available in child class
int data;
function new (int addr, data);
super.new (addr); // Calls 'new' method of parent class
this.data = data;
endfunction
function display ();
$display ("[Child] addr=0x%0h data=0x%0h", addr, data);
endfunction
endclass
module tb;
Packet bc; // bc stands for BaseClass
ExtPacket sc; // sc stands for SubClass
initial begin
bc = new (32'hface_cafe);
bc.display ();
sc = new (32'hfeed_feed, 32'h1234_5678);
sc.display ();
end
endmodule
Simulation Log
ncsim> run
[Base] addr=0xfacecafe
[Child] addr=0xfeedfeed data=0x12345678
ncsim: *W,RNQUIE: Simulation is complete.
It becomes a little more tricky when you try to assign a child class instance to a base class handle or vice versa. This is covered in more details under Polymorphism
SystemVerilog Polymorphism
Polymorphism allows the use of a variable of the base class type to hold subclass objects and to reference the methods of those subclasses directly from the superclass variable. It also allows a child class method to have a different definition than its parent class if the parent class method is virtual in nature.
Parent and Child Assignment
A class handle is just a container to hold either parent or child class objects. It is important to understand how parent class handles holding child objects and vice-versa behave in SystemVerilog.
Assign Child Class to Base Class
Taking the same example from Inheritance, we’ll assign a sub/child class instance sc to a base class handle bc.
module tb;
Packet bc; // bc stands for BaseClass
ExtPacket sc; // sc stands for SubClass
initial begin
sc = new (32'hfeed_feed, 32'h1234_5678);
// Assign sub-class to base-class handle
bc = sc;
bc.display ();
sc.display ();
end
endmodule
Simulation Log
Even though bc points to the child class instance, when display() function is called from bc it still invoked the display() function within the base class. This is because the function was called based on the type of the handle instead of the type of object the handle is pointing to. Now let’s try to reference a subclass member via a base class handle for which you’ll get a compilation error.
module tb;
Packet bc; // bc stands for BaseClass
ExtPacket sc; // sc stands for SubClass
initial begin
sc = new (32'hfeed_feed, 32'h1234_5678);
bc = sc;
// Print variable in sub-class that is pointed to by
// base class handle
$display ("data=0x%0h", bc.data);
end
endmodule
Simulation Log
Assign Base Class to Child Class
It is illegal to directly assign a variable of a superclass type to a variable of one of its subclass types and hence you’ll get a compilation error.
module
initial begin
bc = new (32'hface_cafe);
// Assign base class object to sub-class handle
sc = bc;
bc.display ();
end
endmodule
Simulation Log
However, $cast can be used to assign a superclass handle to a variable of a subclass type provided the superclass handle refers to an object that is assignment compatible with the subclass variable.
module
initial begin
bc = new (32'hface_cafe);
// Dynamic cast base class object to sub-class type
$cast (sc, bc);
bc.display ();
end
endmodule
Although the code will compile well, it will have a run-time simulation error because of the failure of $cast. This is because bc is not pointing to an object that is compatible with sc.
Simulation Log
Let’s make bc point to another subclass called sc2 and try the same thing. In this case, bc simply acts like a carrier.
initial begin
ExtPacket sc2;
bc = new (32'hface_cafe);
sc = new (32'hfeed_feed, 32'h1234_5678);
bc = sc;
// Dynamic cast sub class object in base class handle to sub-class type
$cast (sc2, bc);
sc2.display ();
$display ("data=0x%0h", sc2.data);
end
Simulation Log
Virtual Methods
A method in the parent class can be declared as virtual which will enable all child classes to override the method with a different definition, but the prototype containing return type and arguments shall remain the same.
class Base;
rand bit [7:0] addr;
rand bit [7:0] data;
// Parent class has a method called 'display' declared as virtual
virtual function void display(string tag="Thread1");
$display ("[Base] %s: addr=0x%0h data=0x%0h", tag, addr, data);
endfunction
endclass
class Child extends Base;
rand bit en;
// Child class redefines the method to also print 'en' variable
function void display(string tag="Thread1");
$display ("[Child] %s: addr=0x%0h data=0x%0h en=%0d", tag, addr, data, en);
endfunction
endclass
SystemVerilog Virtual Methods
In Inheritance, we saw that methods invoked by a base class handle which points to a child class instance would eventually end up executing the base class method instead of the one in child class. If that function in the base class was declared as virtual, only then the child class method will be executed.
bc = sc; // Base class handle is pointed to a sub class
bc.display (); // This calls the display() in base class and
// not the sub class as we might think
We’ll use the same classes from previous session and do a comparison with and without virtual function.
Without virtual keyword
// Without declaring display() as virtual
class Packet;
int addr;
function new (int addr);
this.addr = addr;
endfunction
// This is a normal function definition which
// starts with the keyword "function"
function void display ();
$display ("[Base] addr=0x%0h", addr);
endfunction
endclass
module tb;
Packet bc;
ExtPacket sc;
initial begin
sc = new (32'hfeed_feed, 32'h1234_5678);
bc = sc;
bc.display ();
end
endmodule
Simulation Log
ncsim> run
[Base] addr=0xfeedfeed
ncsim: *W,RNQUIE: Simulation is complete.
Note that the base class display() function gets executed.
With virtual keyword
class Packet;
int addr;
function new (int addr);
this.addr = addr;
endfunction
// Here the function is declared as "virtual"
// so that a different definition can be given
// in any child class
virtual function void display ();
$display ("[Base] addr=0x%0h", addr);
endfunction
endclass
module tb;
Packet bc;
ExtPacket sc;
initial begin
sc = new (32'hfeed_feed, 32'h1234_5678);
bc = sc;
bc.display ();
end
endmodule
Simulation Log
ncsim> run
[Child] addr=0xfeedfeed data=0x12345678
ncsim: *W,RNQUIE: Simulation is complete.
Note that the child class display() function gets executed when the base class function is made virtual.
The key takeaway is that you should always declare your base class methods as virtual so that already existing base class handles will now refer the function override in the child class.
SystemVerilog Static Variables & Functions
Each class instance would normally have a copy of each of its internal variables.
class Packet;
bit [15:0] addr;
bit [7:0] data;
function new (bit [15:0] ad, bit [7:0] d);
addr = ad;
data = d;
$display ("addr=0x%0h data=0x%0h", addr, data);
endfunction
endclass
module tb;
initial begin
Packet p1, p2, p3;
p1 = new (16'hdead, 8'h12);
p2 = new (16'hface, 8'hab);
p3 = new (16'hcafe, 8'hfc);
end
endmodule
Each of the class objects p1, p2, p3 will have addr and data variables within it.
Simulation Log
ncsim> run
addr=0xdead data=0x12
addr=0xface data=0xab
addr=0xcafe data=0xfc
ncsim: *W,RNQUIE: Simulation is complete.
Static Variables
When a variable inside a class is declared as static, that variable will be the only copy in all class instances. To demonstrate an example we’ll compare a static counter vs a non-static counter. The static counter is declared with static keyword and named as static_ctr while the normal counter variable is named as ctr. Both counters will be incremented within the new() function so that they are updated everytime an object is created.
class Packet;
bit [15:0] addr;
bit [7:0] data;
static int static_ctr = 0;
int ctr = 0;
function new (bit [15:0] ad, bit [7:0] d);
addr = ad;
data = d;
static_ctr++;
ctr++;
$display ("static_ctr=%0d ctr=%0d addr=0x%0h data=0x%0h", static_ctr, ctr, addr, data);
endfunction
endclass
module tb;
initial begin
Packet p1, p2, p3;
p1 = new (16'hdead, 8'h12);
p2 = new (16'hface, 8'hab);
p3 = new (16'hcafe, 8'hfc);
end
endmodule
You’ll see that the static counter is shared between all class objects p1, p2 and p3 and hence will increment to 3 when three packets are created. On the other hand, the normal counter variable ctr is not declared as static and hence every class object will have its own copy. This is the reason why ctr is still 1 after all three objects are created.
Simulation Log
ncsim> run
static_ctr=1 ctr=1 addr=0xdead data=0x12
static_ctr=2 ctr=1 addr=0xface data=0xab
static_ctr=3 ctr=1 addr=0xcafe data=0xfc
ncsim: *W,RNQUIE: Simulation is complete.
Declaring a variable as static can be very useful in cases where you want to know the total number of packets generated until a particular time.
Static functions
A static method follows all class scoping and access rules, but the only difference being that it can be called outside the class even with no class instantiation. A static method has no access to non-static members but it can directly access static class properties or call static methods of the same class. Also static methods cannot be virtual. Static function calls using class names need to be made through the scope operator ::.
class Packet;
static int ctr=0;
function new ();
ctr++;
endfunction
static function get_pkt_ctr ();
$display ("ctr=%0d", ctr);
endfunction
endclass
module tb;
Packet pkt [6];
initial begin
for (int i = 0; i < $size(pkt); i++) begin
pkt[i] = new;
end
Packet::get_pkt_ctr(); // Static call using :: operator
pkt[5].get_pkt_ctr(); // Normal call using instance
end
endmodule
Simulation Log
ncsim> run
ctr=6
ctr=6
ncsim: *W,RNQUIE: Simulation is complete.
Let’s add in a non-static member called mode and try to call that from our static function.
class Packet;
static int ctr=0;
bit [1:0] mode;
function new ();
ctr++;
endfunction
static function get_pkt_ctr ();
$display ("ctr=%0d mode=%0d", ctr, mode);
endfunction
endclass
It’s not allowed and will result in a compilation error.
Simulation Log
$display ("ctr=%0d mode=%0d", ctr, mode);
|
ncvlog: *E,CLSNSU (static-function.sv,10|40): A static class method cannot access non static class members.
SystemVerilog Copying Objects
In a previous post, key topics on class handles and objects were discussed which is essential to understand how shallow copy and [deep copy works.
Shallow Copy
Contents in pkt will be copied into pkt2 when pkt is used along with the new() constructor for the new object.
Packet pkt, pkt2;
pkt = new;
pkt2 = new pkt;
This method is known as a shallow copy, because all of the variables are copied across integers, strings, instance handles, etc but nested objects are not copied entirely. Only their handles will be assigned to the new object and hence both the packets will point to the same nested object instance. To illustrate this point let’s look at an example.
class Header;
int id;
function new (int id);
this.id = id;
endfunction
function showId();
$display ("id=0x%0d", id);
endfunction
endclass
class Packet;
int addr;
int data;
Header hdr;
function new (int addr, int data, int id);
hdr = new (id);
this.addr = addr;
this.data = data;
endfunction
function display (string name);
$display ("[%s] addr=0x%0h data=0x%0h id=%0d", name, addr, data, hdr.id);
endfunction
endclass
module tb;
Packet p1, p2;
initial begin
// Create a new pkt object called p1
p1 = new (32'hface_cafe, 32'h1234_5678, 26);
p1.display ("p1");
// Shallow copy p1 into p2; p2 is a new object with contents in p1
p2 = new p1;
p2.display ("p2");
// Now let's change the addr and id in p1
p1.addr = 32'habcd_ef12;
p1.data = 32'h5a5a_5a5a;
p1.hdr.id = 17;
p1.display ("p1");
// Print p2 and see that hdr.id points to the hdr in p1, while
// addr and data remain unchanged.
p2.display ("p2");
end
endmodule
The class Packet contains a nested class called Header. First we created a packet called p1 and assigned it with some values. Then p2 was created as a copy of p1 using the shallow copy method. To prove that only handles and not the entire object is copied, members of the p1 packet is modified including members within the nested class. When the contents in p2 are printed, we can see that the id member in the nested class remains the same.
Simulation Log
ncsim> run
[p1] addr=0xfacecafe data=0x12345678 id=26
[p2] addr=0xfacecafe data=0x12345678 id=26
[p1] addr=0xabcdef12 data=0x5a5a5a5a id=17
[p2] addr=0xfacecafe data=0x12345678 id=17
ncsim: *W,RNQUIE: Simulation is complete.
Deep Copy
A deep copy is where everything (including nested objects) is copied and typically custom code is required for this purpose.
Packet p1 = new;
Packet p2 = new;
p2.copy (p1);
Let’s add in a custom function called copy() within the Packet class to the example given above.
class Packet;
...
function copy (Packet p);
this.addr = p.addr;
this.data = p.data;
this.hdr.id = p.hdr.id;
endfunction
...
endclass
module tb;
Packet p1, p2;
initial begin
p1 = new (32'hface_cafe, 32'h1234_5678, 32'h1a);
p1.display ("p1");
p2 = new (1,2,3); // give some values
p2.copy (p1);
p2.display ("p2");
// Now let's change the addr and id in p1
p1.addr = 32'habcd_ef12;
p1.data = 32'h5a5a_5a5a;
p1.hdr.id = 32'h11;
p1.display ("p1");
// Now let's print p2 - you'll see the changes made to hdr id
// but not addr
p2.display ("p2");
end
endmodule
Note that we have invoked the custom copy() function here instead of the shallow copy method, and hence Header object contents are expected to be copied into p2 as well.
Simulation Log
ncsim> run
[p1] addr=0xfacecafe data=0x12345678 id=26
[p2] addr=0xfacecafe data=0x12345678 id=26
[p1] addr=0xabcdef12 data=0x5a5a5a5a id=17
[p2] addr=0xfacecafe data=0x12345678 id=26
ncsim: *W,RNQUIE: Simulation is complete.
Note that id of object p2 still holds onto the previous value even though p1’s id field was changed.
SystemVerilog Parameterized Classes
Why do we need parameterization for classes ?
At times it would be much easier to write a generic class which can be instantiated in multiple ways to achieve different array sizes or data types. This avoids the need to re-write code for specific features like size or type and instead allow a single specification to be used for different objects. This is achieved by extending the SystemVerilog parameter mechanism to classes.
Parameters are like constants that are local to the specified class. Classes are allowed to have default value for each parameter that can be overridden during class instantiation.
Syntax
// Declare parameterized class
class <name_of_class> #(<parameters>);
class Trans #(addr = 32);
// Override class parameter
<name_of_class> #(<parameters>) <name_of_inst>;
Trans #(.addr(16)) obj;
Examples
Parameterized Classes
Given below is a parameterized class which has size as the parameter that can be changed during instantiation.
// A class is parameterized by #()
// Here, we define a parameter called "size" and gives it
// a default value of 8. The "size" parameter is used to
// define the size of the "out" variable
class something #(int size = 8);
bit [size-1:0] out;
endclass
module tb;
// Override default value of 8 with the given values in #()
something #(16) sth1; // pass 16 as "size" to this class object
something #(.size (8)) sth2; // pass 8 as "size" to this class object
typedef something #(4) td_nibble; // create an alias for a class with "size" = 4 as "nibble"
td_nibble nibble;
initial begin
// 1. Instantiate class objects
sth1 = new;
sth2 = new;
nibble = new;
// 2. Print size of "out" variable. $bits() system task will return
// the number of bits in a given variable
$display ("sth1.out = %0d bits", $bits(sth1.out));
$display ("sth2.out = %0d bits", $bits(sth2.out));
$display ("nibble.out = %0d bits", $bits(nibble.out));
end
endmodule
Simulation Log
ncsim> run
sth1.out = 16 bits
sth2.out = 8 bits
nibble.out = 4 bits
ncsim: *W,RNQUIE: Simulation is complete.
Pass datatype as a parameter
Data-type is parameterized in this case and can be overridden during instantiation. In the previous case, we defined parameters to have a specific value.
// "T" is a parameter that is set to have a default value of "int"
// Hence "items" will be "int" by default
class stack #(type T = int);
T item;
function T add_a (T a);
return item + a;
endfunction
endclass
module tb;
stack st; // st.item is by default of int type
stack #(bit[3:0]) bs; // bs.item will become a 4-bit vector
stack #(real) rs; // rs.item will become a real number
initial begin
st = new;
bs = new;
rs = new;
// Assign different values, and add 10 to these values
// Then print the result - Note the different values printed
// that are affected by change in data type
st.item = -456;
$display ("st.item = %0d", st.add_a (10));
bs.item = 8'hA1;
$display ("bs.item = %0d", bs.add_a (10));
rs.item = 3.14;
$display ("rs.item = %0.2f", rs.add_a (10));
end
endmodule
Note that any type can be supplied as a parameter, including user-defined type such as class or struct.
Simulation Log
ncsim> run
st.item = -446
bs.item = 11
rs.item = 13.14
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog ’extern’
Class definitions can become very long with a lot of lines between class and endclass. This makes it difficult to understand what all functions and variables exist within the class because each function and task occupy quite a lot of lines.
Using extern qualifier in method declaration indicates that the implementation is done outside the body of this class.
Example
class ABC;
// Let this function be declared here and defined later
// by "extern" qualifier
extern function void display();
endclass
// Outside the class body, we have the implementation of the
// function declared as "extern"
function void ABC::display();
$display ("Hello world");
endfunction
module tb;
// Lets simply create a class object and call the display method
initial begin
ABC abc = new();
abc.display();
end
endmodule
Simulation Log
ncsim> run
Hello world
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog local
A member declared as local is available only to the methods of the same class, and are not accessible by child classes. However, nonlocal methods that access local members can be inherited and overridden by child class.
Example
In the following example, we will declare two variables - one public and another local. We expect to see an error when a local member of the class is accessed from somewhere outside the class. This is because the keyword local is used to keep members local and visible only within the same class.
When accessed from outside the class
class ABC;
// By default, all variables are public and for this example,
// let's create two variables - one public and the other "local"
byte public_var;
local byte local_var;
// This function simply prints these variable contents
function void display();
$display ("public_var=0x%0h, local_var=0x%0h", public_var, local_var);
endfunction
endclass
module tb;
initial begin
// Create a new class object, and call display method
ABC abc = new();
abc.display();
// Public variables can be accessed via the class handle
$display ("public_var = 0x%0h", abc.public_var);
// However, local variables cannot be accessed from outside
$display ("local_var = 0x%0h", abc.local_var);
end
endmodule
As expected, the compiler gives out a compilation error pointing to the line where a local member is accessed from outside the class.
Simulation Log
$display ("local_var = 0x%0h", abc.local_var);
|
ncvlog: *E,CLSNLO (testbench.sv,24|47): Access to local member 'local_var' in class 'ABC' is not allowed here.
irun: *E,VLGERR: An error occurred during parsing. Review the log file for errors with the code *E and fix those identified problems to proceed. Exiting with code (status 1).
In the above example, we can remove the line that causes a compilation error and see that we get a good output. The only other function that accesses the local member is the display() function.
module tb;
initial begin
ABC abc = new();
// This should be able to print local members of class ABC
// because display() is a member of ABC also
abc.display();
// Public variables can always be accessed via the class handle
$display ("public_var = 0x%0h", abc.public_var);
end
endmodule
Simulation Log
ncsim> run
public_var=0x0, local_var=0x0
public_var = 0x0
ncsim: *W,RNQUIE: Simulation is complete.
When accessed by child classes
In this example, let us try to access the local member from within a child class. We expect to see an error here also because local is not visible to child classes either.
// Define a base class and let the variable be "local" to this class
class ABC;
local byte local_var;
endclass
// Define another class that extends ABC and have a function that tries
// to access the local variable in ABC
class DEF extends ABC;
function show();
$display ("local_var = 0x%0h", local_var);
endfunction
endclass
module tb;
initial begin
// Create a new object of the child class, and call the show method
// This will give a compile time error because child classes cannot access
// base class "local" variables and methods
DEF def = new();
def.show();
end
endmodule
As expected, child classes cannot access the local members of their parent class.
Simulation Log
$display ("local_var = 0x%0h", local_var);
|
ncvlog: *E,CLSNLO (testbench.sv,10|43): Access to local member 'local_var' in class 'ABC' is not allowed here.
irun: *E,VLGERR: An error occurred during parsing. Review the log file for errors with the code *E and fix those identified problems to proceed. Exiting with code (status 1).
SystemVerilog Abstract Class
SystemVerilog prohibits a class declared as virtual to be directly instantiated and is called an abstract class.
Syntax
virtual class <class_name>
// class definition
endclass
However, this class can be extended to form other sub-classes which can then be instantiated. This is useful to enforce testcase developers to always extend a base class to form another class for their needs. So base classes are usually declared asvirtual although it is not mandatory.
Normal Class Example
class BaseClass;
int data;
function new();
data = 32'hc0de_c0de;
endfunction
endclass
module tb;
BaseClass base;
initial begin
base = new();
$display ("data=0x%0h", base.data);
end
endmodule
Simulation Log
ncsim> run
data=0xc0dec0de
ncsim: *W,RNQUIE: Simulation is complete.
Abstract Class Example
Let us declare the class BaseClass as virtual to make it an abstract class and see what happens.
virtual class BaseClass;
int data;
function new();
data = 32'hc0de_c0de;
endfunction
endclass
module tb;
BaseClass base;
initial begin
base = new();
$display ("data=0x%0h", base.data);
end
endmodule
A compilation error is reported by the simulator as shown below since abstract classes are not allowed to be instantiated.
Simulation Log
base = new();
|
ncvlog: *E,CNIABC (testbench.sv,12|5): An abstract (virtual) class cannot be instantiated.
Extending Abstract Classes
Abstract classes can be extended just like any other SystemVerilog class using the extends keyword like shown below.
virtual class BaseClass;
int data;
function new();
data = 32'hc0de_c0de;
endfunction
endclass
class ChildClass extends BaseClass;
function new();
data = 32'hfade_fade;
endfunction
endclass
module tb;
ChildClass child;
initial begin
child = new();
$display ("data=0x%0h", child.data);
end
endmodule
It can be seen from the simulation output below that it is perfectly valid to extend abstract classes to form other classes that can be instantiated using new() method.
Simulation Log
ncsim> run
data=0xfadefade
ncsim: *W,RNQUIE: Simulation is complete.
Pure Virtual Methods
A virtual method inside an abstract class can be declared with the keyword pure and is called a pure virtual method. Such methods only require a prototype to be specified within the abstract class and the implementation is left to defined within the sub-classes.
Pure Method Example
virtual class BaseClass;
int data;
pure virtual function int getData();
endclass
class ChildClass extends BaseClass;
virtual function int getData();
data = 32'hcafe_cafe;
return data;
endfunction
endclass
module tb;
ChildClass child;
initial begin
child = new();
$display ("data = 0x%0h", child.getData());
end
endmodule
The pure virtual method prototype and its implementation should have the same arguments and return type.
Simulation Log
ncsim> run
data = 0xcafecafe
ncsim: *W,RNQUIE: Simulation is complete.
SystemVerilog Testbench Example 1
In a previous article, concepts and components of a simple testbench was discussed. Let us look at a practical SystemVerilog testbench example with all those verification components and how concepts in SystemVerilog has been used to create a reusable environment.
Design
// Note that in this protocol, write data is provided
// in a single clock along with the address while read
// data is received on the next clock, and no transactions
// can be started during that time indicated by "ready"
// signal.
module reg_ctrl
# (
parameter ADDR_WIDTH = 8,
parameter DATA_WIDTH = 16,
parameter DEPTH = 256,
parameter RESET_VAL = 16'h1234
)
( input clk,
input rstn,
input [ADDR_WIDTH-1:0] addr,
input sel,
input wr,
input [DATA_WIDTH-1:0] wdata,
output reg [DATA_WIDTH-1:0] rdata,
output reg ready);
// Some memory element to store data for each addr
reg [DATA_WIDTH-1:0] ctrl [DEPTH];
reg ready_dly;
wire ready_pe;
// If reset is asserted, clear the memory element
// Else store data to addr for valid writes
// For reads, provide read data back
always @ (posedge clk) begin
if (!rstn) begin
for (int i = 0; i < DEPTH; i += 1) begin
ctrl[i] <= RESET_VAL;
end
end else begin
if (sel & ready & wr) begin
ctrl[addr] <= wdata;
end
if (sel & ready & !wr) begin
rdata <= ctrl[addr];
end else begin
rdata <= 0;
end
end
end
// Ready is driven using this always block
// During reset, drive ready as 1
// Else drive ready low for a clock low
// for a read until the data is given back
always @ (posedge clk) begin
if (!rstn) begin
ready <= 1;
end else begin
if (sel & ready_pe) begin
ready <= 1;
end
if (sel & ready & !wr) begin
ready <= 0;
end
end
end
// Drive internal signal accordingly
always @ (posedge clk) begin
if (!rstn) ready_dly <= 1;
else ready_dly <= ready;
end
assign ready_pe = ~ready & ready_dly;
endmodule
Transaction Object
class reg_item;
// This is the base transaction object that will be used
// in the environment to initiate new transactions and
// capture transactions at DUT interface
rand bit [7:0] addr;
rand bit [15:0] wdata;
bit [15:0] rdata;
rand bit wr;
// This function allows us to print contents of the data packet
// so that it is easier to track in a logfile
function void print(string tag="");
$display ("T=%0t [%s] addr=0x%0h wr=%0d wdata=0x%0h rdata=0x%0h",
$time, tag, addr, wr, wdata, rdata);
endfunction
endclass
Driver
// The driver is responsible for driving transactions to the DUT
// All it does is to get a transaction from the mailbox if it is
// available and drive it out into the DUT interface.
class driver;
virtual reg_if vif;
event drv_done;
mailbox drv_mbx;
task run();
$display ("T=%0t [Driver] starting ...", $time);
@ (posedge vif.clk);
// Try to get a new transaction every time and then assign
// packet contents to the interface. But do this only if the
// design is ready to accept new transactions
forever begin
reg_item item;
$display ("T=%0t [Driver] waiting for item ...", $time);
drv_mbx.get(item);
item.print("Driver");
vif.sel <= 1;
vif.addr <= item.addr;
vif.wr <= item.wr;
vif.wdata <= item.wdata;
@ (posedge vif.clk);
while (!vif.ready) begin
$display ("T=%0t [Driver] wait until ready is high", $time);
@(posedge vif.clk);
end
// When transfer is over, raise the done event
vif.sel <= 0; ->drv_done;
end
endtask
endclass
Monitor
// The monitor has a virtual interface handle with which it can monitor
// the events happening on the interface. It sees new transactions and then
// captures information into a packet and sends it to the scoreboard
// using another mailbox.
class monitor;
virtual reg_if vif;
mailbox scb_mbx; // Mailbox connected to scoreboard
task run();
$display ("T=%0t [Monitor] starting ...", $time);
// Check forever at every clock edge to see if there is a
// valid transaction and if yes, capture info into a class
// object and send it to the scoreboard when the transaction
// is over.
forever begin
@ (posedge vif.clk);
if (vif.sel) begin
reg_item item = new;
item.addr = vif.addr;
item.wr = vif.wr;
item.wdata = vif.wdata;
if (!vif.wr) begin
@(posedge vif.clk);
item.rdata = vif.rdata;
end
item.print("Monitor");
scb_mbx.put(item);
end
end
endtask
endclass
Scoreboard
// The scoreboard is responsible to check data integrity. Since the design
// stores data it receives for each address, scoreboard helps to check if the
// same data is received when the same address is read at any later point
// in time. So the scoreboard has a "memory" element which updates it
// internally for every write operation.
class scoreboard;
mailbox scb_mbx;
reg_item refq[256];
task run();
forever begin
reg_item item;
scb_mbx.get(item);
item.print("Scoreboard");
if (item.wr) begin
if (refq[item.addr] == null)
refq[item.addr] = new;
refq[item.addr] = item;
$display ("T=%0t [Scoreboard] Store addr=0x%0h wr=0x%0h data=0x%0h", $time, item.addr, item.wr, item.wdata);
end
if (!item.wr) begin
if (refq[item.addr] == null)
if (item.rdata != 'h1234)
$display ("T=%0t [Scoreboard] ERROR! First time read, addr=0x%0h exp=1234 act=0x%0h",
$time, item.addr, item.rdata);
else
$display ("T=%0t [Scoreboard] PASS! First time read, addr=0x%0h exp=1234 act=0x%0h",
$time, item.addr, item.rdata);
else
if (item.rdata != refq[item.addr].wdata)
$display ("T=%0t [Scoreboard] ERROR! addr=0x%0h exp=0x%0h act=0x%0h",
$time, item.addr, refq[item.addr].wdata, item.rdata);
else
$display ("T=%0t [Scoreboard] PASS! addr=0x%0h exp=0x%0h act=0x%0h",
$time, item.addr, refq[item.addr].wdata, item.rdata);
end
end
endtask
endclass
Environment
// The environment is a container object simply to hold all verification
// components together. This environment can then be reused later and all
// components in it would be automatically connected and available for use
// This is an environment without a generator.
class env;
driver d0; // Driver to design
monitor m0; // Monitor from design
scoreboard s0; // Scoreboard connected to monitor
mailbox scb_mbx; // Top level mailbox for SCB <-> MON
virtual reg_if vif; // Virtual interface handle
// Instantiate all testbench components
function new();
d0 = new;
m0 = new;
s0 = new;
scb_mbx = new();
endfunction
// Assign handles and start all components so that
// they all become active and wait for transactions to be
// available
virtual task run();
d0.vif = vif;
m0.vif = vif;
m0.scb_mbx = scb_mbx;
s0.scb_mbx = scb_mbx;
fork
s0.run();
d0.run();
m0.run();
join_any
endtask
endclass
Test
// an environment without the generator and hence the stimulus should be
// written in the test.
class test;
env e0;
mailbox drv_mbx;
function new();
drv_mbx = new();
e0 = new();
endfunction
virtual task run();
e0.d0.drv_mbx = drv_mbx;
fork
e0.run();
join_none
apply_stim();
endtask
virtual task apply_stim();
reg_item item;
$display ("T=%0t [Test] Starting stimulus ...", $time);
item = new;
item.randomize() with { addr == 8'haa; wr == 1; };
drv_mbx.put(item);
item = new;
item.randomize() with { addr == 8'haa; wr == 0; };
drv_mbx.put(item);
endtask
endclass
Interface
// The interface allows verification components to access DUT signals
// using a virtual interface handle
interface reg_if (input bit clk);
logic rstn;
logic [7:0] addr;
logic [15:0] wdata;
logic [15:0] rdata;
logic wr;
logic sel;
logic ready;
endinterface
Testbench Top
// Top level testbench contains the interface, DUT and test handles which
// can be used to start test components once the DUT comes out of reset. Or
// the reset can also be a part of the test class in which case all you need
// to do is start the test's run method.
module tb;
reg clk;
always #10 clk = ~clk;
reg_if _if (clk);
reg_ctrl u0 ( .clk (clk),
.addr (_if.addr),
.rstn(_if.rstn),
.sel (_if.sel),
.wr (_if.wr),
.wdata (_if.wdata),
.rdata (_if.rdata),
.ready (_if.ready));
initial begin
new_test t0;
clk <= 0;
_if.rstn <= 0;
_if.sel <= 0;
#20 _if.rstn <= 1;
t0 = new;
t0.e0.vif = _if;
t0.run();
// Once the main stimulus is over, wait for some time
// until all transactions are finished and then end
// simulation. Note that $finish is required because
// there are components that are running forever in
// the background like clk, monitor, driver, etc
#200 $finish;
end
// Simulator dependent system tasks that can be used to
// dump simulation waves.
initial begin
$dumpvars;
$dumpfile("dump.vcd");
end
endmodule
SystemVerilog Testbench Example 2
This is another example of a SystemVerilog testbench using OOP concepts like inheritance, polymorphism to build a functional testbench for a simple design.
Design
module switch
# (parameter ADDR_WIDTH = 8,
parameter DATA_WIDTH = 16,
parameter ADDR_DIV = 8'h3F
)
( input clk,
input rstn,
input vld,
input [ADDR_WIDTH-1:0] addr,
input [DATA_WIDTH-1:0] data,
output reg [ADDR_WIDTH-1:0] addr_a,
output reg [DATA_WIDTH-1:0] data_a,
output reg [ADDR_WIDTH-1:0] addr_b,
output reg [DATA_WIDTH-1:0] data_b
);
always @ (posedge clk) begin
if (!rstn) begin
addr_a <= 0;
data_a <= 0;
addr_b <= 0;
data_b <= 0; end else begin if (vld) begin if (addr >= 0 & addr <= ADDR_DIV) begin
addr_a <= addr;
data_a <= data;
addr_b <= 0;
data_b <= 0;
end else begin
addr_a <= 0;
data_a <= 0;
addr_b <= addr;
data_b <= data;
end
end
end
end
endmodule
Transaction Object
// This is the base transaction object that will be used
// in the environment to initiate new transactions and
// capture transactions at DUT interface
class switch_item;
rand bit [7:0] addr;
rand bit [15:0] data;
bit [7:0] addr_a;
bit [15:0] data_a;
bit [7:0] addr_b;
bit [15:0] data_b;
// This function allows us to print contents of the data
// packet so that it is easier to track in a logfile
function void print (string tag="");
$display ("T=%0t %s addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h addr_b=0x%0h data_b=0x%0h",
$time, tag, addr, data, addr_a, data_a, addr_b, data_b);
endfunction
endclass
Generator
// The generator class is used to generate a random
// number of transactions with random addresses and data
// that can be driven to the design
class generator;
mailbox drv_mbx;
event drv_done;
int num = 20;
task run();
for (int i = 0; i < num; i++) begin
switch_item item = new;
item.randomize();
$display ("T=%0t [Generator] Loop:%0d/%0d create next item", $time, i+1, num);
drv_mbx.put(item);
@(drv_done);
end
$display ("T=%0t [Generator] Done generation of %0d items", $time, num);
endtask
endclass
Driver
// The driver is responsible for driving transactions to the DUT
// All it does is to get a transaction from the mailbox if it is
// available and drive it out into the DUT interface.
class driver;
virtual switch_if vif;
event drv_done;
mailbox drv_mbx;
task run();
$display ("T=%0t [Driver] starting ...", $time);
@ (posedge vif.clk);
// Try to get a new transaction every time and then assign
// packet contents to the interface. But do this only if the
// design is ready to accept new transactions
forever begin
switch_item item;
$display ("T=%0t [Driver] waiting for item ...", $time);
drv_mbx.get(item);
item.print("Driver");
vif.vld <= 1;
vif.addr <= item.addr;
vif.data <= item.data;
// When transfer is over, raise the done event
@ (posedge vif.clk);
vif.vld <= 0; ->drv_done;
end
endtask
endclass
Monitor
// The monitor has a virtual interface handle with which
// it can monitor the events happening on the interface.
// It sees new transactions and then captures information
// into a packet and sends it to the scoreboard
// using another mailbox.
class monitor;
virtual switch_if vif;
mailbox scb_mbx;
semaphore sema4;
function new ();
sema4 = new(1);
endfunction
task run();
$display ("T=%0t [Monitor] starting ...", $time);
// To get a pipeline effect of transfers, fork two threads
// where each thread uses a semaphore for the address phase
fork
sample_port("Thread0");
sample_port("Thread1");
join
endtask
task sample_port(string tag="");
// This task monitors the interface for a complete
// transaction and pushes into the mailbox when the
// transaction is complete
forever begin
@(posedge vif.clk);
if (vif.rstn & vif.vld) begin
switch_item item = new;
sema4.get();
item.addr = vif.addr;
item.data = vif.data;
$display("T=%0t [Monitor] %s First part over",
$time, tag);
@(posedge vif.clk);
sema4.put();
item.addr_a = vif.addr_a;
item.data_a = vif.data_a;
item.addr_b = vif.addr_b;
item.data_b = vif.data_b;
$display("T=%0t [Monitor] %s Second part over",
$time, tag);
scb_mbx.put(item);
item.print({"Monitor_", tag});
end
end
endtask
endclass
Scoreboard
// The scoreboard is responsible to check data integrity. Since
// the design routes packets based on an address range, the
// scoreboard checks that the packet's address is within valid
// range.
class scoreboard;
mailbox scb_mbx;
task run();
forever begin
switch_item item;
scb_mbx.get(item);
if (item.addr inside {[0:'h3f]}) begin
if (item.addr_a != item.addr | item.data_a != item.data)
$display ("T=%0t [Scoreboard] ERROR! Mismatch addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h", $time, item.addr, item.data, item.addr_a, item.data_a);
else
$display ("T=%0t [Scoreboard] PASS! Mismatch addr=0x%0h data=0x%0h addr_a=0x%0h data_a=0x%0h", $time, item.addr, item.data, item.addr_a, item.data_a);
end else begin
if (item.addr_b != item.addr | item.data_b != item.data)
$display ("T=%0t [Scoreboard] ERROR! Mismatch addr=0x%0h data=0x%0h addr_b=0x%0h data_b=0x%0h", $time, item.addr, item.data, item.addr_b, item.data_b);
else
$display ("T=%0t [Scoreboard] PASS! Mismatch addr=0x%0h data=0x%0h addr_b=0x%0h data_b=0x%0h", $time, item.addr, item.data, item.addr_b, item.data_b);
end
end
endtask
endclass
Environment
// The environment is a container object simply to hold
// all verification components together. This environment can
// then be reused later and all components in it would be
// automatically connected and available for use
class env;
driver d0; // Driver handle
monitor m0; // Monitor handle
generator g0; // Generator Handle
scoreboard s0; // Scoreboard handle
mailbox drv_mbx; // Connect GEN -> DRV
mailbox scb_mbx; // Connect MON -> SCB
event drv_done; // Indicates when driver is done
virtual switch_if vif; // Virtual interface handle
function new();
d0 = new;
m0 = new;
g0 = new;
s0 = new;
drv_mbx = new();
scb_mbx = new();
d0.drv_mbx = drv_mbx;
g0.drv_mbx = drv_mbx;
m0.scb_mbx = scb_mbx;
s0.scb_mbx = scb_mbx;
d0.drv_done = drv_done;
g0.drv_done = drv_done;
endfunction
virtual task run();
d0.vif = vif;
m0.vif = vif;
fork
d0.run();
m0.run();
g0.run();
s0.run();
join_any
endtask
endclass
Test
// Test class instantiates the environment and starts it.
class test;
env e0;
function new();
e0 = new;
endfunction
task run();
e0.run();
endtask
endclass
Interface
// Design interface used to monitor activity and capture/drive
// transactions
interface switch_if (input bit clk);
logic rstn;
logic vld;
logic [7:0] addr;
logic [15:0] data;
logic [7:0] addr_a;
logic [15:0] data_a;
logic [7:0] addr_b;
logic [15:0] data_b;
endinterface
Testbench Top
// Top level testbench module to instantiate design, interface
// start clocks and run the test
module tb;
reg clk;
always #10 clk =~ clk;
switch_if _if (clk);
switch u0 ( .clk(clk),
.rstn(_if.rstn),
.addr(_if.addr),
.data(_if.data),
.vld (_if.vld),
.addr_a(_if.addr_a),
.data_a(_if.data_a),
.addr_b(_if.addr_b),
.data_b(_if.data_b));
test t0;
initial begin
{clk, _if.rstn} <= 0;
// Apply reset and start stimulus
#20 _if.rstn <= 1;
t0 = new;
t0.e0.vif = _if;
t0.run();
// Because multiple components and clock are running
// in the background, we need to call $finish explicitly
#50 $finish;
end
// System tasks to dump VCD waveform file
initial begin
$dumpvars;
$dumpfile ("dump.vcd");
end
endmodule
SystemVerilog Testbench Example Adder
Here is an example of how a SystemVerilog testbench can be constructed to verify functionality of a simple adder. Remember that the goal here is to develop a modular and scalable testbench architecture with all the standard verification components in a testbench.
You can also write Verilog code for testing such simple circuits, but bigger and more complex designs typically require a scalable testbench architecture and this is an example of how to build a scalable testbench. Different designs require different driver, monitor and scoreboard implementation that depends on design specifics.
Design
// An adder is combinational logic and does not
// have a clock
module my_adder (adder_if _if);
always_comb begin
if (_if.rstn) begin
_if.sum <= 0;
_if.carry <= 0;
end else begin
{_if.carry, _if.sum} <= _if.a + _if.b;
end
end
endmodule
Transaction Object
// To verify that the adder adds, we also need to check that it
// does not add when rstn is 0, and hence rstn should also be
// randomized along with a and b.
class Packet;
rand bit rstn;
rand bit[7:0] a;
rand bit[7:0] b;
bit [7:0] sum;
bit carry;
// Print contents of the data packet
function void print(string tag="");
$display ("T=%0t %s a=0x%0h b=0x%0h sum=0x%0h carry=0x%0h", $time, tag, a, b, sum, carry);
endfunction
// This is a utility function to allow copying contents in
// one Packet variable to another.
function void copy(Packet tmp);
this.a = tmp.a;
this.b = tmp.b;
this.rstn = tmp.rstn;
this.sum = tmp.sum;
this.carry = tmp.carry;
endfunction
endclass
Driver
class driver;
virtual adder_if m_adder_vif;
virtual clk_if m_clk_vif;
event drv_done;
mailbox drv_mbx;
task run();
$display ("T=%0t [Driver] starting ...", $time);
// Try to get a new transaction every time and then assign
// packet contents to the interface. But do this only if the
// design is ready to accept new transactions
forever begin
Packet item;
$display ("T=%0t [Driver] waiting for item ...", $time);
drv_mbx.get(item);
@ (posedge m_clk_vif.tb_clk);
item.print("Driver");
m_adder_vif.rstn <= item.rstn;
m_adder_vif.a <= item.a;
m_adder_vif.b <= item.b; ->drv_done;
end
endtask
endclass
Monitor
// The monitor has a virtual interface handle with which it can monitor
// the events happening on the interface. It sees new transactions and then
// captures information into a packet and sends it to the scoreboard
// using another mailbox.
class monitor;
virtual adder_if m_adder_vif;
virtual clk_if m_clk_vif;
mailbox scb_mbx; // Mailbox connected to scoreboard
task run();
$display ("T=%0t [Monitor] starting ...", $time);
// Check forever at every clock edge to see if there is a
// valid transaction and if yes, capture info into a class
// object and send it to the scoreboard when the transaction
// is over.
forever begin
Packet m_pkt = new();
@(posedge m_clk_vif.tb_clk);
#1;
m_pkt.a = m_adder_vif.a;
m_pkt.b = m_adder_vif.b;
m_pkt.rstn = m_adder_vif.rstn;
m_pkt.sum = m_adder_vif.sum;
m_pkt.carry = m_adder_vif.carry;
m_pkt.print("Monitor");
scb_mbx.put(m_pkt);
end
endtask
endclass
Scoreboard
// The scoreboard is responsible to check data integrity. Since the design
// simple adds inputs to give sum and carry, scoreboard helps to check if the
// output has changed for given set of inputs based on expected logic
class scoreboard;
mailbox scb_mbx;
task run();
forever begin
Packet item, ref_item;
scb_mbx.get(item);
item.print("Scoreboard");
// Copy contents from received packet into a new packet so
// just to get a and b.
ref_item = new();
ref_item.copy(item);
// Let us calculate the expected values in carry and sum
if (ref_item.rstn)
{ref_item.carry, ref_item.sum} = ref_item.a + ref_item.b;
else
{ref_item.carry, ref_item.sum} = 0;
// Now, carry and sum outputs in the reference variable can be compared
// with those in the received packet
if (ref_item.carry != item.carry) begin
$display("[%0t] Scoreboard Error! Carry mismatch ref_item=0x%0h item=0x%0h", $time, ref_item.carry, item.carry);
end else begin
$display("[%0t] Scoreboard Pass! Carry match ref_item=0x%0h item=0x%0h", $time, ref_item.carry, item.carry);
end
if (ref_item.sum != item.sum) begin
$display("[%0t] Scoreboard Error! Sum mismatch ref_item=0x%0h item=0x%0h", $time, ref_item.sum, item.sum);
end else begin
$display("[%0t] Scoreboard Pass! Sum match ref_item=0x%0h item=0x%0h", $time, ref_item.sum, item.sum);
end
end
endtask
endclass
Generator
// Sometimes we simply need to generate N random transactions to random
// locations so a generator would be useful to do just that. In this case
// loop determines how many transactions need to be sent
class generator;
int loop = 10;
event drv_done;
mailbox drv_mbx;
task run();
for (int i = 0; i < loop; i++) begin
Packet item = new;
item.randomize();
$display ("T=%0t [Generator] Loop:%0d/%0d create next item", $time, i+1, loop);
drv_mbx.put(item);
$display ("T=%0t [Generator] Wait for driver to be done", $time);
@(drv_done);
end
endtask
endclass
Environment
// Lets say that the environment class was already there, and generator is
// a new component that needs to be included in the ENV.
class env;
generator g0; // Generate transactions
driver d0; // Driver to design
monitor m0; // Monitor from design
scoreboard s0; // Scoreboard connected to monitor
mailbox scb_mbx; // Top level mailbox for SCB <-> MON
virtual adder_if m_adder_vif; // Virtual interface handle
virtual clk_if m_clk_vif; // TB clk
event drv_done;
mailbox drv_mbx;
function new();
d0 = new;
m0 = new;
s0 = new;
scb_mbx = new();
g0 = new;
drv_mbx = new;
endfunction
virtual task run();
// Connect virtual interface handles
d0.m_adder_vif = m_adder_vif;
m0.m_adder_vif = m_adder_vif;
d0.m_clk_vif = m_clk_vif;
m0.m_clk_vif = m_clk_vif;
// Connect mailboxes between each component
d0.drv_mbx = drv_mbx;
g0.drv_mbx = drv_mbx;
m0.scb_mbx = scb_mbx;
s0.scb_mbx = scb_mbx;
// Connect event handles
d0.drv_done = drv_done;
g0.drv_done = drv_done;
// Start all components - a fork join_any is used because
// the stimulus is generated by the generator and we want the
// simulation to exit only when the generator has finished
// creating all transactions. Until then all other components
// have to run in the background.
fork
s0.run();
d0.run();
m0.run();
g0.run();
join_any
endtask
endclass
Test
// The test can instantiate any environment. In this test, we are using
// an environment without the generator and hence the stimulus should be
// written in the test.
class test;
env e0;
mailbox drv_mbx;
function new();
drv_mbx = new();
e0 = new();
endfunction
virtual task run();
e0.d0.drv_mbx = drv_mbx;
e0.run();
endtask
endclass
Interface
// Adder interface contains all signals that the adder requires
// to operate
interface adder_if();
logic rstn;
logic [7:0] a;
logic [7:0] b;
logic [7:0] sum;
logic carry;
endinterface
// Although an adder does not have a clock, let us create a mock clock
// used in the testbench to synchronize when value is driven and when
// value is sampled. Typically combinational logic is used between
// sequential elements like FF in a real circuit. So, let us assume
// that inputs to the adder is provided at some posedge clock. But because
// the design does not have clock in its input, we will keep this clock
// in a separate interface that is available only to testbench components
interface clk_if();
logic tb_clk;
initial tb_clk <= 0;
always #10 tb_clk = ~tb_clk;
endinterface
Testbench Top
module tb;
bit tb_clk;
clk_if m_clk_if ();
adder_if m_adder_if ();
my_adder u0 (m_adder_if);
initial begin
test t0;
t0 = new;
t0.e0.m_adder_vif = m_adder_if;
t0.e0.m_clk_vif = m_clk_if;
t0.run();
// Once the main stimulus is over, wait for some time
// until all transactions are finished and then end
// simulation. Note that $finish is required because
// there are components that are running forever in
// the background like clk, monitor, driver, etc
#50 $finish;
end
endmodule
Buggy Design
Although the previous simulation showed everything as pass, how do we know if there is a bug in the checker ? Let us introduce a bug in the design to see if the checker fails.
module my_adder (adder_if _if);
always_comb begin
// Let sum and carry be reset when rstn is 1 instead of 0
// A simple but yet possible design bug
if (_if.rstn) begin
_if.sum <= 0;
_if.carry <= 0;
end else begin
{_if.carry, _if.sum} <= _if.a + _if.b;
end
end
endmodule
See that the checker now reports an error which proves that the checker is implemented correctly.
ncsim> run
T=0 [Driver] starting ...
T=0 [Driver] waiting for item ...
T=0 [Monitor] starting ...
T=0 [Generator] Loop:1/5 create next item
T=0 [Generator] Wait for driver to be done
T=10 Driver a=0x16 b=0x11 sum=0x0 carry=0x0
T=10 [Driver] waiting for item ...
T=10 [Generator] Loop:2/5 create next item
T=10 [Generator] Wait for driver to be done
T=11 Monitor a=0x16 b=0x11 sum=0x27 carry=0x0
T=11 Scoreboard a=0x16 b=0x11 sum=0x27 carry=0x0
[11] Scoreboard Pass! Carry match ref_item=0x0 item=0x0
[11] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x27
T=30 Driver a=0xde b=0x6 sum=0x0 carry=0x0
T=30 [Driver] waiting for item ...
T=30 [Generator] Loop:3/5 create next item
T=30 [Generator] Wait for driver to be done
T=31 Monitor a=0xde b=0x6 sum=0xe4 carry=0x0
T=31 Scoreboard a=0xde b=0x6 sum=0xe4 carry=0x0
[31] Scoreboard Pass! Carry match ref_item=0x0 item=0x0
[31] Scoreboard Error! Sum mismatch ref_item=0x0 item=0xe4
T=50 Driver a=0xb1 b=0xbd sum=0x0 carry=0x0
T=50 [Driver] waiting for item ...
T=50 [Generator] Loop:4/5 create next item
T=50 [Generator] Wait for driver to be done
T=51 Monitor a=0xb1 b=0xbd sum=0x6e carry=0x1
T=51 Scoreboard a=0xb1 b=0xbd sum=0x6e carry=0x1
[51] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1
[51] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x6e
T=70 Driver a=0x63 b=0xfb sum=0x0 carry=0x0
T=70 [Driver] waiting for item ...
T=70 [Generator] Loop:5/5 create next item
T=70 [Generator] Wait for driver to be done
T=71 Monitor a=0x63 b=0xfb sum=0x0 carry=0x0
T=71 Scoreboard a=0x63 b=0xfb sum=0x0 carry=0x0
[71] Scoreboard Error! Carry mismatch ref_item=0x1 item=0x0
[71] Scoreboard Error! Sum mismatch ref_item=0x5e item=0x0
T=90 Driver a=0x71 b=0xbc sum=0x0 carry=0x0
T=90 [Driver] waiting for item ...
T=91 Monitor a=0x71 b=0xbc sum=0x2d carry=0x1
T=91 Scoreboard a=0x71 b=0xbc sum=0x2d carry=0x1
[91] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1
[91] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x2d
T=111 Monitor a=0x71 b=0xbc sum=0x2d carry=0x1
T=111 Scoreboard a=0x71 b=0xbc sum=0x2d carry=0x1
[111] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1
[111] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x2d
T=131 Monitor a=0x71 b=0xbc sum=0x2d carry=0x1
T=131 Scoreboard a=0x71 b=0xbc sum=0x2d carry=0x1
[131] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1
[131] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x2d
Simulation complete via $finish(1) at time 140 NS + 0
./testbench.sv:265 #50 $finish;