Verilog学习笔记(4):仿真验证与Testbench编写

这篇具有很好参考价值的文章主要介绍了Verilog学习笔记(4):仿真验证与Testbench编写。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


1.Verilog电路仿真和验证概述

仿真,也叫模拟,是通过使用EDA仿真工具,通过输入测试信号,比对输出信号(波形、文本或者VCD文件)和期望值,来确认是否得到与期望所一致的正确的设计结果,验证设计的正确性。
验证是一个证明设计思路如何实现,保证设计在功能上正确的一个过程。

验证在Verilog设计的整个流程中分为4个阶段:

  • 阶段1:功能验证;
  • 阶段2:综合后验证;
  • 阶段3:时序验证;
  • 阶段4:板级验证。

2.Verilog测试程序设计基础

2.1Testbench及其结构

在仿真的时候Testbench用来产生测试激励给待验证设计( Design Under Verification, DUV),或者称为待测设计(Design UnderTest, DUT) 。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
测试程序的一般结构:
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
Testbench是一个测试平台,信号集成在模块内部,没有输入输出。在Testbench模块内,例化待测设计的顶层模块,并把测试行为的代码封装在内,直接对待测系统提供测试激励。

例:T触发器测试程序

module TFF_tb;
reg clk, rst_n, T;
wire data_out;
TFF U1(.data_out(data_out), .T(T), .clk(clk), .rst_n(rst_n)); //对被测模块实例化
	always
		#5 clk = ~clk;
	Initial 
		begin
			clk = 0;
			#3 rst_n = 0;
			#5 rst_n = 1;
			T = 1;
			#30 T = 0;
			#20 T = 1;
		end
	Initial
		begin
			$monitor($time,"T=%b, clk=%b,rst_n=%b,data_out=%b", T, clk, rst_n,data_out);
		end
endmodule

T触发器的波形仿真和文本输出:
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
从下图可以清晰地看出Testbench的主要功能:
(1)为DUT提供激励信号0。
(2)正确实例化DUT。
(3)将仿真数据显示在终端或者存为文件,也可以显示在波形窗口中以供分析检查。
(4)复杂设计可以使用EDA工具,或者通过用户接口自动比较仿真结果与理想值,实现结果的自动检查。

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
在编写Testbench时需要注意的问题:
(1)testbench代码不需要可综合
Testbench代码只是硬件行为描述不是硬件设计。
(2)行为级描述效率高
Verilog HDL语言具备5个描述层次,分别为开关级、门级、RTL级、算法级和系统级。
(3)掌握结构化、程式化的描述方式
结构化的描述有利于设计维护,可通过initial、always以及assign语句将不同的测试激励划分开来。
一般不要将所有的测试都放在一个语句块中

2.2测试平台举例

测试平台需要产生时钟信号、复位信号和一系列的仿真向量,观察DUT的响应,确认仿真结果。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
(1)组合逻辑电路仿真环境搭建
全加器真值表:
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

module adder1 (a, b, ci, so, co); 
input a, b, ci ; 
output so, co ; 
assign { co , so } = a + b + ci ; 
endmodule

根据全加器的真值表编写的全加器测试程序如下:

module adder1_tb ;
 wire so, co;
 reg a, b, ci; 
 adder1 U1(a, b, ci, so, co); //模块例化
 initial //测试信号产生 
 	begin
 		 a = 0; b = 0; ci = 0; 
 		 #20 a = 0; b = 0; ci = 1; 
 		 #20 a = 0; b = 1; ci = 0; 
 		 #20 a = 0; b = 1; ci = 1; 
 		 #20 a = 1; b = 0; ci = 0;
 		 #20 a = 1; b = 0; ci = 1; 
 		 #20 a = 1; b = 1; ci = 0; 
 		 #20 a = 1; b = 1; ci = 1; 
 		 #200 $finish; 
	end 
endmodule

全加器的输入a、b和ci定义为reg型变量,把输出so和co定义为wire型变量;用模块例化语句
"adder1 U1(a,b,ci,so,co);"把全加器设计电路例化到测试仿真环境中;用initial块语句改变输入的变化并生成测试条件,输入的变化语句完全根据全加器的真值表编写
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
(2)时序逻辑电路仿真环境的搭建
在于时序逻辑电路仿真环境中,需要考虑时序、定时信息和全局复位、置位等信号,并定义这些信号。

用Verilog编写的十进制加法计数器:

module cnt10(cIk ,rst, ena, q, cout); 
input clk,rst,ena; 
output [3:0] q;
output cout; 
reg [3:0] q;
always@(posedge clk or posedge rst) 
	begin
		if(rst)q=4’b0000;
			else if(ena)
				begin
					if(q<9)
						q=q+1;
					else 
						q=0;
				end
	end
assign cout=q[3]&q[0];
endmodule

测试程序代码:

module cnt10_tb; 
reg clk, rst, ena; 
wire [3:0] q;
wire cout;
cnt10 U1(clk ,rst, ena, q, cout); 
always #50 clk = ~clk; 
initial 
	begin
		clk=0;rst=0;ena=1; 
		#1200 rst=1;
		#120 rst=0;
		#2000 ena=0;
		#200 ena=1;
		#20000 $finish; 
	end
endmodule

实例化语句"cnt10 U1(clk,rst,ena,q,cout);“把十进制计数模块例化到仿真环境中;
在always中用语句”#50 clk=~clk;"产生周期为100(标准时间单位)的时钟方波;
用initial块生成复位信号rst和使能控制信号ena的测试条件。
测试结果如图:

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

2.3Verilog仿真结果确认

(1)直接观察波形
通过直接观察各信号波形的输出,比较测试值和期望值的大小,来确定仿真结果的正确性。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

(2)打印文本输出法

