Using Generate and Parameters to Write Reusable SystemVerilog Designs

By John
August 12, 2021

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.

SystemVerilog Parameters

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
);

SystemVerilog Parameterized Module Example

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)
);

SystemVerilog Generate Statements

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.

Generate For Loop in SystemVerilog

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

SystemVerilog Generate For Example

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.

Circuit diagram showing three RAM modules connected to a single bus.

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.

A circuit diagram showing 3 RAM blocks connected to a bus

If Generate Statement in Verilog

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.

SystemVerilog Generate If Example

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.

A circuit diagram showing 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.

A circuit diagram showing four buffers with their inputs tied to ground

Case Generate in Verilog

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.

SystemVerilog Generate Case Example

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.

A circuit diagram showing 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.

A circuit diagram showing four buffers with their inputs tied to ground

Exercises

What is the main advantage of using parameterized modules in SystemVerilog?

We can configure the behavior of our module when we instantiate it. As a result of this, our code is generally easier to reuse.

Why do we use generate blocks in SystemVerilog?

We use generate blocks to control how are designs are compiled and built. We can use generate blocks to either conditionally or iteratively generate code in our design.

What is the main difference between a generate for block and a normal SystemVerilog for loop?

The generate for block is evaluated at compile time, meaning only one branch of the code block is ever compiled. All the code of a for loop is compiled and it is evaluated continuously during simulations.

Write a generate for block which instantiates 2 16 bit synchronous counters. The two counters should use the parameterized module example from earlier in this post.

// Variable for the generate loop
genvar i;

// Array for the outputs
wire [15:0] count_out [1:0]

// Generate the two counters
generate
  for (i=0; i < 2, i = i+1) begin
    counter # (
      .BITS (16)
    ) count_12 (
      .clock  (clock),
      .reset  (reset),
      .count  (count_out[i])
    );
  end
endgenerate 

Write a generate for block which instantiates either an 8 bit counter or a 16 bit counter, based on the value of a parameter. The two counters should use the parameterized module example from earlier in this post. You can use either a generate case or a generate if block to write this code.

// Parameter to control the generate block
parameter COUNT_16 = 0;

// Using a generate case statement
generate
  case (COUNT_16)
    0 : begin
      counter # (
        .BITS (16)
      ) count_16 (
        .clock  (clock),
        .reset  (reset),
        .count  (count16_out)
      );
    end
    default : begin
      counter # (
        .BITS (8)
      ) count_8 (
        .clock  (clock),
        .reset  (reset),
        .count  (count8_out)
      );
    end
  endcase
endgenerate

// Using a generate if statement
generate
  if (COUNT_16) begin
    counter # (
      .BITS (16)
    ) count_16 (
      .clock  (clock),
      .reset  (reset),
      .count  (count16_out)
    );
  end
  else begin
    counter # (
      .BITS (8)
    ) count_8 (
      .clock  (clock),
      .reset  (reset),
      .count  (count8_out)
    );
  end
endgenerate
Enjoyed this post? Why not share it with others.

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe

Join our mailing list and be the first to hear about our latest FPGA tutorials
Sign Up to our Mailing List
© 2024 FPGA Tutorial

Sign up free for exclusive content.

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close
The fpgatutorial.com site logo

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close