简易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值。也就是说,前一个指令已经执行完了,接下来,控制中心指挥系统,要去开展新的指令的取指令工作了。

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

结束语

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

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

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

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

相关推荐
Ronin-Lotus7 分钟前
嵌入式硬件篇---Buck&Boost电路
单片机·嵌入式硬件
逐梦之程39 分钟前
FPGA-Vivado2017.4-建立AXI4用于单片机与FPGA之间数据互通
fpga开发
正在努力的小河2 小时前
Linux设备树简介
linux·运维·服务器
荣光波比2 小时前
Linux(十一)——LVM磁盘配额整理
linux·运维·云计算
LLLLYYYRRRRRTT2 小时前
WordPress (LNMP 架构) 一键部署 Playbook
linux·架构·ansible·mariadb
轻松Ai享生活3 小时前
crash 进程分析流程图
linux
清风6666663 小时前
基于51单片机自动智能浇花系统设计
stm32·单片机·嵌入式硬件·毕业设计·课程设计
大路谈数字化4 小时前
Centos中内存CPU硬盘的查询
linux·运维·centos
luoqice5 小时前
linux下查看 UDP Server 端口的启用情况
linux
玖別ԅ(¯﹃¯ԅ)5 小时前
ADC的实现(单通道,多通道,DMA)
stm32·单片机·嵌入式硬件