home menu IT - Hardware IT - Software Inventions Tech Guides About

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.

General Syntax:

Syntactically, SystemVerilog shares a little in common with a language such as C++, including comments:

    
    //My comment goes here
    /* My multi-line comment goes here */
    
    

and logical operators:

    
    &&  //Logical AND
    &   //Bitwise AND
    ||  //Logical OR
    |   //Bitwise OR
    !   //Logical NOT
    
    

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.

Modules:

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:

    
    module moduleName //Module name
    #(
        //Parameter definitions
    )
    (
        //Input and output definitions
    );
    
    //Module code goes here

    endmodule;
    
    

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:

    
    module TheTopLevelModule //Top-level module declaration
    (
        input a, b, c, //Define I/O for top-level module
        output x, y
    );
    
    //Low-level module instantiation with unique identifier U1:
    TheLowLevelModule U1(/*I/O mappings*/);

    endmodule;

    //Low-level module definition (often done in separate file):
    module TheLowLevelModule
    (
        input A, B, C, //I/O
        output Xout, Yout
    );
    //Low-level module code
    endmodule;
    
    

If a given module uses parameters (see above), the instantiation is altered thus:

    
    //Module instantiation with unique identifier U1:
    ParamaterisedModule #([parameters]) U1(/*I/O mappings*/);
    
    

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:

    
    LowLevelModule U1(a,b,c,x,y);
    
    

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:

    
    TheLowLevelModule U1(.A(a), .B(b), .C(c), .Xout(x), .Yout(y));
    
    

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!:

    
    TheLowLevelModule U1(.*);
    
    

Data Types:

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:

    
    input logic A,
    input logic B,
    input logic C,
    output logic X,
    output logic Y
    
    

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.

    
    module dataTypesDemoOne
    (
        input [3:0] logic A, //[MSB:LSB] format
        input [3:0] logic B,
        input [3:0] logic C
    );

    assign A = 4'b0001; //Permanently load in 1 in binary format
    assign B = 1'hA; //Permanently load in 10 in hex format
    assign C = 6; //Permanently load in 6 in decimal format

    endmodule;
    
    

Signed variables must be explicitly stated. By default, SystemVerilog assumes all variables to be unsigned:

    
    module dataTypesDemoTwo
    (
        //4-bit signed (2's complement) declaration:
        input [3:0] logic signed A 
    );
    
    

"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:

    
    //Declare in form "logic [[width]] [name] [[height]]"
    logic [7:0] memory [0:9];
    //Access element in form "name [[row]] [[column]]"
    assign A = memory[y][x];
    
    

Occasionally it is useful to group related variables together logically. To do this, structs are used:

	
	//Declare a struct
	struct {
		logic [3:0] var1;
		logic [3:0] var2;
		logic [3:0] var3;
		logic [3:0] var4;
	} testStruct;
	//Access constituent variables from the above struct
	assign testStruct.var1 = 4;
	assign testStruct.var4 = 10;
	
	

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:

	
	//Declare enum with three possible values
	enum {
		CODE1=1,
		CODE2=2,
		CODE3=3 
	} testEnum;
	//Assign a certain permissible value to created enum
	testEnum = CODE2;
	
	

Basic Combinatorial Logic:

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:

    
    module gateLevelDesign
    (
        input A, B, C,
        output X, Y, Z
    );
        //AND gate, of form [name]([output],[inputs])
        and U0(X,A,B);
        //OR gate, of form [name]([output],[inputs])
        or U1(Y,A,B,C);
        //XOR gate, of form [name]([output],[inputs])
        xor U2(Z,A,C);
    endmodule
    
    

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:

    
    module complexCombinatorialOne
    (
        //Define I/O
    );
        always_comb
        begin
            //Define main operations
        end
    endmodule
    
    

Only inside an always_comb block may an If ... Else ... or Case ... statement be used:

    
    module complexCombinatorialTwo
    (
        input logic [3:0] X,
    );
        always_comb
        begin
            if(X > 3)
            begin
                //Operations
            end
            else
            begin
                //Other operations
            end
        end
    endmodule
    
    

The Case ... statement is used to determine actions based on a number of possible values of a given signal, and is implemented as follows:

    
    module complexCombinatorialTwo
    (
        input logic [3:0] X,
    );
        always_comb
        begin
            case([variable name])
            [value 1] : [operations];
            [value 2] : [operations];
            [value 3] : begin
                            [multiple operations]
                        end
            //Default case used if no others applicable
            default : [default operations];
            endcase
        end
    endmodule
    
    

The Case ... statement can also be used with multiple input signals, as exemplified below:

    
    module complexCombinatorialTwo
    (
        input logic A,
        input logic B,
        input logic C,
        output logic X
    );
        always_comb
        begin
            //Concatenate values with {}
            case({A,B,C})
            3'b000 : X=1'b1;
            3'b011 : X=1'b0;
            default : X=1'b0;
			endcase
        end
    endmodule
    
    

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.

Basic Sequential Logic:

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:

    
    module SequentialLogic
    (
        input a,
        input logic Clock, //Clock signal for sequential logic
        output Xout
    );
        logic X; //Output value
        assign Xout = X; //Send output

        //Define clocked sequential block
        always_ff @(posedge Clock)
        begin
            //Sequential counting
            if(a==1)
            begin
                X <= X + 1;
            end
            //Arbitrary default
            else
            begin
                X <= 5;
            end
        end
    endmodule
    
    

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:

    
    module SequentialLogic
    (
        input a,
        input logic Clock, //Clock signal for sequential logic,
        input logic Reset,
        output Xout
    );
        logic X; //Output value
        assign Xout = X; //Send output

        //Define clocked sequential block
        always_ff @(posedge Clock, posedge Reset)
        begin
            //Default value upon reset
            if(Reset==1)
            begin
                X <= 0;
            end
            //Set normal behaviour if not resetting
            else
            begin
                //Sequential counting
                if(a==1)
                begin
                    X <= X + 1;
                end
                //Arbitrary default
                else
                begin
                    X <= 5;
                end
            end
        end
    endmodule
    
    

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.

Brief Introduction to Testbenches:

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:

    
    //Define the simulation timescale
    `timescale 1ns / 1ps

    //Create a testbench module
    module exampleTestBench ();
    logic input1,
    logic input2,
    logic input3,
    logic output1,
    logic output2

    //Instantiate the SystemVerilog module "unit under test"
    lowLevelModule uut
    (
        .*
    );

    //Initial...Begin... used to run code only once
    initial
    begin
        #10 //Delay
        input1=1; //1st combination
        input2=0;
        input3=0;
        #10 //Delay
        input1=0; //2nd combination
        input2=1;
        input3=0;
        #10 //Delay
        input1=0; //3rd combination
        input2=0;
        input3=1;
        #10
    end
    endmodule
    
    

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.

[Back to top]