Now let's take a look at some of the common ways of writing constraint expressions inside a constraint block.

Simple expressions

Note that there can be only one relational operator < <= > >= in an expression.

	class MyClass;
		rand bit [7:0] min, typ, max;
		// Valid expression
		constraint my_range { 0 < min;
		                      typ < max; typ > min; 
		                      max < 128; }
		// Use of multiple operators in a single expression is not allowed
		constraint my_error { 0 < min < typ < max < 128; } // This will set min to 16 and randomize all others constraint my_min { min == 16; } // This will set max to a random value greater than or equal to 64 constraint my_max { max >= 64; }


Constraint blocks are class members just like variables, functions and tasks. They have unique names within a class. These blocks of expressions are typically used to limit the values of random variables within certain values as specified within the constraint block.


	constraint  [name_of_constraint] {  [expression 1];
	                                    [expression N]; }


Expressions listed within the curly braces specify the conditions a solver has to take into account when assigning random values for the variables. It is not necessary to have a single constraint for every variable nor is it required to restrict a constraint block to have conditions pertaining only to a single variable. However you cannot have conflicting constraints spread out in multiple blocks, unless they are turned off using constraint_mode() method which we will see in Disable Constraints.

Variables are declared random using the rand or randc keyword. They can be used on normal variables, arrays, dynamic arrays or queues.


class Packet;
	rand int   		count;
	rand byte  		master [$];
	rand bit [7:0]  data [];


What are direct tests ?

Verification engineers will first create something known as a verification plan that details every feature of the design required to be tested in RTL simulations and how each test will create independent scenarios that target a particular feature.

For example, if there's a peripheral that needs its registers to be configured such that it starts an AXI bus transaction, then we would have different tests to configure those registers differently and achieve a good coverage.

These are direct tests where each test does a particular task to accomplish something.

What are randomized tests ?

Complex designs have a lot of scenarios and many corner cases that are better verified by randomized tests and take much less effort and time. Taking the same example from above, a test will configure the peripheral registers with random values every time the test is run with a different seed thereby achieving different scenarios for every run. This will ensure that we hit corner cases and uncover any hidden bugs.


SystemVerilog allows users to specify constraints in a compact, declarative way which are then processed by an internal solver to generate random values that satisfy all conditions. Basically constraints are nothing more than a way to let us define what legal values should be assigned to the random variables. A normal variable is declared to be random by the keyword rand.

class Pkt;
	rand bit [7:0] addr;
	rand bit [7:0] data;
	constraint addr_limit { addr <= 8'hB; }


The example above declares a class called Pkt with a constraint on its address field. Note that both its properties are prefixed with the rand keyword which tells the solver that these variables should be randomized when asked to. The constraint is called addr_limit and specifies that the solver can allot any random value for the address that is below or equal to 8'h8. Since the 8-bit variable addr is of type bit, it can have any value from 0 to 255, but with the constraint valid values will be limited to 11.

As you can see, this powerful feature will allow us to create variables that are constrained within ranges that are valid for the design and will have a much better verification impact. In the next few sessions, we'll see how to effectively use different constructs within SystemVerilog that allow us to describe constraints in a better way.

A module is the fundamental construct used for building designs. Each module can contain hierarchies of other modules, nets, variables and other procedural blocks to describe any hardware functionality. The testbench on the other hand is a complete environment to verify the design and hence an emphasis is placed on the way it is modeled to make it more re-usable and effective. It must be properly initialized and synchronized avoiding race conditions between the design and the testbench.

What is the need for a program block ?

SystemVerilog program block was introduced for the following reasons.

  • To provide an entry point to the execution of testbenches
  • To create a container to hold all other testbench data such as tasks, class objects and functions
  • Avoid race conditions with the design by getting executed during the reactive region of a simulation cycle

The reactive region is one of the last few phases before simulation time advances, and by then all design element statements would have been executed and testbench will see the updated value. It is important to have this demarcation between the execution of design and testbench statements because it will give a more deterministic output between simulators.


program [name] [port_list];



	program test1 (input clk, reset);
		initial ...
	program test2 (interface wb_if);
		initial ...
	program test3;
		initial ...


A program block can be nested within modules and interfaces and hence multiple programs within the same module can share variables local to that scope. In the example below, mode is a local variable within tb and can be accessed by both programs p1 and p2.

module tb;
	bit [3:0] mode;
	program p1;
	program p2;