跳转至

使用 Vivado 进行仿真

考虑下面的问题:

例子:自定义计数器

在上一篇教程中,我们提到了硬件延迟的概念(还记得那根超长的导线吗?)。现在,我们希望编写一个计数器,每隔一定的时钟周期发出一次信号。这个计数器应当是可定制的,即使用参数化的方式由调用者决定发出信号的间隔。请编写 Verilog 代码实现一个符合要求的计数器。

我们有如下的一段 Verilog 代码:

待测试模块 DUT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
module Counter #(
    parameter   MAX_VALUE = 8'd100
)(
    input                   clk,
    input                   rst,
    output                  out
);

reg [7:0] counter;
always @(posedge clk) begin
    if (rst)
        counter <= 0;
    else begin
        if (counter >= MAX_VALUE)
            counter <= 0;
        else
            counter <= counter + 8'b1;
    end
end

assign out = (counter == MAX_VALUE);
endmodule
理解起来有难度?

这段代码对于初学者来说还是有一点复杂的,我们逐段进行拆解与分析。

首先,我们需要明白: 这段代码实现了一个递增计数器。计数器从 0 开始,在每个时钟周期的上升沿执行一次递增操作。如果数值达到了我们设定的最大值,则重新设定为 0 并开始递增。

1
2
3
4
5
6
7
module Counter #(
    parameter   MAX_VALUE = 8'd100
)(
    input                   clk,
    input                   rst,
    output                  out
);

在模块声明部分,我们首先定义了一个常量 MAX_VALUE,用于指定计数器所能达到的最大值。接下来,我们定义了模块的输入变量包括时钟信号 clk 和同步复位信号 rst,最后定义了模块的输出信号 out

9
reg [7:0] counter;

第 9 行定义的变量 counter 是我们核心的计数变量,它是一个 8bits 位宽的 reg 型变量。

思考

这里的 counter 是导线还是真正的寄存器呢?

10
11
12
13
14
15
16
17
18
19
always @(posedge clk) begin
    if (rst)
        counter <= 0;
    else begin
        if (counter >= MAX_VALUE)
            counter <= 0;
        else
            counter <= counter + 8'b1;
    end
end

第 10-19 行的 always 语句是对 counter 的行为级过程赋值操作。我们通过 if 语句描述了这样的逻辑:如果复位信号有效,我们就将计数器归零;否则,如果当前计数值达到了我们定义的最大值,那么计数器也将被归零;否则,计数器自增 1。

21
assign out = (counter == MAX_VALUE);

最后,在第 21 行我们使用 assign 连续赋值语句将输出 out 设定为 counter == MAX_VALUE,也就是当 counter 达到最大值的时候 out 输出 1。在下一个时钟周期,counter 将会被归零,out 也因此输出 0。

那么,这段代码能否按照我们的预期正常工作呢?实际工作中的计数器又是如何运作的呢?接下来,我们将要使用 Vivado 进行仿真工作。


3.1 项目创建

注意

如果你使用的是自己的电脑,请确保你已经安装好了可用的 Vivado。你可以点击这里跳转到相应的教程处。

注意

本小节的内容是基于 Vivado 2023.1 版本编写的。其他版本的界面与操作过程可能会有些不同。

打开 Vivado,点击 Create Project 新建一个 Vivado 工程。

Image title

Image title

点击 Next,填写项目名称与项目路径。例如,本次实验可以将项目名称设定为 Lab2。

Image title

注意

与安装 Vivado 时类似,请确保你的项目路径与项目名称中不包含中文或空格,否则可能带来意想不到的问题。

设置好项目名称后,一直选择 Next(无需更改其他选项),直到遇到选择芯片型号(Default Part)的界面,按照下图所示的配置就可以找到我们使用的芯片型号:xc7a100tcsg324-1

Image title

Tips:芯片选择技巧

你可以直接在 Search 栏中输入 xc7a100tcsg324-1,也可以依次限定下面的内容找到所需要的芯片型号:

  • Category:All

  • Family:Artix-7

  • Package:csg324

  • Speed:-1

注意

请确保你的芯片型号是 xc7a100tcsg324-1错误的芯片型号将导致最终在开发板上的运行结果错误

之后继续选择 Next ,最后点击 Finish 就创建好了一个新的工程项目。

Image title

3.2 ★ 代码编写

打开 Vivado,你将进入下图所示的界面:

Image title

