FPGA——Vivado 仿真

本文为 FPGA 学习总结,欢迎分享交流。

运行环境

  • windows10
  • Vivado 2018.3
  • Modelsim 10.7

如何对编写的代码进行仿真?仿真对于我们 FPGA 的设计十分重要,我们用几个简单的例子进行讲解。

Test bench 文件结构

完整的测试文件结构为:

module Test_bench();//通常无输入无输出
/* 信号或变量声明定义
逻辑设计中输入对应 reg 型
逻辑设计中输出对应 wire 型
使用 initial 或 always 语句产生激励
例化待测试模块
监控和比较输出响应 */
endmodule

根据实际情况会有顺序调换或简化。

时钟激励设计

/*----------------------------------------------------------------
时钟激励产生方法一:50%占空比时钟
----------------------------------------------------------------*/
parameter ClockPeriod=10;
initial
	begin
		clk_i=0;
		forever // 一直产生时钟
			#(ClockPeriod/2) clk_i=~clk_i;
    end

/*----------------------------------------------------------------
时钟激励产生方法二:50%占空比时钟
----------------------------------------------------------------*/
initial
	begin
		clk_i=0;
        always #(ClockPeriod/2) clk_i=~clk_i; // #代表延迟
	end

/*----------------------------------------------------------------
时钟激励产生方法四:产生固定数量的时钟脉冲
----------------------------------------------------------------*/
initial
	begin
		clk_i=0;
        repeat(6) // 下面的代码执行6次,产生3个周期的时钟
			#(ClockPeriod/2) clk_i=~clk_i;
	end

/*----------------------------------------------------------------
时钟激励产生方法五:产生非占空比为 50%的时钟(用于特殊场合)
----------------------------------------------------------------*/
initial
	begin
		clk_i=0;
		forever
			begin
				#((ClockPeriod/2)-2) clk_i=0;
				#((ClockPeriod/2)+2) clk_i=1;
			end
	end

复位信号设计

/*----------------------------------------------------------------
复位信号产生方法一:异步复位
----------------------------------------------------------------*/
initial
	begin
		rst_n_i=1;
		#100; // 延迟后产生,必须在时钟振荡后才起作用
		rst_n_i=0;
		#100;
		rst_n_i=1;
	end

/*----------------------------------------------------------------
复位信号产生方法二:同步复位
----------------------------------------------------------------*/
initial
	begin
		rst_n_i=1;
        @(negedge clk_i) // 与clk_i信号同步,进行复位
		rst_n_i=0;
		#100; //固定时间复位
        repeat(10) @(negedge clk_i); //固定周期数复位,设置同步复位周期为10
		@(negedge clk_i)
		rst_n_i=1;
end

/*----------------------------------------------------------------
复位信号产生方法三:复位任务封装
----------------------------------------------------------------*/
task reset;
	input [31:0] reset_time; //复位时间可调,输入复位时间
	RST_ING=0; //复位方式可调,低电平或高电平
	begin
		rst_n=RST_ING; //复位中
		#reset_time; //复位时间
		rst_n_i=~RST_ING; //撤销复位,复位结束
	end
endtask

双向信号设计

/*----------------------------------------------------------------
双向信号描述一:inout 在 testbench 中定义为 wire 型变量
----------------------------------------------------------------*/
//为双向端口设置中间变量 inout_reg 作为 inout 的输出寄存,其中 inout 变
//量定义为 wire 型,使用输出使能控制传输方向
//inout bir_port;
wire bir_port;
reg bir_port_reg;
reg bi_port_oe;
// oe使能输出bir_port_reg,否则输出高阻1'bz
assign bi_port=bi_port_oe ? bir_port_reg : 1'bz;

/*----------------------------------------------------------------
双向信号描述二:强制 force
----------------------------------------------------------------*/
//当双向端口作为输出口时,不需要对其进行初始化,而只需开通三态门
//当双向端口作为输入时,只需要对其初始化并关闭三态门,初始化赋值需
//使用 wire 型数据,通过force命令来对双向端口进行输入赋值
//assign dinout=(!en) din :16'hz; 完成双向赋值
initial
	begin
		force dinout=20;
		#200
		force dinout=dinout-1;
	end

