Design Tips: Using SystemVerilog Interfaces to Connect Logic in Vivado Synthesis
01 |
What is an interface?
|
SystemVerilog interfaces were developed to make it easier to connect between levels in a design. You can think of these interfaces as a collection of pins that are common to multiple modules. Instead of having to define multiple pins on each module, you only need to define the pins once in the interface, and then only need to define the interface on the module. If the signals involved in the interface are changed later, only the interface needs to be changed.
This allows you to compress a lot of information into fewer lines of code, but it can be a bit difficult to write an interface for the first time. It can also be difficult to interpret an interface someone else has written the first time. This article will introduce the basics of interfaces and how to use them correctly in Vivado.
We will convert a small test case without an interface to a test case using an interface. The sample RTL code for this test case will be presented in the last section of this article.
The schematic for this original test case is shown below:
Original test case
02 |
Defining the interface
|
First, the interface must be defined. All that is needed are the signal names that are common to multiple modules that will be replaced by the interface. Once that list is known, the interface declaration looks like this:
interface my_int;
logic sel;
logic [9:0] data1, data2, result;
endinterface : my_int
The code above declares an interface called "my_int". It also declares four signals, one called "sel" and three 10-bit wide buses called "data1", "data2", and "result". These are the pins of the module that will be replaced by the interface. Note that the interface does not use the "clk" signal even though it is used in both modules. It is OK to put control signals in the interface, but it is a matter of personal preference. I prefer to keep the clock signal separate from the interface.
03 |
Using the interface
|
Once an interface is declared, it can be used just like any port of a module. In lower-level modules, interfaces will be used instead of ports, and the coding style should be changed as follows:
Original copy:
module bottom2(
input clk,
input sel,
input [9:0] data1, data2,
output logic [9:0] result);
Replaced version:
module bottom2(
my_int int1,
input clk);
Note that unlike declaring a port as an input or output, the interface is declared to be of type "my_int" (which is the name given to the interface), and it is also given an instance name of "int1".
Since the pins of the subordinate modules have been removed, they can no longer be referenced in the same way. Instead of referencing the pins directly, they need to be referenced based on the interface name.
The syntax is “<int_name>.<pin_name>.” For example, in the original RTL, the output “result” is assigned to either “data1” or “data2” depending on the “sel” input.
always@(posedge clk) begin
if (sel == 1)
result <= data1;
else
result <= data2;
end
Now, you need to change it to the following:
always@(posedge clk) begin
if (int1.sel == 1)
int1.result <= int1.data1;
else
int1.result <= int1.data2;
end
After changing pins to interfaces in the lower-level modules, references to these pins have been changed to reference interfaces, and the upper-level modules that instantiate these modules also need to be modified.
Before using interfaces, the top-level module pins would be connected to the signals declared in the design. So now we have to connect interfaces instead of connecting signals. The first thing we need to do is declare an interface of the same type.
my_int int3();
The code above declares an interface of type "my_int" and assigns it an instance name "int3".
As before, all references to signals within this interface need to be done using the "<interface_name>.<pin_name>" syntax.
Next, the lower-level modules are instantiated.
bottom2 u1(int3,clk)
The above RTL will instantiate the "bottom2" module, giving it an instance name of "u1". The interface "int1" declared in the "bottom2" module is now associated with the interface "int3" declared one level above. After making these changes, the schematic of the design looks like this:
Design after conversion to interface
04 |
Add Modport
|
After adding the interface, the tool has created the correct connections, but you may notice that the schematic looks a little strange. "data1" and "data2" from two lower levels of the hierarchy appear to be driving the same net. If you go into those lower level blocks, you will see that there is no multi-driver issue because one of the blocks is treating "data1" and "data2" as inputs.
The reason the schematic looks strange is that the interface was created without telling the tool which pins are inputs and which are outputs. When the tool creates the connection, it knows exactly how to connect the pins, so it makes the connection first and then figures out the orientation of the pins when analyzing the behavior.
While this works, it is highly recommended to provide the input/output information of the interface to the tools. This is done by using modports. Modports are declared inside the interface and tell the tools which signals are inputs and which are outputs. Since different modules have pins in different directions, it is common to declare multiple modports per interface.
The syntax for modport is:
modport <name> (<input.output> <pin name>, <input/output> <pin name>....);
For example, the following RTL creates a modport named "b1" with the result signal as output and all other signals as input signals.
modport b2 (input sel, data1, data2, output result)
The modport is then used in the interface declaration of the underlying port list.
module bottom2 (
my_int.b2 int1,
input clk);
The above code tells the tool the following:
-
"bottom2" will use the interface "my_int" and give it an instance name called "int1"
-
In this interface, result will be the output
-
"sel", "data1", and "data2" will be the input.
Once the changes are made, the new schematic will look like this:
Design after adding modport
05 |
in conclusion
|
This article was written to illustrate the usefulness of interfaces when connecting logic with similar signals, but this is not the only use of interfaces. In addition, interfaces can use many features including tasks and functions, and can even be parameterized.
We will explore other features in future articles.
|
Original RTL without interfaces:
|
module bottom1 (
input clk,
input [9:0] d1, d2,
input s1,
input [9:0] result,
output logic sel,
output logic [9:0] data1, data2,
output logic equal);
always@(posedge clk) begin
if (d1 == d2)
equal <= 1;
else
equal <= 0;
end
always@(posedge clk) begin
if (s1 == 1) begin
data1 <= d1;
end
else begin
data2 <= d2;
end
end
always@(posedge clk) begin
sel <= ^result;
end
endmodule
module bottom2 (
input clk,
input sel,
input [9:0] data1, data2,
output logic [9:0] result );
always@(posedge clk) begin
if (sel == 1)
result <= data1;
else
result <= data2;
end
endmodule
module top (
input clk,
input s1,
input [9:0] d1, d2,
output my_sel,
output equal);
logic [9:0] data1, data2, result;
logic sel;
assign my_sel = sel;
bottom1 u0 (.clk(clk), .d1(d1), .d2(d2), .s1(s1), .result(result), .sel(sel), .data1(data1), .data2(data2), .equal(equal));
bottom2 u1 (.clk(clk), .sel(sel), .data1(data1), .data2(data2), .result(result));
endmodule
|
The design of adding the interface for the first time:
|
interface my_int;
logic sel;
logic [9:0] data1, data2, result;
endinterface : my_int
module bottom1 (
my_int int1,
input clk,
input [9:0] d1, d2,
input s1,
output logic equal);
always@(posedge clk) begin
if (d1 == d2)
equal <= 1;
else
equal <= 0;
end
always@(posedge clk) begin
if (s1 == 1) begin
int1.data1 <= d1;
end
else begin
int1.data2 <= d2;
end
end
always@(posedge clk) begin
int1.sel <= ^int1.result;
end
endmodule
module bottom2 (
my_int int1,
input clk);
always@(posedge clk) begin
if (int1.sel == 1)
int1.result <= int1.data1;
else
int1.result <= int1.data2;
end
endmodule
module top (
input clk,
input s1,
input [9:0] d1, d2,
output my_sel,
output equal);
logic [9:0] data1, data2, result;
logic sel;
my_int int3();
assign my_sel = int3.sel;
bottom1 u0 (int3, clk, d1, d2, s1, equal);
bottom2 u1 (int3, clk);
endmodule
|
Designing with modports:
|
interface my_int;
logic sel;
logic [9:0] data1, data2, result;
modport b1 (input result, output sel, data1, data2);
modport b2 (input sel, data1, data2, output result);
endinterface : my_int
module bottom1 (
my_int.b1 int1,
input clk,
input [9:0] d1, d2,
input s1,
output logic equal);
always@(posedge clk) begin
if (d1 == d2)
equal <= 1;
else
equal <= 0;
end
always@(posedge clk) begin
if (s1 == 1) begin
int1.data1 <= d1;
end
else begin
int1.data2 <= d2;
end
end
always@(posedge clk) begin
int1.sel <= ^int1.result;
end
endmodule
module bottom2 (
my_int.b2 int1,
input clk);
always@(posedge clk) begin
if (int1.sel == 1)
int1.result <= int1.data1;
else
int1.result <= int1.data2;
end
endmodule
module top (
input clk,
input s1,
input [9:0] d1, d2,
output my_sel,
output equal);
logic [9:0] data1, data2, result;
logic sel;
my_int int3();
assign my_sel = int3.sel;
bottom1 u0 (int3, clk, d1, d2, s1, equal);
bottom2 u1 (int3, clk);
endmodule
Xilinx official account
Creating a world where everything is smart and adaptable
Long press the QR code to follow us