1.5 SystemVerilog仿真

本节内容摘自《使用SystemVerilog进行RTL建模——基于SystemVerilog的ASIC与FPGA设计》

数字仿真是一种软件程序,它将逻辑值变化(称为激励)应用于数字电路模型的输入,按照实际芯片传播这些逻辑值变化的方式,在模型中传播该激励,并提供观察和验证该激励结果的机制。

SystemVerilog 是一种使用 0 和 1 的数字仿真语言,该语言不表示模拟电压、电容和电阻。SystemVerilog 提供的编程结构用于对数字电路、激励发生器和验证检查器进行建模。

本书重点关注对数字电路进行建模,后续章节将详细讨论和说明这一主题。示例 1.4 展示了一个可以进行仿真的简单数字电路模型,与前面示例 1.3 所示的电路相同。

示例 1.4 具有输入和输出端口的设计模型(一个 32 位加法器 / 减法器)

module rtl_adder_subtracter
(input  logic        clock, // 1-bit scalar input
 input  logic        mode,  // 1-bit scalar input
 input  logic [31:0] a, b,  // 32-bit vector inputs
 output logic [31:0] sum    // 32-bit vector output
);
  timeunit 1ns/1ns;

  always_ff @(posedge clock) begin 
    if (mode == 0) sum <= a + b;
    else           sum <= a - b;
  end

endmodule: rtl_adder_subtracter

在这个例子中,模型具有输入端口和输出端口。为了仿真该模型,必须提供将逻辑值应用于输入端口的激励,并且必须提供响应检查器,以观察输出端口是否符合预期。尽管本书的重点不在于此,但还是提供了激励和响应检查的简要概述,以展示仿真 SystemVerilog 模型所涉及的内容。

测试平台用于进行激励生成和验证响应的封装。在 SystemVerilog 中,有许多方法可以对测试平台进行建模,测试平台中的代码可以从简单的编程语句到复杂的面向对象的事务级编程。示例 1.5 说明了为 32 位加法器 / 减法器设计的简单测试平台。

示例 1.5 32 位加法器 / 减法器模型的测试平台
module test
(output logic [31:0] a, b,
 output logic        mode,
 input  logic [31:0] sum,
 input  logic        clk
);
  timeunit 1ns/1ns;

  // generate stimulus
  initial begin
    repeat (10) begin
      @(negedge clk) ;
      void'(std::randomize(a) with {a >= 10; a <= 20;});
      void'(std::randomize(b) with {b <= 10;});
      void'(std::randomize(mode));
      @(negedge clk) check_results;
    end
    @(negedge clk) $finish;
  end

  // verify results
  task check_results;
    $display("At %0d: \t a=%0d  b=%0d  mode=%b  sum=%0d",
             $time, a, b, mode, sum);
    case (mode)
      1'b0: if (sum !== a + b) $error("expected sum = %0d", a + b);
      1'b1: if (sum !== a - b) $error("expected sum = %0d", a - b);
    endcase
  endtask
      
endmodule: test

示例 1.5 中的主要代码块是一个 initial 过程块,这是一种程序块。程序块包含编程语句和时间信息,以指示仿真器该做什么,以及何时做。 SystemVerilog 有两种主要类型的程序块:initial 和 always 过程块。

(1)initial 过程块使用关键字 initial 定义。尽管名称如此,initial 过程块并不用于初始化设计。相反,initial 只执行一次其编程语句。当最后一条语句被执行时,initial 过程在给定的仿真运行中不会再次执行。initial 过程是不可综合的,并且不用于 RTL 建模。本书专注于编写用于仿真和综合的 RTL 模型,因此不会更深入讨论 initial。

(2)always 过程块是用关键字 always、always_comb、always_ff和always_latch定义的。always 过程是一个无限循环。当 always 过程完成最后一条语句时,程序会自动返回到开头,并重新开始该过程。对于 RTL 建模,always 过程必须以敏感列表开始,例如在示例 1.4 中显示的 @ (posedge clk) 定义。各种形式的 always 过程将在 6 章~第 9 章更详细地讨论。

