Types, Operator, and Expressions
Like ordinary software programming languages, SystemVerilog allows designers manipulate different types and data objects when modeling the hardware. Although bears much similarity to languages such as C/C++ (in fact SystemVerilog is influenced by them), SystemVerilog is a language to model hardware, which has different semantics than software programming models. This chapter explores the type system and associated operators in SystemVerilog.
2.1 Data Types
SystemVerilog uses the terminology of data type and data objects to distinguish between objects and its data type. As specified in the language specification, a data type is a set of values and a set of operations that can be performed on those values. A data object is a named entity that has a data value and a data type associated with it.
2.1.1 4-State Values
One major distinction between SystemVerilog and other software programming languages is that the value set in SystemVerilog consists of the following four basic values:
- 0: represents a logic zero or false condition
- 1: represents a logic one or true condition
- x: represents an unknown value
- z: represents a high-impedance state.
Values 0 and 1 serve the same purpose as in languages such as C/C++, but x and z are hardware specific. High-impedance value z typically implies a physically disconnected state, i.e. an infinitely high resistance. It commonly appears in designs where a pin used as a bi-directional bus, e.g. tri-state. Unknown value x means the system is able to determine the value, which may happen in various condition. One common situation for x to appear in simulation is usage of uninitialized memory cells. Another common scenario is low-power designs where some part of circuit is “shut-down”, i.e. no supply voltage. Any signal coming out from the shut-down region will be x. Keep in mind that x can propagate through your circuit if not taken care of, since any logic operation on unknown values results in unknown values.
Notice that the truth table for 4-state values are slightly different than the normal 2-state values. Any 4-state value OR
with 1
is 1
, any 4-state value AND
with 0 is 0, and any 4-state value XOR
with x
is x
. This can be useful when dealing with x-prorogation.
Data types that only use 0 or 1 have 2-state values.
2.1.2 Basic data types
logic
is the most commonly used basic data types in SystemVerilog. logic
signal has 4-state values. It can either represent a combinational signal or a sequential signal, since the downstream tools such simulator and synthesis tools will determine whether to instantiate a flip-flop based on the usage. A rule of thumb is that any signals in your synthesizable design should be declared as logic
(a few exceptions apply, which will be discussed). This is to ensure that the simulator and synthesis tool agree with each other, avoiding critical bugs that can only been discovered through gate-level simulation or even post-silicon tests. For instance, if a signal is declared as 2-state value, the simulator will happily compute with 0 and 1s. However, since the actual silicon has z and x values, we will see a discrepancy after the synthesis, which leads to a potential bug.
By default, a signal declared as logic
only has one bit value. To declare a multi-bit signal, we can do logic[15:0] a
, which declares a 16-bit signal. Notice that we can also do logic[0:15] a
. The difference is bit-ordering, similar to endianness for byte-ordering. Typically bit-ordering follows the same endianness as the byte order. It is highly recommend to use a consist bit-ordering throughout the design to avoid potential bugs. In this book we will use big-endian and high:low
bit-ordering.
By default, every declared variable is unsigned. To declare a signed variable, use signed
keyword such as logic signed [15:0] a
. Although SystemVerilog allows you to mix signed variable with unsigned, similar to C/C++, it leaves a potential bug that cannot be caught by the compiler (some tools may produce a warning), we shall never mix signed and unsigned arithmetics and any assumption about the how the automatic conversion work is grounded for future errors that is difficult to debug.
To declare an array, we can add extra dimension to the “left”, such as logic[3:0][15:0] a
, which declares 4 16-bit logics. We can adding arbitrary more dimensions as well. The array created by such approached is called packed array. To access the first 16-bit logic
, we can do a[0]
. Notice that we can also slice out a sub-array, such as a[1:0]
, which gives as the first two values.
Notice that we can also do logic[15:0] a[3:0]
, which is called unpacked array. Although unpacked array has advantage over packed array such as giving simulator more flexibility to allocate arrays since they do not need to be contiguous, because of that, we cannot slice a sub-array, nor can we bulk assign values. Again, choosing which representation depends on the project style, as long as it is consistent throughout the entire project. In this book we will use packed array.
2.1.2.1 2-state Variables
SystemVerilog also defines 2-state types, typically used for test benches or functional models that are more high-level. Unlike C, these data types has pre-defined widths, as show in Table 2.
Type | Bit width |
---|---|
bit | 1 |
byte | 8 |
shortint | 16 |
int | 32 |
longint | 64 |
2.1.3 Enumeration
SystemVerilog defines enumerations as a set of integral named constants, similar to that of C/C++. Enumeration need to be declared with a type with the default type be int
. Since int
is unwelcoming in synthesizable RTL, we shall always use logic
data types. An example of enumeration is shown below:
Here are some rules regarding the name and integral values of enumeration:
Values in enum can be integers and increment from an initial value of 0. This, however, can be overridden, as shown below. In this case,
STATE2
will be 2 andSTATE3
will be 3.- Both the enumeration names and their integer values shall be unique. This can be combined with the first rule.
The integer values in enumerate will be casted to their corresponding type. An overflow will be treated as an error
Enumeration are strongly typed. Although directly assigning integer to enumerate variables will trigger an automatic cast, we highly recommend to use explicit cast.
There are several helper functions with enumerated types:
first()
: returns the value of the first member of the enumeration.last()
: returns the value of the last member of the enumeration.next(int unsigned N = 1)
: returnsN
th next enumeration stating from the current value.prev(int unsigned N = 1)
: returnsN
th previous enumeration value starting from the current value.
Notice that next()
and prev()
are type-safe ways to increment enumeration values, which is highly recommend to use compared to simply addition followed by mod.
2.1.4 Struct
To represent data in a more meaningful way, SystemVerilog allows users to define a struct
similar to C/C++. The members of a struct
can be any data type, thus nesting struct
is allowed. Since struct
represents an aggregated values, similar to array, we have the concept of packed and unpacked struct. By default, without packed
keyword, the struct
is unpacked. Again we will use packed struct in this book. Here is an example to define an instruction type;
2.1.5 User-Defined Types
Like C, SystemVerilog allows complex data types to be aliased to a new type using the keyword typedef
to make the code more readable. The syntax is similar to that of C as well:
typedef [old_type] new_type;
For instance, to define the state enumeration as a new type, we can do something such as
enum
definition is between typedef
and state_enum_t
. We can then use state_enum_t
as a type to declare state
variable. Notice that we add suffix _t
to indicate that state_enum_t
is a user-defined type. This is a useful naming convention that has been adopted in many design style guides and we will follow this convention in the book.
Similar to enum
, we can use typedef
to give struct
a type name and re-use it later.
typedef struct packed {
logic[19:0] imm;
logic[5:0] rd;
} data_t;
typedef struct packed {
data_t data;
logic[5:0] opcode;
} inst_t;
inst_t instruction; // instantiate inst_t
In the example here we first define data_t
and refer to it when defining inst_t
. Since we use packed
for both struct
, we can actually assign the instruction to a 32-bit signal.
We can also use typedef
to define other types. For instance, typedef logic[7:0] byte_logic
defines a byte_logic
as 8-bit logic. We can stack typedef
on top of each other. For instance, typedef byte_logic[7:0] int_logic
defines int_logic
to be a packed array of byte_logic
.
2.1.6 Union
Although SystemVerilog offers the union
similar to that of C/C++, it introduces a potential type loophole since the union can be updated using a value of one memory type and read as a value of another member type. To avoid this, SystemVerilog introduce a new construct called tagged union. A tagged union stores both the member values and a tag. The tag and value can only be updated together using a type-checked expression, as specified by the SystemVerilog standard. We will focus on tagged union since it offers more type safety and it is an underrated feature introduced by SystemVerilog.
In the example here, we will try to specify RISC-V basic instruction formats. The complete code can be found in 02/tagged_union.sv
. Unfortunately, at the time of writing, only the latest vcs
supports tagged union
feature.
// definition of r_inst_t, i_inst_t, s_inst_t, and u_inst_t are omitted
typedef union tagged packed {
r_inst_t r_inst;
i_inst_t i_inst;
s_inst_t s_inst;
u_inst_t u_inst;
} risc_v_inst_t;
initial begin
risc_v_inst_t i1;
i1 = tagged i_inst '{1, 2, 3, 4, 5};
assert (i1.i_inst.opcode == 5);
// the following code result an error at runtime
// i1.s_inst.opcode = 5;
// siwtch tag
i1 = tagged u_inst '{1, 2, 3};
assert (i1.u_inst.opcode == 3);
end
Notice that we use the keyword packed
here, which tells the compiler to check the bit-width for each union member to make sure they are match. In most cases we want the same size, hence packed
needed. However, in the case where tagged union has unmatched bit-width, the tools need to layout the memory carefully. Readers are encouraged to refer to Section 7.3.2 in the SystemVerilog standard for more details.
In the example, we have explicitly specify the tag for the variable, and the tag value is checked at runtime to ensure the type correctness.
For completeness, we cover the ordinary union
here, which shares the same semantics as that of C/C++.
In this example, we can assign value to raw_value
and then read out appropriate values from inst
.
2.1.7 Non-Synthesizable Data Types
When modeling high-level logic designs, such as functional model used for test benches, we can use non-synthesizable data types to reduce the amount of work. For instance, SystemVerilog offers data type real
and shortreal
the same way as double
and float
in C, respectively, which conforms to IEEE Standard 754. This can be useful when developing models that needs floating point. HOwever, please keep in mind that if you want to use floating points in your synthesizable design, you have to either use vendor provided IPs, or implement your own. The former approach is recommended, since they have been thoroughly verified and optimized for various design metrics.
Another data-type non-synthesizable data type is string
. string
in SystemVerilog is close to that of Python
where the space allocation is fixed. However, although string
itself is not synthesizable, logics can be assigned directly from string literals. The tools will automatically convert the string literal into bytes based on the ASCII table behind the scene. Please keep this in mind when using string literals in your synthesizable design, as shown in the code usage in the code blow. Notice that since the string literal is represented as logic, we can display it in both string and number format, whereas the the string
data type cannot.
module String;
string non_synthesizable_variable = "SystemVerilog";
logic[103:0] synthesizable_variable = "SystemVerilog";
initial begin
// the line below results in an error:
// A variable of type string is supplied to (%d).
// $display("%s, %d", non_synthesizable_variable, non_synthesizable_variable);
// the line below displays:
// SystemVerilog: 6613524751007090216398647750503
$display("%s: %d", synthesizable_variable, synthesizable_variable);
end
endmodule
One thing to keep in mind when using string literal assigned to numeric types such as logic
: if the string length smaller than the bit width of the variable, 0 padding happens. SystemVerilog will pad 0 to the left, which can be a potential problem as normal software programming languages do not have such behavior.
2.1.7.1 Other Data Types
There are other data types in SystemVerilog we have not covered so far, such as event
and chandle
. We will cover chandle
later in DPI, which is a data type used for foreign language pointers.
2.1.8 Type Casting
Many types in SystemVerilog are strongly typed, meaning a wrong type assignment will trigger either a compiler warning or error, such as directly assigning logic values to enum variables. Unfortunately its predecessor Verilog is weak-typed and SystemVerilog inherits all its shortcomings, such as mixing with signed and unsigned assignments. To make the code less error-prone, it is recommended to stick with explicit casting whenever there is a type mismatch, rather than relying on the language’s default type conversions.
A data type can be casted into another using the cast '
operation. The general syntax is
cast ::= casting_type'(expression)
For instance, suppose we have a enum type state_enum_t
defined earlier, and we want to directly assign a logic to it (not recommended but in some cases it is necessary), we can cast the logic variable to the enum type we want, as shown below.
To cast an unsigned number to signed, we can use signed'()
, and unsigned
()` to cast signed to unsigned numbers.
2.1.9 Variable scopes and lifetime
Variables in SystemVerilog can have different lifetime depends on where and how they are defined. We will discuss more in details in the next chapter when we discuss various scopes.
2.2 Operator and Expressions
2.2.1 Numeric Literals
Numeric literals in SystemVerilog can be specified in decimal, hexadecimal, octal, or binary format. The general syntax to specify a numeric literals is shown below (taken from SystemVerilog standard A.8):
decimal_number ::=
unsigned_number
| [ size ] decimal_base unsigned_number
| [ size ] decimal_base x_digit { _ }
| [ size ] decimal_base z_digit { _ }
binary_number ::= [ size ] binary_base binary_value
octal_number ::= [ size ] octal_base octal_value
hex_number ::= [ size ] hex_base hex_value
unsigned_number 33 ::= decimal_digit { _ | decimal_digit }
decimal_base 33 ::= ' [ s | S ] d | ' [ s | S ] D
binary_base 33 ::= ' [ s | S ] b | ' [ s | S ] B
octal_base 33 ::= ' [ s | S ] o | ' [ s | S ] O
hex_base 33 ::= ' [ s | S ] h | ' [ s | S ] H
By default, any numeric literal without any additional specification is in unsigned decimal base, such as 42
. To specify its size, we need '
operator to separate the size and value, such as 16'42
, which specifies a 16-bit value. We can also specify different base using letters such as d
, b
, and h
. For instance, 16'h10
specifies a 16-bit value 0x10. If we want the numbers to be signed, we can use s
after the '
, such as -16's10
, which specifies a 16-bit signed value -10. When representing non-decimal numbers, especially binary, we can use _
as a delimiter to annotate bytes. For instance, to represent 123 in a 8-bit unsigned number in binary, we can use 8'b0111_1011
, where we put _
for every 4 bits. One thing to keep in mind that although SystemVerilog allows you to drop the size, called unsized number, certain rule apply:
- The number of bits that mark up the unsized number shall lbe at least 32. This implies that if you assigned an unsized number to a variable with fewer than 32 bits, truncation will happen.
- If unsized number is used in an expression, the other values has higher number of bit width, the unsized number will be extended with respect to the highest bit, including
x
andz
.
As a result, we only recommend to use unsized number such as 0, where the extension and truncation do not affect the actual value. For other occasions the size is recommended since it helps linters to catch size mismatch.
2.2.2 Struct and Array Literals
Although we can set the struct members individually when initializing the struct variable, SystemVerilog provides a concise way to do so. Suppose we have a struct and a variable defined as
Instead of assigning values to a
and b
individually, we can use the structure assignment patterns by using '{}
:
// assigning individually
value.a = 4'5;
value.b = 16'42;
// structure assignment pattern
value = '{4'5, 16'42};
This assignment pattern also works for an array of structures, which is similar to that of C++:
Array literals assignment is the same as struct literal, where each item in the array can be specified using '{}
syntax. Notice that we can use replication operator {{}}
to make the code more readable.
2.2.3 Operators
SystemVerilog has a rich set of operators similar to that of C/C++ with enhancement to deal with bit vectors. The complete operators and data types is listed in Table 3, which is taken from Table 11-1 from the SystemVerilog standard.
Operator token | Name | Operand data types |
---|---|---|
= |
Binary assignment operator | Any |
+= -= /= *= |
Binary arithmetic assignment operators | Integral, real, shortreal |
%= |
Binary arithmetic modulus assignment operator | Integral |
&= |= ^= |
Binary bitwise assignment operators | Integral |
>>= <<= |
Binary logical shift assignment operators | Integral |
>>>= <<<= |
Binary arithmetic shift assignment operators | Integral |
?: |
Conditional operator | Any |
+ - |
Unary arithmetic operators | Integral, real, shortreal |
! |
Unary logical negation operator | Integral |
~ & ~& | ~| ^ ~^ ^~ |
Unary logical reduction operators | Integral |
+ - * / |
Binary arithmetic operators | Integral, real, shortreal |
% |
Binary arithmetic modulus operator | Integral |
& | ^ ^~ ~^ |
Binary bitwise operators | Integral |
>> << |
Binary logical shift operators | Integral |
>>> <<< |
Binary arithmetic shift operators | Integral |
&& || |
Binary logical operators | Integral, real, shortreal |
< <= > >= |
Binary relational operators | Integral, real, shortreal |
=== !=== |
Binary case equality operators | Any except real and shortreal |
==? !=? |
Binary wildcard equality operators | Any |
++ -- |
Unary increment, decrement operators | Integral, real, shortreal |
{} {{}} |
Concatenation, replication operators | Integral |
Most of the operators have the same semantics as C/C++. However, there are several operators to which we need to pay attention:
- Unary logical reduction operators. This set of operators forms a reduction tree on every bit of the variable. For instance, if we have a 4-bit variable
logic [3:0] a = 'b0010
.|a
is equivalent to toa[0] | a[1] | a[2] | a[3]
, which is 1, and&a
yields 0. - Concatenation operator
{}
allows multiple variable append to each other, resulting in a wider variable. Suppose we have 2 variableslogic [1:0] a = 'b11
andlogic [3:0] b = 'b0010
,{a, b}
yields'b110010
. We can concat as many as variable as we need. SystemVerilog also offers a shorthand to repetitively concatenate variables together. For instance,{4{'b10}}
yields'b10101010
. - The difference between
==
and===
lies in 4-state value comparison:==
only compares 2-state and===
compares 4-state. Suppose we havea = 1'bx
andc = a == 1'bx
, wherea
andc
are 1-bitlogic
. Because==
only compares 2-state,c
isx
. Ifc
is defined asc = a === 1'bx
,c
will be 1. - Shifter has two forms: arithmetic and logic shifters. Arithmetic shifters does signed extension when shift right where as logic shifters always pad zero when shifting. One common gotcha is that even though a variable is declared as signed, if you use
>>
to shift right, SystemVerilog will not perform signed extension, a behavior different from C! We have seen the actual bug went into silicon before getting caught.