项目代码下载
还是请大家首先准备好本项目所用的源代码。如果已经下载了,那就不用重复下载了。如果还没有下载,那么,请大家点击下方链接,来了解下载本项目的CPU源代码的方法。
准备好了项目源代码以后,我们接着去讲解。
本节前言
想要学习本节,前提是,你得是学习过我讲解取指令模块的有关章节。最好呢,你是从本系列专栏的开头,连续地,学习到这里的。否则,知识不完整,你也不知道哪里落下了,即使你来问我,我可能也不知道,你究竟是差在哪里未理解。
下面的四个链接,是我讲解取指令模块的四篇文章。
以上的四个讲解取指令模块的文章,建议没有学习的同学,还是先去学习一下。如果你学完完了上述的内容,那么,我们可以继续本文的学习。
取指令模块,它指的是【cpu_me01\code\get_instruct.v】代码文件中的模块。取指令模块的代码,我就不贴了。
验证取指令模块的代码,我这里已经是写好了,我来将其内容给贴在下面的代码块中。
`timescale 1ns/1ns
module tb_get_instruct();
reg sys_clk;
reg sys_rst_n;
reg get_inst_en;
reg [15:0] ip;
wire decode_en;
wire [15:0] instruct_code;
wire [15:0] ip_buf;
wire [15:0] instruct_code_wire;
wire rd_en;
wire rd_en_d1;
wire rd_en_d2;
reg [9:0] cnt;
assign ip_buf = get_instruct_inst.ip_buf;
assign instruct_code_wire = get_instruct_inst.instruct_code_wire;
assign rd_en = get_instruct_inst.rd_en;
assign rd_en_d1 = get_instruct_inst.rd_en_d1;
assign rd_en_d2 = get_instruct_inst.rd_en_d2;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#60
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
always @(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 10'd0;
else if (cnt < 10'd20)
cnt <= cnt + 10'd1;
else
cnt <= cnt;
always @(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
begin
get_inst_en <= 1'b0;
ip <= 16'h0000;
end
else if (cnt == 10'd3)
begin
get_inst_en <= 1'b1;
ip <= 16'h0002;
end
else
begin
get_inst_en <= 1'b0;
ip <= 16'h0000;
end
get_instruct get_instruct_inst(
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.get_inst_en(get_inst_en),
.ip(ip),
.decode_en(decode_en),
.instruct_code(instruct_code)
);
endmodule
以上,是验证取指令模块的代码内容了。接下来,我就来讲解代码。
代码讲解
端口声明,连接被验证模块
图1中的代码,是验证模块的前14行的内容。其中,第1行,这个是规定了本仿真验证模块使用的时间尺度。在【1ns/1ns】这部分语句中,左边的【1ns】代表着,本模块的1个时间单位,是1ns。右边的【1ns】用于指定仿真过程中进位取整的精度为1ns。
需要注意的是,如下的写法是错误的。
`timescale 60ns/3ns
这是因为,无论是左边的指定时间单位的数值,还是右边的指定时间精度的数值,只能采用1、10或者100,两边的数值可以相同,也可以不同。
第2行,是规定了本模块的模块名。我们的取指令模块的模块名是【get_instruct】,那么,对该模块的验证,就是在它的前面,加上【tb_】前缀,变为【tb_get_instruct】。
一般地,我们想要验证的模块是什么名字,验证该模块的模块名,都是在被验证模块的前面加上【tb_】前缀。
第3行到第8行,它们对应着取指令模块的输入输出端口。其实我在写仿真验证代码的时候,就是将取指令模块的输入输出端口给复制过来的。复制过来以后,将端口类型【input wire】或者【input】修改为【reg】,将【output wire】或者【output】修改为【wire】,就可以了。
我们可以来看一看取指令模块的端口声明部分。
我们将图2的3到9行,与图1的3到8行做一个对比,就会明白【tb_get_instruct】模块的3到8行的写法了。
我们再回到图1里面。图1的10到14行,声明了几个wire型变量,这几个变量,其实在取指令模块里面都有。我们在图1里面,也就是在【tb_get_instruct】模块里面声明这几个变量,其实是为了将验证模块与取指令模块的对应变量连接起来,以观测取指令模块中相应变量的波形变化情况。
我们来看一看取指令模块声明的几个变量。
在图3中,可以看到,在取指令模块中声明的几个变量,有reg型的,也有wire型的。而在图1的10到14行,我们声明的同名变量,均为wire型的。这是因为,我们在验证模块中声明的变量,是为了与取指令模块中的相应变脸连接起来,实时地观察取指令模块中对应变量的值。想要实现实时观测的效果,而没有仿真的延迟,那么,在仿真验证模块中,我们就需要用wire型变量,来连接取指令模块中的对应变量。
一般来讲,我们若是想要在test bench文件中观测被验证模块中的某一个变量,我们就需要再test bench中声明一个wire型变量,并且用连续赋值语句将验证模块与被验证模块中的相应的变量连接起来。wire型变量,它具备着实时性,可以用来实时观测。
既然提到了连续赋值语句,而且,我们在图1中的10到14行声明几个wire型变量,正是为了与取指令模块中的对应同名变量相连接,那么,我们肯定是在test bench中,用到了连续赋值语句。那么,我们就来看一看我们的连续赋值语句。如下面的代码块所示。
assign ip_buf = get_instruct_inst.ip_buf;
assign instruct_code_wire = get_instruct_inst.instruct_code_wire;
assign rd_en = get_instruct_inst.rd_en;
assign rd_en_d1 = get_instruct_inst.rd_en_d1;
assign rd_en_d2 = get_instruct_inst.rd_en_d2;
上面的代码块,都用到了【tb_get_instruct】模块中所实例化的取指令模块的模块名,【get_instruct_inst】。实例化取指令模块的代码,我们一会儿再看。
从上面的代码块,我们看到,我们的确是通过连续赋值语句,将本验证模块【tb_get_instruct】中的几个变量与取指令模块中的同名变量给连接起来了。
接下来呢,我们再来看一看验证模块中,实例化取指令模块的代码。如下图所示。
图4的写法,其实,也就是一种套路写法。第59行,是模块与实例名。第60行到65行,是【get_instruct】模块与本验证模块【tb_get_instruct】的变量连接情况。写这个变量连接的代码的时候,我是将取指令模块的端口声明部分,给复制过来,然后呢,将端口类型给去掉,把【[15:0]】等等的向量类型也给去掉,仅仅留下端口名。然后呢,在每一个端口名前面加上英文的句点符号。然后呢,每一行的端口右边加上括号,括号内写上本模块的要连接的变量名。
一般地,我去写验证模块的时候,我都将验证模块与被验证模块的连接变量设置为相同的名称。当然了,随着代码复杂度的增加,免不了地,以后,会有被验证模块的端口名与验证模块的连接变量名不一致的情况。
在写实例化代码的时候,在初学阶段,究竟是否要将验证模块的连接变量与被验证模块的端口名设置为相同,这个你自己来决定。我的建议是,你在跟着哪本书或者哪个教程来学习,你就先学习哪种写法。
在我们的专栏里面,在写实例化代码的时候,我倾向于,将被验证模块的端口名与验证模块的连接变量设置为相同的名称。
在图4里面,写完了实例化代码以后,在68行,是【endmodule】字样。也就是,实例化代码,位于【tb_get_instruct】的最后面的位置。
一般地,书写 test bench 代码文件的时候,实例化代码,都是写在最后的位置。至少我这里是这样子来写的。你当然可以自由地设置其书写位置,不过,我确实是这么来写的。貌似,我在学习FPGA开发版的配套教程的时候,里面也是这么写的。
某些个一般的通行的惯例,大家跟着照做就可以了。
系统时钟与系统复位信号
图5中的代码,就是系统时钟与系统复位的逻辑了。对于系统时钟,我在学习FPGA开发板配套教程的时候,一般地,都像图5所示的那样子,将时钟的初始值设置为1,然后呢,每10个时间单位变化一次,20个单位为一个时钟周期。
而对于系统复位信号,则是刚开始的时候,将其设置为0,以进行整个系统的复位操作。然后呢,过了一段时间,将其设置为1。
在我们的系统里,我们规定了,1个时间单位是1纳秒。所以,一个时钟周期为20ns,也就是本系统的工作频率,为50MHz。
1/20ns = 1/(20 × 10^(-9)) = 10^3 / (20 × 10^(-6)) = 1000 ÷ 20 ÷ (10^(-6))
= 50 × 10^6 = 50MHz
我在学习FPGA的时候,基本的时钟频率,就是50MHz。在我们的这个入门型CPU里面,我也将时钟频率,设置为50MHz。
大家还可以在图5中看到,对于系统时钟信号,我用的是阻塞型赋值运算,而对于系统复位信号,我用的是非阻塞赋值运算。大家自己在写 test bench 文件的时候,也可以这么来写。这就是一种惯例。我们所能够见到的 FPGA 开发板教程,大概也是这么来写的。
系统时钟与系统复位信号,都是test bench文件给设计模块发出的激励信号。
cnt变量与剩余的激励信号
如图6所示,在代码的16行,我声明了本测试文件所用的cnt变量。它是用来查数的,用来设置,将有效的激励信号【ip】与【get_inst_en】发送给【get_instruct】模块的时机。
我们来看一看代码。
图7里面,是关于cnt的逻辑,就是查数。系统复位信号起作用时,cnt为0。当系统处于非复位状态时,cnt开始查数,从1查到20,然后停在20这个数上。
图8是【ip】与【get_inst_en】两个变量的逻辑。系统复位信号起作用时,这俩变量的值为0。当检测到计数器cnt的值为3时,两个信号分别被赋予有效的值,【get_inst_en】被非阻塞赋值为1,【ip】被非阻塞赋值为【2】。
这俩信号的有效时间,在本测试文件里面,都是仅有1个时钟周期。过了这个有效的时钟周期以后,【ip】与【get_inst_en】又复位为0了。
在正式的逻辑里,【get_inst_en】的有效时间,的确是1个时钟周期,但【ip】信号的有效时间则并非一个时钟周期。
在设置地址的时候,我将【ip】的值设置成了2。实际上,将其设置为0到3,都是可以的。我在这个项目里,一共往存有指令的 RAM DISK 里面写入了4条指令。这4条指令位于0到3四个地址中。每一个地址,都是2字节长,而非流行的内存条那样的1字节长。
在这里,本来我也可以将激励信号【ip】设置为0的。但是,设置为0以后,大家就看不出,【ip】是何时变为有效的状态了。所以,我故意将【ip】的有效值设置为一个非零值2。你也可以修改代码,将【ip】的有效值设置为1或3,只要能与复位时的0值区分开即可。
本测试代码模块的总体逻辑
总体逻辑,有这么几点。
- 将本模块与被验证模块【get_instruct】中的端口与变量连接起来
- 给被测试模块的系统时钟与系统复位信号输入激励。本测试模块产生的系统时钟与系统复位信号,作为被测试模块【get_instruct】的激励。
- 设置计数器变量cnt,并在某一个时钟周期里,将有效的指令指针信号【ip】与取指令使能信号【get_inst_en】发送给【get_instruct】模块。
上机仿真
代码与总体逻辑,我都讲完了。接下来,我们准备上机仿真。
首先呢,我们还是要新建一个代码文件,文件名为【tb_get_instruct.v】,将这个代码文件放置在【cpu_me01\test_bench\】路径里面。代码文件的内容,本篇文章开头的代码块里的内容就是,大家将其复制到代码文件中就可以了。
关于新建Verilog代码文件,尤其需要注意的是它的文件格式的问题。因为,FPGA的开发平台,有两个软件是比较流行的,一个是Vivado,另一个是Quartus II。而这俩软件在读取代码文件时,所用的文本编码格式是不同的。
为了防止代码的编码格式而带来的小问题,大家可以参照着你学习FPGA开发板的配套教程里面的内容,来设置你的Notepad++的编码格式。你也可以参照着下面的文章链接,来设置编码格式,新建与保存代码文件。
如果上面的替换文字所示,请参阅链接中的第二分节的内容,来设置编码格式,建立与保存代码文件。
注意,文件的保存位置,为【cpu_me01\test_bench\】里面。
做好了代码编辑与保存的工作以后,我们来上机仿真。
上机仿真
第1步,打开Quartus II 13.1软件并打开了本项目以后,首先来设置以下项目的顶层模块,将顶层模块设置为设计块【get_instruct】。我们依次选择【Assignments】,【Settings】菜单项,过程如下图所示。
在图10对话框的左侧,点击选择【General】选项,结果如下图所示。
然后呢,在图11所示的对话框里面,找到下图所示的区域。
在红色框线中,输入【get_instruct】,结果如下图所示
然后点击右下角的【OK】按钮。
点击了OK按钮以后,上面的设置会生效,并且会退出对话框。
第2步,新建测试平台。
我们依次选择【Assignments】,【Settings】菜单项,在弹出的对话框的左侧的栏目里,选择【Simulation】,过程如下图所示。
在图15里面的右边,找到下图所示的区域。
在红色框线所示的右侧的框框里,点击右侧的下拉三角号,并保持与图16同样的选择。
然后在图15里面,找到下图所示的区域。
在图17里面,左边有几个单选按钮,请点选中间的单选按钮,跟图17保持一致。然后点击右侧的红色框线所示的按钮,结果如下图所示。
在图18里面,我们点击右侧的【New】按钮,以新建测试平台。结果如下图所示。
在图19的上方,有【Test bench name】一项,我们在它右边的文本框中输入【tb_get_instruct】。结果如下图所示。
之所以输入这个名,是因为,我们本分节所新建的文件名与模块名,都是这个名字。一般地,新建测试平台的时候,都要将测试平台的名字,设置为test bench 代码文件的测试模块名。
设置完了以后,我们再来看看图19所示的对话框中,如下图所示的区域。
这个区域里,有两个单选按钮,我们点选下方的单选按钮,结果如下图所示。
然后呢,【End simulation at:】的紧接着的右边的文本框里,我们可以用来设置仿真时间的数值,最右边的框框里,可以用来选择仿真的时间单位。我们分别将两个框的内容设置为【600】和【ns】,结果如下图所示。
然后呢,我们再来看图19所示的对话框中,如下图所示的区域。
这个对话框,是用来添加测试模块的代码文件的。在【File name】的右边是一个文本框,再往右,是【...】按钮。我们点击三个点按钮,结果如下图所示。
在图25所示的对话框里,我们浏览到【cpu_me01\test_bench\】中的【tb_get_instruct.v】代码文件,然后点击右下角的【Open】按钮,如下图所示。
然后呢,在图27里面,点击右边的【Add】按钮。接着再点击下方的【OK】按钮,如下图所示。
我们再继续点击几次【OK】按钮,直到来到下图所示的对话框。
在图29的右边,我们找到下图所示的区域。
在图30的红色框线所示的区域,有一个下拉三角,我们点击这个三角号,如下图所示。
在图31中,出现了【tb_get_instruct】这一选项,这个就是我们刚刚建立的【tb_get_instruct】测试平台。我们点击选择【tb_get_instruct】,结果如下图所示。
从图32中,我们看到,文本框中的内容,已经变为了【tb_get_instruct】了。这个文本框,是用来设置测试平台的。一个设计项目,可以连接的测试平台有很多。本节,我们是想要将【tb_get_instruct】设置为测试平台。进行好了上述的设置以后,我们点击对话框右下角的【OK】按钮,回到软件的主界面。
第3步,重新编译代码。
我们在软件主界面里找到下述区域。
我们右击图33中的红色框线所示的【分析 & 综合】选项,在弹出的对话框中,点击【Start】选项。或者,如果【Start】选项为灰色显示的情况下,就在弹出的对话框中点击【Start Again】选项。然后呢,看下方的窗口显示。如果有错误,会显示错误信息。我这里的显示结果如下。
终点看红色框线所示的错误与警告信息。错误信息若不是为0,则我们需要修改代码中的错误。如果错误为0,而警告不为0.那么,很多时候,警告信息我们是可以忽略的。此处,我们忽略警告信息。
接下来,我们依次点击【Tools】,【Run Simulation Tool】,【RTL Simulation】,然后,如果你绑定好了 ModelSim 与 Quartus II,则Quartus II 软件会启动 ModelSim 软件。结果如下。
我们来终点关注下方的选项卡。
我们点击【Wave】选项卡,结果如下图所示。
在图37中,我们找到下图所示的区域。
红色框线所示的按钮,为全局当大按钮,点击它,结果如下图所示。
在图39的左边,有这样的区域,如下图所示。
图40中的区域,为信号列表区域,左边是信号所在的文件与信号名,右边是变量类型。中间的,由红色框线标识的粗竖线,可以拖动它,来调整信号名与变量类型所占据的宽度。由图40来看,信号名一列,宽度小了,致使信号名未能完整地显示。我们将红色框线所示的粗竖线向右拖动一些,结果如下。
这回呢,信号名一列的宽度是够了。可是,在波形图里面,各个波形的显示类型,都是十六进制的。我们希望,将它们设置为十进制的显示格式。具体地,我们需要让信号名中的【cnt】的显示格式设置为十进制。我们在图41中最下方的【cnt】变量上,点击鼠标右键,在弹出的菜单中,依次选择【Radix】,【Decimal】,如下图所示。
选择完了以后,我们再去看右侧的波形图,则cnt字段已经是变为10进制的显示了。如下图所示。
接下来,我们在波形图窗口里,用鼠标左键单击一下cnt值为2的竖直区域里面的某一个位置,调出黄色标记线,使其显示在cnt的值为2的区域中,如下图所示。
然后我们点击几次所所示区域的红色框线所示的放大按钮,
将波形图放大到合适的大小,如下面的图所示。
从图46可以看到,在cnt变为3的那个时钟上升沿,
箭头所示的那一行波形,值还是为0。在cnt变为4的上升沿,箭头所指示的那一行的波形变为1。箭头所示的这一信号波形,为【get_inst_en】信号,取指令使能。
为啥取指令使能信号在cnt变为4的那个时钟上升沿变为高电平,而不是cnt变为3的那个上升沿变为高电平,这个属于是非阻塞赋值的知识。
对于非阻塞赋值,此刻,我就当作是大家已经掌握了。如果有些印象模糊,可以阅读下述链接文章,以了解非阻塞赋值的知识。
额,总是讲解以前讲过的知识,我也觉得烦。以后,再涉及讲过了的知识的时候,我会酌情略去一些个细节。
然后呢,我们来关注一下下图所示的区域。
在图47里面,最左边的红色框线,它是【ip】信号,它是cnt变为4的上升沿变为2,之前与之后,都为0。从左往右数的第2个红色框线,在cnt变为5的上升沿,它变为2,它是【ip_buf】信号。最下方的红色框线,它是【instruct_code_wire】信号,它在cnt变为6的上升沿变为0x2010的。最右边的红色框线,它是【get_instruct】模块中的输出信号【instruct_code】,它在cnt变为7的上升沿变为0x2010的。
其余的信号,还有rd_en,rd_en_d1,rd_en_d2,请大家自行分析波形吧。
结束语
本节的内容并不难。但是,我在讲解的时候,我还是觉得不容易讲解。希望大家能听懂吧。祝大家学习愉快。
标签:入门,get,取指令,代码,instruct,模块,所示,CPU From: https://blog.csdn.net/2401_82825368/article/details/142884909