更新于 

建模方法拓展与测试

结构化建模

结构化建模也称为层次化建模,特点是将一个比较复杂的数字逻辑电路划分为多个子模块,再分别为每一个子模块建模,然后将这些子模块组合在一起,完成所需要的电路。因此,结构化建模描述了一个模块是怎样由简单的模块组成的。

其中根据建模的过程分为自顶向下的设计和自底向上的设计,两者只是在模块构建逻辑上有所差异,但是最终达到的效果是一样的。

我们以一道例题体会一下结构建模,比如我们要将3个2-1多路选择器组合成一个4-1多路选择器。我们可以轻易写出2-1多路选择器的建模形式:

因此接下来我们只是需要用适当的方式自底向上组成4-1多路选择器。因此在4-1多路选择器的建模过程中需要引入二路选择器的实例组件。假设我们现在已经得到了4-1多路选择器的结构图如下:

思路很简单,sel[0]用来决定lowmux和highmux的两个二路选择器的选择信号,sel[1]充当finalmux的选择信号。因此lowmux和highmux分别都有2种可能输出的信号,而finalmux又决定了选择lowmux或者highmux的其中一个输出信号,因此又有2中选择,因此一共有2*2=4可能选择的情况,刚好和4路选择器的功能一致。接下来我们只需要根据上图进行建模了:

1
2
3
4
5
6
7
8
9
module mux4(input logic D0,D1,D2,D3,input logic [1:0]s,output logic y);

logic low,high;
//使用2路选择器模块进行实例化
mux2 lowmux(D0,D1,s[0],low);
mux2 highmux(D2,D3,s[0],high);
mux2 finalmux(low,high,s[1],y);

endmodule

我们发现实际上结构化建模和java的类的使用很相似,就是使用已经建模完成的模块来组装更加高层次复杂的结构化模块。

模块实例化

在结构化建模中,父模块对子模块的调用通过模块实例化实现,其格式如下:

1
模块名 实例化名 (信号列表)
  • 模块名:定义子模块时,紧跟在module关键字后面的名字,例如上例中的mux2
  • 实例化名:父模块为所调用的子模块命名的名称,是该子模块的唯一标识,例如上例中的lowmux,highmux和finalmux
  • 信号列表:父模块与子模块之间端口信号的关联方是,用于实现子模块与父模块的通信。通常有位置关联法和名称关联法。

位置关联法就是我们通常上理解的使用函数是传参的格式,即实例化模块时,按照子模块定义时端口出现的顺序建立端口的连接关系。例如:

1
2
3
//mux2定义的形式
module mux2(input logic D0,D1,intput logic [1:0]s,output logic y);
mux2 lowmux(D0,D1,sel[0],low)

那么D0就是mux2模块的第一个形参关联的端口参数,D1就是第二个。

但是由于在建模时可能一个模块有多达数十个端口信号,那么此时在使用位置关联法就很容易出现错误,因此还有一种名称关联法,就是实例化子模块时,直接通过名称显式建立子模块端口的连接关系,不考虑排列顺序(注意,此时所有的参数又要显式的声明和一个形参绑定)。如:

1
2
3
4
5
//mux2定义的形式
module mux2(input logic D0,D1,intput logic [1:0]s,output logic y);
//实例化一个mux2
//注意实参写在形参后面的小括号后面
mux2 lowmux(.D0(D0),.D1(D1),sel(.sel[0]),y(.low));

此时就与位置无关了,因为每一个实参端口信号都显式的和一个形参端口信号建立了连接。这种方法适用于端口信号很多的模块实例化中。

一定要注意对于名称关联法,形参在外,括号内是传进来的实参信号,同时形参前面有一个.

接下来我们以mux4的两种实例化方法直接对比:

门级建模

门级建模是一种低层次的结构化建模方法,他通过调用SystemVerilog HDL提供的基本逻辑元件描述他们之间的连接,建立数字逻辑电路的模型。

门级建模可以理解为将逻辑电路图转化为SystemVerilog HDL描述的建模过程。门级建模只能用于描述组合逻辑电路,不能用来描述复杂的时序逻辑电路,这是因为门级建模使用的都是逻辑门元件。因此门级建模实例化的语句:‘

1
门级元件名<实例名>(端口列表)

其中实例名可以忽略不写,即为一个匿名门元件实例。一般常用到的门级元件有:

