Bootstrap

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

项目代码下载

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

CSDN文章:下载本项目代码

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

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

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

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

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

本节前言

在上一节,我讲解了【instruct_unit】模块的代码。这是一个连接模块,用于连接控制中心与具体的指令部件。从本节开始,我们来讲解具体的指令部件。

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

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

一.    指令功能

【sdal】指令的格式如下:

Sdal n

其功能为:将8位立即数n,送入累加器da的低8位,并扩充为16位无符号整数。

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

我们接着往下学习。

二.    端口列表

图1

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

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

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

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

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

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

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

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

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

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

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

图2

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

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

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

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

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

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

第三个状态,是【IMM_NUM_READ】。由图2中的21行代码可知,本地参数【IMM_NUM_READ】的值,是 2 。

第四个状态,是【REG_WRITE】。由图2中的22行代码可知,本地参数【REG_WRITE】的值,是 3 。

第三个状态和第四个状态,是【sdal】指令的核心操作状态。

【sdal】指令,它的功能是将指令中的8位立即数,加载到累加器 da 中。这个功能,我主要是将其分为两步来完成的。第一步,将指令中的8为立即数加载到一个内部寄存器里面。第二步,将这个内部寄存器中的值,传递给累加器 da 。那么,在这里,第一步的工作,其实就是第三个状态【IMM_NUM_READ】所要执行的任务。而第二步工作,就是第四个状态【REG_WRITE】所要执行的任务。

我们接着往下看。

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

四.    局部变量

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

图3

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

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

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

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

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

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

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

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

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

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

五.    缓存保留位与操作数

图4

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

从图4可以看出,缓存变量【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】,我们还需要到控制中心里面去看。

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

从图5可以看出,当系统复位之时,执行使能信号【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】。而【sdal】指令单元在检测到【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】与【sdal】指令单元中的本地操作码变量【op_code_this】中的值【5'b00100】相同的时候,那么,就来进行着保留位与操作数的缓存操作。

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

六.    忙标志的逻辑

图6

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;
	else
		busy_flag <= busy_flag;

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

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

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

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

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

七.   job_ok 的逻辑

图7

图6中的【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】的逻辑。

图8

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

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

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

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

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

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

根据图8的第89行和第90行代码,当状态变量【state】处于【IMM_NUM_READ】状态,且【work_ok】为1的时候,状态变量【state】转入【REG_WRITE】状态。

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

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

九.    计数变量 cnt 的逻辑

图9

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

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

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

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

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

根据图9的111行与112行的逻辑,在【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 。

十.    向系统总线发布信号

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

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

图10

图11

图12

图13

图14

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

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

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

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

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

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

【sdal】模块里面的三大总线变量,在系统复位时和在【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这个信号的时候,表示说,【sdal】指令单元,它在向控制中心发布指令,让控制中心执行更新 ip 的微操作。

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

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

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

图15

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

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

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

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

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

