首发于 SystemVerilog
【可综合SV】Connecting Modules and Interfaces

【可综合SV】Connecting Modules and Interfaces

Connecting Modules and Interfaces

51837 - Design Assistant for Vivado Synthesis - Help with SystemVerilog Support - Connecting Modules and Interfaces (xilinx.com)

SystemVerilog Connecting Modules and Interfaces structures that are supported by Vivado Synthesis

支持的模块连接方式

Vivado 合成支持以下四种实例化和连接模块的方式:

  • by ordered list(顺序列表)
  • by name(SystemVerilog新增)
  • by named ports (命名端口)
  • by wildcard ports (通配符端口)(SystemVerilog新增)

四种方式的具体体现如下例

module module_connect(
    input int a, b,
    output int r1,
    output int sum,
    output int r4,
    output logic [15:0] r2
 adder #(.delay (5.0)) i1 (a, b, r1);        // 32-bit 2-state adder : ordered list
 adder #(.delay (5.0)) i3 (.a, .b, .sum);    // 32-bit 2-state adder : name
 adder #(.dtype (logic[15:0])) i2 (a, b, r2);// 16 bit 4-state adder : named ports
 adder #(.delay (5.0)) i4 (.*, .sum(r4));    // 32-bit 2-state adder : wildcard ports
endmodule
module adder #(parameter type dtype = int, parameter realtime delay = 4) (
    input dtype a, b, 
    output dtype sum
    assign sum = a + b;
endmodule

其中使用名字 .name 或通配符 .* 来连接模块可以很大程度减少连接错误或者遗漏。主要是由于:

  • 所连接的线网(net)必须是显式声明的 。点名和点星方式本身并不会隐含一个线网,这可防止传统Verilog中隐式线网有时的网表错误。
  • 端口长度(size)与线网必须相同 。传统Verilog允许连接的长度不匹配,然而通常这都是由错误或者丢失的线网导致的。
  • 针对点星方法,必须显示声明未连接的端口。 传统Verilog会将模块实例中任何未显式列出的端口都推断为不连接的。然而没有连接的端口通常都是无意的编码错误。点星方式要求显式列出所有未连接端口。 需注意的是,点名并没有这种额外检查

这里插入Xilinx提供的SV的使用若干建议:

55194 - Vivado Synthesis - SystemVerilog 的 Vivado Synthesis 最佳实践是什么? (xilinx.com)

文中提及:不要混合使用 2 状态和 4 状态类型,使用 .* 减少模块连接错误。下面补充基本状态类型:

无符号 有符号
2状态 bit byte(8-bit),shortint(16-bit),int(32-bit),longint(64-bit)
4状态 logic,reg,net-type integer(32-bit)

net-type主要可以分成5类,一般前两类是可综合,后三类用于仿真:

类型 信号 说明
1 Wire and Tri nets wire,Tri wire可用于被单个门或连续赋值驱动,Tri可用于多个驱动器驱动一个net
2 Supply nets supply0,supply1 用来对电路中的电源建模,这些net有supply强度
3 Wired nets wand,triand,wor,trior 用于对wired 逻辑配置建模
4 Trireg net trireg Trireg存储一个值,被用来对充电存储建模
5 Tri0 and tri1 nets Tri0,tri1 对具有上拉电阻和下拉电阻的net建模

可参考:

使用接口的好处

这段传统Verilog代码片段:

module top;
    wire sig1, sig2, sig3, sig4;
    wire clock, reset;
mod_a u1 (  .sig1(sig1),
            .sig2(sig2),
            .sig3(sig3),
            .sig4(sig4),
            .clk(clock),
            .rstN(reset) );
mod_b u2    (.sig1(sig1),
            .sig2(sig2),
            .sig3(sig3),
            .sig4(sig4),
            .clk(clock),
            .rstN(reset) );
endmodule
module mod_a (  input sig1, sig2,
                input clk, rstN,
                output sig3, sig4);
endmodule
module mod_b (  input sig3, sig4,
                input clk, rstN,
                output sig1, sig2);
endmodule

上述例子中将信号多次例化或引用,一旦设计发生了更改,比如在两个模块实例间需要添加一个额外信号,或者是改变了端口长度(size),我们就要在该代码分别进行修改,甚至很可能要在不同的源文件中进行修改。

SystemVerilog的接口(interface)允许我们将信号声明信息封装于一个地方。下例同上例一样,只是用了interface。现在如果要在两个模块间添加一个信号,或事更改向量长度(size)的话,就只需要修改一处代码了。

interface intf_1;
    wire sig1, sig2, sig3, sig4;
endinterface: intf_1
module top;
    wire clock, reset;
intf_1 i1();
mod_a u1 (.a1(i1), .clk(clock), .rstN(reset) );
mod_b u2 (.b1(i1), .clk(clock), .rstN(reset) );
endmodule: top
module mod_a (  interface a1,
                input clk, rstN);
endmodule: mod_a
module mod_b (  intf_1 b1,
                input clk, rstN);
endmodule: mod_b

通用接口端口(general interface port)

看上例中的mod_a,它的端口a1被声明为了interface端口类型(port type),而非传统的input,output或inout端口方向。这被称为“ 通用接口端口(general interface port) ”。将任意接口定义的实例(instance of any interface definition)连到通用接口端口都是合法的。

注意: 通用接口端口是可综合的,但带有通用接口端口的模块 不可作为elaboration的top module

特定类型接口端口(type-specific interface port)

在上例的mod_b中,接口端口b1被声明为了intf_1端口类型,这被称为“ 特定类型接口端口(type-specific interface port) ”。只有将intf_1接口的实例连接于该特定类型接口端口才是合法的。

建议
在模块设计方案中仅使用特定类型接口端口。设计方案所期待的是接口端口内的特定信号。特定类型接口端口能确保所预期的接口,以及该接口内的信号,能被连到该口。

如何使用接口

接口是模块之间特定通信的一种方式。接口由一组网表(nets)和变量(variables)组合在一起,用于在模块之间建立连接。Vivado 合成支持以下接口结构:

  • Interface definition ports
  • Interface data type declarations
  • Interface modport definitions
  • Interface tasks and functions; must be fully automatic
  • Interface procedural code(always blocks); must follow synthesis rules
  • Parameterized Interfaces

上文中提到的 fully automatic 应该说的是tasks和functions不能被 static 修饰,经过测试在 task 内部使用 static 修饰的全局变量也能通过综合:

static logic a = 1;
interface if;
    logic b;
    task tif (output b);
        b = a;
    endtask
endinterface

另外在前文最佳实践中提及:

Functions are automatic by default.

首先可以创建一个头文件用于放置使用的接口,例如 intf.sv, 在需要调用的模块前包括即可

`include "intf.sv"
module top(
endmodule

Interface modport definitions

创建好头文件后,在其中定义需要使用的接口。下面这个例子中,展示了支持结构中的 definition ports、 data type declarations、modport definitions 和 Parameterized Interfaces

interface randomnum_intf # (parameter N = 4) // Parameterized Interfaces
  (input clk);                               // definition ports
  // data type declarations
  logic request, ready;
  logic [N-1:0]  value;
  logic          newport;
  // modport definitions
  modport randomnum_p (input request, output ready, value);
  modport controller_p (input value, ready, output request);
endinterface

这里着重介绍一下 modport。使用 modport 可以将接口进行分组(可以是部分信号),并指定端口的方向。那么定义好的接口如何在模块中声明和使用:

`include "intf.sv"
module randomnum(
    input rst,
    randomnum_intf.randomnum_p int2,    //  modport 声明
    randomnum_intf int3                 //  inout
 always_ff @(posedge int3.clk)
  if(rst)
    int2.ready <= int3.request;
    int2.ready <= int2.request;

对于没有使用 modport指定端口方向的信号(如 newport ),或者在声明时没有使用modport名,将使用点对点的无信号方向的连接方式,在综合时会被作为 inout接口

[Synth 8-330] inout connections inferred for interface port 'randomnum_intf' with no modport

Interface tasks and functions

下面这个例子展示了支持结构中的 tasks and functions

interface display_intf;
  logic won, lost;
  logic [4:0] user_total, fpga_total;
  task digi_display (input won, lost, 
                     input [4:0] user_total, fpga_total,
                     output [7:0] left_half1, left_half0, right_half1, right_half0
    case(user_total)
     5'b00000:  {left_half1, left_half0} = {8'b00000011,8'b00000010};//00
     5'b11111:  {left_half1, left_half0} = {8'b00001101,8'b10011110};//31
    endcase
    if(won || lost)
        case(fpga_total)
          5'b00000: {right_half1, right_half0} = {8'b00000011,8'b00000011};//00    
          5'b11111: {right_half1, right_half0} = {8'b00001101,8'b10011111};//31
        endcase
        {right_half1, right_half0} = {8'b11111111,8'b11111111};    
    endtask 
endinterface

特别需要注意的是,虽然 task 中使用了端口方向,但是接口中端口方向仍然是 inout

[Synth 8-330] inout connections inferred for interface port 'display_intf' with no modport

那么如何在模块中使用接口定义的 task :

`include "intf.sv"
module display(
    input clk, rst,
    display_intf int1,  //  声明接口
logic [7:0] left_half1, left_half0, right_half1, right_half0;
//task call 
always_comb
int1.digi_display (.won(int1.won), .lost(int1.lost), 
                   .user_total(int1.user_total), .fpga_total(int1.fpga_total),
                   .* );

用过组合逻辑对接口中的 task 进行驱动。

Top Level

上面两个例子主要说明了在模块中如何使用接口,这些模块最终会被放置在顶层模块下,而在顶层模块中我们需要对接口进行声明:

`include "intf.sv"
module top(input clk);
randomnum_intf #(.N(4)) int3(.clk(clk));    // 接口例化