假设我们现在要使用门级建模构造一个mux2二路选择器,那么根据下面的图

我们可以建模:

1
2
3
4
5
6
7
module mux2(input logic D0,D1,sel,output logic y)
logic a,b,sel_inv;
not N1(sel_inv,sel);
and N2(a,D0,sel_inv);
and N3(b,D1,sel);
or(y,a,b);//注意实例名可以忽略
endmodule

结构化建模总结

  • 模块只能以实例化的方式嵌套在其他模块中(和java的外部类的实例化引用思路一样),嵌套层次是没有限制的,但是不能在一个模块内部使用关键词module和endmodule取定义另一个模块,也不能再always语句内部引用子模块。
  • 实例化的子模块可以是一个在SystemVerilog文件中定义好的模块,或者是用别的HDL语言(例如Verilog,VHDL)设计的模块,还可以是IP(Intellectual Property,知识产权)核模块。
  • 在一条实例化子模块的语句中,不能一部分端口用位置关联,另一部分用名称关联,即不能混用这两种方式建立端口之间的链接。
  • 原则上在一个模块中可以使用行为建模和结构化建模,但是为了便于设计层次化、模块化,建议在一个模块中不要混用两种建模方式。

参数化建模

在SystemVerilog HDL中,参数化建模是指使用关键字“parameter"声明一个标识符来代表一个常量,程序中凡是出现这个常量的位置,都可以用这个标识符来代替,从而大大增加了设计的灵活度和程序的可读性。参数化建模最常见的用法就是参数化建模,如下图:

左边的模块中定义了字符"#"同时声明为WITH代表位宽8,那么所有的[WIDTH-1:0]代表的都是一个位宽为8的线信号量,当我们需要修改时只需要修改定义语句即可完成对全部模块的相对应的位宽信号的修改。我们再看中间的模块和右侧模块定义的都是一个4路选择器,只是中间定义的是位宽为8的信号,那么当我们需要修改为32位宽时,只能手动逐一的对每一个[7:0]进行修改为[31:0],这很麻烦耗时,而右侧的使用了参数化建模,所有的#只要定义为32位宽即可完成所有的信号的位宽更新,这就是参数化建模的优点。

我们可以将parameter语句置于模块内部,与参数化建模的区别是对模块化实例化时无法修改该参数的值。并且通过预编译指令`define(即宏定义指令)可以声明一个标识符来代表一个常量,通常位于模块外部,该常量是一个全局常量,其作用范围是从定义起点到整个程序的结束,既可以对多个文件起作用。这样parameter只能决定一个模块自身内部的信号的更新,而`define指令定义的是对多个模块起修改作用的全部常量语句,两者搭配,可以高效的完成所有文件的信号的更新操作。如下:

