IEEE Standard for SystemVerilog—Unified Hardware Design, Specification, and Verification Language笔记3

IEEE Standard for SystemVerilog—Unified Hardware Design, Specification, and Verification Language笔记第3章(Design and verification building blocks)

概要

  1. 标准目的

    • 标准名称:IEEE Std 1800-2017
    • 目的:为SystemVerilog语言提供统一的硬件设计、规范和验证标准。
    • 版权:© 2018 IEEE,保留所有权利。
  2. 设计和验证构建块

    • 模块(Modules):SystemVerilog的基本构建块,用于表示设计块,也可以包含验证代码和模块间的互连。
    • 程序(Programs):为测试台环境建模提供构建块,与模块相比,它更注重测试环境的完整建模而非硬件级细节。
    • 接口(Interfaces):封装设计块之间的通信,促进设计的重用和迁移,可以包含参数、常量、变量、函数和任务。
    • 检查器(Checkers):封装断言和建模代码的验证块,用于创建形式验证的辅助模型。
    • 原始类型(Primitives):表示低级逻辑门和开关,包括内置原始类型和用户定义的原始类型(UDP)。
    • 子程序(Subroutines):包括任务和函数,用于封装可执行代码,可以从多个位置调用。
    • 包(Packages):提供声明空间,可以由其他构建块共享,包含类型定义、函数、任务等。
    • 配置(Configurations):指定设计配置的绑定信息,将模块实例与具体的SystemVerilog源代码关联起来。
  3. 编译和阐述

    • 编译过程:读取SystemVerilog源代码,分析语法和语义错误,可能保存编译结果为中间格式或直接进入阐述阶段。
    • 阐述过程:将设计组件绑定在一起,包括实例化模块、计算参数值、解析层次名称、建立网络连接等,为仿真做准备。
  4. 编译单元

    • 定义:一组一个或多个SystemVerilog源代码文件组成的集合。
    • 编译单元作用域:每个编译单元有一个局部作用域,包含所有位于其他作用域之外的声明。
    • $unit:用于访问编译单元作用域中声明的标识符的特殊名称。
  5. 名称空间

    • 全局名称空间:包括定义名称空间和包名称空间。
    • 局部名称空间:包括编译单元作用域名称空间、模块名称空间、块名称空间、端口名称空间和属性名称空间。
    • 规则:每个名称空间中的名称必须唯一,不得重复声明。
  6. 仿真时间和精度

    • 时间单位:表示时间和延迟的测量单位,范围从秒到飞秒。
    • 时间精度:指定延迟的准确度,必须至少与时间单位一样精确。
    • 指定方式:可以使用timescale编译器指令或timeunittimeprecision关键字来指定时间和精度。
  7. 示例和详细语法

    • 模块示例:提供了一个简单的2到1多路复用器的模块示例,展示了如何声明端口、连续赋值和过程块。
    • 程序示例:展示了一个简单的程序示例,说明了如何在测试台环境中使用程序块。
    • 接口示例:展示了接口的定义和使用,包括如何封装通信协议和连接设计块。

本章详细定义了SystemVerilog语言的标准、构建块、编译和阐述过程、名称空间、仿真时间和精度,并通过示例展示了如何在实际设计中应用这些概念和规则。理解这些内容对于使用SystemVerilog进行硬件设计和验证至关重要。

内容

3.1 一般性概述

本条款描述了以下内容:

  • 模块、程序、接口、检查器和原始类型的目的
  • 子程序的概述
  • 包的概述
  • 配置的概述
  • 设计层次结构的概述
  • 编译和阐述的定义
  • 声明名称空间
  • 仿真时间、时间单位和时间精度

本条款定义了几个重要的SystemVerilog术语和概念,这些术语和概念贯穿本文档始终。本条款还概述了用于表示硬件设计及其验证环境的建模块的用途和使用方法。

3.2 设计元素

设计元素是SystemVerilog的模块(见第23章)、程序(见第24章)、接口(见第25章)、检查器(见第17章)、包(见第26章)、原始类型(见第28章)或配置(见第33章)。这些结构分别通过moduleprograminterfacecheckerpackageprimitiveconfig关键字引入。