项目界面主要包含 4 大区域。其中 Project Manager 为工程管理窗口,可以完成添加代码、仿真、综合、烧写 FPGA 等一系列操作;Sources 窗口显示代码层级列表,分为设计文件、约束文件和仿真文件三组;Project Summary 窗口显示工程的各种基本信息;Information 窗口显示各种项目的信息参数。

目前我们的项目内还空空如也。因此,我们首先需要创建一个设计文件。在 Project Manager 窗口中点击 Add Sources 可以打开下图所示的窗口。可供添加的文件类型包括:约束文件、设计文件、仿真文件等,此处我们需要添加 Verilog 设计文件,因此选择 “Add or create design sources”。

Image title

Tips:三种不同的文件

约束文件用于明确模块的输入输出端口与开发板上物理端口的对应关系,设计文件是我们电路模块的具体设计,仿真文件则用于对待测试模块进行仿真测试。

单击 Next,再单击 Create File,我们便可以创建一个全新的设计文件。这里可以设置文件类型与文件名,其中文件类型包括:Verilog、Verilog Header、SystemVerilog、VHDL 和 Memory File。在这里以及后续的实验中,我们都选择 Verilog 作为设计语言。文件名可以简单命名为 "Counter" 或其他你喜欢的名字。完成后单击 OK,再单击 Finish 关闭界面。

Image title

最后,在弹出的 Design Module 窗口中单击 OK,再单击 YES 即可完成文件创建。

如何把别处的源代码文件加入项目?

在添加文件时,点击 Create File 旁边的 Add Files,就可以把其他位置的设计文件包含入项目了。

需要注意的是,这里并不能将对应位置的设计文件真正复制入项目文件夹,而只是进行了目录添加。也就是说,在其他项目也包含了这个设计文件的情况下,如果在本项目中修改这个文件,那么其他项目中的这个文件也会被修改。因此,一种比较好的做法是先将这些文件复制到本项目的目录中,再进行文件添加操作。

创建完成后,在 Sources 窗口中我们便可以看到创建好的 Counter.v 文件。双击这个文件就可以在编辑器中编辑它了。

Tips:默认编辑器

如果你没有将 VSCode 设定为 Vivado 的默认编辑器,那么打开的将会是 Vivado 内置的代码编辑界面。Windows 用户可以参考这里学习如何将 VSCode 设定为 Vivado 的默认编辑器。

在打开的编辑器中,输入我们之前的代码:

待测试模块 DUT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
module Counter #(
    parameter   MAX_VALUE = 8'd100
)(
    input                   clk,
    input                   rst,
    output                  out
);

reg [7:0] counter;
always @(posedge clk) begin
    if (rst)
        counter <= 0;
    else begin
        if (counter >= MAX_VALUE)
            counter <= 0;
        else
            counter <= counter + 8'b1;
    end
end

assign out = (counter == MAX_VALUE);
endmodule

3.3 ★ 电路检查与 RTL

现在我们的项目中已经有了一个设计文件 Counter.v。接下来,我们将进入分析阶段。请确保你的目标设计文件已经被正确设定为 Top 文件(Set as Top)。

Tips:TOP 文件

由于不同文件的模块存在互相例化的情况,因此 Vivado 规定:只对手动规定的 Top 文件执行用户操作,这样可以保证我们能够一直对想要的模块进行分析(画电路图、运行检查器、仿真、综合、实现等)。所以,在进入分析阶段前都要记得把待分析模块设为顶层模块。

你可以在 Sources 窗口中右键某一模块,在弹出的窗口中选择 Set as Top 将其设定为 Top 模块。Vivado 会自动将项目创建的第一个文件设定为 Top 文件。如果该文件出现语法错误或不可用,则 Top 文件会空缺。

正常情况下,此时你的 Sources 窗口应当是如下的结构:

Image title

Counter 模块的名称被加粗,代表其已经被设定为 Top 文件。

什么?我不小心把 Sources 或者其他窗口叉掉了!

不用担心,可以在最上方工具栏中选择 Window \(\rightarrow\) Sources 或其他栏目以打开对应的窗口。

如果你使用的是 2023.1 版本的 Vivado,在左侧 Project Manager 窗口中可以看到 Run Linter 按钮。这是 2023.1 版本 Vivado 新增的代码检查器,可以检查出大部分潜在的问题。

Image title

点击 Run Linter 后会在界面下方打开一个 Linter 窗口。如果这里没有弹出 Warnings 以及 Critical Warnings 则表明目前的设计没有明显的问题。

Image title

Tips:Linter 报出的警告不一定会影响设计正确性

