第25章 interface

25.1 通用

本条款描述了以下内容:

--- 接口的目的

--- 接口语法

--- 接口模块端口

--- 接口方法

--- 参数化接口

--- 虚拟接口

--- 访问接口对象

25.2 Overview

数字系统中模块之间的通信是一个至关重要的领域,它会影响到从 RTL 编码的便捷性到硬件软件划分、性能分析、总线实现选择以及协议检查等方方面面。SystemVerilog 中的接口结构正是为了封装模块之间的通信而专门设计的,它使得从抽象的系统级设计通过逐步细化直至更低层次的寄存器传输和结构设计视图的迁移能够实现顺畅进行。通过封装模块之间的通信,接口结构还促进了设计的复用。接口功能的包含是 SystemVerilog 的一个重要优势。

在最底层,接口是一个带有名称的网束或变量集合。接口在设计中被实例化,并可以通过端口作为一个单一元素进行访问,同时在需要的地方还可以通过引用组件的网或变量来使用。设计中很大一部分通常由端口列表和端口连接列表组成,这些只是名称的重复。将一组名称替换为一个名称这一操作能够显著缩减描述的篇幅,并提高其可维护性。

该接口的额外功能还源于其能够将功能与连接性进行封装,从而使接口在最高层面上更像一个类模板。接口可以有参数、常量、变量、函数和任务。接口中的元素类型可以进行声明,或者也可以作为参数传递进来。成员变量和函数是相对于接口的实例名称(即实例成员)进行引用的。因此,通过接口连接的模块可以直接调用该接口的子程序成员来驱动通信。由于将功能封装在接口中并使其与模块相隔离,因此可以轻松地通过替换具有相同成员但以不同抽象级别实现的接口来更改通信协议的抽象级别和/或粒度。通过该接口连接的模块无需进行任何更改。

为了为模块端口提供方向信息并控制特定模块内任务和函数的使用,提供了 modport 构造。正如其名称所示,这些方向是从模块内部所能看到的。

除了子例程方法之外,接口还可以包含过程(i.e., initial or always procedures )以及连续赋值,这些对于系统级建模和测试平台应用非常有用。这使得接口能够包含例如其自身的协议检查器,该检查器能够自动验证通过该接口连接的所有模块是否符合指定的协议。其他应用,如功能覆盖记录和报告、协议检查以及断言,也可以集成到接口中。

这些方法可以是抽象的,即在一个模块中定义,在另一个模块中调用,使用导出和导入构造。这可以通过使用层次路径名来实现,但这样做会妨碍复用,因为名称将是设计特定的。更好的方法是将子例程名称声明在接口中,并使用接口实例中的本地层次名称进行定义和调用。广播通信是通过"分叉-合并"任务来实现的,这些任务可以在多个模块中定义,并且可以并行执行。

25.3 Interface syntax

  • `interface_ansi_header`:ANSI 风格接口头,支持在声明中直接定义端口。

  • `list_of_port_declarations`:可选的一组端口声明(如 `input logic clk; output logic rst_n;`)。

  • `non_port_interface_item`:接口中非端口的成员,如 `modport`、`clocking`、`method`、`property` 等。

  • `timeunits_declaration`:时间单位声明(可选)。

  • `endinterface`:结束接口定义。

  • `attribute_instance`:可选的属性(如 `( always_ready )`)。

  • `interface_identifier`:接口名。

  • `modport`:关键字,用于定义一个端口访问模式。

  • `modport_item`:一个 `modport` 的具体定义。

该接口结构提供了一种新的层次结构。它可以包含更小的接口,并且可以通过端口进行传递。

接口的目的是封装通信。在较低层次上,这意味着将变量和网络打包到接口中,并且可以通过在模块端口中设置端口方向来施加访问限制。模块可以被设计为通用的,以便接口可以更改。以下示例展示了这些特性。在更高的抽象层次上,通信可以通过任务和函数来实现。接口可以包含子程序定义或仅仅是子程序原型,定义在一个模块中,调用在另一个模块中。(see 25.7 and 25.7.3).

