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.

Table 2: 2-state data types in SystemVerilog. All types are signed by default. Keyword unsigned is needed to make it unsigned.
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:

  1. 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 and STATE3 will be 3.

  2. Both the enumeration names and their integer values shall be unique. This can be combined with the first rule.
  3. The integer values in enumerate will be casted to their corresponding type. An overflow will be treated as an error

  4. 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:

  1. first(): returns the value of the first member of the enumeration.

  2. last(): returns the value of the last member of the enumeration.
  3. next(int unsigned N = 1): returns Nth next enumeration stating from the current value.

  4. prev(int unsigned N = 1): returns Nth 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.

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.

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.

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:

  1. 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.
  2. 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 and z.

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 '{}:

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.

Table 3: SystemVerilog operators. Table adapted from SystemVerilog standard Table 11-1.
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:

  1. 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 to a[0] | a[1] | a[2] | a[3], which is 1, and &a yields 0.
  2. Concatenation operator {} allows multiple variable append to each other, resulting in a wider variable. Suppose we have 2 variables logic [1:0] a = 'b11 and logic [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.
  3. The difference between == and === lies in 4-state value comparison: == only compares 2-state and === compares 4-state. Suppose we have a = 1'bx and c = a == 1'bx, where a and c are 1-bit logic. Because == only compares 2-state, c is x. If c is defined as c = a === 1'bx, c will be 1.
  4. 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.