Accessing elements of instance arrays in SystemVerilog requires elaboration-time constants, which can limit flexibility and scalability.
Using generate constructs or virtual interfaces helps overcome these limitations, enabling more robust and adaptable verification environments.
The Problem
When writing a testbench, we often need to access specific elements from various arrays of instances.
According to the IEEE 1800-2023 Standard for SystemVerilog, hierarchical access can be achieved using the following syntax (Chapter 23.6, Syntax 23-7):
hierarchical_identifier ::= [ $root . ] { identifier constant_bit_select . } identifier
In other words, you need a constant to access elements of an array.
Let’s say we need to configure five instances of the following interface:
interface intf;
logic [2:0] signal;
endinterface
Accessing them using constants could look something like this:
module top;
localparam nof_ifs = 5;
parameter zero = 0;
`define one 1
const int two = 2;
//Array of interfaces
intf intf_array[nof_ifs]();
initial begin
//Accessing by constants
intf_array[zero].signal = 0;
intf_array[`one].signal = 1;
//const variables will not work since they are run-time constants
//and will result in an error
intf_array[two].signal = 2;
intf_array[3].signal = 3;
intf_array[nof_ifs-1].signal = 4;
end
endmodule
In this example the following types of constants are used for accessing the elements of an array: parameters, defines and literal numbers.
You might assume that a const variable could be used to access elements of an array – but that’s not actually the case. The LRM states (Chapter 6.20) that the const keyword defines a run-time constant. However, for this type of element accessing, elaboration-time constants need to be used. You can find a more detailed explanation on the differences between parameters and const variables in this paper.
Using constants to access elements can be a hassle. It leads to repetitive code and doesn’t scale well if the number of instances increases or decreases.
One might think of using a loop to simplify access – but that approach doesn’t work as expected. The loop iterator isn’t treated as a constant during elaboration.
For example:
module top;
localparam nof_ifs = 5;
// Array of interfaces
intf intf_array[nof_ifs]();
initial begin
for(int i = 0; i < nof_ifs; i++)
intf_array[i].signal = i;
end
endmodule
[OUTPUT]: *Error: Illegal operand for constant expression [4(IEEE)].
First Solution – Use Generate
To address this limitation and make the testbench more robust and maintainable, there are more effective alternatives.
One possible solution is to take advantage of generate constructs. generate constructs, together with genvars are evaluated during elaboration time, allowing the use of loops for array index accessing.
Therefore, they offer a scalable and clean structure for growing designs.
module top;
localparam nof_ifs = 5;
// Array of interfaces
intf intf_array[nof_ifs]();
generate
for(genvar i = 0; i < nof_ifs; i++)
initial begin
intf_array[i].signal = i;
end
endgenerate
endmodule
Second Solution – Use Virtual Interfaces
Another, less common, but powerful approach involves the use of virtual interfaces. This method decouples the interface from its physical instance. It enables dynamic access through handles, which can be passed around and managed more flexibly during simulation.
module top;
localparam nof_ifs = 5;
// Array of interfaces
intf intf_array[nof_ifs]();
// Array of virtual interfaces
virtual intf virtual_intf_array[nof_ifs] = intf_array;
initial begin
for(int i = 0; i < nof_ifs; i++)
virtual_intf_array[i].signal = i;
endmodule
That’s all! Read, Share and Subscribe to keep the community alive!