module adder1_tb; 
wire so,co; 
reg a,b,ci; 
adderl U1(a,b,ci,so,co);
initial
	begin
		a=0;b=0;ci=0;
		#20 a=0;b=0;ci=1;
		#20 a=0;b=1 ;ci=0;
		#20 a=0;b=1 ;ci=1;
		#20 a=1 ;b=0;ci=0;
		#20 a=1 ;b=0;ci=1;
		#20 a=1 ;b=1 ;ci=0;
		#20 a=1;b=1;ci=1;
		#200 $finish; 
	end
	$monitor($time, "%b %b %b -> %b %b”,a, b, ci, so, co);
endmodule

输出结果:

0 0 0 0 -> 0 0
20 0 0 1 -> 1 0
40 0 1 0 -> 1 0
60 0 1 1 -> 0 1
80 1 0 0-> 1 0

系统任务打印任务:
$display:直接输出到标准输出设备;
$monitor:监控参数变化;
$fdisplay:输出到文件等。

(3)自动检查仿真结果
自动检查仿真结果是通过在设计代码中的关键节点添加断言监控器,形成对电路逻辑综合的注释或是对设计特点的说明,以提高设计模块的观察性。

(4)使用VCD文件
Verilog提供一系列系统任务用于记录信号值变化保存到标准的VCD(Value Change Dump)格式数据库中。VCD文件是一种标准格式的波形记录文件,只记录发生变化的波形。

2.4Verilog仿真效率

因为要通过串行软件代码完成并行语义的转化,Verilog行为级仿真代码的执行时间比较长。

提高Verilog HDL代码的仿真代码执行时间:
(1)减小层次结构
仿真代码的层次越少,执行时间就越短。
(2) 减少门级代码的使用
由于门级建模属于结构级建模,建议仿真代码尽量使用行为级语句,建模层次越抽象,执行时间就越短。
(3) 仿真精度越高,效率越低
计时单位值与计时精度值的差距越大,则模拟时间越长。`timescale仿真时间标度。
(4) 进程越少,效率越高
代码中的语句块越少仿真越快,这是因为仿真器在不同进程之间进行切换也需要时间。
(5) 减少仿真器的输出显小
Verilog语言包含一些系统任务,可以在仿真器的控制台显示窗口输出一些提示信息,但会降低仿真器的执行效率。

3.与仿真相关的系统任务

3.1$display和 $write

语法格式:
$display("<format_specifiers>", <signal1 , signal2, ..., signaln>);
$write("<format_specifiers>", <signal1, signa12, ..., signaln>);

"<format_specifiers>"通常称为“格式控制”
"<signal1, signa12, …, signaln>"为”信号输出列表“
$display自动地在输出后进行换行
$write输出特定信息是不换行

输出格式说明,由“%"和格式字符组成,其作用是将输出的数据转换成指定的格式输出。
常用的几种输出格式如下表:
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
一些特殊的字符可以通过表中的转换序列来输出:
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
例:$display和 $write语句

module disp_tb; 
reg[31 :0] rval; 
pulldown(pd);
initial
	begin
		rval=101;
		$display("\\\t%%\n\"\123”);
		$display("rval=%h hex %d decimal" rval, rval);
		$display("rval=%o otal %b binary", rval, rval);
		$display("rval has %c ascii character value",rval);
		$display("pd strength value is %v", pd);
		$display("current scope is %m");
		$display("%s is ascii value for 101",101);
		$write("simulation time is");
		$write("%t\n", $time); 
	end
endmodule

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
在$display中,输出列表中数据的显示宽度是自动按照输出格式进行调整的,总是用表达式的最大值所占的位数来显示表达式的当前值。

3.2$monitor与 $stobe

$monitor与$stobe都提供了监控和输出参数列表中字符或变量的值的功能
(1)$monitor语法格式:
$monitor("<format_specifiers>", <signal1, signal2, ..., signaln>);
任务$monitor提供了监控和输出参数列表中的表达式或变量值的功能。每当参数列表中变量或表达式的值发生变化时,整个参数列表中变量或表达式的值都将输出显示。
例如:$monitor($time, , “rxd=%b txd=%b”,rxd txd);
注意在上面的语句中,", ,"代表一个空参数。空参数在输出时显示为空格。

$monitoron和$monitoroff任务的作用是通过打开和关闭监控标志来控制监控任务$monitor的启动和停止,这样使得程序员可以很容易的控制$monitor何时发生。
$monitor与$display的不同处在于$monitor往往在initial块中调用,只要不调用$monitoroff,$monitor便不间断地对所设定的信号进行监视。

例:$monitor系统任务的应用

module monitor_tb;
	integer a,b;
	initial 
		begin
			a = 2;
			b = 2;
			forever 
				begin
					#5 a = a + b;
					#5 b = a - 1;
				end
		end
	initial $monitor($time,"a = %d,b = %d",a, b);
endmodule

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
(2)$strobe语法格式:
$strobe(<functions_or_signals>);
$strobe("<string_and/or_variables>",<functions_or_signals>);
探测任务用于在某时刻所有时间处理完后,在这个时间步的结尾输出一行格式化的文本。常用的系统任务如下:
$stobe:在所有时间处理完后,以十进制格式输出一行格式化的文本;
$strobeb:在所有时间处理完后,以二进制格式输出一行格式化的文本;
$strobeo:在所有时间处理完后,以八进制格式输出一行格式化的文本;
$strobeh:在所有时间处理完后,以十六进制格式输出一行格式化的文本。

$strobe任务在被调用的时刻所有的赋值语句都完成了,才输出相应的文字信息。$strobe任务提供了另一种数据显示机制,可以保证数据只在所有赋值语句被执行完毕后才被显示。

3.3$time和 $reltime

  • 用这两个时间系统函数可以得到当前的仿真时刻,不同的是,$time函数以64位整数值的形式返回仿真时间,而 $realtime函数则以实数型数据返回仿真时间。
    (1)系统函数 $time
    例:$time系统任务的应用实例
`timescale 1ns/1ns
module time_tb;
	reg ts;
	parameter dalay = 2;
	initial 
		begin
			#delay ts = 1;
			#delay ts = 0;
			#delay ts = 1;
			#delay ts = 0;
		end
	initial
		$monitor($time,,"ts = %b",ts);
	$time
endmodule

输出结果:

0 ts = x
3 ts = 1
5 ts = 0
8 ts = 1
10 ts = 0

(2) $realtime系统函数

$realtime返回的时间数字是一个实型数,该数字也是以时间尺度为基准的。
例:$realtime 系统任务的应用实例:

`timescalel 1ns/1ns 
module realtime_tb; 
reg set; 
parameter p=2; 
	initial
		begin
			$monitor($realtime, ,"set=b%" ,set); 
			#p set=0;
			#p set=1;
		end
endmodule

输出结果:

0 set=x
2 set=0
4 set=1

3.4$finish和 $stop

系统任务$finish和$stop是用于对仿真过程进行控制,分别表示结束仿真和中断仿真。其语法格式:
$finish;
$fimsh(n);
$stop;
$stop(n);
其中,n是$finish和$stop的参数,n可以取0、1或2几个值,分别表示如下含义,如下表所示。

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
$finish的作用是退出仿真器,返回主操作系统,也就是结束仿真过程。任务$finish可以带参数,根据参数的值输出不同的特征信息。如果不带参数,默认$finish的参数值为1。
$stop任务的作用是把EDA工具(例如仿真器)置成暂停模式,在仿真环境下给出一个交互式的命令提示符,将控制权交给用户。这个任务可以带有参数表达式。根据参数值(0,1或2)的不同,输出不同的信息。参数值越大,输出的信息越多。

$finish的实例

module finish_tb;
	integer a,b;
	initial 
		begin
			a = 2;
			b = 4;
			forever
			 begin
			 	#5 a = a + b;
			 	#5 b = a - 1;
			 end
		end
	initial #40 $finish; //程序执行到40个时间单位时退出仿真器
	initial 
		begin
			$monitor($time,"a = %d,b = %d",a,b);
		end
endmodule

$stop的实例

module stop_tb;
	integer a,b;
	initial 
		begin
			a = 2;
			b = 4;
			forever
			 begin
			 	#5 a = a + b;
			 	#5 b = a - 1;
			 end
		end
	initial #40 $stop; //程序执行到40个时间单位时停止仿真
	initial 
		begin
			$monitor($time,"a = %d,b = %d",a,b);
		end
endmodule

3.5$readmemh和 $readmem

在Verilog程序中有两个系统任务$readmemh和$readmem用来从文件读取数据到存储器中。在两个系统任务可以在仿真的任何时刻被执行使用,其语法格式共有一下:
(1) $readmemb(“<file_name>”,<memory_name>);
(2) $readmemb(“<file_name>”,<memory_name>,<start_addr>);
(3) $readmemb(“<file_name>”,<memory_name>,<start_addr>,<finish_addr>);
(4) $readmemh(“<file_name>” ,<memory_name>);
(5) $readmemh(“<file_name>”,<memory_name>,<start_addr>);
(6) $readmemh(“<file_name>”,<memory_name>,<start_addr>,<finish_addr>);

例:$readmemh和$readmem系统任务实例

module read_mem_tb;
	reg [7:0] memory_b [0:7];
	reg [7:0] memory_h [0:31];
	integer i;
	initial
		begin
			$readmemb("init_b.txt",memory_b); //把数据文件init_b.txt读入存储器中的指定地址
			$readmemh("init_h.txt",memory_h);
			for(i = 0;i < 8; i=i+1)
				begin
					$display("memory_b[%0d] = %b",i.memory_b[i]);
					$display("memory_h[%0h] = %h",i.memory_h[i]);
				end
		end
endmodule

文件init_b.txt和init_h.txt包含初始化数据。用@<address>在数据文件中指定地址。其中,"init_b.txt"指定二进制数据从第二位地址开始写入;而"init_h.txt"指定十六进制数据从地一位地址写入。样本文件如下:

init_b.txt:

@002 
11111111 01010101
00000000 10101010
@001
1111zzzz 00001111

init_h.txt:

@001
00000000000000000000000000000011
00000000000000000000000000000111
00000000000000000000000000001111
00000000000000000000000000011111

3.6$random

$random是产生随机数的系统函数,每次调用该函数将返回一个32位的随机数,该随机数是一个带符号的整数。语法格式:
$random%<number>;
这个系统函数提供了一个产生随机数的手段。当函数被调用时返回一个32bit 的随机数。它是一个带符号的整形数。
$random一般的用法是:$ramdom%b,其中b>0,它给出了一个范围在(-b + 1):(b - 1)中的随机数。

实例:

`timescale 1ns/1ns 
module random_pulse(dout); 
output [9:0] dout;
reg dout;
integer delay1,delay2,k; 
initial
	begin
		#10 dout=0;
		for(k=0;k<100;k=k+1)
			begin
				delay1 = 20 * ({$random} % 6);
				delay2 = 20 * (1 +{$random} % 3); 
				#delay1 dout= 1 << ({$random}%10);
				$delay2 dout = 0;
			end
	end
endmodule

4.信号时间赋值语句

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

4.1时间延迟的语法说明

延迟语句用于对各条语句的执行时间进行控制,从而决速满足用户的时序要求。Verilog语言中延时控制的语法格式有两类:
(1)#<延迟时间>行为语句;
(2)#<延迟时间>;
其中,符号"#"是延迟控制的关键字符,<延迟时间>可以是直接指定的延迟时间量,并以多少个仿真时间单位的形式给出。在仿真过程中,所有时延都根据时间单位定义。

下面是带时延的赋值语句示例。
#2 Sum = A ^ B; //#2指定2个时间单位后,将A异或B的值赋值给Sum

根据时间控制部分在过程赋值语句中出现的位置,可以把过程赋值语句中的时间控制方式分为外部时间控制方式和内部时间控制方式。
(1)外部时间控制方式是时间控制出现在整个过程赋值语句的最左端,也就是出现赋值目标变量的左边的时间控制方式,其语法结构如:
#5 a=b;
在仿真执行时就相当于如下几条语句:

initial
	begin
		#5;
		a = b;
	end

(2)内部时间控制方式是过程赋值语句中的时间控制部分还可以出现在“赋值操作符"和“赋值表达式"之间的时间控制方式。其语法结构如:
a=#5b;
其中时间控制部分"#5"就出现在赋值操作符":"和赋值表达式"b"的中间,因此在这条过程赋值语句内带有内部时间控制方式的时间控制。它在执行时就相当于如下几条语句的执行:

initial
	begin
		temp=b; //先求b的值
		#5;
		a=temp;
    end

4.2时间延迟的描述形式

此处时间延迟的描述形式是指延时控制的描述形式,其分为串行延迟控制、并行延迟控制、阻塞式延迟控制和非阻塞式延迟控制四种形式。以实现两组不同波形的信号为例(如图q0_out和q1_out),说明四种不同时间延迟的描述形式。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
(1)串行延迟控制
串行延迟控制是最为常见的信号延迟控制,它是由begin、end过程块加上延迟赋值语句构成,其中延迟赋值语句可以为外部时间控制方式,也可以为内部时间控制方式。在<延迟时间>之后也可根据情况来确定是否执行相应的行为语句。
在<延迟时间>后面有相应的行为语句,则仿真进程遇到这条带有延迟控制的行为语句后并不立即执行行为语句指定的操作,而是要延迟等待到“<延迟时间> ’ 所指定的时间量过去后才真正开始执行行为语句指定的操作。

例:Verilog串行延迟控制方式设计上图的信号

`timescale 1ns/1ns 
module serial_delay(q0_out, q1_out); 
output q0_out, q1 _out;
reg q0_out, q1_out;
initial
	begin
		q0_out=1'b0;
		#50 q0_out=l'b1;
		#100 q0_out=1'b0;
		#100 q0_out=1'b1;
		#50 q0_out=1'b0;
		#100 q0_out=1'b1;
		#50 q0_out=1'b0;
		#50 q0_out=1'b1;
		#50 q0_out=1'b0; 
	end
initial
	begin
		q1_out=1'b0;
		#100 q1_out=1'b1;
		#100 q1_out=1'b0;
		#50 q1_out=1'b1;
		#100 q1_out=1'b0:
		#50 q1_out=1'b1;
		#100 q1_out=1'b0;
		#50 q1_out=1'b1:
		#50 q1_out=1'b0; 
	end
endmodule

(2)并行延迟控制
并行延迟控制方式是通过fork-join过程块加上延迟赋值语句构成,其中延迟赋值语句同串行延迟控制方式一样,既可以是外部时间控制方式,也可以是内部时间控制方式。在<延迟时间>之后也可根据情况来确定是否执行相应的行力语句。
在<延迟时间>后面有相应的行为语句,则仿真进程遇到这条带有延迟控制的行为语句后并不立即执行行为语句指定的操作,而是要延迟等待到"<延迟时间>"所指定的时间量过去后才真正开始执行行为语句指定的操作。但并行延迟控制方式与串行延迟控制方式不同在于并行延迟控制方式中的多条延迟语句时并行执行的,并不需要等待上一条语句的执行完成才开始执行当前的语句。

例:Verilog并行延迟控制方式设计上例波形图

`timescale 1ns/1ns 
module parallel_delay (q0_out,q1_out); 
output q0_out,q1_out; 
reg q0_out,q1_out; 
initial 
	fork
		q0_out=1'b0;
		#50 q0_out=1'b1;
		#150 q0_out=1'b0;
		#250 q0_out=1'b1;
		#300 q0_out=1'b0;
		#400 q0_out=1'b1;
		#450 q0_out=1'b0;
		#500 q0_out=1'b1;
		#600 q0_out=1'b0;
	join
initial 
	fork 
		ql_out=1'b0;
		#100 q1_out=1'b1;
		#200 q1_out=1'b0;
		#250 q1_out=1'b1;
		#350 q1_out=1'b0;
		#400 q1_out=1'b1;
		#500 q1_out=1'b0;
		#550 q1_out=1'b1;
		#600 q1_out=1'b0;
	join
endmodule

(3)阻塞式延迟控制
以赋值操作符"=“来标识的赋值操作称为“阻塞式过程赋值”。阻塞式延迟控制是在阻塞式过程赋值基础上带有延时控制的情况,例如:

initial
	begin
		a = 0;
		a = #5 1;
		a = #10 0;
		a = #15 1;
	end

各条阻塞式赋值语句将依次得到执行,并且在第一条语句所指定的赋值操作没有完成之前第二条语句不会开始执行。因此在仿真进程开始时刻将"0"值赋给a,此条赋值语句完成之后才开始执行第二条赋值语句;在完成第一条赋值语句之后,延迟5个时间单位将"1"赋值给a;同理第三条赋值语句是在第二条赋值语句完成之后延迟10个时间单位才开始执行,将"0"赋值给a;最后一条赋值语句是在前三条语句都完成的时刻,延迟15个时间单位,将"1"赋值给a。下图给出了上例中信号a的波形。上述两例都采用的是阻塞式赋值语句。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
4)非阻塞式延迟控制
以赋值操作符"<=“来标识的赋值操作称为"非阻塞式过程赋值”。非阻塞式延迟控制是在非阻塞式过程赋值基础上带有延时控制的情况。如下例所示:

