欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 游戏 > 简易CPU设计入门:指令单元(四)

简易CPU设计入门:指令单元(四)

2025/2/5 11:26:12 来源:https://blog.csdn.net/2401_82825368/article/details/145433683  浏览:    关键词:简易CPU设计入门:指令单元(四)

项目代码下载

请大家首先准备好本项目所用的源代码。如果已经下载了,那就不用重复下载了。如果还没有下载,那么,请大家点击下方链接,来了解下载本项目的CPU源代码的方法。

CSDN文章:下载本项目代码

上述链接为本项目所依据的版本。

在讲解过程中,我还时不时地发现自己在讲解与注释上的一些个错误。有时,我还会添加一点新的资料。在这里,我将动态更新的代码版本发在下面的链接中。

Gitee项目:简易CPU设计入门项目代码:

讲课的时候,我主要依据的是CSDN文章链接。然后呢,如果你为了获得我的最近更新的版本,那就请在Gitee项目链接里下载代码。

准备好了项目源代码以后,我们接着去讲解。

本节前言

在前两节,我讲解了本系统所支持的两条指令,【sdal】和【str】指令。讲完了这俩指令以后,我们还剩下两个指令没有讲。这俩没有讲的指令,一个是加法指令,一个是减法指令。

我们还是来看一下本系统所支持的指令表。

图1,指令表

本节,我们要去讲解的,是【add】指令。

这一指令所在的文件,为位于【......\cpu_me01\code\Instruct_Unit\】路径里面的【add.v】代码文件。

好了,我们开始正文。

一.    指令功能

【add】指令的格式如下:

Add r

其功能为:dram的r单元的数与累加器da相加,结果放在da中

本条指令的操作码,为 0B00010 。

我们接着往下学习。

二.    端口列表

图2,端口列表

图2所示,便是【sdal】模块的端口列表。其实它与【instruct_unit】的端口列表,是一模一样的。那么,对于图1中所示的端口列表的基本介绍,请大家参考下面的链接所示的介绍。

简易CPU设计入门:指令单元(一)-CSDN博客

在上述链接所示文章的第一分节中,就有介绍端口列表。

在你了解了端口列表的内容以后,我们接着往下看。

三.    关于指令的有限状态机

在硬件编程中,大概,有限状态机,是一个很重要的东西。在理论知识的学习中,我们知道,有限状态机,可以分为两种类型。一种是摩尔类型,一种是米里类型。这俩类型,分别是啥意思,请大家看相关的理论书籍。这种理论知识,在数字电路教材中,在Verilog 教材中,应该是都有讲解。

在我这里,我不对两种类型的有限状态机的概念进行细讲。因为,我在学习的时候,对这两种有限状态机,也学得迷迷糊糊。不过呢,虽说学的时候,对概念掌握的不清晰。但是呢,不影响我去写具体的代码。写代码的时候,基本上,我不会考虑说,要使用摩尔类型的有限状态机,还是要去使用米里类型的有限状态机。

在实际使用有限状态机的时候,我只是根据自己的设计,看看,要将整个工作,划分为多少个执行状态,然后呢,各个状态,要分别执行什么任务,以及,不同的状态之间,要如何切换。实际写代码的时候,考虑的是这些个问题。

不过,虽说,我自己暂时地,对有限状态机的不同类型理解不清晰,但是呢,有条件的话,最好呢,你自己还是需要将这种概念,给理解清楚。

理论问题,有理论问题的作用。搞清楚了理论之后,更方便你去谋划,分析,决策。在这里,我还是偷个懒。这会儿,我不想去深究两种类型的有限状态机的具体概念与区别。

我们来看一下,在本节,我将【add】指令划分为了多少个状态。

图3,add 指令的有限状态机

在图3里面,我是采用了【localparam】关键字,设置了几个本地参数,将【add】指令,划分为了八个状态。

第一个状态,是【IDLE】状态。这个状态,它是一个闲置状态。也就是,本指令没有被激活的时候,或者说,CPU目前没有运行【add】指令的时候,那么,【add】模块就处于【IDLE】状态。它是一个闲置状态,是一个尚未运行的状态。从图3的19行代码来看,我们将【IDLE】这个本地参数,定义为 0 。

关于这个【idle】,在硬件编程里,我们会接触到这个东西。其实呢,在软件编程里面,我们同样会接触到这个东西。大家心里有个数就行。以便呢,以后大家去学习软件课程的时候,见到这个【idle】,能够有一个印象。

第二个状态,是【UPDATE_IP】状态。什么叫做更新 IP?IP就是指令指针【instruct pointer】的意思。在CPU执行取指令操作的时候,IP 的值是多少,我们的这个 CPU,就会到哪个指令内存地址里面,去获取指令码。在我们的这个系统里,更新 IP,统一地,都是将当前的指令指针 IP 的值,加1,指向下一条指令内存地址。由图3中的第20行代码可知,本地参数【UPDATE_IP】的值,是 1 。

在英特尔8086CPU之中,更新 IP,可以有多种方式。将指令指针加上本条指令的长度,这个是一种方式。还有其他的方式,以适应于条件转移指令。在这里,我不想对条件转移展开讨论。因为有点麻烦。等以后,我去写 6502 CPU 的时候,我再去细琢磨条件转移指令的视线方式。在此处,我不想去细琢磨,也不想细讲。实际上,这会儿,我就是去讲,估计也暂时讲不清楚。

对于这个更新 IP,大家需要了解的就是,我们的这个项目,统一采用的,是一种简便的方式,都是将现有的指令指针  IP 的值加1,指向下一条指令。具体的逻辑,我们在下面的讲解里面,再去细研究。

第三个状态,是【DA_TO_IN1】。由图3中的21行代码可知,本地参数【DA_TO_IN1】的值,是 2 。我略微解释一下【DA_TO_IN1】的概念,DA,就是累加器的名字,是本系统中的八个通用寄存器中的0号寄存器。IN1,它是1号内部寄存器的意思。IN 就是【inner register】的意思。1呢,就是说,本次使用的是1号内部寄存器,1代表着本次使用的内部寄存器的索引号。

第三个状态【DA_TO_IN1】,它的意思就是,将累加器中的数据,传递给 1 号内部寄存器。

第四个状态,是【RAM_TO_IN0】。由图3中的22行代码可知,本地参数【RAM_TO_IN0】的值,是 3 。第四个状态的任务,是将RAM内存单元中的数据,传递给 0 号内部寄存器。

第五个状态,是【ARI_LOG_TO_IN2】,由图3中的23行的代码可知,本地参数【ARI_LOG_TO_IN2】的值,是 4 。本状态的任务是,对 0 号与 1 号内部寄存器里的数据,执行算术逻辑运算中的加法运算,结果放在 2 号内部寄存器中。