设计元素是用于建模和构建设计及验证环境的主要构建块。这些构建块是声明和过程代码的容器,这些声明和过程代码在本文档后续条款中讨论。

本章描述了这些构建块的目的。有关这些块的语法和语义的完整详细信息,在本文档后续条款中定义。

3.3 模块

SystemVerilog中的基本构建块是模块,它被moduleendmodule关键字包围。模块主要用于表示设计块,但也可以作为验证代码和设计块与验证块之间互连的容器。模块可以包含以下构造之一或多个:

  • 端口,包括端口声明
  • 数据声明,如网络(nets)、变量、结构体和联合体
  • 常量声明
  • 用户定义的类型定义
  • 类定义
  • 从包中导入的声明
  • 子程序定义
  • 其他模块、程序、接口、检查器和原始类型的实例化
  • 类对象的实例化
  • 连续赋值
  • 过程块
  • 生成块
  • 指定块

以下是对上述列表中每个构造的简要说明,但请注意,此列表并非详尽无遗,模块还可以包含其他在本文档后续条款中讨论的构造。

以下是一个简单的模块示例,它表示一个2到1多路复用器:

module mux2to1 (input wire a, b, sel, // 组合端口和类型声明
                 output logic y);
  always_comb begin // 过程块
    if (sel) y = a; // 过程语句
    else y = b;
  end
endmodule : mux2to1

在上面的例子中,mux2to1模块有三个输入端口absel,以及一个输出端口yalways_comb块实现了多路复用器的逻辑,根据选择信号sel的值,选择输入ab作为输出y的值。

模块的详细讨论在本文档的第23条中给出。关于如何使用模块创建设计层次结构的信息,请参阅3.11节。

3.4 程序

程序构建块被programendprogram关键字包围,旨在为测试台环境建模提供支持。虽然模块构建块非常适合用于硬件描述,但在测试台环境中,重点不在于硬件级别的细节,如线网(wires)、结构层次和互连,而在于建模设计被验证的完整环境。程序块服务于以下三个基本目的:

  1. 提供测试台执行的入口点:程序块定义了一个开始执行测试台代码的地方,类似于C语言中的main函数。

  2. 创建封装程序级数据、任务和函数的作用域:程序块创建了一个作用域,该作用域封装了在整个测试台程序中使用的数据、任务和函数,有助于组织和管理测试台代码。

  3. 提供指定响应区域调度的语法上下文:与模块不同,程序块为响应区域的调度提供了明确的语法上下文,这对于模拟中的并发性和同步至关重要。

程序构建块充当设计与测试台之间的清晰分隔符,更重要的是,它指定了专门的模拟执行语义。与时钟块(见第14条)结合使用时,程序构建块提供了设计与测试台之间无竞争的交互,并启用了循环和事务级别的抽象。

程序块可以包含数据声明、类定义、子程序定义、对象实例,以及一个或多个initialfinal过程。然而,程序块不能包含always过程、原始类型实例、模块实例、接口实例或其他程序实例。

SystemVerilog的抽象和建模结构简化了测试台的创建和维护。能够实例化并单独连接每个程序实例,使其能够作为通用模型使用。

以下是一个程序声明的示例:

program test (input clk, input [16:1] addr, inout [7:0] data);
  initial begin
    // 初始化代码
  end
endprogram

在这个示例中,test程序接受时钟信号clk、地址信号addr以及双向数据总线data作为输入。程序内部,initial块用于包含测试台初始化代码。

程序构建块的详细讨论在本文档的第24条中给出。

3.5 接口

接口构建块被interfaceendinterface关键字包围,它封装了设计块之间以及设计与验证块之间的通信,使得从抽象的系统级设计到较低级别的寄存器传输和结构设计能够顺利迁移。通过封装块之间的通信,接口构建块还促进了设计的重用。

在最低级别上,接口是一个命名的网络(nets)或变量束。接口在设计中被实例化,并可以连接到其他实例化模块、接口和程序的接口端口。接口可以通过端口作为单个项目访问,并在需要时引用其组件网络或变量。设计中很大比例的内容通常包括端口列表和端口连接列表,这些只是名称的重复。通过将一组名称替换为单个名称,可以显著减少描述的尺寸并提高可维护性。