过程块可以包含单个语句或一组语句。过程块中的多个语句通过关键字begin和 end组合在一起(验证代码也可以通过关键字 fork和 join、join_any或 join_none组合语句)。在 begin和 end之间的语句按照列出的顺序执行,即从第一条语句开始,到最后一条语句结束。

示例 1.5 中的 initial 过程包含一个重复循环。该循环被定义为执行 10 次。循环的每个过程如下所示:

(1)等待 clk 信号下降沿的到来。

(2)为设计中的 a、b 和 mode 信号生成随机激励。

(3)直到下一个 clk 下降沿的到来,然后调用 check_results 任务(子例程)以验证设计的输出是否与计算的预期结果匹配。

该设计在其时钟输入的上升沿工作。测试平台使用同一时钟的相对边沿,以避免在设计使用的时钟边沿上驱动输入和读取输出。如果测试平台在时钟的上升沿驱动值,那么在设计使用这些输入之前,这些输入的建立时间将为零。同样,如果测试平台在时钟的上升沿验证设计结果,那么这些设计输出的保持时间将为零。

在同一时刻修改和读取一个值被称为仿真竞争条件。使用设计时钟的相对边沿来驱动激励是测试平台避免设计仿真竞争条件的一种简单方法,例如满足设计的建立时间和保持时间要求。SystemVerilog 提供了更有效的方法,使测试平台能够避免与被测试设计之间的竞争条件,这超出了本书的范围。推荐阅读《SystemVerilog 验证:测试平台编写指南(原书第三版)》以获取更多细节。书中介绍了 SystemVerilog 的功能和强大的验证构造,以及良好的验证编码风格。

测试平台被建模为一个具有输入和输出端口的模块,类似于被验证的设计。最后一步是将测试平台端口连接到设计端口,并生成时钟,这是在顶层模块中完成的,示例 1.6 显示了这方面的代码。

示例 1.6 在顶层模块中连接测试平台
module top;
  timeunit 1ns/1ns;

  logic [31:0] a, b;
  logic        mode;
  logic [31:0] sum;
  logic        clk;

  test                 test (.*);
  rtl_adder_subtracter dut  (.*);
  
  initial begin
    clk <= 0;
    forever #5 clk = ~clk;
  end
endmodule: top

1.5.1  SystemVerilog仿真器
一些商业仿真器对 SystemVerilog 语言提供了出色的支持。本书与仿真器无关,不提供任何特定仿真器产品的详细信息。尽管如此,作者仍鼓励读者编写并仿真本书中的许多示例,以及他们自己构思的示例。请参考特定仿真器产品随附的文档,以获取有关调用和运行该仿真器的信息。

所有 SystemVerilog 仿真器都有一些共同点,这些共同点对于如何理解和编写能够正确仿真的 SystemVerilog RTL 模型至关重要。这些特性包括编译、展开、仿真时间和仿真事件调度。接下来的章节将讨论这些仿真方面。

1.5.2  编译(compilation)和展开(elaboration)
SystemVerilog 源代码需要被编译和展开,以便进行仿真。编译涉及检查SystemVerilog 的源代码,以确保其在语法和语义上是正确的,同时符合 IEEE SystemVerilog 标准中定义的规则。展开将组成设计和测试平台的模块绑定在一起。除此以外,展开还解决可配置代码,例如常量的最终值、向量大小和仿真时间缩放。