一个简单的接口声明如下(完整的语法请参见语法 25 - 1):

复制代码
interface identifier;
...
interface_items
...
endinterface [ : identifier ]

接口可以像模块一样以层级结构进行实例化,而且可以有也可以没有端口。例如:

复制代码
myinterface #(100) scalar1(), vector[9:0]();

在这个示例中,已实例化了 11 个类型为"myinterface"的接口,并且每个接口内的第一个参数都更改为 100。其中一个名为"scalar1"的"myinterface"接口被实例化,同时还有 10 个名为"vector9"到"vector0"的"myinterface"接口的数组也被实例化。

接口可以在模块(无论是扁平结构还是层级结构)中进行声明和实例化,但模块既不能在接口中进行声明,也不能在接口中进行实例化。与模块(参见第 23.3 节)和程序(参见第 24.3 节)不同,
接口从不会被隐式实例化。

在实例中的 defparam 如果其端口实际值引用了一个数组化的接口,则不应修改该实例层次结构之外的参数。如果接口端口连接的实际值是对一个接口的层级化引用或者是对一个层级化引用接口的 modport 的引用,则该层级化引用应指向一个接口实例,且不应通过数组化实例或生成块进行解析。

接口最简单的使用方式是将导线打包在一起,如以下示例所示。

25.3.1 Example without using interfaces

This example shows a simple bus implemented without interfaces.

复制代码
module memMod( input logic req,
logic clk,
logic start,
logic [1:0] mode,
logic [7:0] addr,
inout wire [7:0] data,
output bit gnt,
bit rdy );
logic avail;
...
endmodule
module cpuMod(
input logic clk,
logic gnt,
logic rdy,
inout wire [7:0] data,
output logic req,
logic start,
logic [7:0] addr,
logic [1:0] mode );
...
endmodule
module top;
logic req, gnt, start, rdy;
logic clk = 0;
logic [1:0] mode;
logic [7:0] addr;
wire [7:0] data;
memMod mem(req, clk, start, mode, addr, data, gnt, rdy);
cpuMod cpu(clk, gnt, rdy, data, req, start, addr, mode);
endmodule
25.3.2 Interface example using a named bundle

TBD 后续主要是正常的接口链接

复制代码
module memMod (simple_bus sb_intf, input logic clk);
...
endmodule
module cpuMod (simple_bus sb_intf, input logic clk);
...
endmodule

module top;
    logic clk = 0;
    simple_bus sb_intf();
    memMod mem (.*); // implicit port connections
    cpuMod cpu (.*); // implicit port connections
endmodule

clk和sb_intf和接口实例名一致,可以直接使用模糊匹配

25.4 Ports in interfaces

简单接口的一个局限性在于,该接口内部声明的网络和变量仅用于与具有相同网络和变量的端口进行连接。若要共享外部网络或变量,需要进行以下操作:从接口外部建立连接,并与所有实例化该接口的模块端口形成共同连接。为此,需要进行接口端口声明。接口端口列表中的网络或变量与接口内部的其他网络或变量之间的区别在于,只有列表中的网络或变量在实例化接口时才能通过名称或位置进行外部连接。接口端口声明的语法和语义与模块的语法和语义相同(参见第 23.2.2 节)。

复制代码
interface i1 (input a, output b, inout c);
    wire d;
endinterface

线缆 a、b 和 c 可以分别与接口相连,从而能够与其他接口共用。

以下示例展示了如何指定一个带有输入的接口,使得一条线缆能够在该接口的两个实例之间共享:

复制代码
interface simple_bus (input logic clk); // Define the interface
    logic req, gnt;
    logic [7:0] addr, data;
    logic [1:0] mode;
    logic start, rdy;
endinterface: simple_bus

module memMod(simple_bus a); // Uses just the interface
    logic avail;
    always @(posedge a.clk) // the clk signal from the interface
        a.gnt <= a.req & avail; // a.req is in the 'simple_bus' interface
endmodule
module cpuMod(simple_bus b);
...
endmodule