当使用一个比较复杂的设计时,Linter 很有可能报出以下两个警告:

  • 提示信号没有被读:表示这个信号的某几位虽然接进了某个模块,但在这个模块中并没有使用。
  • 提示信号没有被用:表示这个信号虽然被声明了,但是并没有使用。或者说信号的某几位接了常值,而这个常值的位宽小于这个信号的位宽。

以上两个警告可能是真的代码错误,也很有可能是“杞人忧天”。这时一定要仔细核对出错信息,如果发现所有的信息都是“杞人忧天”,那么就可以进行下一步了。

接下来,点击左侧的 Open Elaborated Design ,画出设计电路图:

Image title

这里可以看到电路初步的样子,主要由一个寄存器、一个加法器、两个比较器和一个选择器组成。

思考

我们知道,Verilog 硬件描述语言与实际的电路是一一对应的。那么上面这些元件对应着代码中的哪些语句呢?

Counter.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
module Counter #(
    parameter   MAX_VALUE = 8'd100
)(
    input                   clk,
    input                   rst,
    output                  out
);

reg [7:0] counter;
always @(posedge clk) begin
    if (rst)
        counter <= 0;
    else begin
        if (counter >= MAX_VALUE)
            counter <= 0;
        else
            counter <= counter + 8'b1;
    end
end

assign out = (counter == MAX_VALUE);
endmodule

如果你已经配置好了 TerosHDL 插件,则 Counter.v 模块自动生成的电路结构如下图所示:

Image title

不难发现,RTL 电路与 TerosHDL 生成的电路图有诸多相似之处,而 TerosHDL 中多出的比较器实际上已经被封装在了 RTL 的寄存器模块中。

除了画出电路图外,分析阶段更重要的意义在于打开下方的 Messages 窗口,解决 Elaborated Design 文件夹下所有带具体文件行数的 Warnings。这些具体的警告是千万不可以被忽视的,因为它们是影响硬件设计的重要问题。对于一些很冗长且没有标出具体行数的 Warnings,则可以选择忽视。

Image title

Elaborated Design 文件夹下无 Warnings

Image title

Elaborated Design 文件夹下有 Warnings
Vivado RTL 分析的历史遗留问题

Vivado 在进行了 RTL 分析后,如果直接修改代码源文件,那么 Vivado 会检测到修改痕迹,并在上方提示可以对 RTL 分析进行 Reload。

Image title

这样做会导致新的 Warnings 不会在 Messages 中报出!因此,每次修改设计文件后,请一定要关闭 Elaborated Design 后重新 RTL 分析,才可以得到新的 Warnings!

Image title

提醒:遇事不决 RTL

Messages 里的 Warnings 和 Critical Warnings 在开发的所有阶段都非常非常重要!一定要看!有关这些警告的具体解释,请点击这里

3.4 ★ 仿真与波形图

现在进入到最为核心的仿真环节。Vivado 允许我们通过编写仿真文件进行测试,观察特定的波形图以了解电路的工作情况。

首先,我们需要创建仿真资源文件。习惯上,我们在需要测试的模块名后加上 _tb 后缀来表示这是模块的 testbench 仿真文件

Image title

Image title

现在你可以参考我们之前的教程编写仿真文件了。我们给出的参考如下:

Counter_tb.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
module Counter_tb();
reg clk, rst;
wire out;

initial begin
    clk = 0;
    rst = 1;
    #50;
    rst = 0;
end
always #10 clk = ~clk;

Counter #(8) my_counter (
    .clk(clk),
    .rst(rst),
    .out(out)
);
endmodule
理解起来有难度?

按照之前的教程,一个仿真文件应当包括:激励(Stimulus Block)、输出校验(Output Checker)和被测模块(Design Under Test,DUT)。在这里,输入激励对应着第 5-11 行的 initial 和 always 语句。我们通过这些语句确定了 clkrst 信号的变化逻辑。

 5
 6
 7
 8
 9
10
11
initial begin
    clk = 0;
    rst = 1;
    #50;
    rst = 0;
end
always #10 clk = ~clk;

在 initial 块中,我们首先令 clk 为 0,令 rst 为 1,这代表初始的复位操作。在测试开始前进行复位是保证测试正确性的重要步骤

经过 50 个时间单位后,我们令 rst 为 0。之后就是正常的测试流程了。

第 11 行的 always 语句产生了一个周期为 20 个时间单位的时钟信号。

接下来的 13-17 行我们例化了待测试模块 Counter,并用变量 out 获取该模块的输出。