接口的额外强大功能来自其封装功能和连接性的能力,这使得接口在最高级别上更像是一个类模板。接口可以有参数、常量、变量、函数和任务。接口中元素的类型可以声明,或者类型可以作为参数传递。成员变量和函数相对于实例名称引用为实例成员。因此,通过接口连接的模块可以简单地调用该接口的子程序成员来驱动通信。随着功能被封装在接口中并与模块隔离,通过用包含相同成员但抽象级别不同的接口替换它,可以轻松更改通信协议的抽象级别和/或粒度。通过接口连接的模块无需更改。

为了为模块端口提供方向信息并控制特定模块内子程序的使用,提供了modport构造。顾名思义,方向是从模块角度看到的。

除了子程序方法外,接口还可以包含过程(即initialalways过程)和连续赋值,这对于系统级建模和测试台应用非常有用。这允许接口包括其自己的协议检查器,该检查器自动验证所有通过接口连接的模块是否符合指定的协议。其他应用,如功能覆盖率记录和报告、协议检查和断言,也可以构建到接口中。

以下是一个简单的接口定义和使用的示例:

interface simple_bus(input logic clk); // 定义接口
  logic req, gnt;
  logic [7:0] addr, data;
  logic [1:0] mode;
  logic start, rdy;
endinterface : simple_bus

module memMod(simple_bus a); // 使用simple_bus接口端口的模块
  logic avail;
  // 当memMod在module top中实例化时,a.req是sb_intf实例的'simple_bus'接口的req信号
  always @(posedge a.clk) a.gnt <= a.req & avail;
endmodule

// 其他使用simple_bus接口的模块...
module cpuMod(simple_bus b); // simple_bus interface port
...
endmodule

module top;
  logic clk = 0;
  simple_bus sb_intf(.clk(clk)); // 实例化接口
  memMod mem(.a(sb_intf)); // 将接口连接到模块实例
  cpuMod cpu(.b(sb_intf)); // 将接口连接到模块实例
  // 其他使用sb_intf接口的模块实例连接...
endmodule

在上面的示例中,定义了一个名为simple_bus的接口,该接口具有多个信号,包括请求(req)、授予(gnt)、地址(addr)、数据(data)、模式(mode)、开始(start)和就绪(rdy)信号。然后,在top模块中实例化了该接口,并将其连接到其他模块(如memMod)的相应接口端口上。

接口的详细讨论在本文档的第25条中给出。

3.6 检查器

检查器(Checker)构建块被checkerendchecker关键字包围,表示一个验证块,它封装了断言以及与这些断言相关联的建模代码。检查器的预期用途是作为验证库单元,或作为在形式验证中创建抽象辅助模型的构建块。

检查器构建块提供了一种机制,以结构化方式封装和重用验证逻辑,使得验证过程更加模块化和可重用。通过封装断言,检查器能够确保在验证过程中验证逻辑的清晰和一致性。

检查器构建块的详细讨论在本文档的第17条中给出。

3.7 原始类型

原始类型(Primitives)构建块用于表示低级逻辑门和开关。SystemVerilog包括了一系列内置的原始类型。设计师还可以通过用户定义原始类型(UDP)来补充这些内置原始类型。用户定义原始类型被primitiveendprimitive关键字包围。

内置和用户定义的原始类型允许对时间精确的数字电路进行建模,这些模型通常被称为门级模型。门级建模对于在较低层次上验证硬件设计的正确性和性能非常有用。

原始类型的详细讨论在本文档的第28条至第31条中给出。

3.8 子程序

子程序提供了一种封装可执行代码的机制,这些代码可以从一个或多个位置调用。SystemVerilog中有两种形式的子程序:任务(Tasks)和函数(Functions)。

  • 任务(Tasks):任务作为语句调用。任务可以有任意数量的输入、输出、双向(inout)和引用(ref)参数,但不返回值。任务在执行过程中可以阻塞模拟时间,即任务退出可以发生在任务被调用之后的较晚模拟时间。

  • 函数(Functions):函数可以返回一个值,或者可以被定义为不返回值的void函数。非void函数调用用作表达式中的操作数。函数可以具有输入、输出、双向和引用参数。函数必须在不阻塞模拟时间的情况下执行,但可以派生出阻塞时间的进程。