module top;
    logic clk = 0;
    simple_bus sb_intf1(clk); // Instantiate the interface
    simple_bus sb_intf2(clk); // Instantiate the interface
    memMod mem1(.a(sb_intf1)); // Reference simple_bus 1 to memory 1
    cpuMod cpu1(.b(sb_intf1));
    memMod mem2(.a(sb_intf2)); // Reference simple_bus 2 to memory 2
    cpuMod cpu2(.b(sb_intf2));
endmodule

在上述示例中,实例化的接口名称与 memMod 和 cpuMod 模块中使用的接口名称不一致;因此,对于此示例无法使用隐式端口连接。

25.5 Modports

TBD

25.5.4 Modport expressions

Modport 表达式允许将数组和结构体的元素、元素的连接以及在接口中声明的元素的赋值模式表达式包含在Modport 列表中。这种Modport 表达式会通过端口标识符进行明确命名,并且仅通过模Modport 连接可见。

与模块端口声明中的明确命名端口一样,端口标识符在每个模端口列表中都有其独立的命名空间。当模端口项只是一个简单的端口标识符时,该标识符既可用作接口项的引用,也可用作端口标识符。一旦定义了端口标识符,就不能再有同名的端口定义。例如:

复制代码
interface I;
    logic [7:0] r;
    const int x=1;
    bit R;
    modport A (output .P(r[3:0]), input .Q(x), R);
    modport B (output .P(r[7:4]), input .Q(2), R);
endinterface

module M ( interface i);
    initial i.P = i.Q;
endmodule

module top;
    I i1 ();
    M u1 (i1.A);
    M u2 (i1.B);
    initial #1 $display("%b", i1.r); // displays 00100001
endmodule

端口表达式的自定义类型成为了该端口的类型。端口表达式不应被视为类似赋值的语境。端口表达式应解析为模块端口类型的有效表达式(参见 23.3.3)。在上述示例中,Q 端口不能是输出或输入端口,因为端口表达式是一个常量。端口表达式是可选的,因为可以定义一些不与端口内部任何内容相连的端口。

以下示例说明了具有可参数化客户端数量的总线是如何被描述的:

复制代码
// Bus interface with parameterized number of client modports
interface intf_t #(num_clients = 0);
    bit [num_clients-1:0] req;
    for (genvar i=0; i< num_clients; i++) begin: mps
        modport client_mp (output .client_req( req[i] ));
    end
endinterface

// A generic client that attaches to the bus
module client_m (interface client_ifc);
    // ... code will drive client_ifc.client_req
endmodule

// The bus system with N clients
module bus #(N = 0);
    intf_t #(.num_clients(N)) intf();
    for (genvar j=0; j < N; j++) begin: clients
        client_m client (.client_ifc (intf.mps[j].client_mp));
    end
endmodule
25.5.5 Clocking blocks and modports

"modport"构造也可以用于指定在接口内声明的时钟块的方向。与其它"modport"声明一样,时钟块的方向是从该接口成为模块的端口时所能看到的方向。其语法如语法 25 - 2 所示。

在模块端口声明中所使用的所有时钟块都应与该模块端口本身采用相同的接口进行声明。与所有模块端口声明一样,时钟信号的方向是从该模块中该接口成为端口时所能看到的方向。以下示例展示了如何使用模块端口来创建同步以及异步端口。当与虚拟接口结合使用(参见 25.9.2 节)时,这些结构有助于创建抽象的同步模型。

复制代码
interface A_Bus( input logic clk );
    wire req, gnt;
    wire [7:0] addr, data;
    clocking sb @(posedge clk);
        input gnt;
        output req, addr;
        inout data;
        property p1; req ##[1:3] gnt; endproperty
    endclocking

    modport DUT ( input clk, req, addr, // Device under test modport
        output gnt,
        inout data );
    modport STB ( clocking sb ); // synchronous testbench modport
    modport TB ( input gnt, // asynchronous testbench modport
        output req, addr,
        inout data );
endinterface

上述接口 A_Bus 可以按照以下方式进行实例化:

复制代码
module dev1(A_Bus.DUT b); // Some device: Part of the design
...
endmodule
module dev2(A_Bus.DUT b); // Some device: Part of the design
...
endmodule

