SystemVerilog Data Types This Tutorial Describes The New Data Types
SystemVerilog Data Types This Tutorial Describes The New Data Types
This tutorial describes the new data types that Systemverilog introduces. Most of these
are synthesisable, and should make RTL descriptions easier to write and understand.
SystemVerilog introduces several new data types. Many of these will be familiar to C
programmers. The idea is that algorithms modelled in C can more easiliy be converted to
SystemVerilog if the two languages have the same data types.
Verilog’s variable types are four-state: each bit is 0,1,X or Z. SystemVerilog introduces
new two-state data types, where each bit is 0 or 1 only. You would use these when you do
not need X and Z values, for example in test benches and as for-loop variables. Using
two-state variables in RTL models may enable simulators to be more efficient. Used
appropriately, they should not affect the synthesis results.
TYPE Description Example
Arrays
In Verilog-1995, you could define scalar and vector nets and variables. You could also
define memory arrays, which are one-dimensional arrays of a variable type. Verilog-2001
allowed multi-dimensioned arrays of both nets and variables, and removed some of the
restrictions on memory array usage.
SystemVerilog takes this a stage further and refines the concept of arrays and permits
more operations on arrays.
In SystemVerilog, arrays may have either packed or unpacked dimensions, or both.
Consider this example:
reg [3:0][7:0] register [0:9];
The packed dimensions are [3:0] and [7:0]. The unpacked dimension is [0:9]. (You can
have as many packed and unpacked dimensions as you like.)
Packed dimensions:
By contrast, unpacked dimensions can be arranged in memory in any way that the
simulator chooses. You can reliably copy an array on to another array of the same type.
For arrays with different types, you must use a cast, and there are rules for how an
unpacked type is cast to a packed type. Unpacked arrays can be any type, such as arrays
of reals.
SystemVerilog permits a number of operations on complete unpacked arrays and slices of
unpacked arrays. For these, the arrays or slices involved must have the same type and the
same shape – i.e. exactly the same number and lengths of unpacked dimensions. The
packed dimensions may be different, as long as the array or slice elements have the same
number of bits.
The permitted operations are:
SystemVerilog also includes dynamic arrays (the number of elements may change during
simulation) and associative arrays (which have a non-contiguous range).
To support all these array types, SystemVerilog includes a number of array querying
functions and methods. For example, you could use $dimensions to find the number
dimensions of an array variable.
Typedef
SystemVerilog’s data type system allows you to define quite complex types. To make
this kind of code clear, the typedef facility was introduced. Typedef allows users to create
their own names for type definitions that they will use frequently in their code. Typedefs
can be very convenient when building up complicated array definitions.
typedef reg [7:0] octet;
octet b;
is the same as
reg [7:0] b;
and
typedef octet [3:0]
quadOctet;
quadOctet qBytes [1:10];
is the same as
reg [3:0][7:0] qBytes [1:10];
Enum
Enumerations allow you to define a data type whose values have names. Such data types
are appropriate and useful for representing state values, opcodes and other such non-
numeric or symbolic data.
Typedef is commonly used together with enum, like this:
typedef enum { circle, ellipse, freeform } ClosedCurve;
ClosedCurve c;
The named values of an enumeration type act like constants. The default type is int. You
can copy them to and from variables of the enumeration type, compare them with one
another and so on. Enumerations are strongly typed. You can’t copy a numeric value into
a variable of enumeration type, unless you use a type-cast:
c = 2; // ERROR
c = ClosedCurve'(2); // Casting – okay
However, when you use an enumeration in an expression, the value you are working with
is the literal’s integer equivalent; so, for example, it’s okay to compare an enumeration
variable with an integer; and it’s okay to use an enumeration value in an integer
expression.
Finally, SystemVerilog introduces struct and union data types, similar to those in C.
struct {
int x, y;
} p;
It is often useful to declare a new structure type using typedef and then declare variables
using the new type. Note also that structs may be packed.
typedef struct packed {
int x, y;
} Point;
Point p;
Unions are useful where the same hardware resources (like a register) can store values of
different types (e.g. integer, floating point, …)
New Operators
SystemVerilog adds a number of new operators, mostly borrowed from C. These include
increment (++) and decrement (--), and assignment operators (+=, -=, ...). The wild
equality operators (=== and !==) act like the comparisons in a casex statement, with X
and Z values meaning “don’t care”.
Also from C is the do-while loop statement and break and continue. The new foreach
loop is used with array variables. The for loop has been enhanced, so that the following is
permitted:
for (int i = 15, logic j = 0 ; i > 0 ; i--, j = ~j) ...
Labelling
This is useful for documenting the code. The label at the end must be the same as the one
at the beginning. Modules, tasks and functions may also have their names repeated at the
end:
module MyModule ...
...
endmodule : MyModule
This is especially useful for loops, because they can then be disabled. Despite enhancing
named blocks in this way, one reason for using them is removed: in SystemVerilog
variables may be declared in unnamed blocks!
This means that, in SystemVerilog, you would tend to use the logic data type most of the
time, where in Verilog you would sometimes use reg and sometimes wire. In fact reg and
logic are completely interchangeable, but logic is a more appropriate name.
There are some restrictions, though. You are not allowed to assign the same variable
from more than one continuous assignment or output port connection. This is because
there is no resolution for variables like there is for nets in the case of multiple drivers.
Also, if you assign a variable in one of these way, you may not assign the same variable
using procedural assignments.
Suppose you are using Verilog-2001 and are writing a testbench for a module which has
the following declaration:
module Design (input Clock, Reset, input [7:0] Data, output [7:0] Q);
But this is a bit repetitive. SystemVerilog allows you to use the following shorthand
notation:
Design DUT ( .Clock, .Reset, .Data, .Q );
where appropriately named nets and variables have previously been declared, perhaps
like this:
logic Clock, Reset;
logic [7:0] Data;
logic [7:0] Q;
which means “connect all ports to variables or nets with the same names as the ports”.
You do not need to connect all the ports in this way. For example,
Design DUT ( .Clock(SysClock), .* );
means “connect the Clock port to SysClock, and all the other ports to variables or nets
with the same names as the ports.”
Synthesis Idioms
Verilog is very widely used for RTL synthesis, even though it wasn’t designed as a
synthesis language. It is very easy to write Verilog code that simulates correctly, and yet
produces an incorrect design.For example, it is easy unintentionally to infer transparent
latches. One of the ways in which SystemVerilog addresses this is through the
introduction of new always keywords: always_comb, always_latch and always_ff.
always_comb is used to describe combinational logic. It implicitly creates a complete
sensitivity list by looking at the variables and nets that are read in the process, just like
always @* in Verilog-2001.
always_comb
if (sel)
f = x;
else
f = y;
The advantage of using all these new styles of always is that the synthesis tool can check
the design intent.
Another common mistake in RTL Verilog is the misuse of the parallel_case and
full_case pragmas. The problems arises because these are ignored as comments by
simulators, but they are used to direct synthesis. SystemVerilog addresses this with two
new keywords: priority and unique.
Unlike the pragmas, these keywords apply to if statements as well as case statements.
Each imposes specific simulation behaviour that is readily mapped to synthesised
hardware. unique enforces completeness and uniqueness of the conditional; in other
words, exactly one branch of the conditional should be taken at run-time. If the specific
conditions that pertain at run-time would allow more than one branch of the conditional,
or no branch at all, to be taken, there is a run-time error. For example, it is acceptable for
the selectors in a case statement to overlap, but if that overlap condition is detected at
runtime then it is an error. Similarly it is okay to have a unique case statement with no
default branch, or an if statement with no else branch, but at run time the simulator will
check that some branch is indeed taken. Synthesis tools can use this information, rather as
they might a full_case directive, to infer that no latches should be created.
priority enforces a somewhat less rigorous set of checks, checking only that at least one
branch of the conditional is taken. It therefore allows the possibility that more than one
branch of the conditional could be taken at run-time. It licenses synthesis to create more
extravagant priority logic in such a situation.
At its simplest, an interface is a named bundle of wires, similar to a struct, except that an
interface is allowed as a module port, while a struct is not.
The following example shows the definition and use of a very simple interface:
// Interface definition
interface Bus;
logic [7:0] Addr, Data;
logic RWn;
endinterface
initial
begin
TheBus.RWn = 0; // Drive and monitor the bus
TheBus.Addr = 0;
for (int I=0; I<7; I++)
TheBus.Addr = TheBus.Addr + 1;
TheBus.RWn = 1;
TheBus.Data = mem[0];
end
endmodule
always @*
if (MemBus.RWn)
MemBus.Data = mem[MemBus.Addr];
else
mem[MemBus.Addr] = MemBus.Data;
endmodule
Interface Ports
An interface can also have input, output or inout ports. Only the variables or nets
declared in the port list of an interface can be connected externally by name or position
when the interface is instantiated, and therefore can be shared with other interfaces. The
ports are declared using the ANSI-style.
Here is an example showing an interface with a clock port:
interface ClockedBus (input Clk);
logic[7:0] Addr, Data;
logic RWn;
endinterface
Parameterised Interface
Modports in Interfaces
A new construct related to Interface is also added: Modport. This provides direction
information for module interface ports and controls the use of tasks and functions within
certain modules. The directions of ports are those seen from the module.
This example includes modports, which are used to specify the direction of the signals in
the interface. The directions are the ones seen from the module to which the modport is
connected, in our case the RAM:
interface MSBus (input Clk);
logic [7:0] Addr, Data;
logic RWn;
modport Slave (input Addr, inout Data);
endinterface
module TestRAM;
logic Clk;
MSBus TheBus(.Clk(Clk));
RAM TheRAM (.MemBus(TheBus.Slave));
...
endmodule
Tasks in Interfaces
Tasks and functions can be defined in interfaces, to allow a more abstract level of
modelling.
The next example shows two tasks in an interface being used to model bus functionality.
The tasks are called inside the testRAM module:
interface MSBus (input Clk);
logic [7:0] Addr, Data;
logic RWn;
module TestRAM;
logic Clk;
logic [7:0] data;
MSBus TheBus(.Clk(Clk));
RAM TheRAM (.MemBus(TheBus));
initial
begin
// Write to the RAM
for (int i = 0; i<256; i++)
TheBus.MasterWrite(i[7:0],i[7:0]);
First Example
The testbench to test this counter, without using the clocking construct, might look like
this:
module Test_Counter;
timeunit 1ns;
// Clock generator
always
begin
#5 Clock = 1;
#5 Clock = 0;
end
// Test stimulus
initial
begin
Enable = 0;
Load = 0;
UpDn = 1;
Reset = 1;
#10; // Should be reset
Reset = 0;
#10; // Should do nothing - not enabled
Enable = 1; #20; // Should count up to 2
UpDn = 0;
#40; // Should count downto 254
UpDn = 1;
// etc. ...
end
// Clock generator
always
begin
#5 Clock = 1;
#5 Clock = 0;
end
// Test program
program test_counter;
// SystemVerilog "clocking block"
// Clocking outputs are DUT inputs and vice versa
clocking cb_counter @(posedge Clock);
default input #1step output #4;
output negedge Reset;
output Enable, Load, UpDn, Data;
input Q;
endclocking
endprogram
There are a few important things to note: the testbench is implemented as a module, with
a nested program that contains the clocking block (the full explanation of the advantages
of implementing a testbench using a program can be found in the Program article).
Program blocks can be nested within modules or interfaces. This way multiple co-
operating programs can share variables local to the scope. Nested programs with no ports
or top-level programs that are not explicitly instantiated are implicitly instantiated once.
Implicitly instantiated programs have the same instance and declaration name.
The clocking construct is both the declaration and the instance of that declaration. Note
that the signal directions in the clocking block within the testbench are with respect to the
testbench. So Q is an output of COUNTER, but a clocking input. Note also that widths
are not declared in the clocking block, just the directions.
The signals in the clocking block cb_counter are synchronised on the posedge of Clock,
and by default all signals have a 4ns output (drive) skew and a #1step input (sample)
skew. The skew determines how many time units away from the clock event a signal is to
be sampled or driven. Input skews are implicitly negative (i.e. they always refer to a time
before the clock), whereas output skews always refer to a time after the clock.
An input skew of #1step indicates that the value read by the active edge of the clock is
always the last value of the signal immediately before the corresponding clock edge. A
step is the time precision.
The ## operator is used in the testbench to delay execution by a specified number of
clocking events, or clock cycles.
Clocking block outputs and inouts can be used to drive values onto their corresponding
signals, at a certain clocking event and with the specified skew. An important point to
note is that a drive does not change the clock block input of an inout signal. This is
because reading the input always yields the last sampled value, and not the driven value.
Synchronous signal drives are processed as nonblocking assignments. If multiple
synchronous drives are applied to the same clocking block output or inout at the same
simulation time, a run-time error is issued and the conflicting bits are set to X for 4-state
ports or 0 for 2-state ports.
Here are some examples using the driving signals from the clocking block cb:
cb.Data[2:0] <= 3'h2; // Drive 3-bit slice of Data in current cycle
##1 cb.Data <= 8'hz; // Wait 1 Clk cycle and then drive Data
##2 cb.Data[1] <= 1; // Wait 2 cycles, then drive bit 1 of Data
cb.Data <= ##1 Int_Data; // Remember the value of Int_Data, and then
// drive Data 1 Clk cycle later
cb.Data[7:4] <= 4'b0101;
cb.Data[7:4] <= 4'b0011; // Error: driven value of Data[7:4] is 4’b0xx1
initial begin
cb2.RWn = 0;
cb1.DataInt.Data = 1;
...
end
endprogram
always @*
if (CtrlInt.RWn)
DataInt.Data = mem[DataInt.Addr];
else
mem[DataInt.Addr] = DataInt.Data;
endmodule
module Top;
logic Clk1, Clk2;
The clocking event of a clocking block can be accessed directly by using the clocking
block name, e.g. @(cb) is equivalent to @(posedge Clk). Individual signals from the
clocking block can be accessed using the clocking block name and the dot (.) operator.
All events are synchronised to the clocking block.
Here are some other examples of synchronisation statements:
// Wait for the next change of Data signal from the cb clocking block
@(cb.Data);
Introduction
Assertions are primarily used to validate the behaviour of a design. ("Is it working
correctly?") They may also be used to provide functional coverage information for a
design ("How good is the test?"). Assertions can be checked dynamically by simulation,
or statically by a separate property checker tool – i.e. a formal verification tool that
proves whether or not a design meets its specification. Such tools may require certain
assumptions about the design’s behaviour to be specified.
In SystemVerilog there are two kinds of assertions: immediate (assert) and concurrent
(assert property). Coverage statements (cover property) are concurrent and have the same
syntax as concurrent assertions, as do assume property statements. Another similar
statement – expect – is used in testbenches; it is a procedural statement that checks that
some specified activity occurs. The three types of concurrent assertion statement and the
expect statement make use of sequences and properties that describe the design’s
temporal behaviour – i.e. behaviour over time, as defined by one or more clocks.
Immediate Assertions
Immediate assertions are procedural statements and are mainly used in simulation. An
assertion is basically a statement that something must be true, similar to the if statement.
The difference is that an if statement does not assert that an expression is true, it simply
checks that it is true, e.g.:
An immediate assertion may include a pass statement and/or a fail statement. In our
example the pass statement is omitted, so no action is taken when the assert expression is
true. If the pass statement exists:
it is executed immediately after the evaluation of the assert expression. The statement
associated with an else is called a fail statement and is executed if the assertion fails:
Note that you can omit the pass statement and still have a fail statement:
The failure of an assertion has a severity associated with it. There are three severity
system tasks that can be included in the fail statement to specify a severity level: $fatal,
$error (the default severity) and $warning. In addition, the system task $info indicates
that the assertion failure carries no specific severity.
The pass and fail statements can be any legal SystemVerilog procedural statement. They
can be used, for example, to write out a message, set an error flag, increment a count of
errors, or signal a failure to another part of the testbench.
AeqB: assert (a == b)
Concurrent Assertions
"A Request should be followed by an Acknowledge occurring no more than two clocks
after the Request is asserted."
Concurrent assertions are used to check behaviour such as this. These are statements that
assert that specified properties must be true. For example,
asserts that the expression Read && Write is never true at any point during simulation.
where Req is a simple sequence (it’s just a boolean expression) and ##[1:2] Ack is a more
complex sequence expression, meaning that Ack is true on the next clock, or on the one
following (or both). |-> is the implication operator, so this assertion checks that whenever
Req is asserted, Ack must be asserted on the next clock, or the following clock.
Concurrent assertions like these are checked throughout simulation. They usually appear
outside any initial or always blocks in modules, interfaces and programs. (Concurrent
assertions may also be used as statements in initial or always blocks. A concurrent
assertion in an initial block is only tested on the first clock tick.)
The first assertion example above does not contain a clock. Therefore it is checked at
every point in the simulation. The second assertion is only checked when a rising clock
edge has occurred; the values of Req and Ack are sampled on the rising edge of Clock.
Implication
The implication construct (|->) allows a user to monitor sequences based on satisfying
some criteria, e.g. attach a precondition to a sequence and evaluate the sequence only if
the condition is successful. The left-hand side operand of the implication is called the
antecedent sequence expression, while the right-hand side is called the consequent
sequence expression.
There are two forms of implication: overlapped using operator |->, and non-overlapped
using operator |=>.
For overlapped implication, if there is a match for the antecedent sequence expression,
then the first element of the consequent sequence expression is evaluated on the same
clock tick.
s1 |-> s2;
In the example above, if the sequence s1 matches, then sequence s2 must also match. If
sequence s1 does not match, then the result is true.
For non-overlapped implication, the first element of the consequent sequence expression
is evaluated on the next clock tick.
s1 |=> s2;
‘define true 1
In these examples we have been using, the properties being asserted are specified in the
assert property statements themselves. Properties may also be declared separately, for
example:
property not_read_and_write;
Complex properties are often built using sequences. Sequences, too, may be declared
separately:
sequence request
Req;
endsequence
sequence acknowledge
##[1:2] Ack;
endsequence
property handshake;
endproperty
Assertion Clocking
Concurrent assertions (assert property and cover property statements) use a generalised
model of a clock and are only evaluated when a clock tick occurs. (In fact the values of
the variables in the property are sampled right at the end of the previous time step.)
Everything in between clock ticks is ignored. This model of execution corresponds to the
way a RTL description of a design is interpreted after synthesis.
A clock tick is an atomic moment in time and a clock ticks only once at any simulation
time. The clock can actually be a single signal, a gated clock (e.g. (clk && GatingSig)) or
other more complex expression. When monitoring asynchronous signals, a simulation
time step corresponds to a clock tick.
sequence s;
endsequence
property p;
a |-> s;
endproperty
property p;
endproperty
a ##1 b;
endproperty
property p;
a ##1 b;
endproperty
endclocking
In the following example, the disable iff clause allows an asynchronous reset to be
specified.
property p1;
endproperty
Sequences
Here are some simple examples of sequences. The ## operator delays execution by the
specified number of clocking events, or clock cycles.
The * operator is used to specify a consecutive repetition of the left-hand side operand.
The $ operator can be used to extend a time window to a finite, but unbounded range.
Combining Sequences
The binary operator and is used when both operand expressions are expected to succeed,
but the end times of the operand expressions can be different. The end time of the end
operation is the end time of the sequence that terminates last. A sequence succeeds (i.e. is
true over time) if the boolean expressions containing it are true at the specific clock ticks.
If s1 and s2 are sampled booleans and not sequences, the expression above succeeds if
both s1 and s2 are evaluated to be true.
The binary operator intersect is used when both operand expressions are expected to
succeed, and the end times of the operand expressions must be the same.
The operator or is used when at least one of the two operand sequences is expected to
match. The sequence matches whenever at least one of the operands is evaluated to true.
The first_match operator matches only the first match of possibly multiple matches for an
evaluation attempt of a sequence expression. This allows all subsequent matches to be
discarded from consideration. In this example:
sequence fms;
endsequence
whichever of the (s1 ##1 s2) and (s1 ##2 s2) matches first becomes the result of sequence
fms.
i.e. Expression throughout SequenceExpr means that Expression must evaluate true at
every clock tick during the evaluation of SequenceExpr.
i.e. SequenceExpr1 within SequenceExpr2 means that SeqExpr1 must occur at least
once entirely within SeqExpr2 (both start and end points of SeqExpr1 must be between
the start and the end point of SeqExpr2 ).
Variables can be used in sequences and properties. A common use for this occurs in
pipelines:
`define true 1
property p_pipe;
logic v;
endproperty
In this example, the variable v is assigned the value of DataIn unconditionally on each
clock. Five clocks later, DataOut is expected to equal the assigned value. Each invocation
of the property (here there is one invocation on every clock) has its own copy of v. Notice
the syntax: the assignment to v is separated from a sequence expression by a comma, and
the sequence expression and variable assignment are enclosed in parentheses.
Coverage Statements
In order to monitor sequences and other behavioural aspects of a design for functional
coverage, cover property statements can be used. The syntax of these is the same as that
of assert property. The simulator keeps a count of the number of times the property in the
cover property statement holds or fails. This can be used to determine whether or not
certain aspects of the designs functionality have been exercised.
bit X, Y;
sequence s1;
endsequence
...
endmodule
$rose, $fell and $stable indicate whether or not the value of an expression has changed
between two adjacent clock ticks. For example,
assert property
asserts that if in changes from 0 to 1 between one rising clock and the next, detect must
be 1 on the following clock.
This assertion,
assert property
The system function $past returns the value of an expression in a previous clock cycle.
For example,
assert property
The system functions $onehot and $onehot0 are used for checking one-hot encoded
signals. $onehot(expr) returns true if exactly one bit of expr is high; $onehot0(expr)
returns true if at most one bit of expr is high.
Binding
We have seen that assertions can be included directly in the source code of the modules
in which they apply. They can even be embedded in procedural code. Alternatively,
verification code can be written in a separate program, for example, and that program can
then be bound to a specific module or module instance.
For example, suppose there is a module for which assertions are to be written:
module M (...);
endmodule
The properties, sequences and assertions for the module can be written in a separate
program:
program M_assertions(...);
endprogram
The syntax and meaning of M_assertions is the same as if the program were instanced in
the module itself:
module M (...);
endmodule
Class Declaration
The first of these statements declares c1 to be a C. In other words, the variable c1 can
contain a handle to an object (i.e. an instance) of the class C. The second statement
creates an object and assigns its handle to c1. The two statements could be replaced by
the following statement, which declares a variable, creates a class object and initialises
the variable:
C c1 = new;
Having created a class object, we can use the class methods to assign and look at the data
value, x:
initial
begin
c1.set(3);
$display("c1.x is %d", c1.get());
end
Data Hiding
Although the task set() and the function get() were intended to be the means by which the
class’s member variable x was assigned and retrieved, it would be possible to do this
directly:
initial
begin
c1.x = 3;
$display("c1.x is %d", c1.x);
end
This is because all class members are, by default, publicly visible. To hide x, it must be
declared local:
local int x;
It is now illegal to access c1.x outside the class, except using the class methods.
Parameterised Classes
The default parameter value can be overridden when the class is instantiated.
Register #(4) R4; // data is bit [3:0]
Register #(.N(8)) R8 // data is bit [7:0]
Register R; // data is bit [0:0]
One of the key features of object-oriented programming is the ability to create new
classes that are based on existing classes. A derived class by default inherits the
properties and methods of its parent or base class. However, the derived class may add
new properties and methods, or modify the inherited properties and methods. In other
words, the new class is a more specialised version of the original class.
In SystemVerilog the syntax for deriving or inheriting one class from another is this:
class Derived extends BaseClass;
// New and overridden property and method declarations.
endclass
Consider the example of the class Register, which was used earlier. This could represent
a general-purpose data register, which stores a value. We could derive a new class,
ShiftRegister, from this that represents a specialised type of a register.
class ShiftRegister extends Register;
task shiftleft; data = data << 1; endtask
task shiftright; data = data >> 1; endtask
endclass
Objects of the class ShiftRegister can be manipulated using the original set() and get()
methods, as well as the shiftleft and shiftright methods. However, there is a problem if
the data property in Register was declared as local – it would not be visible in the
extended class! So instead, it would need to be declared protected:
class Register;
protected int data;
...
endclass
A protected member is one that is not visible outside the class, but is visible in derived
classes; local members are not visible, except in the class in which they are declared.
Sometimes, it is useful to create a class without intending to create any objects of the
class. The class exists simply as a base class from which other classes can be derived. In
SystemVerilog this is called an abstract class and is declared by using the word virtual:
virtual class Register;
...
endclass
Methods, too, may be declared virtual. This means that if the method is overridden in a
derived class, the signature (the return type, the number and types of its arguments) must
be the same as that of the virtual method. This provides a mechanism for saying, “I want
all derived classes to have a method that looks exactly like this.” A virtual method in an
abstract class need not have a body – this will have to be defined in a non-abstract
derived class.
Note that methods in a virtual class need not be virtual and that virtual methods may be
declared in non-abstract classes.
In this tutorial we illustrate how to use classes that represent data objects in a
constrained-random testbench..This tutorial illustrates the following key points:
Directed-Random Verification
1. In a directed testing approach, you might select some appropriate data values and
write them into some selected memory locations and then read them out again. One
problem with this approach is that you could miss certain types of system error – for
example errors with certain addresses or when using certain data values.
2. Using a random testing approach, you might find more errors, but unless you run
the simulations for long periods of time, you still might not detect certain problems.
3. In a directed random test, you control how random the data values are using
constraints. For example, you might want to make sure that some memory locations
are tested exhaustively, and that “corner cases” (i.e. significant cases such as the
minimum and maximum address values) are definitely tested. You might want to
write to an ascending or descending sequence of addresses.
SystemVerilog supports all three paradigms: directed, random and directed random
testing. It does this by providing for random data value generation under the control of
constraints.
In order to measure how good a test is, SystemVerilog provides constructs for specifying
functional coverage models and measuring the coverage during simulation. By analysing
the coverage data, tests can be directed to ensure they do indeed test the design
adequately.
Most practical verification problems require you to implement some kind of transaction
in which a collection of data is transferred into or out of the design under test (DUT).
This collection of data may be as simple as the address and data being transferred on a
system bus, or something much more elaborate like a complete image represented as
video data. In any case, it is appropriate to create an abstract data structure that can be
used to represent this information as it moves through the verification system and the
DUT.
As an example of this kind of data modelling we will consider messages in a CANbus
network (CANbus is a networking system used for in-vehicle data buses described in ISO
standard 11898).
The CANbus message format has two possible versions. The simpler 2.0A format, which
we will use for this example, has the following fields:
• An 11-bit "identifier" (address)
• A single-bit field known as "RTR" indicating whether a reply is expected
• Two "reserved" bits, fixed at zero in the 2.0A format
• A 4-bit "data length" field, containing a binary value in the range 0 to 8
• A data payload consisting of 0 to 8 bytes, as indicated by the "data length" field
• A 15-bit CRC (checksum) field
We can easily create a struct to represent this data structure. Each field in the data
structure is directly represented by a field in our struct. Those fields can be given bit
widths using an appropriate SystemVerilog data type, such as bit or logic. For an eight-bit
field, the type byte is used. bit [7:0] could have been used instead – the choice is a matter
of style and convenience. (byte is a signed type, but that is not relevant here.)
struct packed {
bit [10:0] ID; // 11-bit identifier
bit RTR; // reply required?
bit [1:0] rsvd; // "reserved for expansion" bits
bit [3:0] DLC; // 4-bit Data Length Code
byte data[]; // data payload
bit [14:0] CRC; // 15-bit checksum
} message;
We have used struct packed to define a packed data structure. This means that the data
structure can be packed into a single vector, making it easier to use in a system where
information is sent serially, for example.
In SystemVerilog, only specified variables in a class may have constrained random
values generated. So the struct needs to be embedded in a class and given the rand
property:
class CAN_Message;
rand struct packed {
bit [10:0] ID; // 11-bit identifier
bit RTR; // reply required?
bit [1:0] rsvd; // "reserved for expansion" bits
bit [3:0] DLC; // 4-bit Data Length Code
byte data[]; // data payload
bit [14:0] CRC; // 15-bit checksum
} message;
endclass
Class methods
Now we have defined our CAN_message class, we need to add methods to the struct that
can modify or inspect it. We would need to add methods to this class for many purposes,
including calculating the correct 15-bit CRC. For example, consider this very
straightforward method to set or clear the RTR (reply request) bit in a message structure,
ensuring that there is no payload data if the RTR bit is set:
class CAN_Message;
rand struct packed {...} message;
endclass
Note that this method is itself a part of the CAN_message class, so that it can directly
access any fields of the struct, using the .member notation.
Generation (randomize)
The random generator will always honour your constraints. It is sometimes possible to
write conflicting constraints, in which case the generator will fail.
Now that we've completed this class definition, we need to be able to make use of it in
the testbench. As a simple example of this process, suppose we want to build a test that
needs ten distinct messages to do its work. We would create an unpacked array of 10
CAN_message objects:
CAM_message test_messages[10];
We could then initialize the messages with random data like this:
for (int i = 0; i < 10; i++)
test_message[i].randomize();
This is the same as writing a constraint in the CAN_message class like this
constraint c3 { message.DCL == 4; }
Alternatively, we could use the class inheritance mechanism to create a subclass, where
the message length is fixed:
class CAN_message_4 extends CAN_message;
constraint c1 { message DCL == 4; } // Overload c1
endclass
Suppose that the DUT has a serial input for receiving CAN messages. In order to drive
the abstract class data into the DUT, the message struct will need to be serialised. This is
easy, because message was declared as a struct packed.
for (int i = test_message[0].message.size()-1; i>=0; i--)
##1 cb.SerialIn = test_message[0][i];
In this example, we are using a clocking block called cb. The ##1 construct delays until
the next clocking event (e.g. the next clock) and applies the stimulus to the DUT’s
SerialIn input. (See the clocking tutorial or the main article on clocking for details.)
So far in this tutorial we have looked at how random variables and constraints in classes
are used to create tests. SystemVerilog also provides a number of other constructs that are
not covered here, including the ability to create random sequences of tokens..
Functional Coverage
Having seen how to write tests using SystemVerilog, we shall now consider how we can
measure their effectiveness. One way to do this is to measure the functional coverage.
This is a user-defined metric of how much of the design has been tested. (SystemVerilog
also includes the concurrent cover property statement, which is used to count the number
of times a particular sequence or property occurs. For further information see the
Assertion-Based verification Tutorial.)
As an example of functional coverage, consider a variable of a user-defined enumerated
type:
enum {Red, Green, Blue} Colour;
It would be useful to know whether or not the variable Colour has been set to all the
possible values at some point during simulation. To do this you would define a
covergroup containing a single coverpoint:
covergroup cg_Colour @(posedge Clock);
coverpoint Colour;
endgroup
Next you must create an instance of the covergroup. This is like creating a class object:
cg_Colour = new cg_inst;
During simulation, the simulator will count the number of times that Colour takes each of
the values, Red, Green and Blue. The value of Colour is sampled on every rising edge of
Clock. (You don’t have to specify a sampling event; if you don’t then you must sample
the values explicitly, using the covergroup’s sample method – cg_inst.sample());
Bins
In the example we have just used, the simulator will create three bins for the coverpoint –
one for each value of the enumerated type. Suppose we are covering a variable of an
integer type:
bit [15:0] i;
covergroup cg_Short @(posedge Clock);
c : coverpoint i;
endgroup
The simulator could potentially create 216 bins for the coverpoint. (In fact, there is a
default of a maximum of 64 automatically created bins.) It would probably be more
useful to define some bins to hold specific values or ranges of values:
covergroup cg_Short @(posedge Clock);
coverpoint i {
bins zero = { 0 };
bins small = { [1:100] };
bins hunds[3] = { 200,300,400,500,600,700,800,900 };
bins large = { [1000:$] };
bins others[] = default;
};
endgroup
This creates one bin, “zero”, for the value of i being 0; one bin, “small”, for all values of i
between 1 and 100, inclusive; three bins, for the eight values listed, with the first holding
200 and 300, the next 400 and 500 and the last 600, 700, 800 and 900; one bin for values
1000 and above, and one bin for every other value.
Cross Coverage
It is often useful to know how often two (or more) variables have specific pairs (triples
etc.) of values. This is achieved using cross coverage:
logic [3:0] x, y;
This will create 16 bins for each of the coverpoints X and Y and 256 bins for XY – one
for each possible pair of values. Note that automatically created bins are only defined for
2-state values: values containing x or z are excluded.
Covering Transitions
Coverage of transitions may also be collected. An example where this may be used is for
finite state machines. Consider a state machine with three states, Idle, State1 and State2,
where the only legal transitions are those to and from Idle. In addition, the state machine
should only remain in the Idle state for a maximum of 4 clocks.
enum {Idle, State1, State2} State;
This would create a separate bin for each legal transition – including remaining in Idle –
and one bin for all the illegal transitions.
SystemVerilog also provides the illegal_bins construct, which causes the simulator to
stop with an error if an illegal value or transition occurs:
covergroup cg_State @(posedge Clock);
...
illegal_bins illegal = default sequence;
}
endgroup
Coverage options
Options control the behaviour of covergroups and coverpoints. For example, the coverage
results for a particular covergroup or coverpoint may be weighted, or a maximum number
of automatically created bins could be specified. Options such as these can be set in the
covergroup, or procedurally after the covergroup has been instanced.
int i_a, i_b, i_c;
In this example, 10 bins are created for the coverpoints cg_inst.a and cg_inst.b and 20
bins are created for cg_inst.c. cg_inst.a is assigned a weight of 2, whereas the other
coverpoints each have a weight of 1 (the default weight).
There are many other options – refer to the SystemVerilog LRM for details of these.
The other functional coverage features that have not been covered in this tutorial are
covergroup arguments; wildcard bins and block execution events. For details of these,
please refer to the SystemVerilog LRM
An Example
Here an example is presented. A module called Bus contains two functions: write, which
is a SystemVerilog function that is also exported to C, and a function called slave_write
which is imported from C. Both functions return void.
SystemVerilog:
module Bus(input In1, output Out1);
import "DPI" function void slave_write(input int address,
input int data);
export "DPI" function write; // Note – not a function prototype
C:
#include "svdpi.h"
extern void write(int, int); // Imported from SystemVerilog
void slave_write(const int I1, const int I2)
{
buff[I1] = I2;
...
}
Both DPI imported and exported functions can be declared in any place where normal
SystemVerilog functions can be (e.g. package, module, program, interface, constructs).
Also all functions used in DPI complete their execution instantly (zero simulation time),
just as normal SystemVerilog functions.
// Standard C function
// Standard C function
import "DPI" function void free(chandle ptr);
chandle is a special SystemVerilog type that is used for passing C pointers as arguments
to imported DPI functions.
• by an entry in a bootstrap file; Its location is specified with one instance of the
switch -sv_liblist pathname.
• by specifying the file with one instance of the switch -sv_lib
pathname_without_extension (i.e. the filename without the platform specific
extension).
myclibs/lib1
proj2/clibs/lib2
The first line must contain the string: #!SV_LIBRARIES. Then the following lines hold
one and only one library location each. Comment lines can be inserted. A comment line
start with a # and ends with a newline.
Here is an example of a switch list:
-sv_lib myclibs/lib1
-sv_lib proj2/clibs/lib2
The two files above are equivalent, if the pathname root has been set by the switch
-sv_root to /home/user and the following shared object libraries are included:
/home/user/myclibs/lib1.so
/home/user/proj2/clibs/lib2.so
Binary and Source Compatibility
Binary compatibility means an application compiled for a given platform will work with
every SystemVerilog simulator on that platform. Source-level compatibility means an
application needs to be re-compiled for each SystemVerilog simulator and
implementation-specific definitions will be required for the compilation.
Depending on the data types used for imported or exported functions, the C code can be
binary-level or source-level compatible. Binary compatible are:
Result types of both imported and exported functions are restricted to small values.
Small values include:
• void, byte, shortint, int, longint, real, shortreal, chandle, and string
• packed bit arrays up to 32 bits and all types that are eventually equivalent to
packed bit arrays up to 32 bits.
• scalar values of type bit and logic
All SystemVerilog data types are allowed for formal arguments of imported functions.
Imported functions can have input, output and inout arguments. The formal input
arguments cannot be modified. In the C code, they must have a const qualifier. Also, the
initial values of output arguments are undetermined and implementation-dependent as far
as the C function is concerned.
Argument passing
A pair of matching type definitions is required to pass a value through DPI: the
SystemVerilog definition and the C definition.
SystemVerilog types which are directly compatible with C types are presented in the
following table:
SYSTEMVERILOG
C Type
TYPE
byte char
int int
long
longint
long
short
shortint
int
real double
shortreal float
chandle void*
string char*
SystemVerilog and C types
There are SystemVerilog-specific types, including packed types (arrays, structures,
unions), 2-state or 4-state, which have no natural correspondence in C. For these the
designers can choose the layout and representation that best suits their simulation
performance. The representation of data types such as packed bit and logic arrays are
implementation-dependent, therefore applications using them are not binary-compatible
(i.e. an application compiled for a given platform will not work with every
SystemVerilog simulator on that platform).
Packed arrays are treated as one-dimensional, while the unpacked part of an array can
have an arbitrary number of dimensions. Normalised ranges are used for accessing all
arguments except open arrays. (Normalized ranges mean [n-1:0] indexing for the packed
part (packed arrays are restricted to one dimension) and [0:n-1] indexing for a dimension
in the unpacked part of an array.) The ranges for a formal argument specified as an open
array, are those of the actual argument for a particular call.
If a packed part of an array has more than one dimension, it is transformed to a one-
dimensional one, as well as normalised (e.g. packed array of range [L:R] is normalized as
[abs(L-R):0], where index min(L,R) becomes the index 0 and index max(L,R) becomes
the index abs(L-R)). For example:
logic [2:3][1:3][2:0] b [1:8] [63:0]
becomes
logic [17:0] b[0:7] [0:63]
after normalisation.
Enumerated names are not available on the C side of the DPI. enum types are represented
as the types associated with them.
This example includes a struct, a function imported from C and a SystemVerilog function
exported to C. The struct uses three different types: byte, int (which are small values) and
a packed 2-dimensional array. The SystemVerilog struct has to be re-defined in C. Byte
and int are directly compatible with C, while the packed array is redefined using the
macro SV_BIT_PACKED_ARRAY(width, name).
SV_LOGIC_PACKED_ARRAY(width,name) and
SV_BIT_PACKED_ARRAY(width,name) are C macros allowing variables to be
declared to represent SystemVerilog packed arrays of type bit or logic respectively They
are implementation specific, therefore source-compatible, and require "svdpi_src.h" to be
included.
The SystemVerilog function exported to C has an input of a type int (a small value), and
a packed array as an output. The packed array will be passed as a pointer to void.
(SvLogicPackedArrRef is a typdef for void *). The SystemVerilog function is called
inside the C function, the first argument being passed by value, and the second by
reference.
SystemVerilog:
typedef struct {
byte A;
bit [4:1][0:7] B;
int C;
} ABC;
// Imported from C
import "DPI" function void C_Func(input ABC S);
// Exported to C
export "DPI" function SV_Func;
C:
#include "svdpi.h"
#include "svdpi_src.h"
typedef struct {
char A;
SV_BIT_PACKED_ARRAY(4*8, B); // Implementation specific
int C;
} ABC;
and
int svHigh(const svOpenArrayHandle h, int d)
are array querying functions, where h= handle to open array and d=dimension. If d = 0,
then the query refers to the packed part (which is one-dimensional) of an array, and d> 0
refers to the unpacked part of an array.
SystemVerilog:
// 3-dimensional unsized unpacked array
import "DPI" function void MyFunc(input int i [][][]);
MyFunc (Arr_8x4x16);
MyFunc (Arr_4x16x8);
C:
#include "svdpi.h"
A SystemVerilog task does not have a return value and is called as a statement – in an
initial or always block, for example. An important feature of tasks is that, unlike
functions, they may consume simulation time, if they include one or more .timing
controls. Now if an imported DPI task calls an exported DPI task that consumes
simulation time, the imported task will consume time.
Only context imported tasks may call exported tasks:
import "DPI" context task MayDelay();
In SystemVerilog, tasks may be disabled, using a disable statement. When this happens,
the task exits with its outputs undefined. In order to cater for an imported task (or the
exported task it calls) being disabled, the C code must handle this possibility.
Consider an imported DPI task that calls an exported DPI task that does delay:
task Delay (output int t);
#10 t = $stime;
endtask
export "DPI" task Delay;
import "DPI" context task DoesDelay(output int t);
Notice that the C functions DoesDelay and Delay return an int, even though they
correspond to SystemVerilog tasks. The return value of Delay will be 0, unless the
SystemVerilog task DoesDelay is disabled, in which case the C function Delay returns 1.
This must be checked in DoesDelay, which must acknowledge that it has seen the disable
and also return 1:
int DoesDelay (int *t)
{
...
if ( Delay(t) ) { // Was the task DoesDelay disabled?
svAckDisabledState();
return 1;
}
...
return 0;
}
Note that if Delay is disabled whilst DoesDelay is executing, Delay will return 0.
In summary, if a C function that implements an imported DPI task itself calls an exported
DPI task, then