FPGA功能仿真:Verilog测试夹具应用

同【本篇->仿真语法->Graphic Waveform->“Hello world”之Graphic Waveform】小节。
FPGA之道(84)功能仿真之Verilog Test Fixture_数据

仿真示例

如下是针对待仿真设计所编写的一个非常简易的Verilog Test Fixture,所有仿真代码全部书写在文件Verilog_TB.v中。仿真代码如下:

登录后复制

`timescale 1ns / 1ps						// Part 1

module Verilog_TB; 						// Part 2

reg clk; 									// Part 3
reg data;
wire c;

myAndReg DUT (							// Part 4
		.clk(clk), 
		.data(data), 
		.c(c)
	);

always begin								// Part 5
   		clk = 1'b0;
   		#50;
   		clk = 1'b1; 
   		#50;
end

always begin								// Part 6
   		data = 1'b1;
   		#100;
   		data = 1'b1; 
   		#100;
   		data = 1'b0;
   		#100;
   		data = 1'b0; 
   		#100;
   		data = 1'b1;
   		#100;
end

endmodule

示例详解

对比【程序设计篇->编程语法->Verilog基本语法】小节的相关示例和讲解,可以发现本例与它们之间有相似也有不同。有相似,是因为无论用Verilog语法编写设计文件还是仿真文件,文件的类型都是*.v;有不同,是因为设计文件中的代码必须有实际硬件电路相对应,而仿真文件中的代码无须具有现实可实现性。正因为如此,在使用Verilog编写仿真代码时,鼓励大家自由自在的运用软件程序设计中的编程思路。下面就针对上例中的Verilog Test Fixture代码进行详细讲解。
Part 1:时间单位及精度定义部分。timescale是Verilog中的一条编译指令,是用来定义时间单位和时间精度的,为后续代码指明本次仿真所使用的时间单位等信息。具体的说明请参阅【程序设计篇->编程语法->Verilog基本语法->Verilog中的编译指令】小节。
Part 2:该部分是模块声明部分,不过它除了定义了模块的名称以外,没有任何的实质性内容,原因如下:设计代码中的模块,端口声明是必不可少的一项,因为不与外界进行交互的模块即使转换成为实际的数字电路,也不具有任何实际意义(除了发热以外)。 可是仿真代码却不需要端口声明,因为仿真代码本身就是用来模拟产生FPGA设计输入信号和模拟接收FPGA设计输出信号的,如果它还需要和外界进行交互,那么谁又来模拟仿真代码需要的输入和接收仿真代码的输出呢?因此,仿真代码的模块中一般是不需要声明输入输出端口的,除非采用层次化的形式来编写仿真代码,这样非顶层的仿真模块是需要进行端口声明的。 老实说,即使在顶层仿真模块中声明了端口列表,也不会造成任何错误,只不过这样做没有什么意义罢了。
Part 3:变量声明部分。为了给DUT(DUT的概念见Part 4)输送仿真激励以及观察DUT的输出波形,必须通过信号量将DUT的端口与仿真代码连接起来,这时就需要声明一些信号量作为媒介。除此以外,还可以定义一些仿真中的常量,例如时钟周期等等。
Part 4:模块例化语句。 该部分例化的就是我们的FPGA设计,也叫待仿真设计,业内常称之为Design Under Test,简称DUT。 注意,在仿真代码面前,FPGA设计代码中顶层模块的地位已经被动摇,此时整个代码中的“老大”变成了仿真代码中的顶层模块。所以,此时要将整个FPGA设计作为一个元件进行声明,从而方便后续进行仿真调用。 本例中并没有给出FPGA设计myAndReg的具体实现代码,不过这并不影响仿真代码的编写,因为专业的功能仿真其实是把FPGA设计当做黑盒来测试的,这也是业内常说的“黑盒测试”,与此对应的称为“白盒测试”。通常对于我们FPGA开发者来说,都是做“白盒测试”,因为我们的目的是要找到并改正问题,而专业的FPGA验证工作一般都是进行“黑盒测试”,因为他们的侧重点是发现问题。 关于这两者更为详细的讨论请参阅【本篇->仿真思路->如何围绕FPGA项目展开仿真->自顶向下的仿真】小节。
Part 5:时钟信号生成程序块,用来生成输送给DUT的时钟信号,周期100ns,占空比为50%。
Part 6:数据信号生成程序块,用来生成输送给DUT的数据信号,每个数据持续100ns,本例中让数据变化沿对齐时钟下降沿(时钟上升沿为有效采样边沿)。

仿真结果

仿真结果如下图,可以看出DUT的行为符合预期。其中,由于寄存器A、B中保存的数据的不确定性,导致第二个时钟上升沿到来之前,输出都处于不定态。
FPGA之道(84)功能仿真之Verilog Test Fixture_赋值_02

继承描述语法

首先,Verilog Test Fixture也是Verilog代码,因此在【程序设计篇->编程语法->Verilog基本语法】章节中介绍的所有用于FPGA设计描述的Verilog语法都可以应用到Verilog Test Fixture的编写当中,并且语法元素的意义和用法不变。其次,Verilog Test Fixture中的代码并不需要有实际的数字电路与之相对应,即便有这样的数字电路存在,我们也没有必要知道,因为这些代码仅仅用于计算机的仿真,而不会对应到最终的FPGA芯片实现,因此Verilog Test Fixture中还支持很多FPGA设计描述所不支持的语法元素。
综上所述,要想学习Verilog Test Fixture的语法,必须先要学习Verilog的设计描述语法。这也是为什么大部分以Verilog作为熟练FPGA编程语言的开发者们,更加喜欢使用Verilog Test Fixture来对FPGA设计进行仿真,因为上手容易。

时间相关语法

时间对于仿真来说是非常重要的,本小节将介绍Verilog中一些和时间相关的语法。

系统时间单位及精度设定

在Verilog语法体系中,是不支持直接使用“数字+时间单位”的形式来表示仿真时间参数的。例如,如果仿真代码中有“500ns”这样的数据,仿真器是无法识别“ns”的意义的。因此,在Verilog Test Fixture的代码中,凡是时间都直接用数字表示,例如“500”。可是这个“500”不带任何的时间单位,所以它到底代表着500ns、500us还是500s,我们就不得而知。所以,在Verilog Test Fixture仿真代码的最开始,先使用timescale关键字,定义好系统时间单位及精度,这样后续的数字对应的时间意义就会明确。timescale又叫时标指令,是用来定义时间单位和时间精度的,它的语法如下:

登录后复制

`timescale <number0> <unit0> / <number1> <unit1>
	具体的例子和说明请参阅
	【程序设计篇->编程语法->Verilog基本语法->Verilog中的编译指令】小节。
	注意,在本章节——【Verilog Test Fixture】——后续的所有示例中,
	默认所有代码中的时间规格和精度都被设定为:
	`timescale 1ns / 1ps

延时等待语法

本小节将为大家介绍Verilog Test Fixture中一些基本的延时等待语法。

有限等待语句

#<delay_value>;
用于串行执行的语句结构中,表示程序执行到这条语句的时候需要等待<delay_value>时间后才能执行下一条语句,例如:
data = 1’b1; // 开始执行时间 t = 0ns;
#100; // 开始执行时间 t = 0ns;
data = 1’b0; // 开始执行时间 t = 100ns;
如果上述语句是从仿真0时刻开始顺序执行的话,那么第三条语句的执行时间是仿真开始后100ns的时候。注意,仿真器每执行一条语句都会消耗一定的物理时间(因为CPU有指令周期等等原因),但是不一定会消耗仿真时间,如上例中的第二条语句的开始执行时间仍为仿真0ns时刻。这也是为什么对FPGA设计进行1秒钟的仿真常常需要消耗计算机好几个小时甚至更长时间的原因。

无限等待语句

Verilog中没有专门的无限等待语句,这主要是因为Verilog中有initial程序块(将在后续小节中做详细讲解),不过如果真的需要无限等待这样一个功能的话,可以利用无限循环来构建一个类似的功能,例如:

登录后复制

	forever begin
      #1000;
end

变换等待语句

登录后复制

@(<signal>);

用于串行执行的语句结构中,表示当程序执行到该语句后就开始等待,直到信号上发生变化时才停止等待,开始执行后续语句。其功能类似于组合逻辑always中的敏感量表。

边沿等待语句

登录后复制

@(negedge <signal>); // 信号下降沿等待语句
@(posedge <signal>); // 信号上升沿等待语句

边沿等待语句共有两种,一种针对信号下降沿,另一种针对信号上升沿,如上所示。
边沿等待语句用于串行执行的语句结构中,表示当程序执行到该语句后就开始等待,直到信号上发生相应边沿事件时才停止等待,开始执行后续语句。例如,我们可以利用边沿等待语句来模仿寄存器的行为:

登录后复制

always
begin
	@(posedge <signal>);
	Q <= D;
end

条件等待语句

登录后复制

wait (<signal> == <value>);

用于串行执行的语句结构中,表示当程序执行到该语句后就开始等待,直到 == 为真时才停止等待,开始执行后续语句。

赋值等待语句

Verilog中有三种赋值等待语句,分别介绍如下:

连续赋值等待语句

登录后复制

assign #<delay_value> <signal_a> = <signal_b>;

这是一条并行语句,表示将<signal_b>的值延迟<delay_value>时间后赋给<signal_a>,从波形图上来看,<signal_a>与<signal_b>的波形完全一摸一样,只不过相位上滞后<delay_value>时间。我们也可以利用连续赋值等待语句来模拟FPGA中物理连线的线延迟时间参数,例如:

登录后复制

wire #25 ttt = clk;

从仿真波形图来看,两者之间的关系为:
FPGA之道(84)功能仿真之Verilog Test Fixture_非阻塞_03

阻塞赋值等待语句

阻塞赋值主要用于程序块内部的串行语句中,用“=”表示,而阻塞赋值等待语句有两种写法,分别介绍如下:
写法一:

登录后复制

#<delay_value> <signal> = <value>;

它表示当程序执行到该语句时,开始等待,等够<delay_value>时间后,将值赋给,因此,该写法等同于一条有限等待语句加上一条阻塞赋值语句。例如,以下两个always程序块中生成的信号aaa和bbb波形是一摸一样的。

登录后复制

	always begin // example 1
    	#50 aaa = 1'b0;
    	#100 aaa = 1'b1;
end
always begin // example 2
    	#50;
    	bbb = 1'b0;
    	#100
    	bbb = 1'b1;
end




登录后复制
写法二:




登录后复制
<signal> = #<delay_value> <value>;

它表示的值则需要在该语句执行后等待<delay_value>时间后才会赋给,由于阻塞赋值的概念就是必须赋值完成才执行后续语句,程序执行到该条语句后也会等待<delay_value>时间后才开始执行下一条指令。因此,关于阻塞赋值等待语句两种写法的意义虽然略有不同,但功能是完全一样的,例如,以下always程序块中生成的信号ccc波形与前例中的信号aaa、bbb是一摸一样的。

登录后复制

always begin // example 3
    	ccc = #50 1'b0;
    	ccc = #100 1'b1;
end

最后,附上上述三个always输出的信号波形图,可见结果一摸一样。
FPGA之道(84)功能仿真之Verilog Test Fixture_程序块_04

非阻塞赋值等待语句

非阻塞赋值也主要用于程序块内部的串行语句中,用“<=”表示,非阻塞赋值等待语句也有两种写法,但功能却不一样,分别介绍如下:
写法一:

登录后复制

#<delay_value> <signal> <= <value>;

非阻塞赋值等待语句的这种写法与阻塞赋值等待语句的写法和功能完全一样,表示当程序执行到该语句时,开始等待,等够<delay_value>时间后,将值赋给,因此,该写法等同于一条有限等待语句加上一条阻塞或非阻塞赋值语句。例如,以下always程序块中生成的信号ddd和前两例中的信号aaa、bbb、ccc波形是一摸一样的。
always begin // example 4
#50 ddd <= 1’b0;
#100 ddd <= 1’b1;
end
写法二:

登录后复制

<signal> <= #<delay_value> <value>;

虽然同为非阻塞赋值等待语句,但当程序执行到该语句时,并不进行丝毫的等待,而是立马去执行后续语句,而的值则需要在该语句执行后等待<delay_value>时间后才会赋给。例如:

登录后复制

	data <= #50 1'b1; 		// 开始执行时间 t = 0ns,赋值生效时间 t = 50ns;
    #100;				// 开始执行时间 t = 0ns;
data <= #50 1'b0;		// 开始执行时间 t = 100ns,赋值生效时间 t = 150ns;

因此非阻塞赋值等待语句的写法二本身是不消耗仿真时间的,如果always内部一不小心只含有这类语句,将会进入仿真死循环,导致仿真器不停地工作但仿真时间却停滞不前。顺便提一下,非阻塞赋值等待语句的写法二,才是最贴近于寄存器真实行为的仿真语句,也是仿真器在时序仿真中最常用的“伎俩”。

时钟激励语法

本小节将为大家介绍Verilog Test Fixture中一些基本的时钟激励语法。

占空比50%时钟产生方法

方法一:利用always程序块,语法如下:

登录后复制

always begin
<clock> = 1'b0;
#<PERIOD/2>;
<clock> = 1'b1;
#<PERIOD/2>;
end

登录后复制

reg <clock> = 1'b0;
always begin
#<PERIOD/2>;
<clock> = ~ <clock>;
end

方法二:使用initial程序块和forever循环语句,语法如下:

登录后复制

initial begin
<clock> = 1'b0;
#<PERIOD/2>;
forever
#<PERIOD/2> <clock> = ~<clock>;
end

高、低电平参数时钟产生法

已知高、低电平时间参数时,时钟激励的产生方法。
方法一:使用always程序块,语法如下:

登录后复制

always begin
<clock> = 1'b0;
#<LOW_TIME>;
<clock> = 1'b1;
#<HIGH_TIME>;
end

方法二:使用initial程序块和forever循环语句,语法如下:

登录后复制

initial begin
forever begin
<clock> = 1'b0;
#<LOW_TIME>;
<clock> = 1'b1;
#<HIGH_TIME>;
		end
end

占空比、周期参数时钟产生法

已知占空比、周期时间参数时,时钟激励的产生方法。
方法一:使用always程序块,语法如下:

登录后复制

always begin
<clock> = 1'b0;
#(<PERIOD> - (<PERIOD> * <DUTY_CYCLE>));
<clock> = 1'b1;
#(<PERIOD> * <DUTY_CYCLE>);
end

方法二:使用initial程序块和forever循环语句,语法如下:

登录后复制

initial begin
forever begin
<clock> = 1'b0;
#(<PERIOD> - (<PERIOD> * <DUTY_CYCLE>));
<clock> = 1'b1;
#(<PERIOD> * <DUTY_CYCLE>);
		end
end

时钟相位调整法

有时候,我们并不希望时钟信号的变化沿总是与仿真0时刻重合。因此需要在前述方法的基础上进行一些修改,例如,有一时钟信号如下:

登录后复制

always begin
clk = 1'b0;
#50;
clk = 1'b1;
#50;
end

如果需要将其波形沿时间坐标轴向右平移20ns,有三种修改方法。
方法一:修改起始状态。代码如下:

登录后复制

always begin
clk = 1'b1;
#20;
clk = 1'b0;
#50;
clk = 1'b1;
#30;
end

方法二:利用非阻塞赋值等待语句。代码如下:

登录后复制

always begin
clk <= #20 1'b0;
#50;
clk <= #20 1'b1;
#50;
end

方法三:利用连续赋值等待语句。代码如下:

登录后复制

always begin
clkTmp = 1'b0;
#50;
clkTmp = 1'b1;
#50;
end
assign #20 clk = clkTmp;

差分时钟激励产生法

差分时钟激励的产生与单端时钟类似,只不过多了一根线而已。在编写差分时钟激励时,可以先按照要求编写出其对应的单端时钟激励作为正极性时钟端,然后参考该正极性时钟再编写一个高、低电平完全颠倒的(对于50%占空比的时钟,也相当于相位差为180度)单端时钟激励作为负极性时钟端即可。例如:

登录后复制

initial begin
CLK_P = 1'b0;
CLK_N = 1'b1;
forever
#50 {CLK_P, CLK_N} = ~{CLK_P, CLK_N};
end

登录后复制

always begin
CLK_P = 1'b0;
CLK_N = 1'b1;
#30 
CLK_P = 1'b1;
CLK_N = 1'b0;
#70;
end  

等等。

循环仿真语法

在【程序设计篇->编程语法->Verilog基本语法->Verilog的串行语句】小节中,我们介绍过Verilog语法中常用于FPGA设计描述的循环语句体for循环,并给出了一些使用建议和限制情况。不过在Verilog Test Fixture中,这些建议和限制不再需要,大家可以比较随意的使用for循环语句,就好像在C语言中使用for循环一样,因为我们只需要保证代码是软件可执行的即可,而不必再去担心代码的功能是否是FPGA硬件可实现的。
除了for循环语句外,在VHDL Test Bench中还有很多种更加灵活的循环语句体供我们编写仿真代码时使用,分别介绍如下:

while循环

登录后复制

	while (<condition>) begin
<statements>;
end

当为真时重复执行中的内容,直到某次进行条件判断时发现为假则结束循环。例如:

登录后复制

while (sel == 1'b1) begin //当sel为逻辑1时产生时钟信号
#50 clk = 1'b0;
#50 clk = 1'b1;
end

forever循环

登录后复制

	forever begin
<statements>;
end

持续不断的执行中的内容,永不停歇。例如:

登录后复制

forever begin
    #50 clk = 1'b0;
    #50 clk = 1'b1;
end

repeat循环

登录后复制

repeat (<value>) begin
<statements>;
end

持续执行中的内容次后退出,例如:

登录后复制

repeat (5) begin
@(posedge CLK);
end
相当于
@(posedge CLK);
@(posedge CLK);
@(posedge CLK);
@(posedge CLK);
@(posedge CLK);

跳出循环

如果循环执行的过程中,临时想要退出,就要使用跳出循环语句,语法如下:
disable <loop_identifier>;
其中,<loop_identifier>是待跳出的循环的标识符,因此没有标识符的循环是无法跳出的,举例如下:

登录后复制

initial
forever begin : clock_loop
#50 clk = 1'b0;
#50 clk = 1'b1;
end

always @(posedge quit)
disable clock_loop;




登录后复制
上例中,initial中的forever循环用来产生一个周期为100ns的时钟信号,该循环的标识符定义为clock_loop。在always中,一旦等待到quit信号的上升沿,则调用跳出循环语句来结束clock_loop的运行。如果forever循环没有定义标识符,则它将一直执行直到仿真受外界不可抗力(人为、电脑故障等)时才会停止。

程序块语句体

Verilog中有两种程序块语句体,即always和initial,它们分别对应循环执行和单次执行的模式,分别介绍如下:

always程序块

always程序块是循环执行的程序块,当用于FPGA设计描述时,它必须有敏感量表,但是当对于仿真代码的编写时,敏感量表则可以省略不写。这类省略敏感量表的always程序块,将会毫不停歇的从头至尾不断循环执行,因此,必须在其内部放置能够消耗仿真时间的延时等待语句,否则将会造成仿真死循环,例如:

登录后复制

always begin // 会造成仿真死循环
clk = 1'b0;
clk = 1'b1;
end

正确的写法类似如下:

登录后复制

always begin
#50 clk = 1'b0; 
#50 clk = 1'b1;
end

initial程序块

initial程序块是单次执行的程序块,当用于FPGA设计描述时——如果编译器支持的话——initial程序块的作用是为变量等进行初始化工作,此时的initial程序块将会先于所有的always程序块运行;不过当用于仿真代码的编写时,initial程序块将会和所有的always程序块同时开始执行,例如,下例中的clk1和clk2的波形输出完全一致。

登录后复制

always begin
#50 clk1 = 1'b0; 
#50 clk1 = 1'b1;
end
initial begin
	forever begin
#50 clk2 = 1'b0; 
#50 clk2 = 1'b1;
	end
end

在之前的【时间相关语法】章节中,我们说过Verilog中没有专门的无限等待语句,这主要是因为Verilog中有initial程序块,因为无限等待语句的主要作用就是终止某个程序块的继续执行,而initial的作用正是如此,帮助我们完成那些只需要被单次(或有限次)执行的仿真功能,例如系统的复位操作,例如:

登录后复制

initial begin
	rst = 1'b0;
#100 
rst = 1'b1; 
#10 
rst = 1'b0; 
end

字符显示语法

为了让波形图的可视性更上一个阶层,Verilog提供了一种方法可让波形图显示出更具阅读意义的ASCII字符,例如:

登录后复制

reg [(8*8)-1:0] state_string = "power on";
always begin    
    #50 state_string = "work";
    #100 state_string = "idle";
end

两点注意:
1、一个ASCII字符占8个bits,所以要保证state_string的bit数能够存储下最长的那个字符串。
2、在观察state_string的波形时,请将基数设置为ASCII。
仿真结果如下图:
FPGA之道(84)功能仿真之Verilog Test Fixture_sed_05

系统存储载入函数

在Verilog中定义的存储器,可以通过系统存储载入语法进行初始化。随着编译器功能的不断增强,系统存储载入语法不光可以用于仿真,也可以用于实际的FPGA设计介绍如下:

登录后复制

reg [<memory_width>] <reg_name> [<memory_depth>];
initial
//二进制存储载入语法
 $readmemb ("<file_name>", <reg_name>, <start_address>,<end_address>);
//十六进制存储载入语法
//$readmemh ("<file_name>", <reg_name>, <start_address>, <end_address>);

其中,<reg_name>为存储器变量,<file_name>是文件路径,<start_address>, <end_address>两个参数用来指定载入时文件的起始和结束地址。两个函数中,$readmemb是二进制存储载入语法、$readmemh 是十六进制载入语法,只能使用于initial程序块中。注意,<file_name>所指定的文件,只能包含二进制数据 ($readmemb)或十六进制数据($readmemh)、空白、注释和标号,其中,标号的语法为

登录后复制

@<address>;

用来显示指定数据所处的地址,其目的和注释一样,都是用来增强可读性。
例如,如果mem.txt文件与仿真代码文件位于同一个目录下,其中的内容为

登录后复制

	00	//address 0
01	//address 1
10	//address 2
11	//address 3

那么,对于如下代码:

登录后复制

reg [1:0] romA[3:0], romB[0:3];
initial begin
    $readmemb("mem.txt", romA, 0, 3);
    $readmemb("mem.txt", romB, 0, 3);
end

仿真结果为:
FPGA之道(84)功能仿真之Verilog Test Fixture_程序块_06
可见,无论存储器变量在定义的时候深度地址是从低到高还是从高到低,<reg_name>[0]中载入的都是地址<start_address>对应的数据。
对于上例,如果使用标号的话,mem.txt中的内容可以写成如下形式

登录后复制

@000 00
@001 01
@002 10
@003 11

运行结果也是相同的。

系统文件操作函数

Verilog提供了比较丰富的文件操作函数,介绍如下:

文件打开、关闭及状态语法
文件打开

语法如下:

登录后复制

	integer <file_desc>;
<file_desc> = $fopen("<file_name>", "<file_mode>");

其中<file_desc>相当于一个文件句柄,<file_name>是待打开文件的路径和名称,<file_mode>是文件操作模式,共有以下几种可选的操作模式:
“r” —— 以文本形式打开一个文件进行读取
“rb” —— 以二进制形式打开一个文件进行读取
“w” —— 以文本形式打开一个文件进行写入(如果文件已存在则先删除其中内容)
“wb” —— 以二进制形式打开一个文件进行写入(如果文件已存在则先删除其中内容)
“a” —— 以文本形式打开一个文件进行写入(写入内容追加至末尾)
“ab” —— 以二进制形式打开一个文件进行写入(写入内容追加至末尾)
“r+” ——以文本形式打开一个文件进行读、写

文件关闭

语法如下:

登录后复制

$fclose(<file_desc>);

当完成对一个文件的操作后调用该系统函数,用来释放资源。

文件状态

一、错误状态语法:
<640-bit_reg> = $ferror(<file_desc>);
用来指示最近一次文件操作的状态,每次调用都会返回一些信息用来指示最近一次文件操作是否出错。

二、位置状态语法:
<integer_reg> = $ftell(<file_desc>);
用来指示目前文件操作指针的地址,相对于文件开头而言,每次调用返回一个整型数据作为地址偏移量指示。

三、位置重设语法:
= $fseek(<file_desc>, <offset_value>, <operation_number>);
其中<offset_value>是重设位置相对于参考位置的偏移量,而<operation_number>指定参考位置,其可选项列举如下:
0 —— 以文件开头作为参考位置
1 —— 以当前文件操作位置作为参考位置
2 —— 以文件结尾作为参考位置

四、 缓冲区清除语法:

登录后复制

$fflush(<file_desc>);

意思是清除文件缓冲区,当文件以写方式打开时将缓冲区内容全部写入文件。这其实也是一种文件同步方法,由于从数据是经由计算机的缓存后才写入文件的,因此如果不执行 f f l u s h , 缓 存 和 实 际 文 件 中 的 内 容 可 能 会 有 一 些 不 一 致 , 尤 其 是 当 向 文 件 中 连 续 写 入 大 量 数 据 时 , 这 种 感 受 就 会 比 较 明 显 。 不 过 当 对 文 件 进 行 了 fflush,缓存和实际文件中的内容可能会有一些不一致,尤其是当向文件中连续写入大量数据时,这种感受就会比较明显。不过当对文件进行了 fflush,缓存和实际文件中的内容可能会有一些不一致,尤其是当向文件中连续写入大量数据时,这种感受就会比较明显。不过当对文件进行了fclose操作后,缓存中的数据都会同步到文件中,所以如果没有严格要求,是不需要执行fflush的。

五,文件末尾判断语法:

登录后复制

$feof (<file_desc>);

用来判断是否已经到了文件的末尾,如果是,返回1,否则返回0.

读文件相关函数

$fgets
语法如下:

登录后复制

	integer <integer>;
reg [8*<number_of_chars> - 1 : 0] <string_reg>;
<integer> = $fgets(<string_reg>, <file_desc>);

$fgets语法一次读取文件中的一行数据,并以string形式保存起来。其返回值如果为0,则表示文件操作中出现了错误,否则表示该行数据有多少个字符。

登录后复制

$fgetc

语法如下:

登录后复制

reg [7:0] <8-bit_reg>; 
<8-bit_reg> = $fgetc(<file_desc>);

$fgetc语法一次从文件中读取一个字符,以8bits的string形式返回,如果其返回值为-1,则表示读取到了文件末尾。

登录后复制

$fscanf

语法如下:

登录后复制

	integer <integer>;
<integer> = $fscanf(<file_desc>, "<format>", <destination_regs>);

$fscanf是最常用的一种文件读取的方法,它每次从文件中读取一个符合""的数据,然后将其存储到<destination_regs>中。
""的意义与C语言中的格式化字符串类似,在Verilog中,可供使用的格式化变量有:
%b —— 二进制数据
%h —— 十六进制数据
%d —— 十进制数据
%t —— 时间类型数据
%s —— 字符串类型数据
%c —— ASCII字符类型数据
%f —— 实数类型数据
%e —— 指数类型数据
%o —— 八进制数据
%m —— 模块层次名数据
%v —— 驱动强度类型数据
可供使用的转义字符有:
\t —— 制表符
\n —— 回车
\ —— 反斜杠
%% —— 百分号
" —— 双引号
<octal> —— ASCII码子

写文件相关函数

登录后复制

$fdisplay

语法如下:

登录后复制

$fdisplay(<file_desc>, "<format_string>", <variables_list>);

$fdisplay将"<format_string>“中的内容写入到目标文件中去并且自动在文件中进行回车,”<format_string>“的书写方法与读文件语法中的$fscanf中的”"类似,其中的格式化变量由<variables_list>中的值进行替换。<variables_list>中的元素如果多于一个,则需要用逗号隔开。

登录后复制

$fwrite

语法如下:

登录后复制

$fwrite(<file_desc>, "<format_string>", <variables_list>);

$fwrite用法几乎与$fdisplay一样,只不过它不会自动在写完信息的文件中进行回车。

登录后复制

$fstrobe

语法如下:

登录后复制

$fstrobe(<file_desc>, "<format_string>", <variables_list>);

$fstrobe用法也于$fdisplay类似,不过它需要等待队列中的所有仿真事件结束之后才会开始信息的写入。

登录后复制

$fmonitor

语法如下:

登录后复制

$fmonitor(<file_desc>, "<format_string>", <variables_list>);

$fmonitor方法是一种监测写入的方法,即如果发现<variables_list>中的某个变量发生了变化,则将"<format_string>"所代表的内容写入到文件中。与前几个文件写入语法的原理截然不同,$fmonitor类似于C语言中一个回调函数的功能,即只用在代码中显示调用一次,却可以完成多次写入。因此,为了增强可视化效果,$fmonitor也会在每次写入文件后追加一个回车操作。

文件操作示例

读文件示例

登录后复制

real number;
integer number_file;
integer i=1;

initial begin
// Open file numbers.txt for reading
      	number_file = $fopen("numbers.txt", "r");
      	// Produce error and exit if file could not be opened
      	if (number_file == 0) begin
         	$display("Error: Failed to open file, numbers.txt\nExiting Simulation.");
         	$finish;
      	end
      	// Loop while data is being read from file
		i=$fscanf(number_file, "%f", number);
      	while (i>0) begin
         	$display("i = %d", i);
         	$display("Number read from file is %f", number);
         	@(posedge clk);
i=$fscanf(number_file, "%f", number);
      	end
      	// Close out file when finished reading
      	$fclose(number_file);
      	#100;
end

如果number.txt中的内容为:
12.3 12.3 5.6
11.1 56.3
则上例运行后的输出为:

登录后复制

# i =           1
# Number read from file is 12.300000
# i =           1
# Number read from file is 12.300000
# i =           1
# Number read from file is 5.600000
# i =           1
# Number read from file is 11.100000
# i =           1
# Number read from file is 56.300000

写文件示例

登录后复制

integer outfile;
integer Data_out;

initial begin
// Open file output.dat for writing
outfile = $fopen("output.txt", "w");
// Check if file was properly opened and if not, produce error and exit      
if (outfile == 0) begin
$display("Error: File, output.dat could not be opened.\nExiting Simulation.");
$finish;
end      
// If clk changes, write monitor data to a file
$fmonitor (outfile, "Time: %t\t clk = %b", $realtime, clk);
    // Wait for 1 us and end monitoring
    #1000;      
// Close file to end monitoring
$fclose(outfile);
end




登录后复制
运行后,检查output.txt文件中的内容如下:
Time:                    0	 clk = 0

Time: 50000 clk = 1
Time: 100000 clk = 0
Time: 150000 clk = 1
Time: 200000 clk = 0
Time: 250000 clk = 1
Time: 300000 clk = 0
Time: 350000 clk = 1
Time: 400000 clk = 0
Time: 450000 clk = 1
Time: 500000 clk = 0
Time: 550000 clk = 1
Time: 600000 clk = 0
Time: 650000 clk = 1
Time: 700000 clk = 0
Time: 750000 clk = 1
Time: 800000 clk = 0
Time: 850000 clk = 1
Time: 900000 clk = 0
Time: 950000 clk = 1

系统屏幕输出函数

屏幕输出函数完全与文件操作函数中的写入函数一一对应,只不过文件写入函数的操作对象是文件,而屏幕输出函数的操作对象是显示器罢了。以下列举出屏幕输出函数的几种形式:

登录后复制

$display("<string_and/or_variables>", <functions_or_signals>);
$write ("<string_and/or_variables>", <functions_or_signals>);
$strobe ("<string_and/or_variables>", <functions_or_signals>);
$monitor("<string_and/or_variables>", <functions or signals>);

它们的意思可以完全参照对应的文件写入函数来理解,这里不再赘述。

系统随机数生成函数

在仿真的时候,为了提高仿真的充分性,有时候需要产生一些随机的激励信号。我们可以手动编写代码来产生一些伪随机的信号,不过Verilog为我们提供了丰富的随机数生成函数,这极大的方便了随机激励的生成。语法如下:

登录后复制

<reg> = $random(<seed>);

用于产生最基本的伪随机序列。

登录后复制

<reg> = $dist_uniform (<seed>, <start>, <\end>) ; 

用于以均匀概率产生位于, <\end>之间的随机序列。

登录后复制

<reg> = $dist_normal (<seed>, <mean>, <standard_deviation>) ;

用于产生均值为,方差为<standard_deviation>的正态分布随机序列。

登录后复制

<reg> = $dist_exponential (<seed>, <mean>) ; 

用于产生均值为的指数分布随机序列。

登录后复制

<reg> = $dist_poisson (<seed>, <mean>) ; 

用于产生均值为的泊松分布随机序列。

登录后复制

<reg> = $dist_chi_square (<seed>, <degree_of_freedom>) ;

用于产生自由度为<degree_of_freedom>的卡方分布随机序列。

登录后复制

<reg> = $dist_t (<seed>, <degree_of_freedom>) ; 

用于产生自由度为<degree_of_freedom>的t分布随机序列。

登录后复制

<reg> = $dist_erlang (<seed>, <k_stage>, <mean>) ;

用于产生均值为,K阶为<k_stage>的厄兰分布随机序列。

其实计算机是无法产生真正的随机信号的,例如$random产生的其实也是伪随机序列,为了防止每次仿真产生的随机序列都是一样的,使得其更加接近于真正的随机序列,Verilog提供了参数。参数其实就是伪随机序列的种子,种子只要不同,就可以产生出不同的伪随机序列,因此上述随机数生成语法中都有一个参数。
关于随机数生成语法,举例如下:

登录后复制

integer r1,r2,r3,r4,r5,r6,r7,r8,r9;
integer seed, seed2, start, \end , mean, standard_deviation, degree_of_freedom, k_stage;

initial begin
seed=1; seed2=100; start=0; \end =90; mean=5; 
standard_deviation=2; degree_of_freedom=2; k_stage=1; 
end

always begin
@(posedge clk);
r1 = $random(seed)%1000; 
r2 = $random(seed2)%1000;
r3=$dist_uniform (seed, start, \end ) ; 
r4=$dist_normal (seed, mean, standard_deviation) ;
r5=$dist_exponential (seed, mean) ;
r6=$dist_poisson (seed, mean) ;
r7=$dist_chi_square (seed, degree_of_freedom) ;
r8=$dist_t (seed, degree_of_freedom) ;
r9=$dist_erlang (seed, k_stage, mean) ; 
end

关于以上示例代码有两点需要说明:1、由于end是关键字,所以不能作为变量名,因此在前面加上反斜杠作为转义字符,关于Verilog的命名规则请参阅【程序设计篇->编程语法->Verilog基本语法->Verilog数据类型->命名规则】小节。2、虽然类似seed这样的参数都是整型,但是如果直接在这些系统函数中使用整型常量,是无法得到预期的随机序列的,例如若写成$random(100),则该随机序列将为常0。
该示例的运行结果波形图如下:
FPGA之道(84)功能仿真之Verilog Test Fixture_非阻塞_07

系统仿真时间函数

仿真时间获取语法
Verilog中共有三种获取仿真时间的系统函数,分别介绍如下:

登录后复制

$time




登录后复制
$time以64bits的整型数据来返回当前系统所处的仿真时间时刻。




登录后复制
$stime




登录后复制
$stime以32bits的整型数据来返回当前系统所处的仿真时间时刻。
这相当于$time函数返回值的低32位。




登录后复制
$realtime




登录后复制
$ realtime以实数形式来返回当前系统所处的仿真时间时刻。

时间格式设定函数

以上三种仿真时间获取函数的返回值都不太容易被方便的理解清楚,因此,需要时间格式设定函数来对其进行一些转化。时间格式设定函数的语法如下:

登录后复制

$timeformat (<unit>, <precision>, <suffix_string>, <min_field_width>);

其中,为整数,表示时间单位,例如-9表示时间单位是10的-9次方秒,即纳秒;表示精度位数,它表示在时间单位之后,允许有几位小数;<suffix_string>为单位字符串,用来让时间显示具有阅读意义;<min_field_width>,表示时间区域的最小字符宽度。
注意,时间格式设定函数只能使用在initial程序块中,例如定义微秒、纳秒、皮秒时间格式,可以使用类似下面的代码:

登录后复制

	initial begin
		$timeformat (-6, 6, " us", 13); // 微秒
   		$timeformat (-9, 3, " ns", 13); // 纳秒
$timeformat (-12, 1, " ps", 13); // 皮秒
end

而转换的方法就是利用格式化字符串中的%t变量,这样在进行系统屏幕输出或者文件写入函数等操作时,就可以输出比较直观的时间信息了。

系统仿真时间示例

登录后复制

initial begin
		$timeformat (-9, 3, " ns", 13);
   		#100;
   		$display("original time = %d",$time);
   		$display("original stime = %d",$stime);
   		$display("original realtime = %f",$realtime);
   		$display("format time = %t",$time);
   		$display("format stime = %t",$stime);
   		$display("format realtime = %t",$realtime);
   
   		$timeformat (-6, 6, " us", 13);
  		#100;
   		$display("original time = %d;",$time);
   		$display("original stime = %d;",$stime);
   		$display("original realtime = %f;",$realtime);
   		$display("format time = %t;",$time);
   		$display("format stime = %t;",$stime);
   		$display("format realtime = %t;",$realtime);
   		$stop;
end

该示例的执行结果如下:

登录后复制

# original time =                  100;
# originali stime =        100;
# original realtime = 100.000000;
# format time =    100.000 ns;
# format stime =    100.000 ns;
# format realtime =    100.000 ns;
# original time =                  200;
# originali stime =        200;
# original realtime = 200.000000;
# format time =   0.200000 us;
# format stime =   0.200000 us;
# format realtime =   0.200000 us;

系统仿真进度控制任务

Verilog中还提供了两个仿真进度控制任务,$stop$finish。其中$stop用来暂停当前仿真执行,可以通过run按钮来恢复仿真;而$finish函数则直接结束当前仿真的执行。
例如:

登录后复制

always begin
    tReg = ~ tReg;
    #100;
    $stop;
end

可以通过不断的点击run按钮来一步步往下仿真,而

登录后复制

always begin
    tReg = ~ tReg;
    #100;
    $finish;
end

则直接退出仿真程序。

免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删

QR Code
微信扫一扫,欢迎咨询~

联系我们
武汉格发信息技术有限公司
湖北省武汉市经开区科技园西路6号103孵化器
电话:155-2731-8020 座机:027-59821821
邮件:tanzw@gofarlic.com
Copyright © 2023 Gofarsoft Co.,Ltd. 保留所有权利
遇到许可问题?该如何解决!?
评估许可证实际采购量? 
不清楚软件许可证使用数据? 
收到软件厂商律师函!?  
想要少购买点许可证,节省费用? 
收到软件厂商侵权通告!?  
有正版license,但许可证不够用,需要新购? 
联系方式 155-2731-8020
预留信息,一起解决您的问题
* 姓名:
* 手机:

* 公司名称:

姓名不为空

手机不正确

公司不为空