initial 
	begin
		a <= 0;
		a <= #5 1;
		a <= #10 0;
		a <= #15 1;
	end

在上例中各条非阻塞式赋值语句均以并行方式执行,虽然执行语句在begin-end串行块中,但其执行方式与并行延迟控制方式一致,在仿真进程开始时刻同时执行四条延迟赋值语句。在仿真进程开始时,将"0"值赋值给a;在离仿真开始时刻5个时间单位时,将"1"值赋值给a;在离仿真开始时刻10个时间单位时,将"0"值赋值给a;最后在离仿真开始时刻15个时间单位时,将"1"值赋值给a。下图给出了上例中信号a的波形。verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
例:Verilog非阻塞延迟控制方式设计

`timescale 1ns/1ns 
module non_blocking_delay(q0_out,q1_out);
output q0_out,q1_out;
reg q0_out,q1_out; 
initial
	begin
		q0_out <= 1'b0; 
		q0.out <= #50 1'b1; 
		q0_out <= #150 1'b0; 
		q0_out <= #250 1'b1;
		q0_out <= #300 1'b0; 
	 	q0_out <= #400 1'b1; 
		q0_out <= #450 1'b0;
	  	q0_out <= #500 1'b1;
	 	q0_out <= #600 1'b0;
	end
initial
	begin 
		q1_out <= 1'b0; 
		q1_out <= #100 1'b1; 
		q1_out <= #200 1'b0;
		q1_out <= #250 1'b1; 
		q1_out <= #350 1'b0; 
		q1_out <= #400 1'b1; 
		q1_out  <= #500 1'b0; 
		q1_out <= #550 1'b1; 
		q1_out <= #600 1'b0;
	end
endmodule

4.3边沿触发时间控制

边沿触发事件控制的语法格式可以为如下四种形式:
形式1:@(<事件表达式>)行为语句;
形式2:@(<事件表达式>);
形式3:@(<事件表达式1>or<事件表达式2>or…or<事件表达式n>) 行为语句;
形式4:@(<事件表达式1>or<事件表达式2>or…or<事件表达式n>);

1,事件表达式
 在事件表达式中,可以以三种形式出现:
   形式1:<信号名>
   形式2:posedge<信号名>
   形式3:negedge<信号名>

 其中,"<信号名>“可以是任何数据类型的标量或矢量。
 形式1中,代表触发事件的”<信号名>"在指定的信号发生逻辑变化时,执行下面的语句,如下例:
  @(in) out=in;
 当敏感事件In发生逻辑变化时(包括正跳变和负跳变),执行对应的赋值语句,将in的值赋值给out。

 形式2中,代表触发事件的"posedge <信号名>"在指定的信号发生了正跳变时,执行下面的语句,如下例:
  @(posedge(n) out=in;
 当敏感事件in发生正跳变时,执行对应的赋值语句,将in的值赋值给out。

 形式3中,代表触发事件的"negedge <信号名>"在指定的信号发生了负跳变时,执行下面的语句,如下例:
  @(negedge in) out=in;
 当敏感事件in发生负跳变时,执行对应的赋值语句,将in的值赋值给out。

 在信号发生逻辑变化(正跳变或负跳变)的过程中,信号的值是从0、1、x、 z四个值中的一个值变化到另一个值;而信号发生正跳变的过程是信号山低电平向高电平的转变,负跳变是信号山高电平向低电平的转变。下表为 Verilog HDL中规定的正跳变和负跳变:

正跳变 负跳变
0→x 1→x
0→z 1→z
0→1 1→0
x→1 x→0
z→1 z→0

2.边沿触发语法格式
形式1:@(<事件表达式>) 行为语句;
 这种语法格式的敏感事件列表内只包含了一个触发事件,只有当这个指定的触发事件发生之后,后面的行为语句才能启动执行。在仿真进程中遇到这种带有事件控制的行为语句时,如果指定的触发事件还没有发生,则仿真进程就会停留在此处等待,直到指定触发事件发生之后再启动执行后面的行为语句,仿真进程继续向下进行。
例:时钟脉冲计数器

module clk_counter(clk, count_out);
input clk;
output count_out;
reg[3:0] count_out;
initial
	count_out = 0;
always@(posedge clk)
	count_out = count_out + 1; //在clk的每个正跳变边沿count_out增加1
endmodule

形式2:@(<事件表达式>);
 这种语法格式的敏感事件列表内也只包含了一个触发事件,没有行为语句来指定触发事件发生时要执行的操作。这种格式的事件控制语句的执行过程与延时控制语句中没有行为语句的情况类似,仿真进程在遇到这条事件控制语句后会进入等待状态,直到指定的触发事件发生后才结束等待状态,退出该事件控制语句的执行并开始下一条语句的执行。

例:用于测定输入时钟正电平,负电平持续时间以及时钟周期的模块

module clk_time(clk);
	input clk;
	time posedge_time,negegde_time;
	time high_last_time,low_last_time,last_time;
	initial
		begin
			@(posedge clk);
				posedge_time = $time;
			@(negedge clk);
				negedge_time = $time;
			@(negedge clk);
				last_time = $time - posedge_time;
				high_last_time = negedge_time - posedge_time;
				low_last_time = last_time - high_last_time;
			$display("The clk stay in High level for: %t",high_last_time);
			$display("The clk stay in Low level for: %t",low_last_time);
			$display("The clk stay in signal Period  for: %t",last_time);
		end
endmodule

形式3:@(<事件表达式1>or<事件表达式2>…or<事件表达式n>) 行为语句;
 这种语法格式的“敏感事件列表”内指定了由不同"<事件表达式>“代表的多个触发事件,这些”<事件表达式>"之间要用关键词”or“组合起来。只要这些触发事件中的任何一个得到发生,就启动行为语句的执行。在仿真进程遇到这种格式的边沿触发事件控制语句时,如果所有的触发事件都没有发生,则仿真进程就会进入等待状态,直到其中的某一个出发事件发生后才启动执行后面给出的行为语句,仿真进程继续向下进行。

形式4:@(<事件表达式1>or<事件表达式2>or…or<事件表达式n>)
 同第三种语法格式一样,这种语法格式内指定了多个触发事件。但是在这种格式中没有行为语句。在这种情况下,该语句的执行过程与第二种语法格式的执行过程类似,仿真进程在遇到这条事件控制语句后会进入等待状态,直到敏感事件列表包含的多个触发事件中的任何一个得到发生后才结束等待状态,退出该事件控制语句并开始执行该事件控制语句后的下一条语句。
例:在触发事件发生后退出事件控制语句

module display(a,b);
input a,b;
wire a,b;
always@(posedge a or negndge b);
	begin
		$display("One of a and b changed in time: %t",$time);
	end
endmodule

4.4电平敏感事件控制

电平敏感时间控制是另一种事件控制方式,与边沿触发事件控制不同,它是在指定的条件表达式为真时启动需要执行的语句。电平敏感时间控制是用关键词“wait"来表示。
电平触发事件控制的语法格式可以为如下两种:
形式1:wait(条件表达式)行为语句;
形式2:wait(条件表达式);
电平敏感事件控制的第一种形式中包含了行为语句,它可以是串行块(begin-end)语句或并行块(fork-join)语句,也可以是单条行为语句。在这种事件控制语句形式下,行为语句启动执行的触发条件是:条件表达式的值为"真(逻辑1)“。如果当仿真进程执行到这条电平敏感控制语句时条件表达式的值是"真”,那么语句块立即得到执行,否则语句块要一直等到条件表达式变为“真"时再开始执行。

例如:

wait(enable == 1)
begin
	d = a & b;
	d = d | c;
end

wait语句的作用是根据条件表达式的真假来控制下面begin-end语句块的执行,在使能信号enable变为高电平后,也就是enable==1的语句为真时进行a,b, c 之间的与或操作;若使能信号enable未变为高电平,则begin-end语句块的执行需要等到enable变为高电平之后才开始执行。


电平敏感事件控制的第2种形式中没有包含行为语句。在这种电平敏感事件控制语句形式下,如果当仿真进程执行到该wait控制语句时条件表达式的值是"真",那么立即结束该wait事件控制语句的执行,仿真进程继续往下进行;而如果当仿真进程执行到这条wait控制语句时条件表达式的值是"假",则仿真进程进入等待状态,一直等到条件表达式取值变为"真"时才退出等待状态同时结束该wait语句的执行,仿真进程继续往下进行。这种形式的电平敏感时间控制常常用来对串行块中各条语句的执行时序进行控制。

begin
	wait(enable == 1)
	d = a & b;
	d = d | c;
end

5.任务和函数

在Verilog语言中提供了任务和函数,可以将较大的行为级设计划分为较小的代码段,允许设计者将需要在多个地方重复使用的相同代码提取出来,编写成任务和函数,这样可以使代码更加简洁和易懂。

5.1任务

  1. 任务的定义
    任务定义的语法格式:
    task<任务名>;
    端口和类型声明
    局部变量声明
    begin
    语句1;
    语句1;
    ......
    语句n;
    end
    endtask

任务定义是嵌入在关键字task和endtask之间的,其中关键词task标志着一个任务定义结构的开端,endtask标志着一个任务定义结构的结束。“<任务名>” 是所定义任务的名称。在"<任务名>"后面不能出现输入输入端口列表。

例以读存储器数据为例说明任务定义的操作

task read_memory;
	input[15:0] address;
	output[31:0] data;
	reg[3:0] counter;
	reg[7:0] temp[1:4];
	begin
		for(counter=1;counter<=4;counter=counter+1)
			temp[counter]=mem[address+counter-1];
		data={temp[1],temp[2],temp[3],temp[4]};
	end
endtask

任务定义时需注意以下事项:
(1)在第一行"task"语句中不能列出端口名列表。
(2)任务中可以有延时语句、敏感事件控制语句等事件控制语句。
(3)任务可以没有或可以有一个或多个输入、输出和双向端口。
(4)任务可以没有返回值,也可以通过输出端口或双向端口返回一个或多个返回值。
(5)任务可以调用其它的任务或函数,也可以调用该任务本身。
(6)任务定义结构内不允许出现过程块(initial或always过程块)。
(7)任务定义结构内可以出现disable终止语句,这条语句的执行将中断正 在执行的任务。在任务被中断后,程序流程将返回到调用任务的地方继续向下执行 。

2.任务调用
任务的调用是通过"任务调用语句"来实现的。任务调用语句列出了传入任务的参数值和接收结果的变量值,任务的调用格式如下:
<任务名>(端口1,端口,...,端口n);
例:以测试仿真中常用的方式来说明任务的调用

module task_tb;
 reg[7:0] mem[127:0];
 reg[15:0] a;
 reg[31:0] b;
 initial
	begin
		a = 0;
		read_mem(a,b);//任务的第一次任务调用
		#10;
		a = 64;
		read_mem(a,b);
	end
task read_memory;
 input[15:0] address;
 output[31:0] data;
 reg[3:0] counter;
 reg[7:0] temp[1:4];
	begin
		for(counter=1;counter<=4;counter=counter+1)
			temp[counter]=mem[address+counter-1];
		data={temp[1],temp[2],temp[3],temp[4]};
	end
endtask

使用任务可以使程序更加简洁易懂,以实际中的交通控制灯为例说明任务的定义,调用的特点。

module traffic_lights(red, amber, green);
output red, amber, green; 
reg [2:1] order; 
reg clock, red, amber, green; 
parameter ON=1, OFF=0, RED_TICS=350,AMBER_TICS=30,GREEN_TICS=200;//产生时钟脉冲
always
	begin
		#100 clock = 0;
		#100 clock= 1;
end
//任务的定义,该任务用于实现交通灯的开启
task light; 
 output red; 
 output amber;
 output green;
 input [31:0] tic_time; 
 input [2:1] order; 
 	begin
		red = OFF;
		green = OFF; 
		amber = OFF; 
		case(order)
			2'1b01: red = ON;
			2'b10: green = ON;
			2'b11: amber = ON; 
		endcase
		repeat(tic_time)@(posedge clock); 
		red = OFF;
		green = OFF; 
		amber = OFF; 
	end
endtask
//任务调用,交通灯初始化
initial
	begin
		order = 2'b00;
		light(red, amber, green, 0, order);
	 end
 //任务调用,交通灯控制时序
always
	begin 
		order=2’b01;
		light(red, amber, green,RED_TICS,order);//调用开灯任务,开红灯
		order=2'b10;
		light(red, amber, green,GREEN_TICS,order);//调用开灯任务,开绿灯
		order=2'b11;
		light(red, amber, green,AMBER_TICS,order);//调用开灯任务,开黄灯
	end
endmodule

5.2函数

1.函数的定义
function<返回值类型或位宽><函数名>;
<输入参量与类型声明>
<局部变量声明>
begin
语句1;
语句;
...
语句n;
end
endfunction
函数定义是嵌入在关键字function和endfunction之间的,其中关键词function标志着一个函数定义结构的开端,endfunction标志着一个函数定义结构的结束。"<函数名>"是给被定义函数取的名称。这个函数名在函数定义结构内部还代表着一个内部变量,函数调用后的返回值是通过这个函数名变量传递给调用语句的。

<返回值类型或位宽>是一个可选项,它是用来对函数调用返回数据的类型或宽度进行说明,它可以有如下三种形式:
(1)“[msb Isb]”:这种形式说明函数名所代表的返回数据变量时一个多位的寄存器变量,它的位宽由[ msb:lsb]指定,比函数定义语句:

function[7:0] adder;

就定义了一个函数"adder",它的函数名"adder"还代表着一个8位宽的寄存器变量,其中最高位为第7位,最低位为第0位。
(2)“integer”:这种形式说明函数名代表的返回变量是一个整数型变量。
(3)“real”:这种形式说明函数名代表的返回变量是一个实数型变量。

"<输入参量与类型声明> "是对函数各个输入端口的宽度和类型进行说明,在函数定义中,必须至少有一个输入端口(input)的声明,不能有输出端口(output)的声明。数据类型声明语句用来对函数内用到的局部变量进行宽度和类型说明,这个说明语句的语法与进行模块定义时的相应说明语句语法是一致的。
"<局部变量说明>"是对函数内部局部变量进行宽度和类型的说明。
由"begin与"end"关键词界定的一系列语句和任务一样,用来指明函数被调用时要执行的操作,在函数被调用时,这些语句将以串行方式得到执行。

例:统计输入数据中"0"的个数

function[3:0] out;
input[7:0] x;
reg[3:0] count;
integer i;
	begin
		count=0;
		for(i=0;i<=7;i=i+1;)
			if(x[i]==1'b0)
				count=count+1;
		out0=count;
	end
endmodule

在进行函数定义时需要注意以下几点
(1)与任务一样,函数定义结构只能出现在模块中,而不能出现在过程块内。
(2)函数至少必须有一个输入端口。
(3)函数不能有任何类型的输出端口(output端口)和双向端口(inout端口)。
(4)在函数定义结构中的行为语句部分内不能出现任何类型的时间控制描述,也不允许使用disable终止语句。
(5)与任务定义一样,函数定义结构内部不能出现过程块。
(6)在一个函数内可以对其它函数进行调用,但是函数不能调用其它任务。
(7)在第一行"function"语句中不能出现端口名列表。
(8)在函数声明的时候,在Verilog的内部隐含地声明了一个名为function_identifier(函数标识符)的寄存器类型变量,函数的输出结果将通过这个寄存器类型变量被传递回来。

2.函数的调用
函数的调用是通过将函数作为表达式中的操作数来实现的。函数的调用格式如下:
<函数名>(<输入表达式1>,<输入表达式2>,...,<输入表达式n>);
其中,输入表达式应与函数定义结构中说明的输入端口一 一对应,它们代表着各个输入端口的输入数据。
函数调用时要注意以下几点:
(1)函数的调用不能单独作为一条语句出现,它只能作为一个操作数出现在调用语句内。
(2)函数的调用既能出现在过程块中,也能出现在assign连续赋值语句中。
(3)函数定义中声明的所有局部寄存器都是静态的,即函数中的局部寄存器在函数的多个调用之间保持它们的值。

module tryfact_tb; 
function[31:0] factorial;
	input[3:0] operand; 
	reg[3:0] index; 
	begin
		factorial = 1;
		for(index=1;index<=operand;index=index+1)
		factorial = index * factorial; 
	end
endfunction 
reg[31:0] result;
reg[3:O] n; 
initial
	begin
		result=1;
		for(n=1;n<=9;n=n+1)
			begin
				result = factorial(n);
				$display("n= %d result= %d", n, result);
			end
	end
endmodule

上例由函数定义和initia|过程块构成,其中定义了一个名为factorial函数,该函数是一个进行阶乘运算的函数,具有一个4位的输入端口,同时返回一个32位的寄存器类型的值;在initial块中定义了两个寄存器变量分别为32位的result和4位的n,initial块对1至9进行阶乘运算,并打印出结果值。 
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

5.3任务与函数的区别

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

6.典型测试向量的设计

6.1变量初始化

在Verilog语言中,有两种方法可以初始化变量 一种是利用初始化变量,另一种就是在定义变量时直接赋值初始化。这两种初始化任务是不可综合的,主要用于仿真过程。
(1)initial初始化方式
在大多数情况下,Testbench中变量初始化的工作通过initial过程块来完成,可以产生丰富的仿真激励。
initial语句只执行一次,即在设计被开始模拟执行时开始(0时刻)直到过程结束,专门用于对输入信号进行初始化和产生特定的信号波形。一个Testbench可以包含多initial过程语句块,所有的initia|过程都同时执行需要注意的是,initial语句中的变量必须为reg类型。

例:利用initial初始化方式的测试向量产出

module counter(clk,cnt);
	output [3:0] cnt;
	reg clk;
	reg [3:0] temp;
	initial temp = 0;
	initial clk = 0;
endmodule

(2)定义变量时初始化在定义变量时初始化的语法非常简单,直接用"="在变量右端赋值即可,如:
reg [7:0] cnt=8’b00000000;
就将8比特的寄存器变量cnt初始化为0。

6.2数据信号测试向量产生

数据信号的产生有两种形式:其一是初始化和产生都在单个initial块中进行;其一是初始化在initial语句中完成,而产生在always语句块中完成。前者适合不规则数据序列,并且要求长度较短;后者适合具有一定规律的数据序列,长度不限。

例:产生位宽为4的质数序列{1、2、3、5、7、11、13 },并且重复两次,其中样值间隔为4个仿真时间单位
由于该序列无明显规律,因此利用Initial语句最为合适。

`timescale 1ns/1ps 
module sequence_tb;
	reg [3:0] qout; 
	parameter sample_period = 4; 
	parameter queue_num = 2; 
	initial
		begin
			q_out =0;
			repeat( queue_num) 
				begin
					# sample_period q_out = 1;
					# sample_period q_out = 2;
					# sample_period q_out = 3;
					# sample_period q out = 5;
					# sample_period q_out = 7;
					# sample_period q_out= 11;
					# sample_period q_out = 13;
				end
	end
