In this blog post we look at the use of SystemVerilog parameters and the generate statement to write code which can be reused across multiple FPGA designs. This includes examples of a parameterized module, a generate for block, generate if block and generate case block.
As with most programming languages, we should try to make as much of our code as possible reusable.
This allows us to reduce development time for future projects as we can more easily port code from one design to another.
When we write synthesizable SystemVerilog code, we have two constructs available to us which can help us to create reusable code - parameters and generate statements.
In addition to this, we can also use SystemVerilog classes to create reusable testbench code. We talk about this topic in more depth in a different post.
We use parameters and generate statements to write code which is more generic. This allows us to easily modify the behavior of a module when we instantiate it in our design.
In the rest of this post, we look at parameters and generate blocks in more detail.
In SystemVerilog, parameters are a type of local constant which we can assign a value to when we instantiate a module.
The scope of a parameter is limited to a single instance of the module that we declared it in.
As a result of this, we can instantiate the same SystemVerilog module multiple times and assign different values to the parameter.
This allows us to configure the behavior of a module when we instantiate it.
When we declare a module in SystemVerilog, we have to specify the interface to it.
We can then use this interface to connect our module to different modules in our FPGA design.
As a part of this interface, we can declare parameters as well as the inputs and outputs to the module.
The SystemVerilog code below shows the general syntax we use to declare a parameter in our module interface.
When we declare a parameter in a SystemVerilog module like this, we call this a parameterized module.
module <module_name> #(
parameter <parameter_name> = <default_value>
)
(
// Port declarations
);
We use the <parameter_name> field in the above code to give a unique name to our parameter,
We can then use this name to call the value of the parameter in our code, just as we would with any normal variable.
We can also optionally assign a default value to our parameter using the <default_value> field.
This is useful as it allows us to instantiate the component without having to assign a value to the parameter.
When we instantiate a module in a SystemVerilog design, we can assign a value to the parameter using either named association or positional association.
This is exactly the same as assigning a signal to an input or output on the module.
The SystemVerilog code below shows the general syntax we use to assign data to a parameter when instantiating a module.
// Example of named association
<module_name> # (
// If the module uses parameters they are connected here
.<parameter_name> (<parameter_value>)
)
<instance_name> (
// port connections
);
// Example of positional association
<module_name> # (<parameter_values>)
<instance_name> (
// port connections
);
In order to better demonstrate how we use parameterized modules in SystemVerilog, let's consider a basic example.
For this example, we will create a simple design which instantiates a 12 bit synchronous counter and an 8 bit counter.
We could do this by writing two separate counter modules which have a different number of output bits.
However, this approach is inefficient as the code for both modules will be virtually identical.
A more efficient approach is to write a single counter module and use a parameter to adjust the width of the counter circuit.
As it is not important to understanding how we use parameterized modules, we will exclude the functional code in this example.
Instead, we will look only at how we declare and instantiate a parameterized module in SystemVerilog.
The code snippet below shows how we would write the module interface for the parameterized module in SystemVerilog.
module counter #(
parameter BITS = 8;
)
(
input logic clock,
input logic reset,
output logic [BITS-1 : 0] count
);
In this example we can see how we use a parameter to adjust the size of a signal in SystemVerilog.
Rather than using a hard coded number to declare the width of the output, we substitute the parameter value into the port declaration.
This is actually one of the most common use cases for parameterized modules in SystemVerilog
In the SystemVerilog code above, we defined the default value of the BITS parameter as 8.
As a result of this, we only need to assign the parameter a value when we want an output that isn't 8 bits.
The code snippet below shows how we would instantiate this module when we want a 12 bit output.
In this instance, we must over ride the default value of the parameter when we instantiate the module.
counter # (
.BITS (12)
) count_12 (
.clock (clock),
.reset (reset),
.count (count_out)
);
We use generate statements in SystemVerilog to either iteratively or conditionally create blocks of code in our design.
This technique allows us to selectively include or exclude blocks of code or to create several instances of a code block in our design.
We can only use generate statements in concurrent SystemVerilog code. This means we can't use generate statements inside procedural blocks such as always blocks.
In addition to this, we have to use either an if statement, case statement or a for loop together with the generate keyword.
We use the if and case generate statements to conditionally generate code whilst the for generate statement iteratively generates code.
We can write any valid SystemVerilog code which we require inside generate blocks. This includes always blocks, module instantiations and other generate statements.
Let's look at each of the different types of generate statement in more detail.
We can use a SystemVerilog for loop inside of a generate block to iteratively create multiple instances of a piece of code.
We typically use this approach to describe a circuit which has a fixed, repetitive structure.
For example, we may wish to describe an array of RAM blocks which are controlled by a single data bus.
If we use the generate for loop to do this then we will write less code than if we manually instantiate all of the individual RAM blocks.
The code snippet below shows the general syntax which we use to write a generate for block in SystemVerilog.
// Declare the loop variable
genvar <name>;
// Code for the generate for block
generate
for (<initial_condition>; <stop_condition>; <increment>) begin
// Code to execute
end
endgenerate
As we can see from this example, the code for a generate for block is virtually identical to the code for a SystemVerilog for loop.
However, there are two important differences between these two approaches.
Firstly, we have to declare the loop variable using the genvar keyword when we use the generate for statement.
Secondly, we declare the loop inside of a generate block rather than a procedural block such as a SystemVerilog always block.
However, there is also a fundamental difference in the way these two types of construct behave.
When we use the generate for statement we are actually telling our compiler to create multiple instances of our code block.
In contrast, when we write a normal for loop we are telling our compiler to create a single instance of the code block which executes multiple times.
To better demonstrate this difference, let's consider a very basic example.
In this example, we will use both types of statement to assign data to a 2 bit wide vector.
The code snippet below shows how we implement this in SystemVerilog.
// Example using the for loop
always @(posedge clock) begin
for (i = 0; i < 2; i = i + 1) begin
sig_a[i] = 1'b0;
end
end
// Example using the generate for block
generate
for (i = 0; i < 2; i = i + 1) begin
always @(posedge clock) begin
sig_a[i] = 1'b0;
end
end
endgenerate
Although both of these examples will produce the same result, the underlying structure which is produced by them is different.
If we were to unroll the for loop example, we would get the code show below.
always @(posedge clock) begin
sig_a[0] = 1'b0;
sig_a[1] = 1'b0;
end
In contrast, if we were to unroll the generate for loop then we would get the code shown below.
always @(posedge clock) begin
sig_a[0] = 1'b0;
end
always @(posedge clock) begin
sig_a[1] = 1'b0;
end
In order to better understand how the generate for block works, let's consider a basic example.
For this example, we will create an array of 3 RAM blocks which we then connect to a single bus.
Each of the RAM blocks in this example has 4-bit address and data inputs as well as a write enable port. We will connect all of these signals to a single bus.
In addition, each of our RAM blocks has a 4-bit data output and an enable input. However, we will not connect these signals to a bus.
The circuit diagram below shows the basic structure which we are going to describe.
In order to connect the RAM enable ports to our bus, we will declare a 3 bit vector. We then connect a different bit to each of the RAM blocks based on the value of the loop variable.
For the output data bus, we will create an array which consists of 3 4-bit vectors. Again, we can then use the loop variable to assign different elements of this array as required.
The SystemVerilog code below shows how we would implement this circuit using the for generate statement.
// rd data array
wire [3:0] rd_data [2:0];
// vector for the enable signals
wire [2:0] enable;
// Genvar to use in the for loop
genvar i;
generate
for (i=0; i<=2; i=i+1) begin
ram ram_i (
.clock (clock),
.enable (enable[i]),
.wr_en (wr_en),
.addr (addr),
.wr_data (wr_data),
.rd_data (rd_data[i])
);
end
endgenerate
After synthesizing this code, we get the circuit shown below.
We can use a SystemVerilog if statement inside of a generate block to conditionally include blocks of code in our design.
We use the if generate statement when we have blocks of code which we only want to include in our design under certain circumstances.
For example, we may have a test function which we only want to include in debug versions of our design.
In this case, we would use a generate if statement to ensure that we only generate our test function in debug builds of our design.
The code snippet below shows the general syntax of the SystemVerilog if generate statement.
generate
if (<condition1>) begin
// Code to execute
end
else if (<condition2>) begin
// Code to execute
end
else begin
// Code to execute
end
endgenerate
As we can see from this example, the syntax for an if generate statement is virtually identical to the syntax we use for the SystemVerilog if statement.
However, there is a fundamental difference between these two approaches.
When we write a generate if statement we are actually telling our compiler to create an instance of the code block based on some condition.
As a result of this, only one of the branches in our code will be compiled. Our compiler will simply ignore the code in the other branches.
In contrast, when we use the if statement the entire if statement is compiled and each branch of the statement can be executed.
Each time the if statement code is triggered during simulation, the condition is evaluated to determine which branch to execute.
To better demonstrate how we use the if generate statement in SystemVerilog, let's consider a basic example.
For this example, we will write a 4-bit counter which we can use as a test function.
As this is only a test function, we only want to generate the counter when we create a debug build.
When we create a production build, we want to tie all of the output bits to ground instead.
We can use a parameter inside of our code to determine when we are creating a production build or a debug build.
The SystemVerilog code below shows how we would implement this using a generate if statement.
// Use a parameter to control our build
parameter debug_build = 0;
// Conditionally generate a counter
generate
if (debug_build) begin
// Code for the counter
always @(posedge clock, posedge reset) begin
if (reset) begin
count <= 4'h0;
end
else begin
count <= count + 1;
end
end
end
else begin
initial begin
count <= 4'h0;
end
end
endgenerate
When we set the debug_build variable to 1, the synthesizer produces the circuit shown below. In this case, the synthesis tool has produced a four bit counter circuit.
However, when we set the debug_build parameter to 0 then the synthesis tool produces the circuit shown below. In this instance, the synthesis tool has tied all bits of the count signal to ground.
We can use the SystemVerilog case statement inside of generate blocks in order to conditionally include blocks of code in our design.
The generate case statement essentially performs the same function as the generate if statement.
This means that we use the case generate statement when we have blocks of code which we only want to include in our design under certain circumstances.
For example, we may have a test function which we only want to include in our design when we are creating a debug build of coude.
In this case, we could use the generate case statement to determine which version of the code gets built.
The code snippet below shows the general syntax we use to write a generate case statement in SystemVerilog.
generate
case (<variable>)
<value1> : begin
// This branch executes when <variable> = <value1>
end
<value2> : begin
// This branch executes when <variable> = <value2>
end
default : begin
// This branch executes in all other cases
end
endcase
endgenerate
As we can see from this example, the syntax for this approach is virtually identical to the syntax we saw in the post on the SystemVerilog case statement.
However, there is a fundamental difference between these two approaches.
When we write a generate case statement we are actually telling our compiler to create an instance of the code block based on a given condition.
As a result of this, only one of the branches in our code will be compiled. Our compiler will simply ignore the code in the other branches.
In contrast, when we use the case statement all of the code will get compiled and each branch of the statement can be executed.
Each time the case statement code is triggered during simulation, the condition is evaluated to determine which branch to execute.
To better demonstrate how the SystemVerilog generate case statement works, let's consider a basic example.
As the case generate statement performs a similar function to the if generate statement, we will look at the same example again.
This means that we will write a test function which outputs the value of a 4-bit counter.
As this is only a test function, we only want to generate the counter output when we create a debug build.
When we build a production version of our code, we tie the the counter outputs to ground instead.
We will use a parameter to determine when we should build a debug version and when we should build the production version.
The SystemVerilog code below shows the implementation of this example using the generate case statement.
// Use a parameter to control our build
parameter debug_build = 0;
// Conditionally generate a counter
generate
case (debug_build)
1 : begin
// Code for the counter
always @(posedge clock, posedge reset) begin
if (reset) begin
count <= 4'h0;
end
else begin
count <= count + 1;
end
end
end
default : begin
initial begin
count <= 4'h0;
end
end
endcase
endgenerate
When we set the debug_build variable to 1, the synthesizer produces the circuit shown below. In this case, the synthesis tool has produced a four bit counter circuit.
However, when we set the debug_build parameter to 0 then the synthesis tool produces the circuit shown below. In this instance, the synthesis tool has tied all bits of the count signal to ground.