子程序的详细讨论在本文档的第13条中给出。

3.9 包

包(Packages)提供了一种声明空间,可以由其他构建块共享。包声明可以导入到其他构建块中,包括其他包。包是在packageendpackage关键字之间定义的。

包的主要目的是组织和封装相关的声明,如类型定义、函数、任务等,以便在多个模块、接口、程序或检查器之间共享。与模块、接口、程序和检查器不同,包不表示硬件或验证逻辑的独立单元,而是提供了一套可重用的声明和代码。

以下是一个简单的包定义示例:

package ComplexPkg;
  typedef struct {
    shortreal i, r;
  } Complex;

  function Complex add(Complex a, b);
    add.r = a.r + b.r;
    add.i = a.i + b.i;
  endfunction
  
function Complex mul(Complex a, b);
	mul.r = (a.r * b.r) - (a.i * b.i);
	mul.i = (a.r * b.i) + (a.i * b.r);
endfunction

 // 其他函数和类型定义...
endpackage : ComplexPkg

在上面的示例中,ComplexPkg包定义了一个名为Complex的结构体类型,用于表示复数,并提供了一个名为add的函数,用于将两个复数相加。

包的详细语法和语义在本文档的第26条中描述。

3.10 配置

SystemVerilog 提供了指定设计配置的能力,这些配置指定了模块实例到具体 SystemVerilog 源代码的绑定信息。配置使用库(Libraries)。库是模块、接口、程序、检查器、原始类型、包和其他配置的集合。单独的库映射文件指定库中包含的单元(cells)的源代码位置。库映射文件的名称通常作为命令行选项指定给读取 SystemVerilog 源代码的模拟器或其他软件工具。

配置允许设计师为同一硬件设计指定不同的验证或测试环境,而无需修改设计本身的源代码。通过使用配置,可以在不更改设计的情况下,轻松切换不同的测试台配置或优化不同的测试场景。

配置的详细讨论在本文档的第33条中给出。

3.11 设计层次结构概述

模块、程序、接口、检查器和原始类型等基本构建块被用来构建设计层次结构。层次结构是通过一个构建块实例化另一个构建块来创建的。当一个模块包含另一个模块、接口、程序或检查器的实例时,就创建了一个新的层次级别。通过层次级别的通信主要是通过连接到实例化模块、接口、程序或检查器的端口来完成的。

以下是一个简单的示例,展示了两个模块声明,它们利用了一些简单的声明。模块 top 包含一个 mux2to1 模块的实例,创建了一个具有两个层次的设计。

module top; // 无端口的模块
  logic in1, in2, select; // 变量声明
  wire out1; // 网络声明

  mux2to1 m1 (.a(in1), .b(in2), .sel(select), .y(out1)); // 模块实例
endmodule : top

module mux2to1(input wire a, b, sel, // 组合端口和类型声明
               output logic y);
  // 使用内置原始类型实例的网表...
	not g1 (sel_n, sel);
	and g2 (a_s, a, sel_n);
	and g3 (b_s, b, sel);
	or g4 (y, a_s, b_s);
endmodule : mux2to1

模块可以实例化其他模块、程序、接口、检查器和原始类型,从而创建一个层次结构树。接口也可以实例化其他构建块并创建层次结构树。程序和检查器可以实例化其他检查器。原始类型不能实例化其他构建块,它们在层次结构树中是叶子节点。

通常情况下,一个被阐述但未被明确实例化的模块或程序会在层次结构树的顶部被隐式实例化一次,并成为顶级层次块(参见第23.3条和第24.3条)。SystemVerilog 允许有多个顶级块。

在任何层次级别的标识符都可以使用层次路径名从任何其他层次级别引用(参见第23.6条)。

实例化语法和设计层次结构的详细讨论在本文档的第23条中给出。

3.12 编译和阐述

编译是读取SystemVerilog源代码、解密加密代码并分析源代码以检查语法和语义错误的过程。实现可能在一个或多个阶段执行编译。实现可能会将编译结果保存在专有的中间格式中,或者将编译结果直接传递给阐述阶段。并非所有语法和语义都可以在编译过程中检查。一些检查只能在阐述过程中或阐述完成后进行。

