How to Write a Basic Module in SystemVerilog
This post is the first in a series which discusses how SystemVerilog is used in FPGA design. We begin by looking at the way we structure a SystemVerilog design using the module keyword. This includes a discussion of parameters, ports and instantiation as well as a full example.
SystemVerilog is actually a superset of the Verilog language. As a result of this, if you have already read the post on verilog modules then most of this post should already be familiar. The only exception to this is the section on nested modules. These are a new feature which were introduced as a part of the SystemVerilog language.
Structuring SystemVerilog Designs
When we design FPGAs, it is important to remember one fundamental principle – we are designing hardware and not writing a computer program.
Therefore, we must describe the behavior of a number of different components which we then connect together. This expectation is reflected in the way that we structure our SystemVerilog design files.
As with every electronic component, we need to know the external interface to our component. This information allows us to connect it to other components in our system.
We also need to know how the component behaves so that we can use it in our system.
In SystemVerilog, we use a construct called a module to define this information. The SystemVerilog module is equivalent to the entity architecture pair in VHDL.
The code snippet below shows the general syntax for the declaration of a module in SystemVerilog.
module ( // We can optionally declare parameters here parameter <parameter_name> = <default_value>; ) <module_name> ( // All IO are defined here <direction> <data_type> <size> <port_name> ); // Functional RTL (or structural) code endmodule : <module_name>
In this construct, <module_name> would be the name of the module which is being designed. Although we can declare a number of modules in a single file, it is good practise to have one file corresponding to one module.
It is also good practice to keep the name of the file and the module the same. This makes it simpler to manage large designs with many components.
We don’t need to include the <module_name> field after the endmodule keyword. Although this can make the code easier to follow, it is optional and is often omitted.
In SystemVerilog we use the // characters to denote that we are writing a comment.
We use comments to include important information about our code which others may find useful. The SystemVerilog compiler ignores anything which we write in our comments.
In the SystemVerilog code snippet above, we can see this in practice as comments are used to describe the functionality of the code.
Parameters in SystemVerilog Modules
Parameters are a local form of constant which we can use to configure a module in SystemVerilog.
When we instantiate our module in another part of our design, we can assign the parameters a value to configure the behavior of the module.
As parameters have a limited scope, we can call the same module multiple times and assign different values to the parameters each time.
Therefore, parameters allow us to modify the behaviour of our module on the go.
Parameters are an optional part of the SyetmVerilog module declaration and in the majority of cases we won’t need to include them.
However, parameters allow us to write more generic module interfaces which are easier to reuse in other SystemVerilog designs.
After we have declared a parameter in our module, we can use it in the same way as a normal variable.
However, we must remember that it is a constant value so we can only read it. As a result of this, we can only assign a value to the parameter when it is declared.
We discuss SystemVerilog parameters in more depth in a later post.
Functional Code
We use the space underneath the module IO declaration to define how our module functions.
We most commonly use RTL for this but we can also write structural code or describe primitives.
These topics are discussed in more detail in later SystemVerilog tutorials.
When we have finished writing the code which describes the behaviour of our module, we use the endmodule keyword.
Any code which we write after this keyword will not be included in our module.
SystemVerilog Module Ports
We use ports within the module declaration to define the inputs and output of a SystemVerilog module.
We can think of module ports as being equivalent to pins in a traditional electronic component.
The code snippet below shows the general syntax we use to declare ports.
<direction> <data_type> <size> <port_name>
The <port_name> field in the module declaration is used to give a unique name to the port.
We use the <direction> field in the above construct to declare our ports as either input, output or inout. This correspond to inputs, outputs and bidirectional ports respectively.
We use the <data_type> field to declare the type of data the port expects.
The logic type was introduced in SystemVerilog to replace the verilog reg and wire types. This is the most commonly used type in SystemVerilog and we use this type to represent any binary data.
There are several other data types which we can use in SystemVerilog and we discuss these in more detail in the next post.
We may also wish to use multi bit, vector type ports in our module. When this is the case, we can use the <size> field to declare the number of bits in the port.
When we define the size of a vector type, we must indicate the most significant and least significant bit (MSB and LSB) in the vector. Therefore, we use the construct [MSB:LSB] when declaring the size of a port.
The example below shows the declaration of an 8 bit little-endian input called example_in.
input logic [7:0] example_in;
Little endian data is the most commonly used convention in FPGA design. In this example, bit 7 is the most significant bit.
We could also define the MSB as being in position 0 if we declare the size field as [0:7]. This convention, which is known as big-endian data, is used less frequently when designing FPGAs.
SystemVerilog Module Instantiation
We can invoke a SystemVerilog module which we have already written in another part of a design. This process of invoking modules in SystemVerilog is known as instantiation.
Each time we instantiate a module, we create a unique object which has its own name, parameters and IO connections.
In a SystemVerilog design, we refer to every instantiated module as an instance of the module. We use instantiation to create a number of different instances which we use to build a more complex design.
We can think of module instantiation in SystemVerilog as being equivalent to placing a component in a traditional electronic circuit.
Once we have created all of the instances we require in our design, we must interconnect them to create a complete system. This is exactly the same as wiring components together in a traditional electronic system.
SystemVerilog provides us with two methods we can use for module instantiation – named instantiation and positional instantiation.
Positional Module Instantiation
When using the positional instantiation approach in SystemVerilog, we use an ordered list to connect the module ports. The order of the list we use must match the order in which the ports were declared in our module.
As an example, if we declare the clock first, followed by a reset then we must connect the clock signal to the module IO first.
The SystemVerilog code snippet below shows the general syntax for positional module instantiation.
<module_name> # ( // If the module uses parameters they are connected here <parameter_value> ) <instance_name> ( // Connection to the module ports <signal_name>, // this connects to the first port <signal_name>. // this connects to the second port );
The <module_name> field must match the name we gave the module when it was declared.
We use the <instance_name> field to give a unique name to an instantiated module in our design.
As the order of our ports may change as our design evolves, this method can be difficult to maintain.
Positional Module Instantiation Example
Let’s consider a basic practical example to show how we use positional instantiation in practise.
For this example, we will create an instance of the simple circuit shown below.
When we use positional instantiation, the order of the ports in the module declaration is important. The code snippet below shows how we would declare a module for this circuit.
or_and ( input logic a, input logic b, input logic c, output logic logic_out );
Finally, the SystemVerilog code snippet below shows how we would create an instance of this module using positional instantiation.
// Example using positional instantiation example_or_and or_and ( in_a, in_b, in_c, or_and_out );
Named Module Instantiation
When we use named module instantiation in SystemVerilog, we explicitly define the name of the port we are connecting our signal to.
Unlike positional instantiation, the order in which we declare the ports is not important.
This method is generally preferable to positional instantiation as it produces code which is easier to read and understand.
It is also easier to maintain as we can modify ports without having to worry about the order in which we declare them.
The SystemVerilog code snippet below shows the general syntax for named module instantiation.
<module_name> # ( // If the module uses parameters they are connected here .<parameter_name> (<parameter_value>) ) <instance_name> ( // Connection to the module ports .<port_name> (<signal_name>), .<port_name> (signal_name>) );
The <module_name>, <parameter_name> and <port_name> fields must match the names we used when defining the module.
The <instance_name> has the same function for both positional and named instantiations.
Named Module Instantiation Example
Let’s consider a basic practical example to show how we use named instantiation in practise.
For this example, we will create an instance of the simple circuit shown below. This is the same circuit we previously used in the positional instantiation example.
The SystemVerilog code snippet below shows how we would create an instance of this module using named instantiation.
// Example using positional instantiation example_or_and or_and ( .a (in_a), .b (in_b), .c (in_c), .logic_out (or_and_out) );
Nested Modules in SystemVerilog
When we design an FPGA using verilog, we can not declare a module inside of another module.
Instead, we must declare a module in another part of our design and only then can we instantiate it inside another module.
However, this changed in SystemVerilog and we can now write nested modules. As a result, we can declare a module inside of another module in SystemVerilog.
The code snippet below shows the method we use to declare a nested module in SystemVerilog.
// Outer module declaration module <outer_module_name> ( // All IO are defined here <direction> <data_type> <size> <port_name> ); // Inner module declaration module <inner_module_name>( // All IO are defined here <direction> <data_type> <size> <port_name> ); endmodule : <inner_module_name> endmodule : <outer_module_name>
Both the modules in this example use the same syntax which we talked about in the section on SystemVerilog modules.
When we write a nested module, the scope is limited to the module we declared it in. As a result, we can only instantiate a nested module inside of the module it is contained in.
The nested module also has visibility of all the signals in the outer module. However, the signals contained in the nested module are not visible to the outer module.
Nested Module Example
To better demonstrate how we use nested modules in SystemVerilog, let’s consider a basic example.
For this example, we will write a module to implement the simple circuit shown below. We used this same circuit in our previous examples for named and positional association.
In order to show how we use nested modules, we will write the and gate as a separate module.
The SystemVerilog code below shows how we would structure this circuit using a nested module.
module or_and ( input logic a, input logic b, input logic c, output logic logic_out ); // Declaration of our inner module (and gate) module and_gate ( input logic a, input logic b, output logic and_out ); // Code for the and gate does here endmodule : and_gate // Code for outer module (or_and) goes here endmodule : or_and
In this example, we can see how we declare the top level of the module using the same method we discussed before. We have labeled the top level module or_and in this example.
We then declare a section module inside of the top level module which we would use to implement an and gate. In this example, we have labeled the nested module and_gate.
We would still need to instantiate the nested module inside of the top level if we wanted to connect it to our design. If we don’t do this, our nested module is simply ignored by our SystemVerilog tools.
SystemVerilog Module Example
In order to fully understand all of the concepts which we have discussed in this post, let’s look at a basic example.
In this example, we will create a synchronous counter circuit which uses a parameter and then instantiate two instances of it. One of these instantiations will have 12-bits in the output whilst the other will have only 8 bits.
We will exclude the RTL for these modules here as we have not yet learnt how to write this. Instead we will simply define the IO of our modules and the interconnection between them.
The counter module will have two inputs – clock and reset – and a single output – the counter value.
In addition to this, we will also require a single parameter which we will use to define the number of bits in the output.
The SystemVerilog code snippet below shows the declaration of our counter module.
module counter #( parameter WIDTH = 8 ) ( input logic clock, input logic reset, output logic [WIDTH-1:0] count );
We now need a module which we can use to instantiate two instances of this counter. This module will have two inputs – clock and reset – and two outputs coming from the instantiated counters.
In the counter module, we defined the default counter output as 8 bits. This means that we can instantiate the 8 bit counter without overriding the parameter value.
However, when we instantiate the 12 bit counter, we must also override the value of the WIDTH parameter and set it to 12.
The code snippet below shows the code for this module when using named instantiation to connect to the ports.
module top_level ( input logic clock, input logic reset, output logic [7:0] count_8, output logic [11:0] count_12 ); // Instantiation of the 8 bit counter // In this instance we can use the default // value fo the parameter counter 8bit_count ( .clock (clock), .reset (reset), .count (count_8) ); // Instantiation of the 12 bit counter // In this instance we must override the // value fo the WIDTH parameter counter # ( .WIDTH (12) ) ( .clock (clock), .reset (reset), .count (count_12) ); endmodule : top_level