IEEE SystemVerilog 标准并未定义明确的编译和展开过程。该标准允许每个仿真器供应商以其认为的最适合该产品的方式定义此过程并划分编译和展开。一些仿真器将编译和展开过程合并为一个步骤,而另一些仿真器则将这些过程分为单独的步骤。一些仿真器可能在编译阶段捕获源代码中的某些类型的错误,而另一些仿真器则在展开阶段捕获这些错误。这些差异不会影响本书中讨论的RTL 编码风格和指南,但了解所使用的仿真器如何处理 RTL 源代码的编译和展开是有帮助的。请参考特定仿真器的文档,了解该产品如何处理编译和展开。

1. 源代码顺序
SystemVerilog 语言与大多数编程语言一样,在源代码的编译顺序上存在某些依赖。特别是用户定义的类型声明和 package 声明必须在引用这些定义之前进行编译。用户定义的类型声明和 package 通常与使用这些声明的 RTL 代码位于不同的文件中。这意味着设计工程师必须确保这些文件按照正确的顺序进行编译,从而使得在引用之前已经对这些声明进行了编译。

并非所有声明都依赖于顺序。例如,SystemVerilog 允许在模块编译之前引用模块名称。在模块内,可以在定义之前调用任务和函数,只要定义存在于模块内即可。

2. 全局声明和 $unit 声明空间
SystemVerilog 允许在 $unit的全局声明空间中进行某些类型的定义。$unit中的声明可以被多个文件共享。全局声明依赖于编译顺序,因此必须在被引用之前编译。全局 $unit不是一个自包含的建模空间,任何文件都可以添加定义到 $unit。这会导致全局定义杂乱无章,难以确保在引用定义之前对其进行编译。SystemVerilog 编译器指令,如 'define文本宏和 'timescale时间缩放,也属于 $unit空间,这些内容必须在受指令影响的代码之前进行编译。

最佳实践指南 1.1

使用 package 进行共享声明,而不是使用 $unit声明空间。package 的相关内容将在 4.2 节讨论。

3. 单文件和多文件编译
IEEE SystemVerilog 标准定义了涉及多个文件时的两种编译 / 展开方式:多文件编译和单文件编译。

多文件编译方式允许多个源代码文件一起编译。一个文件中的全局声明和编译指令在编译后可见于其他文件中的源代码。

单文件编译方式允许每个文件独立编译。一个文件中的任何全局声明或编译指令仅在该文件内可见。无论文件的编译顺序如何,其他文件将无法看到这些声明或指令。

所有的仿真器和综合编译器都支持多文件方式,但并不是所有工具都支持单文件编译。然而,支持这两种方式的工具并不一定默认使用相同的方式。一些工具默认使用单文件编译并需要特定工具的调用选项来进行多文件编译。其他工具默认使用多文件编译,并需要调用选项来进行单文件编译或增量重新编译。

4. 仿真器产品的限制
我们希望所有的 SystemVerilog 仿真器都以相同的方式,完整地支持整个SystemVerilog 语言。然而,没有任何仿真器是完美的(尽管 EDA 供应商的销售人员可能会声称如此)。虽然主要的商业 SystemVerilog 仿真器支持大部分SystemVerilog 语言,但几乎没有任何 SystemVerilog 仿真器实现 100% 的最新SystemVerilog 标准。似乎不可避免的是,总会有一些 SystemVerilog 特性在某些仿真器中有效,而在另一些仿真器中无效。换言之,相同的语言特性在不同仿真器中,可能产生不同的仿真结果。

仿真器无法完全支持整个 SystemVerilog 语言的一个原因可能是, SystemVerilog 是一个庞大且复杂的语言标准。实现 SystemVerilog 标准中的每个特性都是一项烦琐、耗时且需要大量工程努力的工作。仿真器供应商就算打算实现所有 SystemVerilog 标准,也需要大量的人力资源来完成。

仿真器对 SystemVerilog 支持差异的第二个原因是,不同企业对SystemVerilog 标准的解释存在歧义。这些歧义有些来源于 SystemVerilog 语言的复杂性。尽管 IEEE1800 标准委员会尽了最大的努力,但有时标准中使用的措辞,仍然可以有多种解释。可能对某些人来说,令人惊讶的是,IEEE 故意使SystemVerilog 存在模糊性。有时,IEEE 标准委员会故意允许仿真器之间存在差异,以便让每个仿真器供应商能够以其认为最佳的方式优化模拟性能和实现。