阐述(elaboration)是将组成设计的组件绑定在一起的过程。这些组件可以包括模块实例、程序实例、接口实例、检查器实例、原始类型实例以及设计层次结构的顶层。阐述发生在解析源代码之后和模拟之前,涉及实例化扩展、计算参数值、解析层次结构名称、建立网络连接等,并通常包括为模拟做准备的各种准备工作。

尽管本标准定义了编译和阐述的结果,但编译和阐述步骤在实现中并不要求是两个独立的阶段。在本标准中,术语“编译”、“编译过程”和“编译器”通常指的是合并了编译和阐述过程的整体。例如,当标准引用“编译时错误”时,允许实现在任何模拟开始之前报告该错误。

关于设计元素编译顺序的要求,本标准通常不做具体规定。但有两个例外:关于“编译单元”的规则(见3.12.1节),其中实际文件边界在编译过程中具有重要意义;以及关于引用包项的规则(见26.3节),其中包的编译必须在引用它之前完成。

SystemVerilog支持单文件和多文件编译,通过编译单元(compilation units)的使用来实现(见3.12.1)。编译单元是一个或多个SystemVerilog源代码文件的集合,这些文件被一起编译。每个编译单元有自己的作用域,其中包含所有位于其他作用域之外的声明。编译单元作用域可以包含任何可以在包中定义的项,并且可以绑定构造(如实例化)。然而,编译单元作用域并不是一个包,它不能被导入到其他作用域中。

3.12.1 编译单元

SystemVerilog 支持使用编译单元进行单独编译。以下是提供的一些术语和定义:

  • 编译单元(compilation unit):一个或多个SystemVerilog源代码文件的集合,这些文件被一起编译。
  • 编译单元作用域(compilation-unit scope):一个作用域,它是编译单元本地的。它包含了所有位于任何其他作用域之外的声明。
  • $unit:用于显式访问编译单元作用域中标识符的名称。

关于哪些文件构成编译单元的确切机制是工具特定的。然而,符合标准的工具应提供允许以下两种情况的使用模型:

a) 单文件编译单元:给定编译命令行上的所有文件构成一个单独的编译单元(在这种情况下,这些文件内的声明按照正常的可见性规则在整个文件集中都是可访问的)。

b) 多文件编译单元:每个文件是一个单独的编译单元(在这种情况下,每个编译单元作用域中的声明仅在其对应的文件内可访问)。

使用一个或多个include指令包含的文件的内容成为包含它们的文件的编译单元的一部分。

如果文件的末尾有未完成的声明,那么包含该文件的编译单元将扩展到每个后续文件,直到没有未完成的声明为止。

除了文件到编译单元的上述映射外,还可能存在其他映射方式,且定义这些映射的机制是工具特定的,可能不具有可移植性。

尽管编译单元作用域不是一个包,但它可以包含任何可以在包中定义的项(参见第26.2条),并且可以绑定构造(参见第23.11条)。这些项位于编译单元作用域名称空间(参见3.13条)。

以下项目在所有编译单元中都是可见的:模块、原始类型、程序、接口和包。在编译单元作用域中定义的项不能通过名称从编译单元外部访问。可以使用PLI访问编译单元作用域中的项,PLI必须提供一个迭代器来遍历所有编译单元。

编译单元作用域中的项可以对标识符进行层次化引用。对于向上名称引用(见23.8),编译单元作用域被视为顶级设计单元。这意味着,如果这些引用不是指向在编译单元作用域内创建的标识符,或者不是通过将包导入到编译单元作用域中而使其可见的标识符,那么它们将被视为从设计顶层开始的完整路径名($root,如23.3.1中所述)。

在单独编译的单元中,工具一旦看到编译器指令,就会将其应用于所有后续的源代码文本。然而,来自一个单独编译单元的编译器指令不应影响其他编译单元。这可能导致单独编译单元与将它们作为包含整个源代码的单个编译单元进行编译时的行为差异。

编译单元作用域内的项在编译单元外部无法通过名称直接访问。但是,这些项可以通过PLI(Programming Language Interface,编程语言接口)进行访问,PLI必须提供一个迭代器来遍历所有编译单元。

