SystemVerilog Cheat Sheet - 11/01/23
SystemVerilog is a Hardware Description Language (HDL). That is to say, it is a programming language used to describe the physical connection and operation of logic circuits.
Whereas a conventional
programming language defines a set of operations to be carried out by a fixed set of circuitry such as a CPU, a HDL is equivalent to instead describing the
physical interconnection and operation of the physical circuitry itself. When programming in any Hardware Description Language, it is therefore important to keep in mind that the code being
created should reflect physical behaviour of logic gates.
Once written, HDL code is run through a compiler. This synthesises it into a series of logic blocks, often for subsequent programming of a Field-Programmable
Gate Array (FPGA). An FPGA is essentially a specialised integrated circuit, consisting of a (large) number of logic blocks whose interconnection can be modified by
application of a synthesised piece of HDL code, thus effectively re-wiring
the chip's internals.
Whereas conventional microprocessors are sequential devices, only performing one operation after another, FPGAs can be configured, with suitable HDL code, to carry out many logical operations in parallel. This makes them highly attractive for applications such as video processing, where, for example, red, green, and blue pixel values must be processed concurrently.
This article is intended to serve as a useful introductory cheat sheet
to the SystemVerilog language.
Syntactically, SystemVerilog shares a little in common with a language such as C++, including comments:
and logical operators:
Most basic arithmetic operators (+, -, *, /) perform their expected functions in SystemVerilog, too.
Statements are terminated with a semicolon (;).
Different to the likes of C++, however, is the way in which related lines of code are grouped, for example in a module (see below). Whereas C++ uses curly brackets ({}), SystemVerilog uses begin... and end... keywords to group related lines of code. See the subsequent sections for specific examples.
SystemVerilog is built around modules: pieces of code with inputs, internal processing, and outputs. Modules are comparable with functions in a non-HDL language: they can be embedded
as a black box
with inputs and outputs, and lend themselves to good hierarchichal program design. Unlike non-HDL functions, modules can be viewed as a physical logic circuit with inputs,
outputs, and custom user-defined internal circuitry. A very common way to design a SystemVerilog program is as one high-level module, containing instances of smaller self-contained modules.
Modules are defined using the following syntax:
In the above code snippet, one will notice the presence of a separate parameters section. Parameters are values which are defined during a module's instatiation and stay constant throughout the module. They can therefore be used to scale the module's behaviour. For example, the bit-width of busses within a module may be defined using parameters, thus giving the module the potential to be used in SystemVerilog programs involving different bit-widths.
Modules are embedded (instantiated) within a superior (often called top-level) module using the following syntax to call them and identify each embedded instance:
If a given module uses parameters (see above), the instantiation is altered thus:
In this case, U1 defines the name of this particular instance of the TheLowLevelModule module. The section commented I/O mappings represents the connections between the I/O defined in the high-level module, and the I/O of the low-level module. Looking at the example started directly above, this port mapping can be done in a few different ways:
Positional port mapping connects the high-level module's I/O lines to those of the low-level module in the same order as defined in the low-level module. Therefore, only the high-level I/O is explicitly stated in the declaration:
Explicit port mapping states exactly to which low-level line each high-level signal should be connected. If in doubt, this is the most robust interconnection method:
If the high-level and low-level modules' I/O ports are identically and uniquely named, Implicit port mapping may be used to make the connections with minimal effort. In this case, no port names need be stated in the declaration at all!:
The logic type is one of the most important fundamental data types in the SystemVerilog language. It represents a 1-bit value whose state can be 0 (logic 0), 1 (logic 1), X
(uninitialised), or Z (floating). The final two states are a reflection of the real-world
nature of FPGA programming: the physical connections defined by the HDL, whilst ideally
always defined, could indeed take on an undefined/floating state, under certain circumstances.
Below are shown an example set of declarations for module inputs and outputs, this time explicitly stating them to be of the logic type:
In a reflection of the fact that SystemVerilog describes physical connections and behaviour, multi-bit logic types are used to store and transfer numbers, in binary form. A multi-bit logic type is known as a bus. This is a reflection of the way in which multi-bit values are transferred physically in hardware, wherein a parallel set of lines, also known as a bus, is used.
Below are shown an example set of declarations for 4-bit-wide busses, along with some example methods by which values may be assigned to them.
Signed variables must be explicitly stated. By default, SystemVerilog assumes all variables to be unsigned:
"Multidimensional busses", known as arrays, may also be created. These can be visualised as a grid, with each cell holding one bit of data and being individually addressable.
An example array declaration and access is shown below:
Occasionally it is useful to group related variables together logically. To do this, structs are used:
A final useful data type I'll mention is the enumerated (enum) type. This acts as a variable with a fixed pool of possible values, each value equating to an underlying numerical code. One of the main benefits of this type is its ability to link a set of numbers (e.g. status outputs from a module) to human-readable text strings, thus enhancing the legibility of a given module's code.
An example declaration of an enum variable is shown below:
Combinatorial logic refers to a system where the output depends only on the current state of the input(s). Combinatorial systems are composed, fundamentally, of logic gates. As mentioned previously, high-level SystemVerilog code may be synthesized into physical logic gates on an FPGA. However, so-called gate-level design is also possible: SystemVerilog primitives - pre-defined logical operators - are used to model the behaviour of individual gates, such as AND, OR, and NOT.
An example module containing gate-level logic is shown below:
Whilst gate-level design very accurately reflects how the circuit will be physically synthesised into logic gates, this also makes it somewhat restrictive. For example, some desired operations might be much better described using If ... Else ... or Case ... statements. One of SystemVerilog's strengths is allowing for these high-level behavioural descriptions, and synthesising them into low-level gate design, thus leaving the programmer without the tedious task of determining how best to implement the desired operations in logic gates: he can instead write out the desired behaviour directly.
Combinatorial logic of the above type is denoted by use of the always_comb block:
Only inside an always_comb block may an If ... Else ... or Case ... statement be used:
The Case ... statement is used to determine actions based on a number of possible values of a given signal, and is implemented as follows:
The Case ... statement can also be used with multiple input signals, as exemplified below:
NB: It is very important that each possible outcome branch of a SystemVerilog always_comb block contain assignments for all the block's defined outputs, even if unchanged.
A sequential logic circuit is defined as one where the output depends both on the current and previous inputs. This requires memory elements, such as flip-flops, to remember the previous states.
A clocked sequential logic counter block is created in SystemVerilog by use of the always_ff block:
The line @(posedge Clock) is termed the sensitivities of the block. These are effectively a set of instant asynchronous triggers
for its sequential behaviour.
Some always_ff blocks may include an asynchronous reset, for example, in the sensitivity list.
Below is shown the same always_ff block as above, including asynchronous reset:
In the above always_ff example, one will notice the use of the <=, rather than = when making assignments. The difference between these two is that the former
is non-blocking. A blocking assignment such as the latter stops (blocks
) the flow of the program until it has completed. A Non-blocking assignment,
on the other hand, does not block the flow of the program: all non-blocking assignments are scheduled and executed simultaneously during every cycle, thus better lending
themselves to sequential, rather than combinatorial, logic.
It may be the case, during a HDL design process, that access to the physical FPGA hardware for testing is not possible or convenient. In these cases, it is common to use a computer-based simulator to model the behaviour of a given HDL module. The simulator provides multiple input combinations to the module under test, and models the module's outputs in each case.
To dictate which input combinations are used, a Testbench program is created. This is a program, also written in SystemVerilog (or, more broadly, a HDL), whose sole
purpose is to provide the timings and input combinations to the simulator program for testing a given SystemVerilog module. A crucial distinction between testbenches and
the real SystemVerilog code under test is that the former, unlike the latter, is a set of instructions for the simulator - in other words, it is effectively a normal
program to instruct the sequential behaviour of a normal
microprocessor in the computer running the simulation software. This means that time-based constructs, such as
while/for loops and delays - which would normally be inadvisable in SystemVerilog modules owing to their not being directly sythesisable - can be employed.
Assuming a hypothetical example module to be tested, with inputs 1-3 and outputs 1-3, the format of a SystemVerilog testbench is generally as follows:
Given the above code, the simulator would change the input values after each delay, and track any changes in the outputs. Many simulators allow for the inputs and outputs
to be plotted on a waveform view
, much like a logic analyser.