e ndmodule

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

6.3时钟信号测试向量产生

例:产生占空比为50%的时钟信号
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发(1)基于initial语句的方法

module clk1(clk);
 output clk;
 parameter clk_period = 10;
 reg clk;
 initial
 	begin
 		clk = 0;
 		forever #(clk_period/2) clk = ~clk;
 	end
 endmodule

(2)基于always语句的方法

module clk2(clk);
 output clk;
 parameter clk_period = 10;
 reg clk;
 initial clk = 0;
 always #(clk_period/2) clk = ~clk;
endmodule

6.4总线信号测试向量产生

总线是运算部件之间数据流通的公共通道。在RTL级描述中,总线指的是由逻辑单元、寄存器、存储器、电路输入或其它总线驱动的一个共享向量。而总线功能模型则是一种将物理的接口时序操作转化成更高抽象层次接口的总线模型,如图:
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
在总线中,对于每个请求端,有一个输入来选择驱动该总线所对应的请求端。选择多个请求端会产生总线冲突,根据总线的类型,冲突会产生不同的结果。当有多个请求端发出请求时,相应的操作由总线的类型决定。在Verilog测试中,总线测试信号通常是通过将片选信号,读(或者写)使能信号、地址信号以及数据信号以task任务的形式来描述,通过调用以task形式的总线信号测试向量来完成相应的总线功能 。
下面以工作频率为100MHz的AHB总线写操作为例,说明以task方式产生总线信号测试向量的方式。下图是AHB总线写操作的时序图,其中,在完成数据的写操作后将片选和写使能信号置为无效(低电平有效)。

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
例:产生一组具有写操作AHB总线功能模型

