详解VHDL如何编写Testbench
1.概述
仿真测试平台文件(Testbench)是可以用来验证所设计的硬件模型正确性的 VHDL模型,它为所测试的元件提供了激励信号,可以以波形的方式显示仿真结果或把测试结果存储到文件中。这里所说的激励信号可以直接集成在测试平台文件中,也可以从外部文件中加载。
一般而言,编写 Testbench 进行测试主要有下面四个步骤
- (1)实例化需要测试的设计(DUT,Design Under Test);
- (2)产生模拟激励(波形);
- (3)将产生的激励加入到被测试模块并观察其输出响应;
- (4)将输出响应与期望进行比较,从而判断设计的正确性。
其中,输出响应可以以波形方式显示或存储测试结果到文件中。
2.Testbench程序基本结构
通常 Testbench 的基本结构包括库的调用、程序包的调用、空实体、结构体描述。在结构体描述中,一般包含有被测试元件的声明、局部信号声明、被测试元件例化、激励信号的产生,如图所示。与一般的 VHDL 程序不同的是,Testbench 里面的实体为空。
2.1 被测试元件的声明方式
先说一个被测实体vote7
,代码如下:
library ieee;
use ieee.std_logic_1164.all;
entity vote7 is
port(
vt : in std_logic_vector(6 downto 0);
result : out std_logic
);
end entity vote7;
architecture rtl of vote7 is
begin
process(vt)
variable sum : integer range 0 to 7;
begin
sum := 0;
for i in 0 to 6 loop
if vt(i) = '1' then
sum := sum + 1;
if sum > 4 then
result <= '1';
else
result <= '0';
end if;
end if;
end loop;
end process;
end architecture;
2.1.1 组件实例化
组件实例化是一种传统且常用的方法,特别适用于较早版本的VHDL(如VHDL-93)。该方法需要在测试平台中先声明一个组件,然后在架构中进行实例化。
步骤
-
组件声明:在测试平台的架构声明部分(通常在
architecture
关键词之后)声明DUT的组件。 -
实例化:在架构的主体部分使用
component
实例化DUT,并进行端口映射。
示例代码
假设有一个被测实体vote7,其声明如下:
library ieee;
use ieee.std_logic_1164.all;
entity tb_vote7 is
end entity tb_vote7;
architecture Behavioral of tb_vote7 is
-- 信号声明
……
-- 组件声明
component vote7
port(
vt : in std_logic_vector(6 downto 0);
result : out std_logic
);
end component;
begin
-- DUT实例化
DUT: vote7
port map (
vt => vt,
result => result
);
-- 激励过程
……
end architecture Behavioral;
2.1.2 直接实体实例化
直接实体实例化(也称为架构实例化)是VHDL-2002及更高版本中引入的一种更简洁的实例化方式。它不需要提前声明组件,直接引用实体和架构即可。
优点
- 简洁:无需组件声明,减少了代码冗余。
- 灵活:可以直接指定要使用的实体和架构。
使用与前述相同的vote7实体,测试平台采用直接实体实例化的方法如下:
library ieee;
use ieee.std_logic_1164.all;
entity tb_vote7 is
end entity tb_vote7;
architecture Behavioral of tb_vote7 is
-- 信号声明
……
begin
-- DUT直接实例化
DUT: entity work.vote7(rtl)
port map (
vt => vt,
result => result
);
-- 激励过程
……
end architecture Behavioral;
代码解析
- DUT实例化:使用
entity work.vote7(rtl)
指定实体vote7
和其架构rtl
,然后进行端口映射。 - 无需组件声明:省略了组件的预先声明,代码更加简洁。
3.激励信号的产生
激励信号产生的方式一般有两种,一种是以一定的离散时间间隔产生激励信号,另一种是基于实体的状态产生激励信号。需要注意的是,在 Testbench 程序中一定要对所有的激励信号赋初始值。下面通过实例,讲述激励信号的产生方法。
3.1 时钟信号的产生
时钟信号属于周期性出现的信号,是同步设计中最重要的信号之一。如图所示,时钟信号分为两类,即占空比为50%的对称时钟信号与占空比不是 50%的非对称时钟信号。
Testbench 中产生时钟信号方式有两种,
- 一种是使用并行的信号赋值语句;
- 一种是使用process进程。
下面分别通过两个例子来说明如何用这两种方法来产生所需的时钟信号。
【例】用并行信号赋值语句产生如图所示的 clk1
、clk2
、clk3
信号。
观察上图,我们发现 clk1
为对称时钟信号,其初始值可以在信号定义时赋值;clk2
和 clk3
为非对称时钟信号,其起始值可以在语句中赋值。这两种信号的产生方式有所不同,相对而言对称时钟信号的产生相对简单一些。
并行信号赋值语句的实现如下:
signal clk1:std_logic := '0';
signal clk2:std_logic;
signal clk3:std_logic;
……
clk1 <= not clk1 after clk_period/2;
clk2 <= '0' after clk_period/4 when clk2 = '1' else
'1' after 3*clk_period/4 when clk2 = '0' else
'1';
clk3 <= '0' after clk_period/4 when clk3 = '1' else
'1' after 3*clk_period/4 when clk3 = '0' else
'0';
……
【例】使用 process 进程产生如图所示的clk1
、clk2
信号。
观察上图,可以发现 clk1
为对称时钟信号,clk2
为非对称时钟信号,但这两种信号用 process
进程实现的方法基本一致。
process
进程实现如下:
signal clk1:std_logic;
signal clk2:std_logic;
……
clk1_gen:process
constant clk_period :time := 40ns;--常量只在该进程中起作用
begin
clk1 <= '1';
wait for clk_period/2;
clk1 <= '0';
wait for clk_period/2;
end process;
clk2_gen:process
constant clk_period :time := 20ns; --常量只在该进程中起作用
begin
clk2 <= '0';
wait for clk_period/4;
clk2 <= '1';
wait for 3*clk_period/4;
end process;
……
3.2 复位信号的产生
Testbench中产生复位信号方式也是两种,一种是并行赋值语句实现,另一种是在进程中设定。下面用例加以说明。
【例 7-5】如图所示,请用并行信号赋值语句产生的reset1信号,用 process 进程产生reset2信号。
程序如下:
……
signal reset1:std_logic;
signal reset2:std_logic;
……
-- 并行信号赋值语句产生的reset1信号
reset1 <= '0','1' after 20 ns,'0' after 40ns;
--用process进程产生reset2信号
reset2_gen:process
begin
reset2 <= '0';
wait for 20 ns;
reset2 <= '1';
wait for 40 ns;
reset2 <= '0';
wait;
end process;
……
3.3 使用delayed属性产生两相关性信号
delayed是VHDL的预定义属性,使用它可以产生两个相关性的信号。如果已经产生了一个时钟信号,在这个时钟信号的基础上,可以使用delayed来使已经产生的时钟信号延迟一点的时间,从而获得另一个时钟信号。
假设已经使用如下的语句定义了一个时钟信号W_CLK
:
W_CLK<= '1' after 30 ns when W_CLK= '0' else
'0' after 20 ns;
然后可以使用如下的延迟语句获得一个新的时钟信号DLY_W_CLK,它比W_CLK延迟了10 ns:
DLY_W_CLK <= W_CLK' delayed(10 ns);
以上两个时钟信号波形如图所示:
【例 】 如图所示,请编程实现信号 periodl,period2,要求用到 DELAYED 属性。
程序如下:
signal period1,period2:std_logic;
……
period1 <= '1' after 30 ns when period1 = '0' else
'0' after 20 ns when period1 = '1' else
'0';
--利用delayed属性,由period1产生period2
period2 <= period1' delayed(10 ns);
3.4 一般激励信号
一般的激励信号通常在 process 进程中定义,而在 process 进程中一般需要使用 wait 语句。所定义的普通的激励信号常用来作模型的输入信号。
【例】 如图 7-18 所示,请编程产生信号 test vectorl 和 test vector2。
程序如下:
signal test_vector1:std_vector_logic(1 downto 0);
signal test_vector2:std_vector_logic(1 downto 0);
……
TB1:process
begin
test_vector1 <= "01";
wait for 10 ns;
test_vector2 <= "10";
wait for 20 ns;
end process;
TB2:process
begin
test_vector2 <= "01";
wait for 10 ns;
test_vector2 <= "10";
wait;
end process;
【例 】 输入信号 test_ab
和 test_sel
均为 2bit,试用 VHDL 产生这两个输入信号以覆盖所有的输入情况。输入信号向量 test_ab
和 test_sel
均为 2bit,产生的输入情况共有(2x2)x(2x2)=16 种可能。
实现的程序如下
signal test_ab : std_logic_vector(1 downto 0);
signal test_sel:std_logic_vector(1 downto 0);
double_loop:process
begin
test_ab <= "00";
test_sel <= "00";
for i in 0 to 3 loop
for j in 0 to 3 loop
wait for 10 ns;
test_ab <= test_ab + 1;
end loop;
test_sel <= test_sel + 1;
end loop;
end process;
程序对应的波形如图所示:
特别注意:如果同一个信号在两个进程中进行赋值,若在某些时间段内发生了冲突,就会出现不定状态,如下例所示。因此同一信号不允许在不同进程中赋值。
【例】同一个信号在两个进程中进行赋值,在某些时问段内发生了冲突,出现不定状态的情况。
程序如下:
……
signal test_vector:std_logic_vector(2 downto 0);
signal reset:std_logic;
……
gen_1:process
begin
reset <= '1';
wait for 100 ns;
reset <= '0';
test_vector <= "000";
wait;
end process;
gen_2:process
begin
wait for 200 ns;
test_vector <= "001";
wait for 200 ns;
test_vector <= "011";
end process;
……
对应的波形如图所示:
3.5 动态激励信号
动态激励信号,就是输入激励信号与被仿真的实体(DUT)的行为模型相关,即 DUT 的输入激励信号受模型的行为所影响。
如下信号的定义,模型的输入信号 sig_A
就和模型输出信号 count
相关。
process(count)
begin
case count is
when 2 =>
sig_A <= '1' after 10 ns;
when others =>
sig_A <= '0' after 10 ns;
end case;
end process;
3.6 测试矢量
在实际应用中,常常将一组固定的输入输出矢量值存储在一个常量表或一个 ASCI 文件中,然后将这些值应用到输入信号从而产生激励信号。这里所说的固定输入输出矢量值就称为测试矢量。矢量的值序列可以使用多维数组或使用多列记录来描述。
如下面的数据表存储了输入矢量:
constant no_of_bits:integer := 4;
constant no_of_vectors:integer := 5;
type table_type is array (1 to no_of_bits) of std_logic_vector(1 to no_of_vectors);
constant input_vectors:table_type := ("1001","1000","0010","0000","0110");
signal inputs:std_logic_vector(1 to no_of_vectors);
signal A,B,C:std_logic;
signal D:std_logic_vector(0 to 1);
假设所测试的实体(DUT)具有4个输入:A、B、C和D信号,如果以一般的时间间隔应用测试矢量,则可以使用一个generate语句,例如
G1:for j in 1 to no_of_vectors generate
inputs <= input_vectors(j) after (vector_period*j);
end generate;
A <= inputs(1);
B <= inputs(4);
C <= inputs(1);
D <= inputs(2 to 3);
如果将信号应用于任意时间间隔,则需要使用并行的信号赋值语句产生多个信号的波形,使用这种方法可以将一个矢量赋值给多个信号,如下面的代码:
inputs <= input_vector(1) after 10 ns;
input_vector(2) after 25 ns;
input_vector(3) after 30 ns;
input_vector(4) after 32 ns;
input_vector(5) after 40 ns;
4.高级Testbench编写
高级Testbench 是在简单 Testbench 基础上改进的,能够自动读入测试矢量文件、完成输出值和期望值的比较等功能,如图所示。相比简单Testbench,高级 Testbench 更显得智能化,也减少了人工分析的烦琐工作。
4.1 文件I/O的读写
仿真时,VHDL 允许设计人员从文件加载数据或将数据存储到文件中。比如用户定义的测试矢量可以保存在文件中,然后在仿真时从文件中读取这些测试矢量。另外,仿真的结果也可以保存在文件中。
VHDL 标准中的文件 I/O 主要是由 TEXTIO 程序包提供的,用于仿真且综合工具不能综合标准库 STD 中的 TEXTIO 定义的程序包只能使用 BIT 和 BIT_VECTOR 数据类型,其引用的格式为:
library std;
use std.textio.all;
如果要使用std_logic
和std_logic_vector
,则需要调用std_logic_textio
,格式为:
library ieee;
use ieee.std_logic_textio.all;
4.1.1 TEXTIO介绍
TEXTIO 是 VHDL 标准库 STD 中的一个程序包(package)。在该包中定义了三个基本类型:LINE
类型、TEXT
类型以及 SIDE
类型。另外,还有一个子类型(subtype)WIDTH。此外,在该程序包中还定义了一些访问文件所必须的过程(procedure),如图所示。
其中,TEXT为ASCII文件类型。定义成TEXT类型的文件是长度可变的ASCII文件,需要注意的是VHDL’87 和 VHDL’93 在使用文件方面由较大的差异,在编译时注意选择对应的标准。
side
只能有两种状态,即right
和left
,分别表示将数据从左边还是从右边写入行变量。该类型主要是在TEXTIO程序包包含的过程中使用。
WIDTH
为自然数的子类型。所谓子类型表示其取值范围是父类型范围的子集。
TEXTIO 也提供了基本的用于访问文本文件的过程。类似于 C++,VHDL 提供了重载功能,即完成相近功能的不同过程可以有相同的过程名,但其参数列表不同,或参数类型不同或参数个数不同。
TEXTIO 提供的基本过程有:
1.procedure READLINE(文件变量;行变量);
用于从指定文件读取一行数据到行变量中。
2.procedure WRITELINE(文件变量:行变量):
用于向指定文件写入行变量所包含的数据。
3.procedure READ(…);
可重载,用于从行变量中读取相应数据类型的数据。
4.procedure WRITE(…);
可重载,用于将数据写入行变量
4.1.2 文件基本操作
1.定义文件
TEXTIO 程序包中可操作的文件主要包含两大类:integer
和 text
。
integer
文件中的数据是以二进制存取的,不能被人识别,只有 integer
型的数据能够存入这类文件。
text
文件是可以读取的 ASCI 码,可以被人识别。integer 、bit vector(x downto x)、string(x downto 1)、std logic_vector(x downto 0)及 bit
等类型都可以被存入此类文件。
对文件进行操作之前,需要对将要进行操作的文件进行定义,在 93 版的 VHDL 中,文件定义的方式如下:
FIE file handle: text open read mode is"目录十文件.后缀"---(输入文件的说明)
FIE file handle: text open write mode is"目录十文件.后缀"---(输出文件的说明)
在 87 版的 VHDL 中,文件定义的方式:
FIEL file handle: text is in"目录十文件.后缀"---(输入文件的说明)
FIEL file handle: text is out"目录十文件.后缀"---(输出文件的说明)
如果在支持 93 版的 VHDL 语言中使用了 87 版的格式,仿真时会提示:
warning: FIE declaration was written using 1076-1987 syntax.
2.打开文件
定义文件句柄后就可以在程序中打开指定文件,同时指定打开模式。93 版的 VHDL 可以使用 file open()
进行文件打开操作,其中文件打开操作的函数使用方法如下:
file_open(fstatus,file_handle,filename)
其中,fstatus
指示当前文件状态,但是在使用前首先得定义:
variable fatatus:file_open_status;
状态一般有四种,即open_ok
,status_error
,name_error
,mode_error
。
file handle
即是上一步定义的文件句柄file handle
。
filename
是以双引号括起的文件名,如"datain.txt",也可以加上文件路径。openmode
是指打开该文件的模式,文件打开有read_mode
,write mode
,append_mode
三种。
3.读写文件
打开文件后就可以对文件进行读写操作,其语句格式如下:
--将文件中的一行数据读至行变量中。
realine(文件变量,行变量);
--行变量中保存的数据取n位放至数据变量v中,n为数据变量v的数据位数。在此之前,需要定义好行变量和数据变量。
read(行变量,数据变量);
--将一个数据写到某一行中。
write(行变量,数据变量);
--起始位置为left 或 righ,字符数则表示数据变量写入到行变量后占的位宽。
write(行变量,数据变量,起始位置,字符数);
--将行变量包含的数据写入到指定文件;
wwriteline(文件变量,行变量);
4.关闭文件
在文件读写完毕后,需使用 file)close(file handle)关闭文件。
如果想判断在文件操作中是否读取到文件的末尾,可以使用函数endfile(file_handle)
进行判断,如果到达文件末尾将返回“真(true)”,否则返回“假(false)”。
下面举一个例子,使用了上面介绍的各种语法。
【例】文件I/O读写例程
library ieee;
library std;
use std.textio.all;
use ieee.std_logic_textio.all;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
entity testin is
end entity testin;
architecture rtl of testin is
begin
process
-- 定义text类型得文件句柄
file file_out1,file_in:text;
-- 定义文件状态指示变量
variable fstatus1,fstatus2:file_open_status;
variable count:integer := 5;
variable stringdata:string(5 downto 1) := "SCUTE"; --string型
variable vectordata:bit_vector(5 downto 0) := "001000";
variable value:std_logic_vector(3 downto 0) := "1111";
variable buf,buf1:line;
begin
--创建并打开文件
file_open(fstatus1,file_out1,"DATAIN.TXT",write_mode);
write(file_out1,string'("THE FIRST PAPAMETER IS ="));
readline(input,buf);
write(buf,count);
writeline(file_out1,buf);
wait for 20 ns;
write(buf,string'(THE SECOND PAPAMETER IS = "));
write(buf,value);
writeline(file_out1,buf);
wait for 20 ns;
write(buf,string'("THE THIRD PAPAMETER IS = "));
write(buf,vectordata);
writeline(file_out1,buf);
wait for 20 ns;
write(buf,string'("THE FORTH PAPAMETER IS = "));
write(buf,stringdata);
writeline(file_out1,buf);
write(file_out1,string'("END OF FILE"));
file_close(file_out1);
wait for 100 ns;
file_open(fstatus1,file_out1,"DATAIN.TXT",read_mode);
readline(file_out1,buf);
writeline(output,buf);
file_close(file_out1);
wait for 100 ns;
file_open(fstatus1,file_in,"STD_INPUT",read_mode);
file_open(fstatus2,file_out1,"STD_OUTPUT",write_mode);
readline(file_in,buf);
writeline(file_out1,buf);
wait;
end process;
end rtl;
在modelsim中运行,控制台将做如下操作:
等待数秒后,在modelsim工程目录下将会新建一个“DATAIN.TXT”文本文档,打开文档其内容如图所示:
4.2 断言语句
断言语句(Assert)语句可以在仿真的过程中,检查一个条件并报告信息,一般用于程序调试与时序仿真时的人机对话,也是不可综合的语句。
断言语句的书写格式为:
assert<条件表达式>
report<出错信息>
severity<错误级别>;
其中,ASSERT 后的条件表达式为布尔表达式,用于模拟执行时的真假判断。若其值为“真”则跳过下面两个子句,继续执行后面的语句;若其值为“假”,则表示出错,于是执行 REPORT报告出错信息,同时由 SEVERITY 子句给出错误等级。
ASSERT 后的条件表达式由设计人员自行拟定,没有默认格式。断言语句里面的出错信息与错误等级也都由设计者自行设计,VHDL不自动生成这些信息。而且,REPORT 后的出错信息必须是字符串,需要用双括号括起来,若缺省出错信息,则系统默认输出错误信息报告为"Assertion Violation"。SEVERITY 后的错误级别要求是预定义的四种错误之一, 预定义的四种错误类型分别是:Note(通报)、Warning(警告)、Error(错误)、Failure(失败)。若缺省,则默认为 Error。
4.2.1 断言语句的使用方法
断言语句可以在实体、结构体以及进程中使用。下面通过一个例子初步介绍断言语句在仿真时的应用。
【例】用断言语句判断仿真的时间,如果当前时间为1000ns,则仿真完成,使用 ERROR严重级别终止仿真过程。
程序如下:
process
begin
assert(now <= 1000 ns)
report "simulation completed successfully"
severity error;
end process;
断言语句判断条件的判断结果为 FALSE,则执行后面的报告及严重级语句,否则跳过这些错误报告语句并继续执行。
放在进程内的断言语句叫顺序断言语句,它在进程内按照顺序执行。放在进程外部的断言语句叫并行断言语句。并行断言语句本质上等同于一个进程,该进程只对条件表达式给出的所有信号敏感。
如果把断言语句单独放在一个进程里面,则该进程称为断言进程。断言进程只能放在结构体里面,且不对任何信号进行赋值操作。下例就是一个断言进程语句。
【例】 使用 ASSERT 语句设定一个判断条件,以便对仿真的某个结果或值做出响应
……
process(q)
begin
assert(q /= "1001")
report "the shifter gets the result!"
severity error;
end process;
在上面的程序中,如果信号q等于“1001”,则终止仿真,并输出 The shifter gets the result!。
4.2.2 断言语句的应用实例
下面以一个简单的实例来讲述使用断言语句来响应一个仿真的过程。
【例】 4位加减计数器的仿真。所述4位加减计数器的位数为4位,且带有 CLR 清零端。当 DIR 信号为高电平时,计数器为加1计数器;当 DIR 信号为低电平时,为减1计数器。
4 位加减计数器的设计程序如下:
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
entity counter is
port(
clk,clr,dir:in std_logic;
result :out std_logic_vector(3 downto 0)
);
end entity counter;
architecture rtl of counter is
signal tmp:std_logic_vector(3 downto 0);
begin
process(clk,clr)
begin
if(clr = '1') then
tmp <= "0000";
elsif(clk' event and clk = '1') then
if(dir = '1') then
tmp <= tmp + 1;
else
tmp <= tmp - 1;
end if;
end if;
end process;
result <= tmp;
end rtl;
4位加减计数器的仿真程序如下:
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned;
entity tb_counter is
end entity tb_counter;
atchitecture rtl of tb_counter is
signal clk :std_logic := '0';
signal clr :std_logic := '0';
signal dir :std_logic := '0';
signa result :std_logic_vector(3 downto 0);
constant clk_period:time := 40 ns;
begin
uut:entity work.counter(rtl)
port map(
clk => clk,
clr => clr,
dir => dir,
result => result
);
clk_gen:process
begin
clk <= '0';
wait for clk_period/2;
clk <= '1';
wait for clk_period/2;
end process;
TB:process
begin
clr <= '1';
dir <= '1';
wait for 20 ns;
clr <= '0';
wait for 280 ns;
dir <= '0';
wait for 320 ns;
wait;
end process;
process(result)
begin
assert(result /= "1001")
report "THe counter gets to nine!"
severity error;
end process;
end rtl;
仿真波形如下图所示:
当计数到“1001”时,在信息栏输出