当在作用域内引用标识符时,查找顺序如下:

  1. 首先,搜索嵌套作用域(包括嵌套的模块声明),包括通过包导入声明可用的任何标识符。
  2. 接下来,搜索引用之前定义的编译单元作用域部分(包括通过包导入声明可用的任何标识符)。
  3. 最后,如果标识符遵循层次名称解析规则,则搜索实例层次结构(参见第23.8条和第23.9条)。

$unit是包围编译单元的作用域的名称。其目的是允许对编译单元最外层级别(即那些在编译单元作用域中声明的)的声明进行明确引用。这是通过使用与访问包项相同的作用域解析运算符完成的(参见第26.3条)。

例如,使用$unit显式访问编译单元作用域中的声明:

bit b;
task t;
  int b;
  b = 5 + $unit::b; // $unit::b 是编译单元作用域中的 b
endtask

除了任务(task)和函数(function)名称(参见第23.8.1条)外,仅能对已在编译单元中定义的名称进行引用。显式使用$unit::前缀仅用于名称消歧,并不增加引用后续编译单元项的能力。

例如:

task t;
  int x;
  x = 5 + b; // 非法 - "b" 在后面定义
  x = 5 + $unit::b; // 非法 - $unit 不提供向前引用的特殊能力
endtask
bit b;

编译单元作用域允许用户轻松地在编译单元的范围内共享声明(如类型),而无需从其中声明一个包,然后再从该包中导入声明。由于编译单元作用域没有名称,因此不能通过import声明使用它,并且编译单元作用域中声明的标识符无法通过层次化引用进行访问。但是,在特定的编译单元内,可以使用特殊名称$unit来显式访问其编译单元作用域的声明。

请注意,$unit主要用于名称解析中的消歧,而不是作为在编译单元之间共享信息或状态的机制。

3.13 命名空间

SystemVerilog有八个命名空间用于标识符:两个是全局的(定义名称空间和包名称空间),两个是全局到编译单元的(编译单元名称空间和文本宏名称空间),另外四个是局部的。这八个名称空间描述如下:

a) 定义命名空间:统一了所有在非嵌套模块、原始类型、程序、接口之外定义的非嵌套模块、原始类型、程序、接口标识符。一旦在一个编译单元中使用一个名称来定义一个模块、原始类型、程序或接口,该名称就不应再在任何编译单元中用于声明另一个非嵌套模块、原始类型、程序或接口(在所有其他声明之外)。

b) 包名称空间:统一了所有编译单元中定义的包标识符。一旦在一个编译单元中使用一个名称来定义一个包,该名称就不应再在任何编译单元中用于声明另一个包。

c) 编译单元作用域名称空间:存在于模块、接口、包、检查器、程序和原始类型构造之外。它统一了编译单元作用域内的函数、任务、检查器、参数、命名事件、网络声明、变量声明和用户定义类型的定义。

d) 文本宏名称空间:在编译单元内是全局的。由于文本宏名称以单引号开头,因此它们与其他任何名称空间保持唯一性。文本宏名称按照输入文件中出现的线性顺序定义。对于编译单元中的输入文件集,后续相同名称的定义会覆盖之前的定义。

e) 模块名称空间:由模块、接口、包、程序、检查器和原始类型构造引入。它统一了模块、接口、程序、检查器、函数、任务、命名块、实例名称、参数、命名事件、网络声明、变量声明和用户定义类型在封闭构造内的定义。

f) 块名称空间:由命名或未命名的块、specify、函数和任务构造引入。它统一了封闭构造内的命名块、函数、任务、参数、命名事件、变量声明类型和用户定义类型的定义。

g) 端口名称空间:由模块、接口、原始类型和程序构造引入。它提供了一种在属于不同名称空间的两个对象之间定义连接的方式。连接可以是单向的(输入或输出)或双向的(inout或ref)。端口名称空间与模块和块名称空间重叠。基本上,端口名称空间指定了不同名称空间中名称之间的连接类型。端口类型声明包括input、output、inout和ref。在端口名称空间中引入的端口名称可以通过在模块名称空间中声明具有相同名称的变量或网络来重新引入。