module bus_wr_tb; 
reg clk;
reg cs; 
reg wr;
reg [31:0] addr; 
reg [31:0] data:
initial
	begin
		cs=1'b1;
		wr=1'b1;
		#30;
		bus_wr(32'h1100008a, 32’h11113000); 
		bus_wr(32’h1100009a, 32'h11113001);
		bus_wr(32'h110000aa, 32'h11113002); 
		bus_wr(32'h110000ba, 32’h11113003); 
		bus_wr(32'h110000ca, 32’h11113004); 
		addr=32’bx; 
		data=32’bx;
end
initial clk=1;
always #5 clk=~clk;
task bus_wr;
 input [31:0] ADDR;
 input [31:0] DATA;
 begin
 	cs=1'b0;
 	wr=1'b0;
 	addr = ADDR;
 	data = DATA;
 	#30 cs=1'b1;
 	wr=1'b1;
 end
endtask
endmodule

7.用户自定义元件模型UDP

通过UDP,可以把一块组合逻辑电路或时序逻辑电路封装在一个UDP内,并把这个UDP作为一个基本门元件来使用。需要注意的是,UDP是不能综合的,只能用于仿真。

7.1UDP的定义与调用

UDP的定义格式如下:

primitive <元件名称>(<输出端口名>,<输入端口名1 >,<输入端口名2>,...,<输入端口名n>)
 输出端口类型声明(output);
 输入端口类型声明(input);
 输出端口寄存器变量说明(reg);
 元件初始状态说明(initial); 
 table
	<table表项1>;
	<table表项2>;
	...
	<table表项n>;
 endtable 
endprimitive

和Verilog中的模块相比,UDP具备以下特点。
(1)UDP的输出端口只能有一个,且必须位于端口列表的第一项。只有输出端口能定义为reg类型。
(2)UDP的输入端可有多个,一般时序电路UDP的输入端口最多9个,组合电路UDP 的输入端口可多至10个。
(3)所有端口变量的位宽必须是1比特。
(4)在table表项中,只能出现0、1、x这三种状态,z将被认为是x状态。

根据UDP包含的基本逻辑功能,可以将UDP分为组合电路UDP、时序电路UDP及混合电路UDP,这几类UDP的差别主要体现在table表项的描述上。
UDP的调用和Verilog中模块的调用方法相似,通过位置映射,其语法格式如下:
UDP名 例化名(连接端口1信号名,连接端口2信号名,连接端口3信号名,...);

例:用UDP方式定义一个全加器仿真模型

primitive summ(sum, cin, a, b);//本位和
 output sum;
 input a,b,cin;
	table
  	  //cin a b : sum
		 0 0 0 : 0;
		 0 0 1 : 1;
		 0 1 0 : 1;
		 0 1 1 : 0;
	 	 1 0 0 : 1;
		 1 0 1 : 0;
		 1 1 0 : 0;
	 	 1 1 1 : 1;
	endtable
endprimitive

primitive summ(sum, cin, a, b);//进位
 output cout;
 input a,b,cin;
	table
  	  //cin a b : sum
		 0 0 0 : 0;
		 0 0 1 : 0;
		 0 1 0 : 0;
		 0 1 1 : 1;
	 	 1 0 0 : 0;
		 1 0 1 : 1;
		 1 1 0 : 1;
	 	 1 1 1 : 1;
	endtable
endprimitive

7.2UDP应用实例

(1)组合电路UDP元件
组合逻辑电路的功能列表类似真值表,就是规定了不同的输入值和对应的输出值,表中每一行形式是output,input1,input2,…,排列顺序和端口列表中的顺序相同,合电路UDP的输入端口可多至10个。如果某个输入组合没有定义的输出,那么就把这种情况的输出置为x。

例:3选1多路选择器

primitive mux3_1(Y,in0,in1,in2,s2,s1);
 input in0,in1,in2,s2,s1;
 output Y;
 table
 	//in0 in1 in2 s2 s1 : Y
 	   0   ?   ?  0  0  :  0;//当s2s1=00时,Y=in0
 	   1   ?   ?  0  0  :  1;
 	   ?   0   ?  0  1  :  0;//当s2s1=01时,Y=in1
 	   ?   1   ?  0  1  :  1;
 	   ?   ?   0  1  ?  :  0;//当s2s1=1?时,Y=in2
 	   ?   ?   1  1  ?  :  1;
 	   0   0   ?  0  ?  :  0;
 	   1   1   ?  0  ?  :  1;
 	   0   ?   0  ?  0  :  0;
 	   1   ?   1  ?  0  :  1;
 	   ?   0   0  ?  1  :  0;
 	   ?   1   1  ?  1  :  1;
 endtable
endprimitive

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
(2)时序电路UDP元件
UDP还可以描述具有电平触发和边沿触发特性的时序电路。时序电路拥有内部状态序列,其内部状态必须用寄存器变量进行建模,该寄存器的值就是时序电路的当前状态,它的下一个状态是山放在基元功能列表中的状态转换表决定的,而且寄存器的下一个状态就是这个时序电路UDP的输出值。所以,时序电路UDP由两部分组成一状态寄存器和状态列表。定义时序UDP的工 作也分为两部分一初始化状态寄存器和描述状态列表。
在时序电路的UDP描述中,01、Ox、xl代表着信号的上升沿。下面给出一个上升沿D触发器的UDP开发实例。

例:通过Verilog给出D触发器UDP描述,并在模块中调用UDP组件

primitive D_Edge(Q,Clk,Data);
 output Q;
 reg Q;
 input Data,Clk;
 initial Q = 0;
 table
   //Clk Data : Q(Stata): Q(next)
 	(01)   0  :    ?    :    0;
 	(01)   1  :    ?    :    1;
 	(0x)   1  :    1    :    1;
 	(0x)   0  :    0    :    0;
 	(?0)   ?  :    ?    :    -;// 忽略时钟负边沿
 	?   (??)  :    ?    :    -;// 忽略在稳定时钟上的数据变化
 endtable
endprimitive;

表项(01)表示从0转换到1,表项(0x)表示从0转换到x,表项(?0)表示从任意值(0、1或x)转换到0,表项(??)表示任意转换,输出默认为x。假定D_Edge为UDP定义,它现在就能像基本门一样在模块中使用。

(3)混合电路UDP元件
在同一个表中能够混合电平触发和边沿触发项。在这种情况下,边沿变化在电平触发之前处理,即电平触发项覆盖边沿触发项。下面给出一段带异步清空的D触发器的UDP描述。
例:利用Verilog语言完成异步清零D触发器的UDP描述

primitive D_Async_FF(0, Clk, Clr, Data); 
 output Q;
 reg Q; 
 input Clk, Data, Clr;
 table 
	//Clk Clr Data : (SQtate) : Q(next)
	  (01) 0    0  :     ?    :    0;
	  (01) 0    0  :     ?    :    0;
	  (0x) 0    1  :     1    :    1;
	  (0x) 0    0  :     0    :    0;
	  (?0) 0    ?  :     ?    :    -;
	  (??) 0    ?  :     ?    :    -;
	   ?   1    ?  :     ?    :    0;
	endtable
endprimitive

8.基本门级元件和模块的延时建模

8.1门级延时建模

门级延时可以分为如下几类:
(1)“上升延时”:表示信号由"x"或"z"状态变化到"1"状态时受到的门传输延时。
(2)“下降延时”:表示信号由"1",“x"或"z"状态变化到"0"状态时受到的门传输延时。
(3)“到不定态的延时”:表示信号由"0”、“1"或"z"状态变化到"x"状态时受到的门传输延时。
(4)“截止延时”:表示信号由"0”、"1"或"x"状态变化到"z"状态时受到的门传输延时。

1.门级延时的基本延时表达形式
在门级延时的基本表达形式下,“delay"内可以0至3个延时值,下表给出了指定的不同延时值个数时,delay的4种表达形式。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
(1)当"delay"没有指定门延时值,则默认的延时值为0。这意味着元件实例的"上升延时值”、“下降延时值”、“截止延时值"和"到不定态的延时值"均为0。
在实例:notif1 U0(out, in, ctrl);
门级延时值为"0”,因为没有定义延迟,则元件实例UO的"上升延时值"、“下降延时值”、“截止延时值"和"到不定态的延时值"均为0。
(2)当"delay"内只包含1个延时值时,给定的延时值"d"将同时代表着元件实例的"上升延时值"、“下降延时值”、“截止延时值"和"到不定态的延时值”。在下面的实例引用中,
notif1 #20 U1(out, in, ctrl);
门级延时值为"20",且只包含1个延时值,说明元件实例U1的所有类型的门级延时都是20个单位时间。
(3)当"delay"内包含了2个延时值时,元件实例的"上升延时值"由给定的“d1"指定;“下降延时值"由给定的"d2"指定;而"截止延时值"和"到不定态的延时值”“将由"dl"和"d2"中的最小值指定。在实例中:
notif1 #(10,20)U2(out, in, ctrl);
门级延时值为”(10,20)“,包含了2个延时值10和20,这表明元件实例U2将具有10个单位时间的"上升延时"和20个单位时间的"下降延时”,而它的"截止延时"和"到不定态的延时"将是10和20中的最小值指定,即为10个单位时间。
(4)当"delay"内包含了3个延时值,元件实例的"上升延时值"由给定的"dA"指定;“下降延时值"由给定的“dB"指定;“截止延时"由给定的"dC '指定;而它的"到不定态的延时"将由dA、dB和dC中的最小值指定。在实例:
notif1 #(10,20,30)U3(out, in, ctrl);
门级延时值为“ 00,20,30)”,包含了3个延时值10、20和30,这表明元件实例IJ3具有10个单位时间的“上升延时”、20个单位时间的“下降延时"和30个单位时间的“截止延时",而它的“到不定态的延时"将有
10、20和30中的最小值指定,即为10个单位时间。

2.门级延时的最小、典型、最大延时表达形式
除了基本延时表达形式外,门级延时量还可以采用”最小、典型、最大“延时表达形式,在这种表示方式下,门级延时量中的每一项将由“最小延时"、“典型延时"和”最大延时"二个值来表示,其语法格式如下:
#(d min:d_typ:d_max)

采用"最小、典型、最大"延时表达形式时,delay"内可以包含为1至3个延时值,如下列几种情况:

#(dA_min: dA_typ: dA_max)
#(dA_min: dA_typ: dA_max, dB_min: dB_typ: dB_max)
#(dA_min :dA_typ: dA_max, dB_min: dB_typ: dB_max, dC_min: dC_typ: dC_max)

在实例:and #(4:5:6) U1(out,i1,i2);
“delay"中只包含1个延迟值,其最小值为4、典型值为5、最大值为6。元件实例UI的"上升延时值”、“下降延时值”、"截止延时值"和“到不定态的延时值"如下表:

最小延时 上升延时=4 下降延时:4 到不定态的延时 =4 关断延时:4
典型延时 上升延时:5 下降延时=5 到不定态的延时 关断延时:5
最大延时 上升延时=6 下降延时=6 到不定态的延时 关断延时= 6

在实例:and #(3:4:5,5:6:7)U2(out, i1,i2);
"delay"中包含了2个延迟值,第一个延迟值的最小值为3、典型值为4、最大值为5,第二个延迟值的最小值为5、典型值为6、最大值为7。元件实例U2的"上升延时值"由第一个延时值指定,下降延时值"山第二个延时值指定,"到不定态的延时值"和"截止延时值"均由两个延时值的最小值指定。各值的取值情况如下表:

最小延时 上升延时=3 下降延时=5 到不定态的延时=min(3,5) 关断延时=min(3,5)
典型延时 上升延时=4 下降延时=6 到不定态的延时=min(4,6) 关断延时=min(4,6)
最大延时 上升延时=5 下降延时=7 到不定态的延时=min(5,7) 关断延时=min(5,7)

and # (2:3:4,3:4:5,4:5:6) U3(out,i1,i2);

"delay"中包含了三个延迟值,第一个延迟值的最小值为2、典型值为3、最大值为4,第二个延迟值的最小值为3、典型值为4、最大值为5,第三个延迟值的最小值为4、典型值为5、最大值为6。元件实例U3的"上升延时值"由第一个延时值指定,"下降延时值"由第二个延时值指定,"截止延时值"由第三个延时值指定,而它的"到不定态的延时值"由三个延时值的最小值指定。各值的取值情况如下表。

最小延时 上升延时=2 下降延时=3 到不定态的延时=min(2,3,4) 关断延时=4
典型延时 上升延时=3 下降延时=4 到不定态的延时=min(3,4,5) 关断延时= 5
最大延时 上升延时=4 下降延时=5 到不定态的延时=min(4,5,6) 关断延时= 6

例:用Verilog建立模块D的延迟仿真模块
其门级实现如模块D的逻辑图如下,其中包含了延时时间为5个单位时间的与门和一个延时时间为4个单位时间的或门。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
带有延迟的模块D代码:

module D(out,a,b,c);
 output out;
 input a,b,c;
 wire e;
 and #(5) a1(e,a,b); 
 or #(4) o1(out, e,c);
endmodule

带有延时的模块D的测试激励模块:

module D_tb;
 reg A,B,C;
 wire OUT;
 D d1(.out(OUT), .a(A), .b(B), .c(C));
 initial
 	begin
 		     A=1'b0;B=1'b0;C=1'b0;
 		#10  A=1'b1;B=1'b1;C=1'b1;
 		#10  A=1'b1;B=1'b0;C=1'b0;
 		#20  $finish;
 	end
 endmodule

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

8.2模块延时建模

1.延时说明块Specify Block
在模块输入和输出引脚之间的延迟称为模块路径延迟。在Verilog中,在关键字specify和endspecify之间给路径延迟赋值,关键字之间的语句组成specify块(即指定块)。"specify"与"endspeclfy"分别是延时说明块的起始标识符和终止标识符。

Specify块包含下列操作语句:
(1)定义穿过模块的所有路径延迟
(2)在电路中设置时序检查
(2)定义specparam常量

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
例:上图为例,用specify块来描述途中M模块的路径延迟。代码如下:

module M(out,a,b,c,d);
 input a,b,c,d;
 output out;
 wire e,f;
 assign out=(a&b)|(c&b);//逻辑功能
 specify //包含路径延迟语句的specify块
 	(a=>out)=9;
 	(b=>out)=9;
 	(c=>out)=11;
 	(d=>out)=11;
 endspecify;
 endmodule

2.径延迟描述方式
(1)并行连接
每条路径延迟语句都有一个源域或一个目标域。在上例的路径延迟语句中,a、b、c和d在源域位置,而out是目标域。
在specify块中,用符号"=> "说明并行连接,其语法格式如下:
(<source_field>=><destlnation_field>)=<delay_value>;
其中<delay_value >可以包含1至3个延时量,也可以采用"最小、典型、最大"延时表达形式。在延时量由多个值组成的情况下,应在延时量的外面加上一对括号例如
(a=>out)=(8,9,10);
表示的是输入a到输出b的延迟最小、典型和最大延迟分别是8、9、10个时间单位。

在并行连接中,源域中的每一位与目标域中相应的位连接。如果源和目标域是向量,必须有相同的位数,否则会出现不匹配。因此,并行连接说明了源域的每一位到目标域的每一位之间的延迟。
下图显示了源域和目标域之间的位是如何并行连接的。同时例给出了并行连接的Verilog描述。

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
(2)全连接
在specify块中,用符号"*>"表示全连接,其语法格式如下:
(<source_field>*><destination_field>)=<delay_value>;
在全连接中,源域中的每一位与目标域中的每一位相连接。如果源和目标是向量,则它们不必位数相同。全连接描述源中的每一位和目标中的每一位之间的延迟,如下图所示:

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
3.spacparam声明语句
spacparam用来定义specify块中的参数,如下例使用spacparam语句的specify块示例

module parallel_connected(out, a, b); 
 input a, b;
 output out; 
 wire out; 
 assign out=a&b; //逻辑功能
 //在指定块内部定义参数
specify
 specparam a_to_out = 9; 
 specparam b_to_out = 11;
 (a => out) = a_to_out;
 (b => out) = b_to_out; 
endspecify
endmodule

specparam语句的格式和作用都类似于parameter参数说明语句,但两者又有不同:
(1)specparam语句只能在延时说明块(specify块)中出现;而parameter 语句则不能在延时说明块内出现。
(2)由specparam语句进行定义的参数只能是延时参数;而由parameter语句定义的参数可以是任何数据类型的常数参数。
(3)由specparam语句定义的延时参数只能在延时说明块内使用;而 parameter语句定义的参数则可以在模块内的任意位置处使用。
在模块中提供specify参数是为了方便给延迟赋值,建议用specify参数而不是数值来表示延迟。这样,如果电路的时序说明变化了,用户只需要改变 specify参数值,而不必逐个修改每条路径的延迟值。

8.3与时序检查相关的系统任务

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

9.编译预处理语句

编译预处理是Verilog编译系统的一个组成部分,指编译系统会对一些特殊命令进行预处理,然后将预处理结果和源程序一起再进行通常的编译处理。以"`"(反引号)开始的某些标识符是编译预处理语句。在verilog语言编译时,特定的编译器指令在整个编译过程中有效(编译过程可跨越多个文件),直到遇到其它的不同编译程序指令。常用的编译预处理语句如下:
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

9.1宏定义

`define指令是一个宏定义命令,通过一个指定的标识符来代表一个字符串,可以增加Verilog HDL代码的可读性和可维护性,找出参数或函数不正确或不允许的地方。
`define指令像C语言中的#define指令,可以在模块的内部或外部定义,编译器在编译过程中,遇到该语句将把宏文本替换为宏的名字。
`define的声明语法格式如下:
`define <macro_name><Text>
对于己声明的语法,在代码中的应用格式如下所示,不要漏掉宏名称前的"`"。
`macro_name
例如:
`define MAX_BUS_SIZE 32

reg [`MAX_BUS_SIZE-1:0] AddReg;

一旦`define指令被编译,其在整个编译过程中都有效。例如,通过另一个文件中的`define指令MAX_BUS_SIZE能被多个文件使用。
`undef指令取消前面定义的宏。例如:
`define WORD 16 //建立一个文本宏替代

wire[`WORD:1] Bus;

`undef WORD //在`undef编译指令后,WORD的宏定义不再有

关于宏定义指令,有下面8条规则需要注意。
(1)宏定义的名称可以是大写,也可以是小写,但要注意不要和变量名重复。
(2)和所有编译器伪指令一样,宏定义在超过单个文件边界时仍有效(对工程中的其它源文件),除非被后面的`define,`undef或`resetall伪指令覆盖,否则`define不受范围限制。
(3)当用变量定义宏时,变量可以在宏正文使用,并且在使用宏的时候可以用实际的变量表达式代替。
(4)通过用反斜杠"\"转义中间换行符,宏定义可以跨越几行,新的行是宏正文的一部分。
(5)宏定义行末不需要添加分号表示结束。
(6)宏正文不能分离的语言记号包括注释、数字、字符串、保留的关键字、运算符。
(7)编译器伪指令不允许作为宏的名字。
(8)宏定义的文本也可以是一个表达式。

`define和parameter是有区别的,`define和parameter都可以用于完成文本替换,但其存在本质上的不同,前者是编译之前就预处理,而后者是在正常编译过程中完成替换的。此外,`define和parameter存在下列两点不同之处。
(1)作用域不同:parameter作用于声明的那个文件;`define从编译器读到这条指令开始到编译结束都有效,或者遇到`undef命令使之失效,可以应用于整个工程。如果要让parameter作用于整个项目,可以将声明语句写于单独文件,并用`include让每个文件都包含声明文件。
`define也可以写在代码的任何位置,而parameter则必须在应用之前定义。通常编译器都可以定义编译顺序,或者从最底层模块开始编译,因此写在最底层就可以了。
(2)传递功能不同:parameter可以用作模块例化时的参数传递,实现参数化调用:`define语句则没有此作用。`define语句可以定义表达式,而parameter只能用于定义变量。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

9.2文件包含处理

所谓“文件包含处理"是一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件之中。Verilog语言提供了`include命令用来实现"文件包含"的操作。其一般形式为:include “文件名”。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

9.3仿真时间标度

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
在这条命令中,时间单位参量是用来定义模块中仿真时间和延迟时间的基准单位的。时间精度参量是用来声明该模块的仿真时间的精确程度的,该参量被用来对延迟时间值进行取整操作(仿真前),因此该参量又可以被称为取整精度。如果在同一个程序设计里,存在多个`timescale命令,则用最小的时间精度值来决定仿真的时间单位。另外,时间精度至少要和时间单位一样精确,时间精度值不能大于时间单位值。
在`timescale命令中,用于说明时间单位和时间精度参量值的数字必须是整数,其有效数字为1、10、100,单位为秒(s)、毫秒(ms)、微秒(us)、纳秒(ns)、皮秒(p司、亳皮秒〈fs)。下面举例说明`timescale命令的用法。
例仿真时间标度举例
(1)`timescale 1ns/1ps
模块中所有的时间值都表示是1ns的整数倍。这是因为在`timescale命令中,定义了时间单位是1ns。模因为`timescale命令定义时间精度为1ps,块中的延迟时间可表达为带三位小数的实型数。
(2)`timescale 10us/100ns
在这个例子中,模块中时间值均为10us的整数倍。因为`timesacle命令定义的时间单位是10us。延迟时间的最小分辨度为十分之一微秒(100ns),即延迟时间可表达为带一位小数的实型数。
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

9.4条件编译命令

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

9.5其他语句

verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发
verilog测试文件编写实例,Verilog学习笔记,学习,fpga开发

10.Verilog测试方法简介

在集成电路测试领域,常用的测试方法有完全测试法、随机测试法和自动测试法。
(1)完全测试法对于复杂的设计来说,常常通过检查代码的覆盖率来检查验证工作是否完成的一种重要方法。代码覆盖率可以指示Verilog代码描述的功能有多少在仿真过程中被验证过。通常代码覆盖率包括以下内容:
·语句覆盖率
·路径覆盖率
·状态机覆盖率
·触发覆盖率
·表达式覆盖率
(2)随机测试法
在Verilog中提供了多个用于随机测试的系统命令,通常使用随机测试的系统函数来仿真真实应用的情况,如在通信领域中常用的帧同步搜索电路需要从接收的数据流中检测发送端固定插入的某个特殊的码型,而数据本身也有可能包括该码型,在这种情况下进行随机化测试就更接近于真实应用的情况。其中最常用的系统命令是随机数产生系统任务$random。
(3)自动测试法
在集成电路测试领域,全面准确的测试才能保证大规模集成电路的正常工作,而确保一个设计能够得到全面测试的唯一途径就是实现任务的自动化。通常通过创建一个检验表,使用相应个数的采样值。当修改过源代码后,所有的测试程序都自动被再次执行。但需要注意,使用自动测试可能会存在截断误差。文章来源地址https://www.toymoban.com/news/detail-788626.html

来源:蔡觉平老师的Verilog课程

到了这里,关于Verilog学习笔记(4):仿真验证与Testbench编写的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Verilog Tutorial(6)如何编写一个基础的Testbench

    在自己准备写verilog教程之前,参考了许多资料----FPGA Tutorial网站的这套verilog教程即是其一。这套教程写得不错,只是没有中文,在下只好斗胆翻译过来(加了自己的理解)分享给大家。 这是网站原文:https://fpgatutorial.com/verilog/ 这是系列导航:Verilog教程系列文章导航 本文将

    2023年04月10日
    浏览(36)
  • FPGA学习笔记(五)Testbench(测试平台)文件编写进行Modelsim仿真

    一、FPGA学习笔记(一)入门背景、软件及时钟约束 二、FPGA学习笔记(二)Verilog语法初步学习(语法篇1) 三、FPGA学习笔记(三) 流水灯入门FPGA设计流程 四、FPGA学习笔记(四)通过数码管学习顶层模块和例化的编写 五、FPGA学习笔记(五)Testbench(测试平台)文件编写进行

    2024年02月07日
    浏览(54)
  • ASIC-WORLD Verilog(10)编写测试脚本Testbench的艺术

            在自己准备写一些简单的verilog教程之前,参考了许多资料----Asic-World网站的这套verilog教程即是其一。这套教程写得极好,奈何没有中文,在下只好斗胆翻译过来(加了自己的理解)分享给大家。         这是网站原文:Verilog Tutorial         这是系列导航:

    2024年02月07日
    浏览(38)
  • 「Verilog学习笔记」根据RTL图编写Verilog程序

    专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点,刷题网站用的是牛客网 观察题目给出的RTL图,主要的器件是两个D触发器,一个与门。D触发器含有异步复位信号,且为低电平有效。data_in输入到D触发器,D触发器的输出Q是前一时刻的data_in,即data_in打一

    2024年03月24日
    浏览(39)
  • 基于FPGA的AES加密解密vivado仿真,verilog开发,包含testbench

    目录 1.算法描述 2.仿真效果预览 3.verilog核心程序 4.完整verilog          AES, 高级加密标准, 是采用区块加密的一种标准, 又称Rijndael加密法. 严格上来讲, AES和Rijndael又不是完全一样, AES的区块长度固定为128比特, 秘钥长度可以是128, 192或者256. Rijndael加密法可以支持更大范围的区

    2024年02月01日
    浏览(57)
  • 序列检测器(两种设计方法和四种检测模式|verilog代码|Testbench|仿真结果)

    目录 一、前言 二、状态机法和寄存器法 2.1状态机法 2.11 使用状态机检测“1001” 2.12 verilog代码 2.13 testbench 2.14仿真结果 2.2移位寄存器法 2.21 使用移位寄存器法检测1001 2.22 verilog代码 2.23 testbench 2.24仿真结果 三、重叠检测与非重叠检测(检测序列1001) 3.1重叠检测 3.11 重叠检测

    2024年02月04日
    浏览(47)
  • 边沿检测(上升沿检测、下降沿检测、双边沿检测|verilog代码|Testbench|RTL电路图|仿真结果)

    数字IC经典电路设计 经典电路设计是数字IC设计里基础中的基础,盖大房子的第一部是打造结实可靠的地基,每一篇笔者都会分门别类给出设计原理、设计方法、verilog代码、Testbench、仿真波形。然而实际的数字IC设计过程中考虑的问题远多于此,通过本系列希望大家对数字I

    2024年02月11日
    浏览(41)
  • 数字分频器设计(偶数分频、奇数分频、小数分频、半整数分频、状态机分频|verilog代码|Testbench|仿真结果)

    目录 一、前言 二、偶数分频 2.1 触发器级联法 2.2 计数器法 2.3 verilog代码 2.4 Testbench 2.5 仿真结果 三、奇数分频 3.1 占空比非50%奇数分频 3.2 占空比50%奇数分频 3.3 Verilog代码 3.4 Testbench 3.5 仿真结果 四、小数分频 4.1 双模前置分频法 4.2 Verilog代码 4.3 Testbench 4.4 仿真结果 五、半整

    2024年02月04日
    浏览(48)
  • 数字分频器设计(偶数分频、奇数分频、小数分频、半整数分频、状态机分频|verilog代码|Testbench|仿真结果)

    数字IC经典电路设计 经典电路设计是数字IC设计里基础中的基础,盖大房子的第一部是打造结实可靠的地基,每一篇笔者都会分门别类给出设计原理、设计方法、verilog代码、Testbench、仿真波形。然而实际的数字IC设计过程中考虑的问题远多于此,通过本系列希望大家对数字I

    2024年02月08日
    浏览(45)
  • 奇偶校验器设计(奇偶校验与奇偶检测,XOR法和计数器法|verilog代码|Testbench|仿真结果)

    数字IC经典电路设计 经典电路设计是数字IC设计里基础中的基础,盖大房子的第一部是打造结实可靠的地基,每一篇笔者都会分门别类给出设计原理、设计方法、verilog代码、Testbench、仿真波形。然而实际的数字IC设计过程中考虑的问题远多于此,通过本系列希望大家对数字I

    2024年02月11日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包