编写完仿真文件后,不要忘记在 Sources 窗口中 Simulation Sources 文件夹下将我们的仿真文件设为 Top。理论上,现在的文件结构应当如下图所示:

Image title

其中仿真文件 Counter_tb.v 中例化了 Counter.v 模块,因此在文件目录中表现为包含的形式。

接下来,点击左侧 SIMULATION 栏下的 Run Simulation 即可开始仿真。你将看到下图所示的窗口。

Image title

Image title

我们来简要介绍一下上图中的基本元素。左侧为模块与元素窗口,用于管理项目中不同层次模块内的信号;右侧为仿真波形窗口,用于显示各个时刻下不同信号的值。下图标注了一些常用的快捷按钮:

Image title

你可以点击左侧 Scope 选项卡中的模块名选中需要查看的模块,再在中间的 Objects 选项卡中选择模块内部信号,将其拖拽到波形图的 Name 列中,随后重新运行仿真以查看内部特定信号的波形。

Image title

例如,我们将 Counter 模块中的 counter 变量拖动到波形图中,重新运行仿真后得到的波形图如下图所示:

Image title

从图中可以看到,每经过 9 个时钟周期 out 就会发出一次信号,这是符合我们的设计预期的。

思考

为什么是 9 个时钟周期呢?

Tips:避免重新运行

如果不希望每次增加信号时都要重新运行,那么你需要在最上方菜单栏的 Tools\(\rightarrow\)Settings 窗口中打开这两个选项:

Image title

Tips:更改波形样式

Vivado 的仿真支持对信号的自定义显示。你可以在仿真窗口中左键选中某一行信号,按下右键,在弹出的窗口中更改该信号的仿真样式。例如:下图展示了将显示的数据设定为二进制的方法。

Image title

如果仿真中没有发现错误,那么就证明你的设计在仿真上已经没有逻辑错误了。

Tips:仿真类型

细心的同学可能发现了,在点击 Run Simulation 的时候,事实上运行的是 behaviour simulation,我们称之为行为仿真。

Image title

行为仿真是一种在逻辑上近乎正确,而在物理上可能出现问题的仿真,它假定设计的最长组合逻辑通路的延时一定小于时钟周期,因此它并没有考虑到逻辑延迟大于时钟周期的情况。如果想要考虑逻辑延迟,那么就需要等到实现电路后运行时序仿真。由于 Vivado 编译器会将信号重命名或元件重排,因此时序仿真很有可能如同”天书”,我们一般还是以看行为仿真为主要的纠错手段。

3.5 导入测试文件

除了使用肉眼观察波形,我们还可以使用测试数据文件进行调试。这种方法的思路是:每一轮测试包括输入数据以及正确的运行结果,动态比较模块输出与正确结果不一致的测试样例,结合控制台的输出以尽快定位出可能出错的位置。这就需要用到我们先前介绍的系统函数了。

与 C 语言高度类似,Verilog 使用如下的系统函数支持对于文件的访问操作:

  • $fopen:打开指定的文件;
  • $fclose:关闭指定的文件;
  • $fscanf:从文件中读取数据。

除了以上的几种函数,Verilog 还支持其他的文件读写函数,如 $fgets$fgetc$$fread 等。感兴趣的同学可以自行查询其用法。

我们通过下面这个例子向大家展示如何使用测试文件进行测试。

例子:最大值问题

MAX2.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module MAX2 (
    input      [7:0]            num1, num2,
    output reg [7:0]            max
);
always @(*) begin
    if (num1 > num2)
        max = num1;
    else
        max = num2;
end
endmodule

上面这段代码源自 Lab1 的实验练习,用于得到两个 8bits 位宽的二进制数的较大者。

首先在我们的项目中新建设计文件 MAX2.v,并写入上面的代码。接下来,我们添加仿真文件 MAX2_tb.v

MAX2_tb.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module MAX2_tb();
parameter TEST_NUM = 5;
reg clk;
reg [7:0] num1, num2, correct;
wire [7:0] out;

MAX2 max2 (
    .num1(num1),
    .num2(num2),
    .max(out)
);

initial begin
    clk = 0;
    forever begin
        #10;
        clk = ~clk;
    end
end

integer index, fid;
initial begin
    index = 0;
    fid = $fopen("test_data.txt", "r");
    $display("[Testbench]: ========== Ready to start the test. ==========");
    repeat(TEST_NUM) begin
        @(posedge clk);
        $fscanf(fid, "%d, %d, %d", num1, num2, correct);
        index = index + 1;
    end
    #50;
    $fclose(fid);
    $display("[Testbench]: ========== Done. ==========");
    $finish;