第六个状态,是【IN2_TO_REG】,由图3中的24行代码可知,本地参数【IN2_TO_REG】的值是 5 。本状态的任务是,将 2 号内部寄存器中的数据,传递给通用寄存器之中。再具体一些,是传递给 0 号通用寄存器,也就是传递给累加器 da 。

在【add】指令模块中,第三状态到第六状态,是本指令的核心步骤。第三状态和第四状态,它们分别将加法运算中的两个加数加载到 0 号与 1 号内部寄存器之中。第五状态,是将 0 号内部寄存器与 1 号内部寄存器中的数据,也就是将加法运算中的两个加数,进行加法运算,然后将运算结果存放在 2 号内部寄存器之中。第六状态,是将 2 号内部寄存器中的加法运算结果,传递给 0 号通用寄存器。

我们接着往下看。

第七个状态和第八个状态,都是用来完成指令的执行,也就是用来结束本条指令的执行的。它们分别是【INSTRUCT_DONE0】和【INSTRUCT_DONE】状态,对应的值分别为 6 和 7 。

四.    局部变量

我们来看一下本代码文件的局部变量。

图4,局部变量

图4中的第28行代码,它是用来指示本条指令的操作码的。想要指示本条指令的操作码,不一定非得是采用wire型变量,也可以采用宏代码,用【`define】来定义一个宏,这个是可以的。其实也可以用【parameter】或者【localparam】关键字,来定义可覆盖参数或者不可被覆盖的本地参数。通过定义参数的方式,也可以指示本条指令的操作码。

不过呢,在我这里,我还是采用了wire型变量,并且呢,将其赋值为一个固定的常数。根据图4的41行代码,我们是将【op_code_this】变量赋值为了【5'b00010】,它和本节文章中的第一分节中谈到的操作码,是一致的。

图4中的第29行和第30行代码,它们是两个缓存变量,分别用于将输入端口中的【reserve_bit_receive】和【op_rand_receive】给缓存下来。我们在下面的讲解中,仍将会看到这俩缓存变量的讲解。

图4中的第32行代码,是状态变量。它会被赋值为本篇文章的第三分节中的几个状态中的某一个。闲来无事之时,状态变量【state】会被赋值为【IDLE】状态。

图4中的第33行代码,是计数变量。计数,英文单词为【count】,不过,在代码中,常常将其简写为【cnt】。这个计数变量,它在指令的有限状态机的每一个状态开始的时候,它都会被清零。这个变量很有用。具体啥用处,后面会有讲解。

图4中的34行代码,是忙标志。忙标志为 1,就表示本条指令在工作之中。如果为 0,则表示本条指令处于闲置未工作的状态。只有在某一个指令处于忙标志为1 的状态之时,这个指令的各个状态才会有实际的工作,以及会进行状态的转换。如果某一个指令单元的忙标志为 0,则不会有指令状态的跳转,也不会执行对应的状态机中的工作任务。

图4中的36行到39行,是几个代理变量。根据图3中的42行到45行代码,36到39行所示的四个代理变量,分别是对端口列表中的控制总线,地址总线和数据总线的代理,和对端口【job_ok】的代理。

所谓的代理变量,它是指,某某wire型变量,本身不可以直接在过程赋值语句之中,参与组合逻辑与时序逻辑。但是呢,我们可以设置与之对应的 reg 型变量,让 reg 型变量在过程赋值语句之中,参与组合逻辑与时序逻辑。然后呢,将 wire 型变量与 reg 型变量,通过 assign 语句,通过这种数据流语句逻辑,将 reg 型变量和 wire 型变量绑定在一起。这样一来,我们就说,某某 reg 型变量是对应的 wire 型变量的代理变量。

代理变量的概念,是我起的名字。所以呢,你在其他人那里,大概是不方便去使用这种概念的,因为不通用。

这样一来呢,本条指令单元的局部变量,我也讲完了。我们接着往下看。

五.    缓存保留位与操作数

图5
always @(posedge sys_clk or negedge sys_rst_n)if (sys_rst_n == 1'b0)beginreserve_bit_buf <= 3'h0;op_rand_buf <= 8'h0;endelse if (exe_en == 1'b1 && op_code_this == op_code_receive)beginreserve_bit_buf <= reserve_bit_receive;op_rand_buf <= op_rand_receive;endelsebeginreserve_bit_buf <= reserve_bit_buf;op_rand_buf <= op_rand_buf;end

从图5可以看出,缓存变量【reserve_bit_buf】和【op_rand_buf】的逻辑是,在系统复位时,它们俩被清零。在【else】分支中,也就是闲来无事之时,它们俩分别保持现有值不变。而在满足条件【exe_en == 1'b1 && op_code_this == op_code_receive】之时,这俩缓存变量分别将输入端口中的保留位【reserve_bit_receive】与操作数【op_rand_receive】给缓存下来。

在这里,【exe_en == 1'b1 && op_code_this == op_code_receive】,这个条件是什么意思呢?

【exe_en】,我们还需要到控制中心里面去看。

图6,控制中心模块中的代码

从图6可以看出,当系统复位之时,执行使能信号【exe_en】被清零,同时呢,输出给指令单元的操作码信号【op_code_out】,保留位信号【reserve_bit_out】和操作数信号【op_rand_out】也被清零了。

在处于【else】分支,也就是闲来无事之时,执行使能信号【exe_en】被清零了。而输出给指令单元的操作码信号【op_code_out】,保留位信号【reserve_bit_out】和操作数信号【op_rand_out】则是保持现有值不变。

而在译码完成信号【decode_done】为 1 之时,【exe_en】会变为高电平,输出给指令单元的操作码信号【op_code_out】,保留位信号【reserve_bit_out】和操作数信号【op_rand_out】会分别被赋予输入端口中的【op_code_in】,【reserve_bit_in】和【op_rand_in】。这两对信号,都是分别表示着操作码,保留位和操作数。后缀为【_in】的,表示说,它是由译码模块【decode_unit】传过来的输入信号。后缀为【_out】的,表示说,它是缓存了译码模块中的对应信号以后,将其公布给指令单元的输出信号。

在这里,输出给指令单元的操作码信号【op_code_out】,保留位信号【reserve_bit_out】和操作数信号【op_rand_out】的总体逻辑是,系统复位时清零,在【else】分支,也就是闲来无事之时,保持现有值不变。只有在译码完成信号为 1 之时,才会被更新。

而【exe_en】的总体逻辑是,在系统复位与【else】分支里面,它都是 0 值。只有在译码完成信号【decode_done】为 1 之时,它才会被赋值为 1 。而译码完成信号【decode_done】仅仅会维持一个时钟的高电平,因此,【exe_en】的高电平状态也是仅仅会维持一个时钟周期而已。

在这里,译码完成信号【decode_done】是由译码单元【decode_unit】传递过来的。在译码完成信号【decode_done】为1的时候,控制中心里面的【op_code_in】,【reserve_bit_in】和【op_rand_in】会分别保存着译码单元传递过来的,一条指令码中的操作码,保留位和操作数三个部分。

如果大家还没有学习过本项目中的译码单元,那么,可以参考下述链接来学习。

简易CPU设计入门:译码模块-CSDN博客

我们还得接着说【exe_en】信号。由控制中心中的代码可以看出,在某一个指令的周期中,当完成了译码工作以后,【exe_en】会变为高电平,同时控制中心会将操作码,保留位与操作数,通过操作码信号【op_code_out】,保留位信号【reserve_bit_out】和操作数信号【op_rand_out】传递给指令单元中的【op_code_receive】,【reserve_bit_receive】和【op_rand_receive】。而【str】指令单元在检测到【exe_en】为1,且输入信号中的操作码信号【op_code_receive】与本模块中的局部wire型变量【op_code_this】的值相同之时,本模块会将保留位【reserve_bit_receive】与操作数【op_rand_receive】缓存到【reserve_bit_buf】与【op_rand_buf】之中。

在这里,【exe_en】为 1 ,代表着说要开启某一条指令的执行了。那么,要去执行的是哪一条指令呢?这要通过操作码来区分。当指令执行使能信号【exe_en】为1,且待执行的指令的操作码【op_code_receive】与【add】指令单元中的本地操作码变量【op_code_this】中的值【5'b00010】相同的时候,那么,就来进行着保留位与操作数的缓存操作。

缓存保留位与操作数的讲解任务,我就完成了。我们接着往下看。

六.    忙标志的逻辑

图7
always @(posedge sys_clk or negedge sys_rst_n)if (sys_rst_n == 1'b0)busy_flag <= 1'b0;else if (exe_en == 1'b1 && op_code_this == op_code_receive)busy_flag <= 1'b1;else if (state == INSTRUCT_DONE)busy_flag <= 1'b0;elsebusy_flag <= busy_flag;

忙标志的逻辑还是很有意思的。根据图7的65行与66行的代码,在系统复位之时,忙标志被复位。

根据图7的67和68行代码,当指令执行使能信号【exe_en】为1,且输入端口中的操作码信号【op_code_receive】与【add】指令单元的本地操作码变量【op_code_this】的值相同,均为【5'b00010】的时候,忙标志会变为 1 。

在这里,这个忙标志,它并非是仅仅维持一个时钟周期。变为 1 以后,一般地,系统会处于【else】分支。在【else】分支里面,忙标志会保持现有值不变,依旧是 1 值,也就是会持续处于忙的状态。

而根据图7的69和70行代码,当满足【state == INSTRUCT_DONE】的条件的时候,也就是本条指令执行完毕的时候,忙标志会被清零。清零了以后,此后的一段时间里,系统又会处于【else】分支,会保持当时的 0 值不变。

那么,忙标志的逻辑是,系统复位时,被清零。当指令执行使能信号【exe_en】变为 1 了,并且接收到的输入信号中的操作码与本地指令的操作码一致,则本条指令的忙标志会变为 1 。接下来,在指令的执行过程中,忙标志会一直为 1 。而当指令执行完毕,当【state == INSTRUCT_DONE】条件满足之时,忙标志又会被清零。并且,在后续的执行里,若是没有执行到本条指令,则本条指令的忙标志会一直为 0 。

七.   job_ok 的逻辑

图8

图8中的【job_ok_represent】变量是对【job_ok】的代理。【job_ok_represent】的逻辑是,系统复位时,为高阻态。在【else】分支里面,也是高阻态。然后呢,在【state == INSTRUCT_DONE0】的时候,【job_ok_represent】被赋值为 0 ,在【state == INSTRUCT_DONE】的时候,【job_ok_represent】被赋值为 1 。先被赋值为 0,然后才是被赋值为 1 ,这么处理,是因为,直接从高阻态赋值为1,有可能,控制中心模块接收不到 1 值。而先赋值为0,然后赋值为1,则控制中心模块可以顺利地接收到1值。

【state == INSTRUCT_DONE】,这个条件所表达的意思是,某条指令执行完毕。

‘所以,代理变量【job_ok_represent】及其绑定的变量【job_ok】的逻辑是,平时为高阻态,而在执行完了某一条指令的时候,它会临时地变为1值。在这里,我暂时忽略了变为0值的过程,目的在于突出变为1值,标记指令技术的核心作用。

八.    状态切换

本分节,讲解的是状态变量【state】的逻辑。

图9

在图9里面,根据第85行和第86行,系统复位之时,【state】为【IDLE】状态,也就是为闲置状态。根据103和104行,在【else】分支里面,也就是在闲来无事之时,【state】保持现有值不变。

剩余的几行,都是状态切换的逻辑了。

图8的87和88行,它表示说,在指令执行使能为1时,并且输入端口中的操作码信号【op_code_receive】所传递的操作码,与【add】指令单元的本地操作码变量【op_code_this】相同,都是【5'b00010】,则状态变量【state】转入更新 IP 状态【UPDATE_IP】。

根据89行和90行,当状态变量【state】处于更新 IP 状态【UPDATE_IP】之时,且【work_ok】为 1 时,则状态变量【state】转入【DA_TO_IN1】状态。

在这里,【work_ok】为1 ,就代表着一个指令的某一个微操作执行完毕。一个具体的指令可以划分为多个微操作,每一个状态机,都可以代表着一个微操作。完成了一个微操作,也就是完成了某一个状态的任务。完成了某一个状态的任务以后,就应该转入其他的状态了。

【work_ok】与【job_ok】不同,【work_ok】表示的是某一个指令的某一个微操作完成了,而【job_ok】表示的是某一个指令所有的微操作都完成了。

根据图9的第91行和第92行代码,当状态变量【state】处于【DA_TO_IN1】状态,且【work_ok】为1的时候,状态变量【state】转入【RAM_TO_IN0】状态。

根据图9的第93行和第94行代码,当状态变量【state】处于【RAM_TO_IN0】状态,且【work_ok】为1的时候,状态变量【state】转入【ARI_LOG_TO_IN2】状态。

根据图9的第95行和第96行代码,当状态变量【state】处于【ARI_LOG_TO_IN2】状态,且【work_ok】为1的时候,状态变量【state】转入【IN2_TO_REG】状态。

根据图9的第97行和第98行代码,当状态变量【state】处于【IN2_TO_REG】状态,且【work_ok】为1的时候,状态变量【state】转入【INSTRUCT_DONE0】状态。

最后,根据第99行到102行的代码,状态变量进入了【INSTRUCT_DONE0】状态以后,下一个周期会变为【INSTRUCT_DONE】状态,再往下一个时钟周期,则会变为【IDLE】状态。

九.    计数变量 cnt 的逻辑

图10

在图10中,根据107行和108行的逻辑,在系统复位之时,【cnt】被清零。

根据图9的109和110行的逻辑,在指令执行使能标志【exe_en】为 1 ,且输入信号中的操作码信号【op_code_receive】与【add】指令单元的本地操作码变量【op_code_this】相同,都是【5'b00010】的时候,则【cnt】被清零。

根据111行和112行的逻辑,每当【work_ok】为1时,【cnt】还是会被清零。

根据图9的113行和114行的逻辑,当【busy_flag】为 0 值时,则【cnt】被赋值为 100 。

根据115行和116行的逻辑,当【busy_flag】为1,且 【cnt】小于 100 时,则每一个时钟周期,cnt 会自加 1 。

根据图9的117行与118行的逻辑,在【else】分支里面,cnt保持现有值不变。

那么,根据以上的讲述,大体上,【cnt】会在忙标志为 1 时工作,忙标志为 0 时,【cnt】会被赋值为100。为啥要被赋值为 100,我也忘了。你没看错,这个CPU是我写的,然而,它的某些代码的含义,我自己也给忘了。所以呢,如果你的 CPU 知识学得好,那么,你可能会比我更加地了解本 CPU 的逻辑。

然后呢,在忙标志为 1 时,【cnt】标量开始工作,从0数到100,到了100以后,不再往下数。然后呢,在忙标志为 1 的时候,每当完成了一个微操作,导致【work_ok】为 1,那么,【cnt】会被清零。然后呢,在忙标志依然为 1 的情况下,又会从 0 数到 100。最后呢,当指令的所有微操作都完成了,导致忙标志为 0 的时候,【cnt】会被赋值为 100 。

十.    向系统总线发布信号

某一个指令,它之所以能够完成各种的微操作,乃至完成整个的指令功能,就是因为,在适当的时机,指令单元会向系统总线发布各种总线信号。发布了总线信号以后,控制中心会接收到总线信号。并且呢,控制中心会根据接收到的总线信号,向内存读写单元,寄存器读写单元,算术逻辑单元等等的执行部门发布内部总线信号。通过这种信号的传递机制,某一个具体的指令的微操作得到了执行。一个一个的微操作完成了,那么,最终,单独的一个指令功能,也就跟着完成了。

我们还是来看一看,【add】指令单元是如何发布总线信号的吧。

图11
图12
图13
图14
图15
图16
图17

根据图11和图17,在系统复位,或者是处于【else】分支时,三大总线代理变量都会被赋予高阻态值,也就是,【add】会让本模块的三大总线变量与同名的三大系统总线断开连接。

根据图12,当状态变量【state】处于更新 IP 状态【UPDATE_IP】之时,若是【cnt】为 1,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为【16'hffff】,也就是,同名的系统控制总线会被赋予【16'hffff】值。此时,【add】的地址总线变量和数据总线变量则会通过各自的代理变量,各自被赋予高阻态值。也就是,【add】指令单元的地址总线变量与数据总线变量,会与同名的系统地址总线和系统数据总线断开连接。

根据图12,当状态变量【state】处于更新 IP 状态【UPDATE_IP】之时,若是【cnt】为 2 ,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为 24 ,也就是,同名的系统控制总线会被赋予 24 这个十进制值。而【add】指令单元的地址总线变量与数据总线变量,依然会与同名的系统地址总线和系统数据总线断开连接。

也就是说,在状态变量【state】处于更新 IP 状态【UPDATE_IP】之时,分别在【cnt】为 1 和 2 的时候,【add】指令单元向系统控制总线先后写入 【16'hffff】值与 24 这个值。

其实,更新 ip 这个微操作,其核心操作,便是向系统控制总线写入 24 这个值。然而,【add】却先写入了 【16'hffff】这个值,然后才是写入目标值 24 。

为啥要这样子呢?为啥要先写入【16'hffff】这个值呢?

【add】模块里面的三大总线变量,在系统复位时和在【else】分支里面,都是被赋予高阻态值,都是与同名的三大系统总线断开连接的。由高阻态的状态,直接向总线写入有效的信号,那么,控制中心有可能会收不到有效的总线信号。而先写入无关的【16'hffff】值,再写入有效值,则控制中心是可以顺利地接收到有效的总线信号的。

对于这种,先向总线写入无关值,再写入有效值的技术,我们之前多次讲过。在这里,我又一次重复讲解了,是因为,我担心某些个初次接触本专栏的同学,不清楚我的讲解风格。

在这里,我们来看一看,向控制总线写入 24 这个值,它代表着什么含义。

我还是将控制总线的各个信号贴出来。

如果【ctrl_bus】的取值范围是【0 <= ctrl_bus < 4】,表示本次操作为寄存器写操作。
如果【ctrl_bus】的取值范围是【4 <= ctrl_bus < 8】,表示本次操作为寄存器读操作。
如果【ctrl_bus】的取值范围是【8 <= ctrl_bus < 12】,表示本次操作为内存写操作。
如果【ctrl_bus】的取值范围是【12 <= ctrl_bus < 16】,表示本次操作为内存读操作。
如果【ctrl_bus】的取值范围是【16 <= ctrl_bus < 20】,表示本次操作为立即数读操作。
如果【ctrl_bus】的取值范围是【20 <= ctrl_bus < 24】,表示本次操作为算术逻辑运算。
如果【ctrl_bus】的取值范围是【24 <= ctrl_bus < 28】,表示本次操作为更新指令指针寄存器【ip】。
如果【ctrl_bus】的取值范围是【28 <= ctrl_bus < 32】,表示本次操作为停机操作。

根据控制总线的信号列表,当我们向控制总线发布24这个信号的时候,表示说,【add】指令单元,它在向控制中心发布指令,让控制中心执行更新 ip 的微操作。

关于更新 ip 的微操作,我们在下述文章有讲解过。

简易CPU设计入门:控制总线的剩余信号(三)-CSDN博客

根据上述链接的讲述,当我们向控制总线发布的值,它的范围【大于或等于24,且小于28】时,则表明发布的指令是更新指令指针寄存器 ip 。而更新的方式,取决于控制总线的低2位的值,也就是位1与位0的值。16位的控制总线的低2位,其实就是除以4以后的余数部分。【add】在更新 ip 阶段,向控制总线写入的信号值是 24,24 除以 4,余数为 0 。当余数为 0 时,更新 ip 的方式是,让指令指针寄存器 ip 自加 1。如下图所示。

图18

在这里,我并未将更新 ip 相关的全部的控制总线的讲解给讲出来。这是因为,详细的讲解,都在下面的文章链接里面。

简易CPU设计入门:控制总线的剩余信号(三)-CSDN博客

到了这里,【add】指令单元的更新 ip 的微操作,我就算是讲完了。我们接着来讲下一个微操作。

下一个微操作,如图13所示。

图13副本

根据图13,当状态变量【state】处于状态【DA_TO_IN1】之时,若是【cnt】为 1,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为【16'hffff】,也就是,同名的系统控制总线会被赋予【16'hffff】值。此时,【add】的地址总线变量会通过它的代理变量,被赋予0值。也就是,与【add】指令单元的地址总线变量同名的系统地址总线会被赋予 0 值。同时,【add】的数据总线变量会通过它的代理变量,被赋予高阻态值。也就是,【add】指令单元的数据总线变量,会与同名的系统数据总线断开连接。

根据图13,当状态变量【state】处于状态【DA_TO_IN1】之时,若是【cnt】为 2 ,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为 5 ,也就是,同名的系统控制总线会被赋予 5 这个十进制值。【add】指令单元的地址总线变量会通过地址总线代理变量被赋予 0 值。也就是,与【add】模块的址总线变量同名的系统地址总线会被赋予 0 值。而【add】指令单元的数据总线变量会通过它的代理变量,被赋值为高阻态值 ,也就是,【add】指令单元的数据总线变量会与同名的系统数据总线断开连接。

根据控制总线的信号列表,当 我们向控制总线发布 5 这个值时,这代表着说,【add】指令单元,在指示控制中心,进行寄存器读操作。在寄存器读操作里面,控制总线的低 2 位,也就是位1和位0,也就是除以4以后的余数部分,它指示了,将通用寄存器的数值,放在四个内部寄存器中的哪一个里面。

在状态【DA_TO_IN1】里面,【add】向控制总线写入的有效值为 5 , 5 除以 4,余数为 1 ,所以呢, 通用寄存器里面的值,它会被控制中心放在内部寄存器 inner_reg[1] 里面。

本系统共有八个通用寄存器,我们需要将哪个通用寄存器里面的值,送到 1 号内部寄存器呢?指定本次使用的通用寄存器,用的是地址总线中的信号值。我们本次往地址总线中传递的值是 0,所以呢,本次,我们使用的通用寄存器,为0号通用寄存器。

所以呢,在状态【DA_TO_IN1】里面,我们的操作任务,是将 0 号通用寄存器,也就是累加器da里面的数值,传递给 1 号内部寄存器  inner_reg[1] 。

关于寄存器读操作,还请大家阅读下述链接所示的文章。

简易CPU设计入门:通用寄存器的读写_读写寄存器-CSDN博客

上述链接所示文章的第三分节,讲的是寄存器读操作。不过,我还是建议大家,如果要看的话,就看全篇吧。

到了这里,【add】指令单元中的寄存器读操作,我就算是讲完了。接着往下讲。

状态【DA_TO_IN1】的工作任务完成了以后,下一个状态,是【RAM_TO_IN0】。我们来看图14的内容。

图14副本

根据图14,当状态变量【state】处于状态【RAM_TO_IN0】之时,若是【cnt】为 1,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为【16'hffff】,也就是,同名的系统控制总线会被赋予【16'hffff】值。此时,【add】的地址总线变量会通过它的代理变量,被赋予0值。也就是,与【add】指令单元的地址总线变量同名的系统地址总线会被赋予 0 值。同时,【add】的数据总线变量会通过它的代理变量,被赋予高阻态值。也就是,【add】指令单元的数据总线变量,会与同名的系统数据总线断开连接。

根据图14,当状态变量【state】处于状态【RAM_TO_IN0】之时,若是【cnt】为 2 ,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为 12 ,也就是,同名的系统控制总线会被赋予 12 这个十进制值。【add】指令单元的地址总线变量会通过地址总线代理变量被赋予 {8'h0, op_rand_buf} 值。也就是,与【add】模块的址总线变量同名的系统地址总线会被赋予 {8'h0, op_rand_buf} 值。而【add】指令单元的数据总线变量会通过它的代理变量,被赋值为高阻态值 ,也就是,【add】指令单元的数据总线变量会与同名的系统数据总线断开连接。

在【cnt】为 2 时,我们向系统地址总线赋予的值是 {8'h0, op_rand_buf},为啥是这个值呢?我们来回顾一下【add】指令的格式。

add 指令格式:   Add r

在指令格式中,r 由操作数部分给出,代表着本次要去操作的内存单元的地址。本系统所支持的加法指令【add】,它的一个加数由累加器da提供,另一个加数,由内存单元 r 来提供。整个的加法指令的功能是,让累加器 da 中的数与内存单元 r 中的数相加,结果存放在累加器 da 中。

那么,在状态【RAM_TO_IN0】里面,我们需要将内存单元 r 中的数加载到 0 号内部寄存器之中。r 代表着内存单元的地址。在指令中,操作数字段提供了 r 的值。而此时,指令的操作数的值,我们是将其缓存到了 op_rand_buf 变量里面。这是一个8位的操作数,我们将其扩充为16位的数,就成了 {8'h0, op_rand_buf} 。这个16位的数值,它是状态【RAM_TO_IN0】里面需要访问的内存单元的地址。内存地址,当然是将其传递给地址总线,更加地符合使用习惯。地址总线,当然是用来保存待访问的内存单元的地址的啊。

这样一来,我们在【cnt == 2】的时候,给地址总线传递 {8'h0, op_rand_buf} 这个值的原因,我们就讲清楚了。

在【cnt == 2】的时候,我们给控制总线传递的值是 12 。根据控制总线的信号列表,当 我们向控制总线发布 12 这个值时,这代表着说,【add】指令单元,在指示控制中心,进行内存读操作。在内存读操作里面,控制总线的低 2 位,也就是位1和位0,也就是除以4以后的余数部分,它指示了,将内存单元里面的数值,保存到四个内部寄存器中的哪一个里面。

在状态【RAM_TO_IN0】里面,【add】向控制总线写入的有效值为 12 , 12 除以 4,余数为 0 ,所以呢, 内存单元里面的值,它会被控制中心放在 0 号内部寄存器 inner_reg[0] 里面。

所以呢,状态【RAM_TO_IN0】的操作任务,是访问内存单元 {8'h0, op_rand_buf} ,将这个内存单元里面的数给读取出来,传递给 0 号内部寄存器 inner_reg[0] 。

这样一来,状态【RAM_TO_IN0】的内容,我就讲完了。我们接着往下看。

状态【RAM_TO_IN0】的工作任务完成了以后,下一个状态,是【ARI_LOG_TO_IN2】。我们来看图15的内容。

图15副本

根据图15,当状态变量【state】处于状态【ARI_LOG_TO_IN2】之时,若是【cnt】为 1,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为【16'hffff】,也就是,同名的系统控制总线会被赋予【16'hffff】值。此时,【add】的地址总线变量会通过它的代理变量,被赋予0值。也就是,与【add】指令单元的地址总线变量同名的系统地址总线会被赋予 0 值。同时,【add】的数据总线变量会通过它的代理变量,被赋予高阻态值。也就是,【add】指令单元的数据总线变量,会与同名的系统数据总线断开连接。

根据图15,当状态变量【state】处于状态【ARI_LOG_TO_IN2】之时,若是【cnt】为 2 ,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为 22 ,也就是,同名的系统控制总线会被赋予 22 这个十进制值。【add】指令单元的地址总线变量会通过地址总线代理变量被赋予 0 值。也就是,与【add】模块的址总线变量同名的系统地址总线会被赋予 0 值。而【add】指令单元的数据总线变量会通过它的代理变量,被赋值为高阻态值 ,也就是,【add】指令单元的数据总线变量会与同名的系统数据总线断开连接。

在【cnt == 2】的时候,我们向控制总线传递的值是 22 。根据控制总线的信号列表,当 我们向控制总线发布 22 这个值时,这代表着说,【add】指令单元,在指示控制中心,进行算术逻辑操作。在算术逻辑操作里面,控制总线的低 2 位,也就是位1和位0,也就是除以4以后的余数部分,它指示了,将算术逻辑运算结果,保存到四个内部寄存器中的哪一个里面。

在状态【ARI_LOG_TO_IN2】里面,【add】向控制总线写入的有效值为 22 , 22 除以 4,余数为 2 ,所以呢, 算术逻辑运算的结果,它会被控制中心放在 2 号内部寄存器 inner_reg[2] 里面。

算术逻辑运算有多种类型,加法算一种,减法算一种,乘除法,移位运算,位运算等等,都是算术逻辑运算。【add】指令要实现的算术逻辑运算,显然是加法运算。那么,我们如何发布总线信号的时候,告诉控制中心,我们要执行的,是加法运算呢?

答案是,我们需要通过给系统地址总线传递合适的值,来告诉控制中心,我们想要执行的算术逻辑运算的具体类型。

在这里,我给出算术逻辑运算的类型列表。

当传递给地址总线的值是 0 时,表示进行加法运算,具体算法请参阅 add_cell.v

当传递给地址总线的值是 1 时,表示进行减法运算,具体算法请参阅 sub_cell.v

当传递给地址总线的值是 2 时,表示进行乘法运算,具体算法请参阅 multi_cell.v

当传递给地址总线的值是 3 时,表示进行除法运算,具体算法请参阅 div_cell.v

当传递给地址总线的值是 4 时,表示进行取余数法运算,具体算法请参阅 mod_cell.v

当传递给地址总线的值是 5 时,表示进行按位与运算,具体算法请参阅 and_cell.v

当传递给地址总线的值是 6 时,表示进行按位或运算,具体算法请参阅 or_cell.v

当传递给地址总线的值是 7 时,表示进行按位取反运算,具体算法请参阅 not_cell.v

当传递给地址总线的值是 8 时,表示进行按位异或运算,具体算法请参阅 xor_cell.v

当传递给地址总线的值是 9 时,表示进行逻辑左移运算,具体算法请参阅 left_shift_cell.v

当传递给地址总线的值是 10 时,表示进行逻辑右移运算,具体算法请参阅 right_shift_cell.v

根据图15的代码,在【cnt == 2】的时候,我们给地址总线传递的有效值为 0 。根据上面的算术逻辑运算类型列表的内容,当我们给地址总线传递 0 值时,表示我们要执行的是加法运算。

我们来看一下 add_cell.v 的部分内容。

图19
always @(posedge sys_clk or negedge sys_rst_n)if (sys_rst_n == 1'b0)res <= 1'b0;else if ((ctrl_sig_inner[4] == 1'b1) && (addr_sig_inner == 6'd0))res <= oprand1 + oprand0;elseres <= res;

在这里,我们在【add.v】代码文件里,在状态【ARI_LOG_TO_IN2】的【cnt == 2】的时候,给地址总线传递的是什么值,那么,图19中的红色框线所示的部分,出现的值就是什么值。事实上,在算术逻辑单元中,不同的算术逻辑操作,正是通过内部地址总线【addr_sig_inner】中接收的值,来区分不同的算术逻辑运算类型。

需要说明的是,我们在【add.v】里面,在状态【ARI_LOG_TO_IN2】的【cnt == 2】的时候,传递给地址总线的值,也就是我们所指定的算术逻辑运算类型值,会被控制中心模块接收,控制中心会将这一算术逻辑运算类型值传递给内部地址总线【addr_sig_inner】。而算术逻辑单元里面的各个算术逻辑运算 cell 代码文件,会通过内部地址总线【addr_sig_inner】来获知某一微操作的算是逻辑运算类型。

在状态【ARI_LOG_TO_IN2】里面,在这一状态到来之前,0 号内部寄存器和 1 号内部寄存器里面,已经分别保存了等待参与运算的两个数值。而【add.v】在这一状态里给控制总线传递了 22 这个值,22 属于【大于或等于20,且小于24】的范围,所以是算术逻辑运算信号。而 22 除以 4,余数是 2,所以,本次算术逻辑运算的结果,要被保存在 2 号内部寄存器 inner_reg[2] 里面。状态【ARI_LOG_TO_IN2】里面,【add.v】还向地址总线传递了 0 这个值,表示本次的算术逻辑运算类型,为加法运算。

所以呢,状态【ARI_LOG_TO_IN2】的操作任务,是对保存在 0 号内部寄存器与 1 号内部寄存器里面的两个数,进行加法运算,运算结果会被保存在给 2 号内部寄存器 inner_reg[2] 里面。

这样一来,状态【ARI_LOG_TO_IN2】的内容,我就讲完了。我们接着往下看。

状态【ARI_LOG_TO_IN2】的工作任务完成了以后,下一个状态,是【IN2_TO_REG】。我们来看图16的内容。

图16副本

根据图16,当状态变量【state】处于状态【IN2_TO_REG】之时,若是【cnt】为 1,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为【16'hffff】,也就是,同名的系统控制总线会被赋予【16'hffff】值。此时,【add】的地址总线变量会通过它的代理变量,被赋予0值。也就是,与【add】指令单元的地址总线变量同名的系统地址总线会被赋予 0 值。同时,【add】的数据总线变量会通过它的代理变量,被赋予高阻态值。也就是,【add】指令单元的数据总线变量,会与同名的系统数据总线断开连接。

根据图15,当状态变量【state】处于状态【IN2_TO_REG】之时,若是【cnt】为 2 ,则【add】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为 2 ,也就是,同名的系统控制总线会被赋予 2 这个十进制值。【add】指令单元的地址总线变量会通过地址总线代理变量被赋予 0 值。也就是,与【add】模块的址总线变量同名的系统地址总线会被赋予 0 值。而【add】指令单元的数据总线变量会通过它的代理变量,被赋值为高阻态值 ,也就是,【add】指令单元的数据总线变量会与同名的系统数据总线断开连接。

在【cnt == 2】的时候,我们向控制总线传递的值是 2 。根据控制总线的信号列表,当 我们向控制总线发布 2 这个值时,这代表着说,【add】指令单元,在指示控制中心,进行寄存器写操作。在寄存器写操作里面,控制总线的低 2 位,也就是位1和位0,也就是除以4以后的余数部分,它指示了,将四个内部寄存器中的哪一个的内容,传递给通用寄存器。

我们向控制总线传递的值是 2, 2 除以 4,余数是 2,所以,在状态【IN2_TO_REG】里面,我们会将 2 号内部寄存器 inner_reg[2] 的值,传递给通用寄存器。

我们的系统中一共是有 8 个通用寄存器,那么,我们要把 2 号内部寄存器的值,传递给哪一个通用寄存器呢?这个由地址总线的值来指定。我们在状态【IN2_TO_REG】的【cnt == 2】的时候,传递给地址总线的值是 0 ,所以呢,我们会将 2 号内部寄存器的值传递给 0 号通用寄存器,也就是传递给累加器 da 。

所以呢,状态【IN2_TO_REG】的操作任务,是将 2 号内部寄存器的值传递给 0 号通用寄存器,累加器 da 。

图13到图16所示的四个状态,是【add】指令单元的主体操作。在图13里面,【add】指令单元处于【DA_TO_IN1】状态,这一状态的主要任务,是将累加器中的数值,传递给 1 号内部寄存器。

在图 14 里面,【add】指令单元处于【RAM_TO_IN0】状态,这一状态的主要任务,是将内存单元中的数值传递给 0 号内部寄存器。其中,内存单元地址,由本指令的操作数字段指定。

在图 15 里面,【add】指令单元处于【ARI_LOG_TO_IN2】状态,这一状态的主要任务,是将 0 号与 1 号内部寄存器中的两个数,进行加法运算,运算结果保存在 2 号内部寄存器里面。

在图 16 里面,【add】指令单元处于【IN2_TO_REG】状态,这一状态的主要任务,是将 2 号内部寄存器里面的数据,传递给 0 号通用寄存器,累加器 da 之中。

到了这里,【add】指令单元的主体任务,其实就完成得差不多了。

最后,我们来梳理一下【add】指令的运行流程。

十一.    add 指令的执行逻辑梳理

(一)指令执行使能信号

在某一个指令的执行周期里面,在进行完了译码工作以后,译码完成信号【decode_done】变为高电平,这一有效信号由译码单元【decode_unit】传递给控制中心模块。控制中心接收到了译码完成信号以后,向指令单元发布高电平有效的指令执行使能信号【exe_en】,并同时公布操作码,保留位与操作数信号。

指令单元会接收到指令执行使能信号【exe_en】变为高电平的消息。在指令执行使能变为有效以后,若是传过来的操作码信号【op_code_receive】与【add】指令单元的本地操作码信号【op_code_this】相等,均为 5'b00010 ,那么,这就表示,本次要执行的指令,为【add】指令。当接收到了这样的信息以后,【add】指令单元中的缓存变量【reserve_bit_buf】和【op_rand_buf】分别将保留位与操作数给缓存下来。

同时,忙标志【busy_flag】变为 1 ,【cnt】变为 0。

同时,状态变量【state】转入更新 ip 状态【UPDATE_IP】

(二)更新 ip 状态

在状态变量【state】处于更新 ip 状态之时,【cnt】会由0开始计数,每一个时钟周期,【cnt】会自加 1。本状态的核心操作,是【add】指令单元向系统控制总线写入 24 这个值。

24 这个值,根据控制总线信号列表,它是更新指令指针寄存器 ip 的意思。同时,24 这个值,由于在控制总线里面,它除以4之后的余数为0,因此,在控制总线里面,低 2 位的值为0,所以呢,更新 ip 的方式为 0号方式。在 0 号方式里面,指令指针寄存器 ip 自加 1,指向下一个指令内存。

当控制中心的指令微操作完成了以后,控制中心会向指令单元发布【work_ok】信号,以指示本次的更新 ip 微操作的完成。

(三)DA_TO_IN1 状态

在接收到高电平有效的【work_ok】信号以后,状态变量【state】由更新 ip 状态【UPDATE_IP】转为状态【DA_TO_IN1】,【cnt】会由0开始计数,每一个时钟周期,【cnt】会自加 1。本状态的核心操作,是【add】指令单元向系统控制总线写入 5 这个值。

5 这个值,根据控制总线信号列表,它是寄存器读操作 的意思。同时,5 这个值,由于在控制总线里面,它除以4之后的余数为 1,因此,在控制总线里面,低 2 位的值为 1,所以呢,读取的通用寄存器的值,会放在 1 号内部寄存器 inner_reg[1] 里面。

寄存器读操作,所要读取的寄存器是哪一个呢?在【add】指令的状态【DA_TO_IN1】里面,我们向地址总线写入了 0 值。也就是,我们要去读取的通用寄存器,为 0 号通用寄存器,为累加器 da 。

所以,【add】指令在状态【DA_TO_IN1】里面,所要实现的任务,是将0号通用寄存器的内容,传递给 1 号内部寄存器 inner_reg[1] 。

在控制中心里面,当状态【DA_TO_IN1】所对应的寄存器读操作这一微操作完成了以后,控制中心会向指令单元发布【work_ok】信号,以指示本次的寄存器读这一微操作的完成。

(四)RAM_TO_IN0 状态

在接收到高电平有效的【work_ok】信号以后,状态变量【state】由状态【DA_TO_IN1】转为状态【RAM_TO_IN0】,【cnt】会由0开始计数,每一个时钟周期,【cnt】会自加 1。本状态的核心操作,是【add】指令单元向系统控制总线写入 12 这个值。

12 这个值,根据控制总线信号列表,它是内存读操作的意思。同时,12 这个值,由于在控制总线里面,它除以4之后的余数为 0,因此,在控制总线里面,低 2 位的值为 0,所以呢,读取的内存单元中的值,会放在 0 号内部寄存器 inner_reg[0] 里面。

内存读操作,所要读取的内存单元是哪一个呢?在【add】指令的状态【RAM_TO_IN0】里面,我们向地址总线写入了 {8'h0, op_rand_buf} 值。也就是,我们要去读取的内存单元的地址,为 {8'h0, op_rand_buf} 。

在【add】指令格式 【Add r】里面,r 代表着内存单元的地址,由本指令的操作数字段给出。此时,操作数字段被缓存在了【op_rand_buf】变量里面。这个变量的值,就是本状态里面所要访问的内存单元的地址。这是一个8位的变量,我们将其扩充为16位,就形成了 {8'h0, op_rand_buf} 这个东西。我们将这个这个组合变量的值传递给地址总线,以告知控制中心,我们在【RAM_TO_IN0】状态里面,要访问的内存单元的地址是什么。

所以,【add】指令在状态【RAM_TO_IN0】里面,所要实现的任务,是将内存单元 {8'h0, op_rand_buf} 的内容,传递给 0 号内部寄存器 inner_reg[0] 。

在控制中心里面,当状态【RAM_TO_IN0】所对应的内存读操作这一微操作完成了以后,控制中心会向指令单元发布【work_ok】信号,以指示本次的内存读这一微操作的完成。

(五)ARI_LOG_TO_IN2 状态

在接收到高电平有效的【work_ok】信号以后,状态变量【state】由状态【RAM_TO_IN0】转为状态【ARI_LOG_TO_IN2】,【cnt】会由0开始计数,每一个时钟周期,【cnt】会自加 1。本状态的核心操作,是【add】指令单元向系统控制总线写入 22 这个值。

22 这个值,根据控制总线信号列表,它是算术逻辑操作的意思。同时,22 这个值,由于在控制总线里面,它除以4之后的余数为 2,因此,在控制总线里面,低 2 位的值为 2,所以呢,算术逻辑运算的结果,会放在 2 号内部寄存器 inner_reg[2] 里面。

算术逻辑操作,所要进行的运算类型是哪一个呢?算术逻辑运算类型可以是加减乘除运算,可以是位运算等等,那么,本次的算术逻辑运算类型是什么呢?在【add】指令的状态【ARI_LOG_TO_IN2】里面,我们向地址总线写入了 0 值。在算术逻辑运算控制信号里面,向地址总线传入的值,它是表示了我们想要进行的运算类型。0 值,表示在本次的【ARI_LOG_TO_IN2】状态里面,我们要进行的,是加法运算。

加法运算,需要有两个加数,这两个加数,在本状态之前,已经分别被保存在 0 号和 1 号内部寄存器里面了。

【ARI_LOG_TO_IN2】 状态的主要任务,是将 0 号和 1 号内部寄存器中的两个数,进行加法运算,运算结果,保存在 2 号内部寄存器 inner_reg[2] 里面。

在控制中心里面,当状态【ARI_LOG_TO_IN2】所对应的算术逻辑操作这一微操作完成了以后,控制中心会向指令单元发布【work_ok】信号,以指示本次的算术逻辑这一微操作的完成。

(六)IN2_TO_REG 状态

在接收到高电平有效的【work_ok】信号以后,状态变量【state】由状态【ARI_LOG_TO_IN2】转为状态【IN2_TO_REG】,【cnt】会由0开始计数,每一个时钟周期,【cnt】会自加 1。本状态的核心操作,是【add】指令单元向系统控制总线写入 2 这个值。

2 这个值,根据控制总线信号列表,它是寄存器写操作的意思。同时,2 这个值,由于在控制总线里面,它除以4之后的余数为 2,因此,在控制总线里面,低 2 位的值为 2,所以呢,本状态的一个任务,是将 2 号内部寄存器 inner_reg[2] 里面的值传递给通用寄存器。

本系统的通用寄存器有 8 个,那么,本状态要将 2 号内部寄存器里面的值,传递给哪个通用寄存器呢?这个目标通用寄存器的索引号,由地址总线中的值来指定。在本状态里面,我们传递给地址总线的值是 0 ,所以,我们需要将 2 号内部寄存器里面的值,传递给 0 号通用寄存器,累加器 da 。

状态【IN2_TO_REG】的主要任务,是将 2 号内部寄存器 inner_reg[2] 里面的值,传递给 0号通用寄存器,累加器 da 。

在控制中心里面,当状态【IN2_TO_REG】所对应的寄存器写操作这一微操作完成了以后,控制中心会向指令单元发布【work_ok】信号,以指示本次的寄存器写这一微操作的完成。

(七)指令完成

在接收到高电平有效的【work_ok】信号以后,状态变量【state】由状态【IN2_TO_REG】依次转为两个指令完成状态【INSTRUCT_DONE0】和【INSTRUCT_DONE】,接下来,又会重新回到【IDLE】状态。

在指令完成以后,【add】指令单元又会通过代理变量【job_ok_represent】向【job_ok】变量赋值 1 值,进而向同名的【job_ok】总线发布 1 值。

当【job_ok】总线变为1 值以后,控制中心会有它的处理逻辑。

图20
图21

根据图20,在控制中心里面,存在着两个节拍变量,用于对【job_ok】总线变量进行延时计时操作。

根据图21,当【exe_running】为1,且【job_ok_d1】为1之时,则取指令使能会变为 1 值。

【exe_running == 1'b1】,这一条件的意思是,当系统未停机,处于正常运行的状态之时。在我们的系统里,我虽然设置了停机指令,但是呢,基本上,在向指令内存写入指令的时候,我并未写入过停机指令。所以,你可以认为,我们的系统,始终都是处于连续运行状态的。

在系统始终处于连续运行的状态的前提下,对【job_ok】总线变量延时一个时钟周期的节拍变量【job_ok_d1】变为1以后,则取指令使能信号【get_inst_en】变为 1值。也就是说,前一个指令已经执行完了,接下来,控制中心指挥系统,要去开展新的指令的取指令工作了。

新的指令的取指令工作的展开,其实这也意味着新的指令的取指令,译码和执行的循环开始了。

结束语

本节内容很长,希望大家能够学习好本节的内容。

同时,对于我在复制加修改的过程中,未能够正确地处理的部分,还希望大家自行处理好。我这里,有机会,我会来修订本专栏的文章。不过,不见得说,我会在写完了本专栏以后,立即修改全专栏中的错误。

若你有看不懂的地方,或者有改错意见,你可以联系我。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com