项目代码下载
还是请大家首先准备好本项目所用的源代码。如果已经下载了,那就不用重复下载了。如果还没有下载,那么,请大家点击下方链接,来了解下载本项目的CPU源代码的方法。
下载本项目代码
准备好了项目源代码以后,我们接着去讲解。
一. RAM IP 核在取指令模块中的实例引用
在前面的几节里面,我讲解了取指令模块的端口与变量,讲解了RAM IP核的生成等等。本小节呢,我们来看一看,RAM IP核在本模块中的实例引用。
请大家定位到【\cpu_me01\code\get_instruct.v】,查看里面的 RAM IP 核实例引用部分的代码。
在图1里面,RAM IP核的模块当中,有时钟信号【clock】与异步复位信号【aclr】。时钟信号,这个就是用我们的系统复位信号【sys_clk】来连接就行了。
在我们的系统中,系统时钟信号的频率,是设置为50MHz。
RAM IP核里面的复位信号,其实我们也是用系统复位信号【sys_rst_n】来连接的。只不过呢,在我们的一般的设计模块的代码中,我们的复位,是低电平有效复位,而 RAM IP 核中的复位信号,是高电平有效复位。所以呢,我们在图1的65行里面,是用【~sys_rst_n】来作为【.aclr()】的括号里面的信号的。
然后呢,图1的66行,是地址信号,这个呢,我们用本模块的【ip_buf】来来连接。
我们还是来略微回顾一下【ip_buf】的逻辑。如下图所示。
由图2核图3可知,【ip_buf】是在输入信号中的【get_inst_en】变为1以后,通过非阻塞赋值的方式,来缓存输入信号中的指令指针信号【i[p】的。
地址信号,无论我们是从RAM中读取数据,还是向RAM中写入数据,都需要指定地址。那么,这个地址信号呢,就是来自于我们的输入信号中的指令指针信号【ip】。当然了,直接连接到RAM IP核的实例中的信号,是输入信号【ip】的缓存信号【ip_buf】。
然后呢,我们再来看图1中除了时钟、异步复位和地址信号之外,剩下的信号。剩下的信号,分为两组。
图1的68行中的【data】信号和70行的【wren】信号,都是 RAM IP核实例中的信号,这俩信号,用于向 RAM 写入数据。想要写的数据,要传递给data信号。什么时候想要写数据了,就给写使能信号【wren】设置为1,就可以了。要注意的是,当我们想要写入数据的时候,必须要确保写使能【wren】、待写入的数据【data】、地址信号【address】同时有效。不能说,写使能为1的时候,待写入数据和地址信号里面的值是无效的值,那是不行的。
在这里,我们生成的这个RAM IP核,我们的目的,不是要往里面写入数据,而是为了从里面读取指令信息,里面存储的,是我们要去译码与执行的指令码。只是为了从中读取信息,而不会往里面写入数据。
所以呢,在这里,我们已经可以确定,写使能信号【wren】,肯定会是始终为0的。所以呢,在图1中的70行里面,我们传递给【wren】信号的值,固定为【1'b0】。
而待写入的数据【data】,这个其实写啥都行。因为写使能信号始终为0,这样一来,不管我们往【data】信号里面传入什么值,它都写不进去。所以,这一个待写入的数据【data】,我们给它传什么值都行。不过呢,为了提醒自己说,我们不会往里面写入数据,所以呢,我就传递了一个高阻抗的值【16'hz】。
关于这个高阻抗值,其实它是一个很有意思的东西。在这里呢,我暂时不想展开讨论。以后有机会,我会来讨论这个高阻抗的问题。这里,先留着不讲。
这样一来,图1里面,就只剩下两个信号了,分别是69行的【rden】和71行的【q】信号。
以我的为数不多的使用经验来讲,在Quartus II 13.1里面,好多的 IP 核的输出数据信号的名字,都是【q】。在本模块中的 RAM IP 核实例中,也是如此,【q】代表着输出数据。
在图1里面,大家可以看到,我们用本模块的【rd_en】来连接 IP 核实例中的【rden】信号,也就是,读使能对应着读使能。我们还用本模块的wire型变量【instruct_code_wire】来连接着 IP 核中的输出信号【q】。
我们要从RAM中读取的数据,是指令码数据,所以呢,我们就将接收这个数据的变量名,命名为【instruct_code_wire】了。结尾的【wire】表明这是一个wire型变量。
二. reg与wire类型知识点小复习
有一个问题哈,我们可否用reg型变量,来接收 IP 核实例中的输出数据信号【q】呢?
不可以的。
我们来复习一下Verilog里面,关于端口连接规则的知识点。
- 输入端口:从模块内部来讲,本模块的输入端口必须为线网数据类型。从模块外部来讲,连接到本模块的输入端口的变量,可以是reg型,也可以是线网类型。
- 输出端口:从模块内部来讲,本模块的输出端口可以是reg型变量,也可以是线网类型的变量。从模块外部来讲,本模块的输出端口所连接到的外部的变量,必须为线网类型的变量,而不能是reg类型。
- 输入/输出端口:从模块内部来讲,本模块的输入/输出端口必须为线网数据类型,而不可以是reg型。从模块外部来看,本模块的输入/输出端口所连接到的外部的变量,也必须是线网类型
- 关于线网类型:我们平常所说的线网类型,常常是指wire型。实际上,在Verilog里面,线网类型不止有wire型,还包括wand,wor,tri,triand,triort 和 trireg 等等。在初学阶段,我们只需要关注wire类型就可以了,暂时地可以将线网与wire看作是完全相同的概念。然而,随着我们的学习经验的增长,当我们想要进阶学习Verilog,以便可以更加灵活地去设计芯片的时候,我们需要渐渐地去接触Verilog里面的一些个平时不常用的知识,包括线网的其余的数据类型。
Verilog的知识点,我的感觉,它是非常地零散细碎的。建议大家可以建立一个备忘录,学习笔记啥的。或者,你可以在自己的电脑上,设置一个文件夹,专门用来记录Verilog的一些个零散的知识点。注意哦,这种记录,不是要你全面地记录,不是要你去写书。是把你觉得比较零散,容易忘记,或者是真的比较重要的东西,给记录下来。
三. 指令码信号的导出与译码使能
指令码信号,就是我们在第一分节的末尾提到的【instruct_code_wire】,我们来看看下图所示的代码。
如图4所示,第13行里面,我们声明了指令码信号。
我们再来看一看,我们是如何将其导出的。
如图5所示,我们在一个always块里面,同时处理了译码使能信号【decode_en】和指令码信号【instruct_code】。指令码信号【instruct_code】,它是本模块的输出端口,是reg类型的一个变量。大家可以回到上面的图3去看一看指令码信号的端口声明情况。
当系统复位信号到来的时候,译码使能信号【decode_en】与指令码信号【instruct_code】均被设置为0。然后呢,当检测到rd_en_d1信号为1的时候,译码使能被非阻塞赋值为有效的高电平,而指令码信号【instruct_code】被非阻塞赋值为【instruct_code_wire】。
也就是,当检测到【rd_en_d1】为1的时候,wire型变量【instruct_code_wire】,IP核的输出数据信号【q】所连接到的【instruct_code_wire】变量,被赋值给了输出端口【instruct_code】,予以导出了。
然后呢,在else分支里面,我们看到,指令码信号【instruct_code】保持不变,而译码使能信号【decode_en】清零了。也就是,译码使能信号,仅仅维持了一个时钟周期的有效高电平,且仅仅是检测到【rd_en_d1】为1的时候,才维持着一个时钟周期的高电平。
在本系统里面,这种仅维持一个时钟周期的高电平的信号,是很多的。请大家注意这种代码的特点。这种信号,一般地,都是说,复位信号起作用时清零,else分支里面也是清零,仅仅是符合某一个条件的时候才会变为1,并且所检测的那个条件也仅仅是维持着一个时钟周期的高电平。
关于【rd_en_d1】,其实它是【rd_en】信号延时一个时钟周期的信号副本。我们来看下面的代码来回顾这一点。
关于rd_en,rd_en_d1与rd_en_d2的延时逻辑,我在之前的章节里面有讲过。忘记了的,请大家点击下述链接来复习这种延时逻辑。
rd_en的延时逻辑
到了这里,我们差不多应该梳理一下取指令模块的执行逻辑了。
四. 取指令模块的总体执行逻辑
所谓的总体执行逻辑,指的是波形时序变化情况。
想要研究某一个模块的波形时序变化,最好是用波形时序图来表示。然而,在我这里,我不太会画波形图。即使能画好波形图,我大概也是更喜欢用文字或者是电子表格的形式来说明问题。
在本模块里面,开始工作的起始点,是取指令使能信号变为有效的高电平。在我们的系统里面,每一个指令的执行周期里面,取指令使能信号仅有一次变为高电平的时候,且仅仅维持一个时钟周期的高电平。
取指令使能信号,就是输入端口中的【get_inst_en】变量。
然后呢,另外的两个标识着时序的变量,是【rd_en】与【rd_en_d1】
我们来将这个逻辑给展现出来。
首先呢,外部模块传入的取指令使能信号【get_inst_en】变为高电平。
当检测到【get_inst_en】变为高电平以后,【ip_buf】通过非阻塞赋值的方式缓存输入端口【ip】的值,【rd_en】则是被非阻塞赋值为1。
【rd_en】与【ip_buf】,分别连接到 RAM IP 核实例的读使能信号【rden】与地址信号【address】。
在我们的系统中,我们所生成的 RAM IP 核,它是说,当某一个时钟上升沿检测到rden为高电平以后,在下一个时钟上升沿到来时,数据就会通过输出端口【q】予以输出。在我们的代码里,接收【q】的数据的,是wire型变量【instruct_code_wire】。
在【get_inst_en】变为高电平的下一个时钟上升沿,【rd_en】变为1。【rd_en】的有效高电平会传给 RAM IP 核实例。RAM IP 核实例会去进行着读取数据的操作。同时,在检测到【rd_en】为1以后,rd_en_d1会被非阻塞赋值为1。
在rd_en变为高电平的下一个时钟上升沿,rd_en_d1变为高电平,这时 RAM IP 核的数据已经出现在数据输出端口【q】里面了。【q】的数据由【instruct_code_wire】实时接收。由于【instruct_code_wire】变量里面已经存放着有效的指令码数据了,因此,将指令码信号传给译码器以进行译码的条件已经成熟。所以,当检测到【rd_en_d1】为1的时候,译码使能信号【decode_en】被非阻塞赋值为1,本模块的输出端口【instruct_code】被非阻塞赋值为【instruct_code_wire】。
以上,就是取指令模块的代码执行逻辑了。
不知道大家听懂了没,接下来,我再将上面所说的东西,用流程图来略作整理。流程图如下。
结束语
到了这里,取指令模块,其实就差不多是讲完了的。其实还有一点小内容没有讲。那就是,我在生成RAM IP核的同时,我也创建了一个内存初始化文件,用来为 RAM DISK写入一些个固定的指令码。
关于内存初始化文件的写入方法,我就先不讲了。等到讲完了译码模块以后,我们再来将内存初始化文件的问题。那个时候,我们再来看看,我们究竟是往RAM DISK里面写了什么指令码。
本节占用的课节数不少。希望大家能跟上。
在本节,我大概是第一次使用流程图。以后,可能我会多去使用流程图的。希望大家能够渐渐地适应我的讲课方法,学会去看流程图,表格啥的。