end

always @(posedge clk) begin
    #1;
    if (out != correct) begin
        $display("[Module MAX2]: Time %0t: Found ERROR at testcase No.%0d", $time, index);
    end
end
endmodule

第 2-19 行关于例化待测试模块、产生时钟的部分我们不再详细介绍。我们来看后半部分的代码:

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
integer index, fid;
initial begin
    index = 0;
    fid = $fopen("test_data.txt", "r");
    $display("[Testbench]: ========== Ready to start the test. ==========");
    repeat(TEST_NUM) begin
        @(posedge clk);
        $fscanf(fid, "%d, %d, %d", num1, num2, correct);
        index = index + 1;
    end
    #50;
    $fclose(fid);
    $display("[Testbench]: ========== Done. ==========");
    $finish;
end

在第 21 行我们声明了两个整型变量 indexfid,前者用于指示当前测试的编号,后者用于指向当前打开的测试文件。

第 22-35 行的 initial 在仿真的过程中仅执行一次。首先,我们令 index 为 0,随后使用 $fopen 打开测试文件,附带的参数 r 表示采用只读模式。test_data.txt 是包含了测试数据的文本文件。这里我们就需要介绍一下项目结构了。

Lab2/
├── ......
├── Lab2.sim
│   └── sim1
│       └── behav
│           └── xsim
│               ├── ......           <- 行为仿真的默认目录
│               └── test_data.txt    <- 我们的测试文件
├── Lab2.srcs
│   ├── sim_1
│   │   └── new
│   │       └── ......    <- 仿真文件
│   └── sources_1
│       └── new
│           └── ......    <- 设计文件
└── Lab2.xpr

一般来说,你的项目目录应当与上面的结构类似。$fopen 在仿真中默认打开的目录是 ...\Lab2\Lab2.sim\sim_1\behav\xsim,因此你需要将测试文件放入该目录中。测试文件 test_data.txt 的内容如下:

test_data.txt
3,5,5
2,4,4
1,0,1
6,6,6
3,1,3
提醒

请不要在测试文件中自行添加额外的空格与空行,因为这会影响后续读取时的结果。

第 25 行使用 $display 函数打印调试信息。在第 26-30 行,我们使用 repeat 进行 TEST_NUM 次循环。在这里 TEST_NUM 的值为 5,代表我们有 5 个测试样例。

26
27
28
29
30
repeat(TEST_NUM) begin
    @(posedge clk);
    $fscanf(fid, "%d, %d, %d", num1, num2, correct);
    index = index + 1;
end

@(posedge clk); 语句是基于事件的时间控制语句,让循环内的语句仅在时钟上升沿执行。$fscanf 函数接受一个文件描述符 fid,并以类似 C 语言的格式化参数进行数据读入。在第 28 行,我们一次读入一行的三个数据,读完后令 index 自增。

31
32
33
34
#50;
$fclose(fid);
$display("[Testbench]: ========== Done. ==========");
$finish;

循环结束后,经过 50 个时间单位,我们关闭文件,打印调试信息,并使用 $finish 结束仿真。至此文件读写部分就结束了。

37
38
39
40
41
42
43
always @(posedge clk) begin
    #1;
    if (out != correct) begin
        $display("[Module MAX2]: Time %0t: Found ERROR at testcase No.%0d", $time, index);
    end
end
endmodule

最后是自动化比对的部分。我们使用 always @(posedge clk) 语句进行比对。注意这里第 38 行的延时符 #1;,它保证我们进行比对时的信号已经稳定下来。随后使用 if 语句进行判断,如果模块输出的结果和我们给出的正确结果不符合,则打印对应的错误信息。

运行一次仿真后,Tcl Console 的输出如下图所示:

Image title

这表明我们的模块没有任何问题。至此仿真与测试过程结束。


休息一会儿

本部分内容到此结束!你理解了多少呢?

在本小节中,我们介绍了如何使用 Vivado 进行 RTL 分析与仿真测试。RTL 分析为我们带来了较为直观的电路结构,同时可以帮助我们检查潜在的语法错误。仿真测试中我们可以通过观察波形图,或导入测试文件检查待测试模块 DUT 的正确性。

现在,我们已经可以检验先前的代码编写是否正确了。在之后的实验中,你也可以自主创建项目,检查自己设计的正确性。

参考资料

暂无


最后更新: December 25, 2023

评论

Authors: wintermelon008