特殊信号设计

/*----------------------------------------------------------------
特殊激励信号产生描述一:输入信号任务封装
----------------------------------------------------------------*/
task i_data;
input [7:0] dut_data;
begin
	@(posedge data_en); send_data=0;
	@(posedge data_en); send_data=dut_data[0];
	@(posedge data_en); send_data=dut_data[1];
	@(posedge data_en); send_data=dut_data[2];
	@(posedge data_en); send_data=dut_data[3];
	@(posedge data_en); send_data=dut_data[4];
	@(posedge data_en); send_data=dut_data[5];
	@(posedge data_en); send_data=dut_data[6];
	@(posedge data_en); send_data=dut_data[7];
	@(posedge data_en); send_data=1;
	#100;
end
endtask
//调用方法:i_data(8'hXX);

/*----------------------------------------------------------------
特殊激励信号产生描述二:多输入信号任务封装
----------------------------------------------------------------*/
task more_input; // 多个input
input [7:0] a;
input [7:0] b;
input [31:0] times;
output [8:0] c;
begin
	repeat(times) //等待 times 个时钟上升沿
		@(posedge clk_i)
			c=a+b; //时钟上升沿 a,b 相加
end
endtask
//调用方法:more_input(x,y,t,z); //按声明顺序,分别代表a,b,times,c

/*----------------------------------------------------------------
特殊激励信号产生描述三:输入信号产生,一次 SRAM 写信号产生
----------------------------------------------------------------*/
initial
	begin
		cs_n=1; //片选无效
		wr_n=1; //写使能无效
		rd_n=1; //读使能无效
		addr=8'hxx; //地址无效
		data=8'hzz; //数据无效
		#100;
		cs_n=0; //片选有效
		wr_n=0; //写使能有效
		addr=8'hF1; //写入地址
		data=8'h2C; //写入数据
		#100;
		cs_n=1;
		wr_n=1;
		#10;
		addr=8'hxx;
		data=8'hzz;
	end