parameter语句只对mux2内部的信号量起作用,而通过`define指令声明的WIDTH可以修改所有文件的信号位宽。同时要注意宏定义指令结尾处不需要分号。

generate语句

generate语句(即生成语句)可以动态的生成SystemVerilog HDL代码,可以大大简化程序的编写过程。generate语句结合参数化建模方法可以生成可变数量的硬件,此外,generate语句还可用于对向量信号中的多个位进行重复操作,或者对一个模块进行多次实例化,或者根据参数的定义来确定程序中是否应该包含某段SystemVerilog代码。

generate语句通过“generate…endgenerate"来确定生成的代码范围,可以动态生成模块(实例化)、持续赋值语句(assign)、always过程块等内容。通常generate语句还可以和for,if和case语句组合使用。

如下图是generate生成一个由多个2路选择器组成的N路选择器的模块:

这里的几个注意事项见上图。

思考:为什么要用generate语句来包裹而不是直接调用for语句?

在建模过程中,我们通常定义一个变量时都是定义为信号量,并且这个信号量是一个具体的确定值,但是我们分析上面的代码实际上x并不是一个信号量,他是一个由i决定的若干个信号量的一个抽象表示。具体生成多少个x信号量以及对于第i个信号量x的定义都需要由i来决定。同时a信号量也会再generate语句中多次使用表示不同的值。因此generate语句的引入是有必要的,当我们不使用generate语句包裹时,那么如果想创建10000个类似于x的相同作用的信号量,那么首先需要构建10000个不同名的信号变量值,这显然不现实。

SystemVerilog的测试程序

前面我们简单学习过仿真验证的概念,他是在线平台上通过模拟来对一个为构建的组合逻辑电路模块进行建模检测,查看是否完美符合预期功能。

电路验证是确认所设计的电路功能正确性的过程,而仿真是进行电路验证的主要手段,它可以及早发现所存在的设计问题,从而降低了设计风险,节约了设计成本。通常,仿真是通过编写测试程序完成的,测试程序也称为测试台,他是用于测试待测模块功能是否正确的一端SystemVerilog HDL代码,是不可综合的,由激励信号、DUT和输出响应三部分组成。

思考:什么是综合?

综合是真正在组件一个模块元件时所必要的完成功能构建的语句。不可综合的语句一般是不用于完成元件功能的语句,在最终制造阶段舍弃,例如验证语句虽然在构建模块时进行编写,但是在测试完成后构建元件时,这部分代码不会被综合,不参与最终元件的功能实现中。

我们看一个测试程序的例子:

上面的代码就是用于验证的测试代码,它是用来测试一个数学计算模块是否正确的语句:

我们从上面的例子中不难看出我们分别测试的激励信号组合分别是:

也就是说每一次在验证时输入的信号是持续的,当改变信号时,只需要输入需要被修改的信号值即可,未被修改的信号是维持不变的。如上面的第一组激励信号测试组合为a=0,b=0,c=0并且持续时间为10个单位时间,然后我们修改了c的信号值为1,因此第二个激励信号组合为a=0,b=0,c=1并且测试时间也是10个单位长度。因为每一个激励信号的测试时间都是10个单位长度,因此最终屏幕上的输出脉冲为等宽。

测试程序的语句

首先我们可以总结出测试程序的模板如下:

1
2
3
4
5
6
module testbench_name //testbench为顶层模块,不会被其他模块实例化,因此不需要任何端口
//信号定义
//模块实例化,很重要,我们需要实例化一个待测模块
//添加激励信号
//显示输出结果(可以不添加任何显示打印语句,那么只生成波形图)
endmodule

施加激励信号

在SystemVerilog中,施加激励就是想DUT添加输入信号(即测试向量),主要由三种方法:

  1. 通过initial过程过施加(线性)激励
  2. 通过always过程块施加(循环)激励,主要用于产生时钟信号(后面有一章专门介绍,这里不细讲)。
  3. 通过文件施加激励(比较人性化)

首先,通过initial块施加激励就和上图的那个例子一样,每一个仿真时刻只用列出需要改变的信号值,initial只执行一次改变语句。在一个测试程序中可以包含多个initial块,并且他们都是同时并行执行。

需要注意同一个initial块中的激励信号语句是串行执行的,initial块中的激励信号语句是并行执行的,并且在同一个仿真时刻避免为同一个信号赋予不同的信号值,否则将会冲突。

当通过文件施加激励时,只需要将激励(测试向量)存放在一个文本文件中,测试程序从文件中读取激励,对DUT进行测试。

这种方式更加高效,可以预先存储上万个不同的测试样例,测试验证时只需导入测试文件即可。

输出响应

在SystemVerilog中,输出响应是指在向DUT施加激励以后,通过观察DUT输出结果,并与预期结果进行比较,以验证电路是否正确。这个过程可以通过直接观测波形图或者借助SystemVerilog HDL提供的一系列系统任务显示输出结构来实现。毕竟有时候看图不好发现错误,那么可以打印输出信息。

在SystemVerilog中常用的系统任务包括:

系统任务 方法
获取仿真时间 $time$stime,$realtime
显示信号值 $display,$monitor(常用)
结束/中断仿真 $finish,$stop
文件输入 $readmemb,$readmemh,$fopen,$fclose,$fdisplay,$fmonitor
文件输出 $fopen,$fclose,$fdisplay,$fmonitor

获取仿真时间的系统任务的返回值使用由`timescale宏定义指令声明的时间为单位时间,只是$time返回的是一个64位整数时间值,$stime返回的是32位整数时间值,$realtime返回的是一个实数时间值。例如:

1
$monitor($time,"a=%b b=%b c=%b y=%b",a,b,c,y);

一定要注意$time$stime是会取整的,因此不是精确值,而$realtime返回的才是实数准确时间值。