h) 属性名称空间:由附加到语言元素的(* 和 *)构造包围。属性名称只能在属性名称空间中定义和使用。在名称空间内,重新声明先前已声明的名称是非法的。

在名称空间内,重新声明先前已声明的名称是非法的。每个名称空间都提供了其内部标识符的封装和隔离,有助于避免命名冲突并促进模块化设计。

3.14 模拟时间和精度

模拟中的一个重要方面是时间。术语“模拟时间”用于指代模拟器维护的时间值,以模拟正在被模拟的系统描述所需的实际时间。术语“时间”与“模拟时间”可互换使用。

时间值在设计元素中用于表示传播延迟以及过程语句执行之间的模拟时间量。时间值有两个组成部分:时间单位和时间精度。

  • 时间单位:表示时间和延迟的测量单位,可以指定的范围从100秒单位到1飞秒单位。
  • 时间精度:指定延迟的准确度。

时间和延迟的这两个组成部分使用诸如smsusnspsfs的字符串之一来表示,并带有1、10或100的数量级。这些字符串的定义在表3-1中给出。

表3-1—时间单位字符串

字符串测量单位
s
ms毫秒
us微秒
ns纳秒
ps皮秒
fs飞秒

注意:虽然smsnspsfs是秒、毫秒、纳秒、皮秒和飞秒的通常SI单位符号,但由于编码字符集中缺少希腊字母μ(mu),因此使用“us”表示微秒的SI单位符号。

设计元素的时间精度应至少与其时间单位一样精确;它不能是比时间单位更长的单位时间。

3.14.1 时间值舍入

在设计元素(如模块、程序或接口)内,时间精度指定了在模拟中使用延迟值之前如何对其进行舍入。

时间精度是相对于时间单位的。如果精度与时间单位相同,则延迟值将被舍入到整数。如果精度比时间单位小一个数量级,则延迟值将被舍入到小数点后一位。例如,如果指定的时间单位是1ns,精度是100ps,则延迟值将被舍入到小数点后一位(100ps相当于0.1ns)。因此,2.75ns的延迟将被舍入为2.8ns。

设计元素中的时间值是精确到为该设计元素指定的时间精度单位的,即使设计中的其他地方指定了更小的时间精度也是如此。

3.14.2 指定时间单位和精度

时间单位和精度可以通过以下两种方式指定:

  • 使用编译器指令timescale
  • 使用关键字timeunittimeprecision

3.14.2.1 使用timescale编译器指令

timescale编译器指令用于为所有跟随该指令且不包含显式timeunittimeprecision构造的设计元素指定默认的时间单位和精度。一旦在源代码中遇到timescale指令,它就会生效,并且保持有效状态,直到遇到另一个timescale编译器指令。重要的是要注意,timescale指令的作用范围仅限于当前的编译单元;它不会跨越多个编译单元(参见3.12.1章节)。

timescale指令的一般语法格式如下(有关更多详细信息,请参阅第22.7章节):

`timescale time_unit / time_precision

这里,time_unittime_precision分别代表时间单位和时间的精度,它们可以是s(秒)、ms(毫秒)、us(微秒)、ns(纳秒)、ps(皮秒)或fs(飞秒)等时间单位之一,并可以带有1、10或100的数量级。

例如,以下代码示例展示了如何使用timescale指令来设置时间单位和精度:

`timescale 1ns / 10ps
module A (...);
    // 模块A中的代码,使用1ns的时间单位和10ps的时间精度
    ...
endmodule

module B (...);
    // 模块B也遵循相同的`timescale`指令设置
    ...
endmodule