/*----------------------------------------------------------------
Testbench 中@与 wait
----------------------------------------------------------------*/
//@使用沿触发
//wait 语句都是使用电平触发
initial
	begin
		start=1'b1;
        wait(en=1'b1); // 等待直到en=1'b1执行下面的代码
		#10;
		start=1'b0;
	end

仿真控制语句及系统任务描述

/*----------------------------------------------------------------
仿真控制语句及系统任务描述
----------------------------------------------------------------*/
$stop //停止运行仿真,modelsim 中可继续仿真
$stop(n) //带参数系统任务,根据参数 0,1 或 2 不同,输出仿真信息
$finish //结束运行仿真,不可继续仿真
$finish(n) //带参数系统任务,根据参数 0,1 或 2 不同,输出仿真信息
		//0:不输出任何信息
		//1:输出当前仿真时刻和位置
		//2:输出当前仿真时刻、位置和仿真过程中用到的 memory 以及 CPU 时间的统计
$random //产生随机数
$random % n //产生范围-n 到 n 之间的随机数
{$random} % n //产生范围 0 到 n 之间的随机数

/*----------------------------------------------------------------
仿真终端显示描述
----------------------------------------------------------------*/
$monitor //仿真打印输出,大印出仿真过程中的变量,使其终端显示
			/*
				$monitor($time,,,"clk=%d reset=%d out=%d",clk,reset,out);
			*/
$display //终端打印字符串,显示仿真结果等
			/*
				$display(” Simulation start ! ");
				$display(” At time %t,input is %b%b%b,output is %b",$time,a,b,en,z);
			*/
$time //返回 64 位整型时间
$stime //返回 32 位整型时间
$realtime //实行实型模拟时间

/*----------------------------------------------------------------
文本输入方式:$readmemb/$readmemh
----------------------------------------------------------------*/
//激励具有复杂的数据结构
//verilog 提供了读入文本的系统函数
// 可读取编译好的测试模型或波形文件来测试代码
$readmemb/$readmemh("<数据文件名>",<存储器名>);
$readmemb/$readmemh("<数据文件名>",<存储器名>,<起始地址>);
$readmemb/$readmemh("<数据文件名>",<存储器名>,<起始地址>,<结束地址>);
$readmemb:/*读取二进制数据,读取文件内容只能包含:空白位置,注释行,二进制数
				数据中不能包含位宽说明和格式说明,每个数字必须是二进制数字。*/
$readmemh:/*读取十六进制数据,读取文件内容只能包含:空白位置,注释行,十六进制数
				数据中不能包含位宽说明和格式说明,每个数字必须是十六进制数字。*/
 			/*当地址出现在数据文件中,格式为@hh...h,地址与数字之间不允许空白位置,
				可出现多个地址*/
module
	reg [7:0] memory[0:3];//声明 8 个 8 位存储单元
	integer i;
	initial
		begin
			$readmemh("mem.dat",memory);//读取系统文件到存储器中的给定地址
			//显示此时存储器内容
			for(i=0;i<4;i=i+1)
				$display("Memory[%d]=%h",i,memory[i]);
		end
endmodule

/*mem.dat 文件内容
@001
AB CD
@003
A1
*/
//仿真输出为
Memory[0] = xx;
Memory[1] = AB;
Memory[2] = CD;
Memory[3] = A1;

例程

然后我们使用以上的方法写一个程序进行调试练习:

// adder.v
module add(a,b,c,d,e);// 模块接口
input [5:0] a; // 输入信号a
input [5:0] b; // 输入信号b
input [5:0] c; // 输入信号a
input [5:0] d; // 输入信号b
output[7:0] e; // 求和输出信号
wire [6:0]outa1,outa2; // 定义输出网线型
assign e = outa2+outa1; // 把两部分输出结果合并
/*
通常,我们模块的调用写法如下:
被调用的模块名字- 自定义的名字- 括号内信号
这里比如括号内的信号,.ina(ina1)
这种写法最常用,信号的顺序可以调换
另外还有一种写法没可以直接这样写
adder u1 (ina1,inb1,outa1);
这种写法必须确保信号的顺序一致,这种写法几乎没有人采用
*/
adder u1 (.ina(a),.inb(b),.outa(outa1)); // 调用adder 模块,自定义名字为u1
adder u2 (.ina(c),.inb(d),.outa(outa2)); // 调用adder 模块,自定义名字为u2
endmodule

//adder 子模块
module adder(ina,inb,outa );// 模块接口
input [5:0] ina; // ina-输入信号
input [5:0] inb; // inb-输入信号
output [6:0] outa; // outa-输入信号
assign outa = ina + inb; // 求和
endmodule // 模块结束
// tb_adder.v
`timescale 1ns / 1ps
module add_tb();
reg [5:0] a;
reg [5:0] b;
reg [5:0] c;
reg [5:0] d;
wire[7:0] e; // 输出
reg [5:0] i; //中间变量
// 调用被仿真模块模块
add uut (.a(a), .b(b),.c(c),.d(d),.e(e));
initial begin // initial 是仿真用的初始化关键词
a=0;b=0;c=0;d=0; // 必须初始化输入信号
for(i=1;i<31;i=i+1) begin
#10 ;
a = i;
b = i;
c = i;
d = i;
end // 给是输入信号a 赋值
end
initial begin
$monitor($time,,,"%d + %d + %d + %d ={%d}",a,b,c,d,e); // 信号打印输出
#500 $finish;
end
endmodule

进行仿真,得到下面的波形,结果是正确的:

在这里插入图片描述

查看 Tcl Console 也验证了结果的正确性,显示运行了 500ns:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44413191/article/details/107516772