第三个不幸的原因是,并非所有仿真器都完整地支持 SystemVerilog 语言。一些仿真器供应商认为没有必要这样做,这是因为仿真器供应商像所有工程公司一样,拥有有限的工程资源和无限的工程任务。在工程努力重点的妥协中,某些 SystemVerilog 语言特性的支持最终成为低优先级的工程任务。

最后一个仿真器供应商可能选择不支持完整的 SystemVerilog 语言的原因是,该供应商的产品定位,可能只服务于某一个特定的市场。这个市场可能仅需要 SystemVerilog 语言的一个子集。

1.5.3  仿真时间和事件调度
SystemVerilog 标准定义了仿真中如何表示模拟时间的规则、仿真过程中编程语句的评估顺序,以及逻辑值变化传播的规则。

1. 时间单位和时间精度
仿真需要表示时间的流逝。SystemVerilog 标准以 1fs 到 100s 为单位指定时间。

SystemVerilog 还允许指定时间精度。时间精度决定了仿真应使用多少位小数。假如某个模块中的时间具有更多小数位,它将被四舍五入到该精度。精度是相对于时间单位来指定的。例如,如果时间单位是 1ns,则 1ps 的精度将允许 3 位小数的准确性(1ns 是 10-9s,1ps 是 10-12s,相差三个数量级,即 3 位小数)。

在 SystemVerilog模块中使用的时间和时间精度单位可以在模块级别指定,使用 timeunit和 timeprecision语句。例如:

module adder
(input   logic  a,b,ci,
 output  logic  sum,co 
);
  timeunit 1ns;   
  timeprecision 1ps; 
...
endmodule: adder

时间精度也可以与时间单位一起指定,使用斜杠(/)分隔单位和精度, 例如:

timeunit 1ns/1ps;

也可以使用传统 Verilog 的 'timescale编译指令在半全局基础上指定时间单位和时间精度。该指令必须在模块边界之外指定,以将其声明到 $unit空间中(见 1.5.2 节)。

'timescale 1ns/1ps 
module adder
(input   logic  a,b,ci,
 output  logic  sum,co 
);
...
endmodule: adder

编译指令,如 'timescale并不是全局变量,它们仅影响在同一次编译器调用的过程中,该指令读取后编译的代码。在该指令之前编译的代码不会受到该指令的影响。同样,作为单独文件编译的代码也不受该指令的影响。当多个文件同时编译时,'timescale指令可以影响多个文件。

当有不止一个文件具有 'timescale指令时,多个文件的编译顺序变得至关重要。以不同的顺序编译文件可能导致不同的仿真结果,因为 'timescale是半全局行为。

最佳实践指南 1.2

使用 SystemVerilog 的 timeunit关键字来指定仿真时间单位和精度,而不是使用旧 'timescale编译指令。

timeunit和 timeprecision关键字是局部于其使用的模块的,并且不受编译顺序的影响。

未指定 timeunit 和 timeprecision 声明且没有 'timescale 编译指令生效时, IEEE SystemVerilog 标准并未明确仿真器应使用的默认时间单位或精度。在这种情况下,不同的仿真器有不同的行为。

所有仿真器都提供一个调用选项,以设置未在模块内声明或通过 'timescale编译指令声明的模块中使用的默认时间单位和时间精度。一个适用于大多数仿真器的示例调用选项是:

-timescale = 1ns / 1ps

请参考特定仿真器的文档,以获取设置仿真时间单位和精度的命令行调用选项。