对于显示信号值的系统任务$display$monitor,相当有C中的printf函数,输出变量的值显示在控制台上,语法格式为

1
2
$display($time,"显示格式控制符",<输出变量(信号)列表>);
$monitor($time,"显示格式控制符",<输出变量(信号)列表>);

显示格式控制符有:

%h %o %d %b %c %s %t %m
16进制 8进制 10进制 2进制 ASCII 字符串 时间 模块名

例如:

$display$monitor的区别在于前者只有执行到该语句时才进行显示操作,而后者就像一个监视器,只要输出变量列表中的某个变量发生变化,就执行一次显示操作,后者更加方便实用。

结束/中断仿真的系统任务包括:$finish$stop两种,其格式如下:

1
2
3
4
$finish();
$finish(n);
$stop();
$stop(n);

其中参数n可以取0,1等值,0表示不输出任何信息,1表示给出仿真时间。

在SystemVerilog HDL中文件输入不需要打开文件操作,直接读取文件即可,也有$readmemb$readmemh两种,其中前者读取2进制数据,后者读取16进制数据,语法格式如下:

1
2
$readmemb("数据文件名",数组(存储器)名,<起始地址>,<结束地址>);
$readmemh("数据文件名",数据(存储器)名,<起始地址>,<结束地址>);

其中起始地址和结束地址也可以缺省。并且输入文件格式有以下细节:

  • 可以使用"_"提高数据的可读性

  • 可以包含单行或者多行注释

  • 可以使用空格或者换行来区分单个数据

  • 可以设定一个特定地质,规定其后的数据从该地址开始存储。地址必须是16进制,且不区分大小写,并且@和地址之间不允许有空格,即

    1
    地址写法:@hex_addr

假设现在有一个输入文件如下:

现在我们打开这个文件:

那么最终我们读取文件到stim数组的地址数据如下图:

也就是前三个地址是连续的,然后从3-255是空缺的没有地址,然后从256(十六进制的@100)有一个单独的地址数据为1111_1100,然后257-1022又是空缺的,到达1023时再继续存储定义的地址。

文件输出操作需要首先用系统任务$fopen打开文件,然后通过系统任务$fdisplay$fmonitor将需要的保存的信息输入到指定文件中。

例如现在要打开一个MCD文件:

1
2
3
4
5
int MCD;
MCD=$fopen("文件名","操作模式");
$fdisplay(MCD,"显示格式控制符",<输出变量(信号)列表>);
$fmonitor(MCD,"显示格式控制符",<输出变量(信号)列表>);
$fclose(MCD);//一定要记住关闭文件呀

$fopen是打开指定文件并且返回一个32位整数,如果打开失败了则返回0,操作模式为w,w+,a,a+。$fclose是关闭打开的文件,而$display$fmonitor的用法与$display$monitor的用法类似,区别在于不是将输出响应输出到控制台,而是将信息输出到文件中。

自动化测试

我们思考一下无论上面的那种测试方法,最终我们还是需要自己去对比输出结果是否正确,这对于上万个输出响应的测试程序来说,人工复查太难了,因此我们需要机器来自动帮助我们比对判断输出响应是否正确,因此就产生了自动化测试。

实际上自动化测试的过程和仿真验证类似,只是多了一步自动将输出响应和预期结果比对的过程。具体的写法见上图。

测试程序总结

  • 测试程序由激励信号、待测模块DUT和输出响应三部分组成,其本身也是一个SystemVerilog代码(这很重要,需牢记)。并且是最顶层的模块,当没有端口,不可综合。
  • 由于测试程序不会变为电路,因此在测试程序中可以使用所有的SystemVerilog语句。
  • 为了便于复用和管理,测试程序可以通过文件施加激励。
  • 进行验证时,尽量采用自动化对比方式,毕竟人工复查还是容易出错。
  • 虽然观察波形图寻找错误开始时比较困难,但是习惯以后发现波形图能够帮助我们发现那些控制台输出不易察觉的问题。
  • 以上的测试程序本质上都是对使用SystemVerilog HDL所编写的程序进行仿真测试,并没有考虑门延迟,线延迟等问题,因此也称为行为仿真或者前仿真,虽然行为仿真可以发现很多设计问题,但是并不意味着通过了行为仿真电路就一定没有问题了,后面还是需要进行大量的测试的。