`timescale 1ps / 1ps
module C (...);
    // 对于模块C,时间单位和精度被更改为1ps
    ...
endmodule

timescale指令可能导致文件顺序依赖性问题。如果前面的三个模块按照A、B、C的顺序编译(如示例所示),那么模块B将以纳秒为单位进行模拟。如果这三个文件按照C、B、A的顺序编译,那么模块B将以皮秒为单位进行模拟。这可能会根据模块B中指定的时间值导致截然不同的模拟结果。

这个问题强调了在使用timescale指令时需要特别注意的一点:编译顺序可能会影响模拟的时间单位和精度,进而影响模拟结果。由于timescale指令是全局性的,并且会影响后续所有未显式设置时间单位和精度的模块,因此当多个源文件包含不同的timescale指令时,编译顺序就变得至关重要。

为了避免这种依赖性和潜在的不一致性,推荐在模块级别使用timeunit和timeprecision声明来明确设置时间单位和精度,而不是依赖全局的timescale指令。这样可以确保每个模块的时间单位和精度是明确且一致的,无论模块的编译顺序如何。

3.14.2.2 使用timeunittimeprecision关键字

时间单位和精度可以通过timeunittimeprecision关键字分别声明,并设置为时间字面量(参见5.8章节)。timeprecision还可以通过timeunit关键字的可选第二个参数来声明,使用斜杠(/)作为分隔符。例如:

module D (...);
    timeunit 100ps;
    timeprecision 10fs;
    // 模块D中的代码,使用100ps的时间单位和10fs的时间精度
    ...
endmodule

module E (...);
    timeunit 100ps / 10fs; // 使用可选的第二个参数设置时间精度
    // 模块E中的代码,同样使用100ps的时间单位和10fs的时间精度
    ...
endmodule

在设计元素内部定义 timeunit 和 timeprecision 构造可以消除与编译器指令相关的文件顺序依赖性问题。

对于任何模块、程序、包或接口定义或在任何编译单元作用域中,最多只能有一个时间单位和一个时间精度。这将定义一个时间作用域。如果指定了,timeunit和timeprecision声明应在当前时间作用域中的任何其他项之前。虽然timeunit和timeprecision声明可以在后面的项中重复,但它们必须在当前时间作用域内与先前的声明相匹配。

3.14.2.3 timeunittimeprecisiontimescale的优先级

如果模块、程序、包或接口定义中没有指定timeunit,则时间单位应按照以下优先级规则确定:

  1. 如果模块或接口定义是嵌套的,则应从封闭模块或接口中继承时间单位(程序和包不能嵌套)。
  2. 否则,如果之前已在当前编译单元中指定了timescale指令(参见3.12.1章节),则应将时间单位设置为timescale指令的单位。
  3. 否则,如果编译单元作用域外指定了时间单位(在所有其他声明之外),则应将时间单位设置为编译单元的时间单位。
  4. 否则,应使用默认时间单位。

编译单元作用域的时间单位只能通过timeunit声明来设置,而不能通过timescale指令设置。如果没有指定,则应使用默认时间单位。

如果当前时间作用域中没有指定timeprecision,则时间精度的确定应遵循与时间单位相同的优先级规则。

如果某些设计元素指定了时间单位和精度,而其他设计元素没有指定,这将导致错误。默认时间单位和精度是特定于实现的。

全局时间精度(也称为模拟时间单位)是所有timeprecision语句、timeunit声明的timeprecision参数以及所有timescale编译器指令中最小时间精度参数的最小值。步长时间单位等于全局时间精度。与其他表示物理单位的时间单位不同,步长不能用于设置或修改精度或时间单位。

默认时间单位和精度是特定于实现的。

如果某些设计元素指定了时间单位和精度,而其他设计元素没有指定,则这应被视为错误。

3.14.3 模拟时间单位

全局时间精度,也称为模拟时间单位,是设计中所有timeprecision语句、timeunit声明中的timeprecision参数以及所有timescale编译器指令中最小时间精度参数的最小值。这意味着,模拟时间单位是根据设计中指定的所有时间精度中最小的一个来确定的。

步长时间单位等于全局时间精度。步长是模拟过程中时间的最小增量单位,用于确保模拟的连续性和准确性。与其他表示物理单位(如纳秒、皮秒等)的时间单位不同,步长并不直接用于设置或修改时间单位或时间精度,而是由全局时间精度决定,并作为模拟过程中的最小时间间隔。

在模拟过程中,模拟器按照步长递增地推进时间,以模拟硬件系统的行为。步长越小,模拟的精度通常越高,但模拟速度可能会降低,因为需要处理的时间点增多。因此,在选择步长(即全局时间精度)时,需要在模拟精度和模拟性能之间做出权衡。

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值