注意:本书中的大多数代码示例不包括 timeunit和 timeprecision语句。这些语句被省略,从而可以专注于每个示例中的 RTL 代码。即使模型中没有延迟,每个模块、接口或 package 中都有 timeunit 和 timeprecision 语句。这确保模型将在所有仿真器上使用相同的时间单位进行编译。

2. 传播延迟
传播延迟可以在详细的门级建模和更抽象的 RTL 级建模中表示。大多数RTL 模型都是以零传播延迟编写的。RTL 级别的时序通常在时钟周期边界上,在一个时钟周期内没有传播延迟。传播延迟通常在验证测试平台中使用。

# 符号用于表示延迟。以下代码片段展示了在测试平台中使用延迟来建模时钟振荡器。

always #5 clk = ~clk;

可以通过延迟指定特定的时间单位。当延迟需要与模块的单位不同的时间单位时,这尤其有用:

always #5ns clk = ~clk;

3. 模拟时间和仿真事件调度
逻辑值变化被称为仿真事件。SystemVerilog 标准定义了仿真器在仿真中传播逻辑值变化的一般规则。该规则很复杂,感兴趣的读者可以阅读 IEEE 1800 SystemVerilog 标准以获取详细描述,本节仅提供了简要概述。

仿真器会维护一个仿真时间线,这个仿真时间线表示一系列逻辑值被调度的时间点。每个时间点称为一个仿真时间槽。从概念上讲,在这个时间线中,每个时间槽对应于在已编译和展开的 SystemVerilog 源代码中表示的一个最细时间精度所对应时间。因此,如果源代码中使用的最小精度是 1ps,则时间线将在 0ps、1ps、2ps、3ps 等时间槽进行调度,直到仿真结束。

一种常见做法是 RTL 模型使用 1ns 的时间单位和 1ns 的精度(没有小数位精度)。图 1.7 展示了典型 RTL 模型的时间线,该图假设了所有源代码在仿真中展开的最小时间精度为 1ns。

实际上,SystemVerilog 仿真器将优化仿真时间线,仅为实际仿真活动发生的时间创建时间槽。这种优化意味着源代码中使用的时间精度对仿真运行时间性能几乎没有影响。

4. 事件区域(event regions)和事件调度(event scheduling)
每个仿真时间槽被划分为几个事件区域。事件区域用于调度必须由仿真器处理的活动。例如,如果一个时钟振荡器每 5ns 进行一次翻转,从时间 0 开始,那么仿真器将在仿真时间线的 0ns、5ns、10ns 等时刻为时钟信号调度一个仿真事件。同样,如果一个编程语句将在仿真时间 7ns 执行,那么仿真器将在仿真时间线的 7ns 时间槽中调度一个事件来评估该语句。

仿真器可以调度在当前时间槽中处理的事件或在任何未来时间槽中处理的事件,但不能调度在过去时间槽中的事件。尽管许多仿真器确实允许将仿真重置回时间 0 并重新开始仿真,但 SystemVerilog 仿真器只向前推进时间,而不会向后移动。

5. 活动事件和 NBA 更新事件,阻塞和非阻塞赋值
SystemVerilog 将一个仿真时间槽划分为几个事件区域,这些区域以受控的顺序进行处理。这为设计和验证工程师提供了一定的控制权,以决定事件处理的顺序。大多数事件区域用于验证目的,本书中不讨论这些内容。RTL 和门级模型主要使用这两个事件区域:活动事件区域和 NBA 更新事件区域。这两个区域的关系如图 1.8 所示。

关于事件区域在仿真过程中如何处理,有几个重要的细节需要理解:

(1)在 begin-end 语句组中的事件,按照语句列出的顺序被调度到事件区域,并在事件区域处理时按顺序执行。

(2)并发过程的事件,例如多个 always 过程块,按照仿真器选择的任意顺序调度到事件区域,RTL 设计师无法控制并发事件调度的顺序。

