首先参考黑书的CPU(64位)设计:
其将指令的获取和执行分为:取值IF、译码ID、执行EX、访存MEM、写回WB 五个阶段,虽然这些分层更多是为了流水线的实现做准备,但是了解这些并且按照分模块的思想有助于代码的编写的功能的实现(当时一气呵成后除了数码管管脚写反外基本没出什么错误)
在黑书中是循序渐进的引出这些器件和这个架构的,这里限于篇幅就直接将所有部件和结构阐述出来。
CPU的主要构件有:指令地址寄存器PC、指令存储器 (InstMem)、寄存器堆 (RegFile)、立即数生成器(ImmGen)、运算单元 (ALU)、控制单元(CTRL)、数据存储器(DMEM)
指令地址寄存器
程序计数器(PC)指明了当前程序的运行位置,通俗的理解是作为一个指令的地址存在。
所以是在每一条指令执行的最开始(准确的说是每个时钟周期的最开始),我们应该确定当前 PC 的值,从而指向相应的指令内容
输入:PC
(顺序执行用)、branch_PC
(跳转执行用)
控制:顺序执行 or 跳转执行 PCSel
输出:下一条指令的地址 npc
功能:选择顺序执行(PC
=PC+4
)还是跳转执行(PC
=branch_PC
)
指令寄存器
对于每一个程序计数器(PC),我们需要找到对应的指令内容,指令寄存器干的就是这种事情。
输入:PC
输出:指令内容inst
功能:读取对应地址的指令
寄存器堆
基于 RISC-V 指令集的 CPU 特点之一就是拥有 32 个寄存器,按照给定的寄存器号对相应的寄存器进行读写操作也是十分关键的操作
输入:寄存器rs1
、寄存器rs2
、写入寄存器wr
、写入内容wd
控制:读写控制 RegWE
输出:读出的数据 rd1
、rd2
功能:读写寄存器
立即数生成器
对于拥有立即数的指令,我们需要将立即数从中提取出来,并按照相应的指令类型输出不同的立即数
输入:指令 inst
控制:立即数扩展类型 SextOpe
输出:扩展立即数结果 immOut
功能:读写寄存器
运算单元
我们心目中的 CPU,需要完成算术运算、逻辑运算、移位运算和比较运算,这些工作就交给运算单元来做啦(ALU)
输入:操作数 A
和操作数 B
控制:运算类型 ALUOpe
输出:计算结果
控制单元
很多部件都是按照相应功能抽象出来的,那么对于不同的指令,就会对这些部件有不太相同的要求。控制单元就是接受和分析指令内容,控制其他部件正确完成指令要求的。
输入:指令 inst
输出:对各个部件的控制信号
数据存储器
32 个寄存器是不能够满足我们对数据的读写要求的,因此与主存(内存)进行连接和读写操作也是需要考虑的。数据存储器(简称 存储器)不仅包括主存(内存),还包括外设接口等。
输入:读写地址addr
、写入内容wd
控制:读写控制 DRAMWE
、读写扩展控制 DRAM_EX_TYPE
输出:读出内容 rd
功能:读写数据存储器
相信假如没有学过计算机组成原理,看到这里会很乏力,确实是很抽象,所以这里给出我的设计,按照这个设计我们重新来捋一遍(将我们实验课的流程反过来)
我设计的数据通路如下图:
建议照着图边看边比划,举几个栗子:
add x2, x1, x0
指令
这条指令是什么意思捏,就是将 x1
和 x0
寄存器的内容相加,然后写到 x2
中。
首先我们需要从 PC(组件名) 取出我们的指令地址 PC(PC.pc
),将 PC 输入到指令寄存器 IROM 中
指令寄存器 IROM 在接收到 指令地址 PC 后,从中取出相应的指令内容 IROM.inst
输出,就要开始执行这条指令啦
在执行指令的最开始,我们需要将 IROM.inst
指令内容给控制单元(底下那一长条),让它生成控制之后那些部件的控制信号
然后我们需要从 指令内容 inst 中分析出需要操作的寄存器号,从指令格式我们可以看出,rs1
、rs2
、rd
(这里输入为 wr
[ write register ]) 这几个寄存器(假如有)位置都是固定的,那么直接将 IROM.inst
的一部分线连接到寄存器堆 RegFile
上就行
同时我们需要从 指令内容 inst 中分析相应的立即数(这里没有),那么就需要将 IROM.inst
接到 立即数生成器 ImmGen 中
这个时候我们可以得到相应寄存器的内容(RegFile.rd1
、RegFile.rd2
)和立即数 ImmGen.out
,这些东西需要和 程序计数器 PC 一起进行选择,得到输入运算单元的数据
(注:这里出于让图片整洁,将同样信号相反输出的两个多路选择器写成一个了,所以你会看到 out
和 out'
两个输出)
得到相应的操作数,就需要向运算单元输出数据啦。这里我们先不管 COMP,先来康康 ALU,它接受到两个操作数 MUXA.out
和 MUXB.out
后,按照控制信号 ALUop
和 Unsigned
的要求,输出计算结果(num[rs1] + num[rs2]
)
由于无需读写存储器,接下来就跳到了 WB 阶段,这里只有一个多路选择器,按照控制信号,从四个数据中选择出 ALUOut
这个结果,返还给寄存器堆
寄存器堆按照写入的控制信号 RegWe
和 写入的寄存器号 wr
,将得到的数据写到寄存器 x2
中。
那这一条指令就执行完毕了。
jal x2, Label
指令
这条指令是什么意思捏,就是将 PC+4
写到寄存器 x2
中,同时要跳转到 Label
处。
首先我们需要从 PC(组件名) 取出我们的指令地址 PC(PC.pc
),将 PC 输入到指令寄存器 IROM 中
指令寄存器 IROM 在接收到 指令地址 PC 后,从中取出相应的指令内容 IROM.inst
输出,就要开始执行这条指令啦
在执行指令的最开始,我们需要将 IROM.inst
指令内容给控制单元(底下那一长条),让它生成控制之后那些部件的控制信号
然后我们需要从 指令内容 inst 中分析出需要操作的寄存器号,直接将 IROM.inst
的寄存器信息接到寄存器堆 RegFile
上
同时我们需要从 指令内容 inst 中分析相应的立即数(Label
),那么就需要将 IROM.inst
接到 立即数生成器 ImmGen 中
这个时候我们可以得到相应寄存器的内容(RegFile.rd1
、RegFile.rd2
)和立即数 ImmGen.out
,这些东西需要和 程序计数器 PC 一起进行选择,得到输入运算单元的数据
得到相应的操作数,就需要向运算单元输出数据啦。这里我们还是不管 COMP,先来康康 ALU,它接受到两个操作数 MUXA.out
和 MUXB.out
后,按照控制信号 ALUop
和 Unsigned
的要求,输出计算结果(PC + imm
)
由于无需读写存储器,接下来就跳到了 WB 阶段,这里只有一个多路选择器,按照控制信号,从四个数据中选择出 pc4
(也就是 PC + 4
,在 NPC 中计算)这个结果,返还给寄存器堆
寄存器堆按照写入的控制信号 RegWe
和 写入的寄存器号 wr
,将得到的数据写到寄存器 x2
中。
这就完了吗,肯定没有,还差跳转的工作。ALU 计算的结果返回到 NPC 中,然后 NPC 按照控制信号 PCSel
选择这个计算结果作为下一条指令的地址,发给 PC (组件名)
那这一条指令就执行完毕了。
beq x1, x2, Label
指令
这条指令是什么意思捏,就是判断寄存器 x1
和 寄存器 x2
的内容是否相等,假如相等则跳到 PC + Label
处,否则顺序进行(PC + 4
)。
前面工作都一样,直到 EX 阶段
康康 ALU,它接受到两个操作数 MUXA.out
和 MUXB.out
后,按照控制信号 ALUop
和 Unsigned
的要求,输出计算结果(PC + imm
)
这时候要管 COMP 了,它是将输入的两个数 MUXA.out’
和 MUXB.out’
进行比较操作的,然后将比较结果 COMP.out
返还给控制单元
由于无需读写存储器,接下来就跳到了 WB 阶段,这里只有一个多路选择器,按照控制信号,从四个数据中选择出 pc4
(也就是 PC + 4
,在 NPC 中计算)这个结果,返还给寄存器堆
这里不需要写寄存器
这里跳转有跳和不跳两种选择,这是由控制信号 PCSel
决定的。ALU 计算的结果返回到 NPC 中,然后 NPC 按照控制信号 PCSel
选择这个计算结果 还是 PC + 4
作为下一条指令的地址,发给 PC (组件名)
那这一条指令就执行完毕了。
lw x2, 20(x1)
指令
这条指令是什么意思捏,就是将存储器中地址为 20 + num[x1]
的数据写到寄存器 x2
中。
前面工作都一样,直到 EX 阶段
康康 ALU,它接受到两个操作数 MUXA.out
和 MUXB.out
后,按照控制信号 ALUop
和 Unsigned
的要求,输出计算结果(x1 + 20
)
这里需要读存储器,将 ALU 的计算结果 ALU.out
给 DRAM 模块,然后,DRAM 将相应的数据 DRAMRd
输出
接下来 WB 阶段,这里只有一个多路选择器,按照控制信号,从四个数据中选择出 DRAMRd
这个结果,返还给寄存器堆
寄存器堆按照写入的控制信号 RegWe
和 写入的寄存器号 wr
,将得到的数据写到寄存器 x2
中。
那这一条指令就执行完毕了。
lw x2, 20(x1)
指令
这条指令是什么意思捏,就是将存储器中地址为 20 + num[x1]
的数据写到寄存器 x2
中。
前面工作都一样,直到 EX 阶段
康康 ALU,它接受到两个操作数 MUXA.out
和 MUXB.out
后,按照控制信号 ALUop
和 Unsigned
的要求,输出计算结果(x1 + 20
)
这里需要读存储器,将 ALU 的计算结果 ALU.out
给 DRAM 模块,然后,DRAM 将相应的数据 DRAMRd
输出
接下来 WB 阶段,这里只有一个多路选择器,按照控制信号,从四个数据中选择出 DRAMRd
这个结果,返还给寄存器堆
寄存器堆按照写入的控制信号 RegWe
和 写入的寄存器号 wr
,将得到的数据写到寄存器 x2
中。
那这一条指令就执行完毕了。
其他指令大同小异,大伙就自己康康,很快就清晰了
注意:这里有一些指令是需要单独处理的,比如 lui
和 auipc
等等这些,方法其实比较灵活,以 lui
为栗子:
你可以选择在 立即数生成器 ImmGen 中得到相应的立即数并将其做一些不影响数值的计算(加 0 或者移位 0),在写回到寄存器中
你也可以说在 立即数生成器 ImmGen 中得到相应立即数作为低 12 位,然后在 ALU 中将其左移 12 位,再写回到寄存器中
看起来好像后者更麻烦,但是在电路上差别不大,都是复用相同的部件,只是控制信号不同
那么了解相应的部件功能和信号含义后,你也可以设计相应的 CPU 啦!!!
可以不用照着我的设计,毕竟这种东西嘛很自由。比如说我想把比较放在 ALU 中的减法一起完成行不行?也可以,就是少了个 COMP 模块和 ALU 需要多几个输入输出而已。
但是在这之前,建议先写出 数据通路表 和 控制信号取值表 ,将相应的接口和信号控制写出来。这样一方面画图的时候思路会更清晰,写代码的时候也会有个对照表。在画图或者写代码的时候假如有所修改,也可以回来修改这个表格,毕竟它只是你理清思路的一种做法。
这里给出我的数据通路表和控制信号取值表,大伙也可以照着这个格式写一写,很大可能写一半你会发现自己的设计缺陷 : )
但是提早发现总是好的,要不然到后面改一点就可能要动全身