根据图12,当状态变量【state】处于立即数读 状态【IMM_NUM_READ】之时,若是【cnt】为 2 ,则【sdal】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为 16 ,也就是,同名的系统控制总线会被赋予 16 这个十进制值。而【sdal】指令单元的地址总线变量依然会与同名的系统地址总线断开连接。而【sdal】指令单元的数据总线变量会通过它的代理变量,被赋值为 {8'h0, op_rand_buf} ,也就是,将缓存下来的 8 位操作数 op_rand_buf 放在低8位,而将高8位 赋予 0值,并且将这个新的 16位的值,通过【sdal】指令单元的数据总线变量的代理变量,将其赋值给【sdal】指令单元的数据总线变量,进而赋值给同名的系统数据总线。

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

在立即数读 状态【IMM_NUM_READ】里面,【sdal】向控制总线写入的有效值为16 , 16 除以 4,余数为 0 ,所以呢, {8'h0, op_rand_buf} 这个值,在被送入数据总线以后,它会被控制中心放在内部寄存器 inner_reg[0] 里面。

关于立即数读操作,还请大家阅读下述链接所示的文章。

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

到了这里,【sdal】指令单元中的立即数读操作,我就算是讲完了。接着往下讲。

立即数读操作进行完了以后,下一个状态,是寄存器写。我们来看图13的内容。

图13副本

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

根据图13,当状态变量【state】处于寄存器写 状态【REG_WRITE】之时,若是【cnt】为 2 ,则【sdal】的系统控制总线变量会通过控制总线代理变量【ctrl_bus_represent】被赋值为 0 ,也就是,同名的系统控制总线会被赋予 0 这个十进制值。而【sdal】指令单元的数据总线变量依然会与同名的系统数据总线断开连接。而【sdal】指令单元的地址总线变量会通过它的代理变量,被赋值为 0,也就是,将 0 值通过【sdal】指令单元的地址总线变量的代理变量,将其赋值给【sdal】指令单元的地址总线变量,进而赋值给同名的系统地址总线。

根据控制总线信号列表,向控制总线发布 0 值,代表着说,【sdal】指令单元,向控制总线发布了寄存器写指令。控制总线的低2位,也就是控制总线除以 4 以后的余数,它表示了,将内部寄存器中的哪一个的内容写入通用寄存器。我们向控制总线写入了 0 值,0 除以 4,余数为 0,这表示,我们要将 inner_reg[0] 的值写入某一个通用寄存器里面。通用寄存器也有它的地址的。累加器,它是本系统中的八个通用寄存器中的0号。那么,这个0号,要通过系统地址总线来指定。我们在【cnt】为 2 的时候,向系统地址总线写入 0 值,表明,我们是要将内部寄存器的内容,写入 0号通用寄存器里面,也就是写入累加器里面。

在立即数读状态里面,我们将立即数通过数据总线,传递给了0号内部寄存器。而在寄存器写操作连,我们又将0号内部寄存器里面的内容,传递给了 0 号通用寄存器,也就是传递给累加器。由此,我们实现了【sdal】指令的大致的功能。

我们还是来梳理一下【sdal】指令单元的逻辑。

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

(一)指令执行使能信号

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

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

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

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

(二)更新 ip 状态

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

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

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

(三)立即数读状态

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

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

立即数读操作,所要读取的立即数,是怎么来的呢?在【sdal】指令的指令码之中,会有一个8位的操作数字段。这个8位的操作数,此时放在缓存变量【op_rand_buf】里面。将组合量 {8'h0, op_rand_buf} 赋给 【sdal】指令单元的数据总线变量的代理变量,就相当于向数据总线传递了这个立即数。

由于【sdal】在立即数读状态里面,向控制总线传递的值是 16,所以呢,组合量 {8'h0, op_rand_buf} 会被赋给 0 号内部寄存器 inner_reg[0] 。

在控制中心里面,当立即数读操作这一微操作完成了以后,控制中心会向指令单元发布【work_ok】信号,以指示本次的立即数读这一微操作的完成。

(四)寄存器写状态

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

0 这个值,根据控制总线信号列表,它是寄存器写操作 的意思。同时,0 这个值,由于在控制总线里面,它除以4之后的余数为0,因此,在控制总线里面,低 2 位的值为0,所以呢,待写入通用寄存器的立即数,此刻是已经放在 0 号内部寄存器 inner_reg[0] 里面的。

寄存器写操作,要写入哪一个通用寄存器呢?在【sdal】指令单元的寄存器写状态之中,我们向地址总线写入了 0 值。这个 0 值表明,我们想要将内部寄存器里的内容,写入八个通用寄存器里面的 0 号通用寄存器,也就是写入累加器 da 里面。

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

(五)指令完成

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

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

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

图16

图17

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

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

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

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

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

结束语

本节内容,实在是多,我这里,也有些不想写了。

本节的内容,我写了有四五个小时吧。写起来,实在是累。

也许,本节的内容,你读起来会觉得乱。实在是因为,我这里在写的时候,也是着急,累,困倦,急着完成它。

但愿本节的写作大致成功吧。如果有问题,以后,我应该会慢慢地来修改的。

 

 

;