(3)仿真器将在过渡到下一个区域之前,执行该区域内所有已调度的事件。当事件被处理后,它们将永久从事件列表中移除。在仿真继续到下一个区域之前,每个区域将是空的。

(4)当在后续区域处理事件时,它们可能会在之前的区域调度新的事件。在处理完后续区域后,仿真将循环回事件区域,以处理新调度的事件。对事件区域的迭代将持续进行,直到区域为空(即在该仿真时间点没有新的事件被 调度)。

(5)从一个事件区域过渡到下一个事件区域被称为 delta,对事件区域的每次迭代被称为 delta 循环。

注意:正确使用这两个事件区域对于获得正确的 RTL 仿真结果至关重要。

SystemVerilog 有两种类型的赋值运算符:阻塞赋值和非阻塞赋值。阻塞赋值用等号(=)表示,用于建模组合逻辑,如布尔运算和多路复用器;非阻塞赋值用小于等于号(<=)表示,用于建模时序逻辑,如锁存器和触发器。阻塞赋值在活动事件区域中被调度;非阻塞赋值的右侧将在活动事件区域中被评估,而左侧将在 NBA 更新区域中被更新。(NBA 代表非阻塞赋值)

第 7 章和第 8 章将更详细地讨论组合逻辑和时序逻辑的 RTL 建模。这些章节展示并强调了在零延迟 RTL 模型中,如何正确使用阻塞赋值和非阻塞赋值。

6. 事件调度示例
示例 1.7 展示了一个 8 位 D 型寄存器的 RTL 模型,该寄存器在每个时钟的上升沿加载其 d 输入,顶层模块包含一个周期为 10ns 的时钟振荡器,该振荡器在仿真时间零时开始,逻辑值为 0,以及一个生成 d 输入激励的测试模块。

示例 1.7 一个时钟振荡器、激励信号和触发器以说明事件调度

/
// Top module with clock oscillator
/
`begin_keywords "1800-2012"
module top;
  timeunit 1ns/1ns;
  logic       clock;
  logic [7:0] d;
  logic [7:0] q;

  test  i1 (.*);   // connect top module to test module
  d_reg i2 (.*);   // connect top module to d_reg module

  initial begin    // clock oscillator
    clock <= 0;                  // initialize clock at time 0
    forever #5 clock = ~clock;   // toggle clock every 5ns
  end

endmodule: top

/
// Test module with stimulus generator
/
`begin_keywords "1800-2012"
module test (input  logic       clock,
             output logic [7:0] d,
             input  logic [7:0] q
            );
  timeunit 1ns/1ns;

  initial begin
    d = 1;
    #7  d = 2;
    #10 d = 3;
    #10 $finish;
  end

endmodule: test

在模块 top 的时钟初始化中,仿真器将在 0ns 时调度一个活动事件以评估赋值右侧的 clk,并在 0ns 时进行非阻塞更新赋值以更改 clk 的值。活动事件还将在 5ns、10ns 等时刻调度更改 clk。

对于测试激励,仿真器将在 0ns、7ns 和 17ns(7 + 10)时调度 d 上的活动事件,并在 27ns(7 + 10 + 10)时调度 $ finish 命令。

对于 RTL 触发器,仿真器将在 5ns、15ns 和 25ns(clk 的上升沿)时调度活动事件以评估 d,并在 5ns、15ns 和 25ns 时调度 NBA 更新事件以更新 q。

图 1.9 展示了在仿真示例 1.7 时,仿真时间线可能的样子。该图显示了非阻塞赋值的两步过程:一个活动事件将值赋给一个内部临时变量,该变量是信号名称的前缀;另一个 NBA 更新事件将内部临时变量赋值给实际信号。

请注意,图 1.9 是仿真行为的概念性示意图,它并不反映仿真器如何在仿真器的内部算法中实现这种行为。完整的事件调度算法比图中所示的要复杂得多,并在 IEEE 1800 SystemVerilog 标准中进行了详细描述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值