项目代码下载
请大家首先准备好本项目所用的源代码。如果已经下载了,那就不用重复下载了。如果还没有下载,那么,请大家点击下方链接,来了解下载本项目的CPU源代码的方法。
下载本项目代码
准备好了项目源代码以后,我们接着去讲解。
本节前言
上一节,我主要是讲解了寄存器写操作。本节,我开始来讲解寄存器读操作。能否写完,这不好说。上一节的字数实在是多。写完以后,我是觉得挺耗费时间的。
本节代码文件,主要是指位于【......cpu_me01\code\Ctrl_Center\】路径里面的【register_element.v】。
一. 寄存器读使能信号
/*********************************************
ctrl_sig_inner[0]:register write enable:寄存器写使能
ctrl_sig_inner[1]:register read enable:寄存器读使能
ctrl_sig_inner[2]:random memory write ebable:内存写使能
ctrl_sig_inner[3]:random memory read enable:内存读使能
ctrl_sig_inner[4]:Arithmetic and Logic calculate:算术逻辑运算
ctrl_sig_inner[5]:reserve:保留
ctrl_sig_inner[6]:reserve:保留
ctrl_sig_inner[7]:reserve:保留
ctrl_sig_inner[8]:reserve:保留
ctrl_sig_inner[9]:reserve:保留
ctrl_sig_inner[10]:reserve:保留
ctrl_sig_inner[11]:reserve:保留
ctrl_sig_inner[12]:reserve:保留
ctrl_sig_inner[13]:reserve:保留
ctrl_sig_inner[14]:reserve:保留
ctrl_sig_inner[15]:reserve:保留
还有一种运算叫做读取立即数,将立即数放入内部寄存器。
此运算不需要通过内部信号的参与。
************************************************/
在图1中,我们主要看行号39所示的注释代码。也就是下面的东西。
ctrl_sig_inner[1]:register read enable:寄存器读使能
这一行代码的含义是,当内部控制信号总线的位1为1的时候,它是代表着寄存器读使能信号。也就是,控制中心要从某一个通用寄存器里面读取数据了。
二. 变量介绍
我们来看一看寄存器元素模块的代码。
在图2里面,我是展示了两处红色框线。这两处红色框线所示的变量,就是我们本节所要关注的变量了。注意,上一节所用的【work_ok_represent】,我们本节依然是要使用的。因为,寄存器读操作完成了以后,我们还是需要向控制中心传递工作完成信号。
上一节,我们是没有使用19行的【data_sig_represent】的,因为上一节,我们不需要使用它。。而在本节,可就必须要去使用了。具体用法,下面会有讲解。
三个【get_time】组的节拍变量,使我们本节的特色。由于本节不涉及写操作,所以呢,本节,我们不使用【write_time】组的节拍变量。
而reg_id,这个肯定是要使用的。寄存器写操作需要指定写入哪个寄存器,所以需要指定寄存器id。既然需要寄存器id号这个参数,所以呢,我们就需要去使用reg_id这个变量了。写操作时,需要有寄存器id这个参数的参与,在读操作时,我们同样需要指定寄存器id号。不然的话,一共有8个通用寄存器呢,你说说,我们到底是读取哪个通用寄存器的数据呢?
所以,无论是读寄存器的操作还是写寄存器的操作,reg_id这个变量,都是需要去使用的。
三. get_time变量的逻辑
我们来看一下get_time变量的代码逻辑。
截图上的代码字体还是显得小。我还是将代码放在下面的代码块中。
always @(posedge sys_clk or negedge sys_rst_n)if (sys_rst_n == 1'b0)get_time <= 1'b0;else if (ctrl_sig_inner[1] == 1'b1 && addr_sig_inner[3:0] == reg_id)get_time <= 1'b1;elseget_time <= 1'b0;
从图3与代码块可以看到,get_time的基本逻辑就是,默认状态之下,它是0值。只有在满足了某某条件之后,它才会是1值。而那个条件,【ctrl_sig_inner[1] == 1'b1 && addr_sig_inner[3:0] == reg_id】,它其实是由控制中心模块设置的。这个条件的满足,只维持一个时钟周期。也就是说,get_time,在平常的情况下,都是0值。只有在某某条件满足了以后,才维持着一个时钟周期的高电平。
接下来呢,我们来看一看这个条件。
ctrl_sig_inner[1] == 1'b1 && addr_sig_inner[3:0] == reg_id
这个条件的右半部分,它是在判断,内部地址信号总线【addr_sig_inner】的位3到位0的值与本模块实例的 寄存器 id 号是否相等。
条件的坐班部分,它是在判断内部控制信号总线【ctrl_sig_inner】的位1是否为1。内部控制喜好总线若是为1,它就表示着开启了寄存器读使能信号,表示将要进行寄存器读使能操作。
所以呢,这个条件表达式的意思是,如果控制中心通过在内部控制信号总线【ctrl_sig_inner】的位1写入1,其它位写0,而开启了寄存器读使能信号,并且,在内部地址信号总线上指定了8个通用寄存器的其中一个寄存器的id号,则要进行满足此条件的相关操作。
要执行的相关操作是什么呢?
就是【get_time <= 1'b1;】
这样一来,【get_time】的逻辑,我们就讲完了。我们再来看一看【get_time_d1】和【get_time_d2】的代码逻辑。
在我们的这个代码文件里,你可以找到图4所示的,关于【get_time_d1】的代码,但是你找不到【get_time_d2】的代码。实际上,我这里,根本就没有写关于【get_time_d2】的代码。
这是因为,当初在申请变量的时候,我担心变量不够用,所以,申请了【get_time_d1】和【get_time_d2】两个延时变量,而实际上却只用了一个。我个人,其实也是这种习惯。总担心资源不够用,所以,在准备阶段,总是希望多准备一些。
我自己在做菜和买菜的时候,也总是担心菜不够吃,所以呢,我倾向于多买些菜。结果呢,菜做出来,就容易做多了。得吃个三四天才能吃完。结果呢,第一顿还行,到了后面,菜热的次数多了,里面的东西都坨了,我自己也变得缺乏食欲了。但是呢,扔了还觉得可惜。
炖鸡肉,炖鱼,都是这样的。
我的老家是 在吉林省,东北有一个家常菜,叫做大拌凉菜,有的呢,也叫做家常凉菜。我自己去拌凉菜的时候,也常常是说,拌完了以后,就是一大盆子凉菜。不过呢,凉菜还好,凉菜比较下饭。一大盆子凉菜,我吃了三四天以后,假如还有的话,我还是会觉得吃得过瘾。
有空的话呢,大家可以学习着去做东北家常凉菜,或者是,亲自去东北菜馆品尝一下东北家常凉菜。当然了,最好是东北人做的家常凉菜,你可以从中体会到那种,特别地爽口,特别地下饭的那种家常凉菜。
我们接着来讲解。
从图4来看,大家也能够体会到,【get_timne_d1】,其实就是对【get_time】延时一个时钟周期,所形成的东西。所以呢,【get_timne_d1】与【get_timne】,主要的作用,就是用来计时,用来打拍子。
我们接着往下看。
四. data_sig_represent 的代码逻辑
图5讲解了【data_sig_represent】的代码逻辑。默认的情况下,它是被赋值为高阻抗z值。当【get_time】为1的时候,它被赋值为0值。而在延后一个时钟周期的【get_time_d1】为1的时候,【data_sig_represent】被赋值为data_reg的值。
在这里,data_reg,它是本代码文件的同名模块【register_element】的输出端口变量,如下图所示。
这个data_reg变量,我们在上一节讲过了,它是在寄存器写操作中,被写入数据的。在系统复位时,它是0值。这个变量的主要用途,就是用来保存数据。data_reg中的数是多少,则该通用寄存器中的数就是多少。
可以说,data_reg,它是通用寄存器的核心变量。
寄存器读操作,它的主要的目的,就是让某一个通用寄存器的模块内部的【data_reg】变量的值,传递到内部数据信号总线【data_sig_inner】中。
在图6的第7行中,我们看到,【data_sig_inner】是一个【inout】端口类型的【wire】型变量,它本身不能用于时序逻辑之中。我们为【data_sig_inner】设置了一个时序逻辑代理,如下图所示。
在图7中,在95行的位置上,我们看到,【data_sig_inner】与【data_sig_represent】绑定在了一起。这样一来,在图5里面,当【get_time】为1,从而【data_sig_represent】变量被赋值为0值的时候,【data_sig_inner】也跟着【data_sig_represent】同时变为了0值。
同理,在图5里面,在延后一个时钟周期的【get_time_d1】为1,从而【data_sig_represent】被赋值为【data_reg】的值的时候,【data_sig_inner】也跟着【data_sig_represent】同时变为了【data_reg】的值。
所以呢,图5所示的代码逻辑,本质上,其实是对【data_sig_inner】的赋值。平时的时候,【data_sig_inner】被赋值为高阻抗z值。当【get_time】为1的时候,【data_sig_inner】通过代理变量【data_sig_represent】被非阻塞赋值为0值。当【get_time_d1】为1的时候,【data_sig_inner】通过代理变量【data_sig_represent】被非阻塞赋值为【data_reg】的值。
五. 寄存器读操作的总线逻辑
我们想要读取通用寄存器的值,我们的目的,就在于通过代理变量【data_sig_represent】,将某一个通用寄存器的模块内部的【data_reg】变量的值,传递给【data_sig_inner】变量。
而在本篇文章的第四分节的讲述,我们可以看到,我们是先给【data_sig_represent】赋值为0,再赋值为【data_reg】变量的值。为啥不是直接将【data_reg】变量的值赋值给【data_sig_represent】呢?
这一点,其实,我们在寄存器写操作里面,也就是在上一节,我们已经是讲过了。
在这里,【data_sig_inner】变量是一个输入输出类型的变量,同时,它也是一种总线类型的变量。
寄存器元素模块有【data_sig_inner】成员,而8个通用寄存器是有寄存器元素模块实例化得来,所以,8个通用寄存器都会存在着【data_sig_inner】成员。而在控制中心里面,同样是存在着【data_sig_inner】成员,这些个不同的模块,它们都存在着同名的【data_sig_inner】成员,因此,【data_sig_inner】属于是一个总线。
对于总线,它的一般的逻辑是,平时的时候,所有的与之有物理连接的模块,均与之在逻辑上断开连接。这个时候,总线的状态,为高阻抗状态,其值为高阻抗z值。
而在有事的时候,则是会有一个一个模块与总线建立连接,向总线写入数值。
在寄存器读操作里面,控制中心会通过内部控制信号总线发布寄存器读使能信号,此时,所有的通用寄存器都会收到寄存器读使能信号。然而,在发布寄存器读使能信号的时候,控制中心还会在内部地址信号总线上指定寄存器id值。
在8个通用寄存器里面,仅有一个通用寄存器的 reg_id 变量的值,等于内部地址信号总线上指定的寄存器 id号,所以呢,只有与指定寄存器id号相等的寄存器,它的get_time与get_time_d1才会相继变为1,它的data_sig_inner才会被先后赋值为0值与data_reg的值。
而其余的,与内部地址信号总线上指定的id号不等的通用寄存器,它们的get_time与get_time_d1始终都会是0值,因而它们的data_sig_represent 与 data_sig_inner 始终都会是高阻抗z值,因而它们在寄存器读操作的微操作周期中,始终都是与【data_sig_inner】总线断开连接的。
在寄存器读操作里面,8个通用寄存器,与内部地址信号总线上指定的寄存器id号不等的7个通用寄存器,它们始终与【data_sig_inner】总线处于断开连接的状态。而只有与指定的寄存器id号相等的寄存器,它的模块内部的【get_time】与【get_time_d1】才会相继变为1,因而,它的模块内部的输入输出端口类型的【data_sig_inner】才会通过代理变量【data_sig_represent】被先后赋值为0值和data_reg的值。
当指定的id号的通用寄存器,向其内部的【data_sig_inner】变量赋值一个非高阻抗的值的时候,这个通用寄存器就与总线【data_sig_inner】建立了连接。
8个通用寄存器里面,有7个寄存器与【data_sig_inner】总线断开连接,只有与指定的id号相等的寄存器,才与【data_sig_inner】总线建立了连接。这个时候,建立了连接的这个通用寄存器,它向它自己模块内部的【data_sig_inner】变量写入了什么值,则【data_sig_inner】总线就等于什么值。
当响应了寄存器读操作的通用寄存器的模块内部的【get_time】为1的时候,这个寄存器的模块内部的【data_sig_inner】变量就通过代理变量【data_sig_represent】被非阻塞赋值为0值。而此时,这个通用寄存器与【data_sig_inner】总线建立了连接,【data_sig_inner】总线的值等于这个通用寄存器的模块内部的【data_sig_inner】变量的值,也变为了0值。是从高阻抗值z变为0值的。
当响应了寄存器读操作的通用寄存器的模块内部的【get_time_d1】为1的时候,这个通用寄存器的模块内部的【data_sig_inner】变量就通过代理变量的非阻塞赋值的方式,被赋值为了data_reg的值了。而此时,这个通用寄存器与【data_sig_inner】总线建立了连接,【data_sig_inner】总线的值等于这个通用寄存器的模块内部的【data_sig_inner】变量的值,也变为了【data_reg】的值。这是从之前的0值,变为现在的【data_reg】的值的。
这样一来,通过向通用寄存器的模块内部的【data_sig_represent】变量赋值,就实现了给模块内部的【data_sig_inner】变量赋值,进而实现了给【data_sig_inner】总线赋值。
值既然都传递给【data_sig_inner】总线了,那么,控制中心模块,也就可以接收到来自指定寄存器的data_reg所保存的值了。
上面提问的问题,我们还没有回答,为啥我们要先给【data_sig_inner】总线传递0值,再传递【data_reg】所保存的值呢?直接传递【data_reg】的值不是更方便直接吗?
我们需要往总线里面传递数据。总线,在通常情况下,与之有物理连接的各个线路,在逻辑上都与它处于断开连接的状态。这个时候,总线是处于高阻态的。我们想要给一个本来处于高阻态的总线,传递一个有效值,那么,我们需要在传递有效值之前,先传递一个无意义值。在本节,我们给【data_sig_inner】总线传递的0值,就属于无意义值。如果直接给【data_sig_inner】总线传递有效值,那么,【data_sig_inner】总线从高阻态直接变化为有效值,则接收方在时钟上升沿的时候,很可能检测不到这个有效值。而如果我们在给【data_sig_inner】总线传递有效值之前,先给【data_sig_inner】总线传递一个无意义的非高阻抗值,那么,【data_sig_inner】总线就可以接收到有效值了。
当然了,我们先给【data_sig_inner】总线传递无意义值0,再给【data_sig_inner】总线传递有效的来自【data_reg】的值,则中间的无意义值0,接收方依然是检测不到它的。但是,这个无意义值0,无论能否检测到,我们都不关心。我们所关心的是,要确保接收方,也就是控制中心模块,能够确定地检测到有效值,检测到来自【data_reg】的值。
涉及总线逻辑的时候,我觉得,讲起来还是挺难的。不知道,本节的讲解,大家听的怎么样。对于总线逻辑,我建议大家多参考上一节的讲解。上一节内容的链接如下。
简易CPU设计入门:本系统中的通用寄存器(四)-CSDN博客
六. 通过 work_ok_inner 变量传递完成信号
我们来看代码截图。
在图8里面,我们本节需要重点关注的,是红色框线所示的部分。从图8的红色框线部分可以看到,对于寄存器读操作来讲,平时的时候,work_ok_represent 都是处于高阻态的。当【get_time】为1的时候,【work_ok_represent 】被非阻塞赋值为0。当【get_time_d1】为1的时候,【work_ok_represent 】被非阻塞赋值为1。
通过下图所示的代码,我们可以知道,【work_ok_represent】变量是【work_ok_inner】的时序逻辑代理。
也就是,当【get_time】为1的时候,【work_ok_inner】通过代理变量【work_ok_represent 】被非阻塞赋值为0。当【get_time_d1】为1的时候,【work_ok_inner】通过代理变量【work_ok_represent 】被非阻塞赋值为1。
【work_ok_inner】变量,通过上一节的学习,我们已经知道,它属于是一个总线类型的变量。8个通用寄存器的模块内部都有这个变量,控制中心里面也有。8个通用寄存器与控制中心,共享【work_ok_inner】总线。
这样一来,当初控制中心在内部地址信号总线上指定寄存器 id 号时,与这个id号相等的寄存器,当这个指定的寄存器通过模块内部的【work_ok_represent 】变量给模块内部的【work_ok_inner】变量先后赋值为0和1的时候,【work_ok_inner】总线也就相继变为了0值和1值。
当【work_ok_inner】总线变为1值的时候,控制中心就可以检测到这个1值了。当控制中心检测到【work_ok_inner】总线为1的时候,就证明,寄存器读操作完成了。
所以呢,各位,我们本节的讲解,差不多就该结束了。
结束语
这两节的篇幅,我认为是比较长的。我个人在讲解的时候,我也觉得,讲得挺费劲的。
但愿大家能够学好这两节的内容吧。
祝大家学习愉快。