module top;
    logic clk;
    A_Bus b1( clk );
    A_Bus b2( clk );
    dev1 d1( b1 );
    dev2 d2( b2 );
    T tb( b1, b2 );
endmodule

program T (A_Bus.STB b1, A_Bus.STB b2 ); // testbench: 2 synchronous ports
    assert property (b1.sb.p1); // assert property from within program
    initial begin
        b1.sb.req <= 1;
        wait( b1.sb.gnt == 1 );
        ...
        b1.sb.req <= 0;
        b2.sb.req <= 1;
        wait( b2.sb.gnt == 1 );
        ...
        b2.sb.req <= 0;
    end
endprogram

此示例展示了通过接口端口 b1 和 b2 的时钟化模块端口所指定的同步接口来使用程序块的情况。除了时钟化块信号的程序驱动和采样外,该程序还对其中一个接口 b1 的属性 p1 进行了断言。

25.6 Interfaces and specify blocks

指定块用于描述模块内的各种路径,并进行时序检查以验证模块输入端发生的事件是否满足由该模块所描述的器件的时序约束。模块路径是从模块输入端口到输出端口,而时序检查则是相对于模块输入端进行的。指定块将这些端口称为终端描述符。模块输入输出端口既可以作为输入端口也可以作为输出端口。当某个端口实例为接口时,接口中的每个信号都会成为可用端口,其默认方向由接口定义或由 modport 限制。引用端口不能在指定块中用作终端。

以下是一个将接口与指定块一起使用的示例:

复制代码
interface itf;
    logic c,q,d;
    modport flop (input c,d, output q);
endinterface

module dtype (itf.flop ch);
    always_ff @(posedge ch.c) ch.q <= ch.d;
    specify
    ( posedge ch.c => (ch.q+:ch.d)) = (5,6);
    $setup( ch.d, posedge ch.c, 1 );
    endspecify
endmodule

25.7 Tasks and functions in interfaces

子程序(任务和函数)可以定义在接口内部,也可以定义在与之相连的一个或多个模块内部。这使得模型的抽象程度更高。例如,"读取"和"写入"可以被定义为任务,而无需提及任何线路,主模块只需调用这些任务即可。在 modport 中,这些任务被声明为导入任务。

函数原型指定了定义在其他地方的函数的参数类型和方向以及返回值。同样,任务原型指定了定义在其他地方的任务的参数类型和方向。在 modport 中,导入和导出构造既可以使用子程序原型,也可以仅使用标识符。唯一的例外是在使用 modport 从另一个模块导入子程序以及使用默认参数值或按名称绑定参数的情况下,此时必须使用完整的原型。

原型中的参数数量和类型应与子程序声明中的参数类型相匹配。类型匹配的规则在 6.22.1 中有详细说明。在子程序调用中如果需要默认参数值,应在原型中指定。如果在原型和声明中都为参数指定了默认值,则这些指定的值不必相同,但使用的默认值应为原型中指定的那个。原型中的形式参数名是可选的,除非使用了默认参数值或名称绑定或声明了额外的未展开维度。原型中的形式参数名应与声明中的形式参数名相同。

如果一个模块连接到包含导出子程序的 modport,并且该模块未定义该子程序,则将出现解释错误。同样,如果 modport 包含导出子程序原型,而模块中定义的子程序与该原型不完全匹配,则也将出现解释错误。如果子程序是通过使用层级名称在模块中定义的,那么它们也应在接口中声明为"extern",或者在 modport 中声明为"export"。

任务(而非函数)可以在被两次实例化的模块中定义,例如,两个由同一 CPU 驱动的存储器。这种多重任务定义可以通过在接口中使用"extern forkjoin"声明来实现。

相关推荐
aixingkong9211 小时前
Nvidia为例 XPU的Multi-Host、Socket Direct 、GPUDirect、RDMA 技术点分析
服务器·网络·硬件架构·硬件工程
雯宝3 小时前
mnt_init 函数
linux
律宏阔9 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔9 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴9 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千10 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3710 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3710 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3710 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW10 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux