二、CPU搭建的方法
0、CPU基础结构
指令在CPU上执行一般有五个阶段,这五个阶段也对应了CPU逻辑上的五个模块。
取指令:从存储器中取出指令的二进制码。
指令译码:将二进制码译码,获取其中蕴含信息来控制硬件或者取出寄存器中数据。
指令执行:CPU对数据进行运算,得出结果。
存储器访问:从存储器中取出数据,或将数据写入存储器。
结果写回:将数据写回寄存器。

1、指令集的分类
CPU的结构和指令集对应,但是指令集指令一般很多,将其分类可以降低CPU设计的工作量。分类是将经过硬件相同(即所需数据通路相同)的指令归为一类,这样可以简化数据通路和控制器的设计。
2、找出功能硬件
根据CPU基础结构列出所需硬件,再依据指令来增加硬件。
2.1 基础结构
首先按照CPU基础的五个阶段列出硬件。
取指令阶段:
PC:指令计数器,记录指令在存储器上的地址。
IM:指令存储器,存储指令。
ADD4:自增4模块,用于给指令地址加4成为下一条指令地址,以便跳到下一条指令。
指令译码阶段:
RF:寄存器堆,有32个通用寄存器。
指令执行阶段:
ALU:算术逻辑单元,对数据进行运算。
存储器访问阶段:
DM:数据存储器,存储数据。
写回阶段:
RF:寄存器堆,有32个通用寄存器。
2.2 迭代扩展
然后根据指令集来迭代增加硬件,这里以lw指令、ori指令、beq指令为例子扩展功能硬件。
注:依据指令集扩展的硬件并修改的相应数据通路,在只要求正确性的前提下,是可以有很多设计方案的,他们会各有差异,但都功能正确。以下方案是为了展示设计过程而给出的,不必拘泥于此,能保证功能且尽量保持简单的设计都可行。下文中也会提到设计的多样性。
lw指令

以lw $b, x($a)为例:a是指令码的[25:21]字段,即rs字段;b是指令码的[20:16]字段,即rt字段;x是指令码的低16位,即I16字段(16位立即数字段)。
阶段 任务 取指令阶段 lw执行完之后顺序执行下一条指令即可。无需新增硬件单元。 指令译码阶段 从指令码中选出rs,rt,I16字段(即译出a,b,x),再从RF中取出编号为a的寄存器数据,以及将x作符号扩展。因此需要添加位扩展单元用来对x做符号扩展。 指令执行阶段 需要将x扩展得到的字和a号寄存器的数据在ALU中相加得到将要读取的数据地址addr。给ALU赋予加法功能就足以承担,无需新增硬件单元。 存储器访问阶段 将DM中地址为addr的数据data取出。无需新增硬件单元。 写回阶段 将data写回b号寄存器。无需新增硬件单元。
ori指令

以ori $b, $a, x为例,a是指令码的[25:21],即rs字段,b是指令码的[20:16],即rt字段,x是16位立即数字段。
阶段 任务 取指令阶段 顺序执行无跳转,无需新增硬件单元。 指令译码阶段 译码得到a,b,x(同上),从RF中取出a号寄存器的数据,将x作无符号扩展处理。这里只需要给扩展单元增加功能,无需新增硬件单元。 指令执行阶段 将a和扩展后的x作按位或运算得到结果AO(ALU OUTPUT)。只需要再ALU上增加新的功能,无需新增硬件单元。 存储器访问阶段 无需DM参与。无需新增硬件单元。 写回阶段 将AO写回b号寄存器。无需新增硬件单元。
找完ori所需硬件,实际上就找到了大部分立即数计算指令所需硬件,比如:addiu,andi,xori,nori等,区别在于ALU的功能和立即数符号扩展类型。
beq指令

以beq $a,$b,offset为例,a是rs字段,b是rt字段,offset是立即数字段。
阶段 任务 取指令阶段 可能顺序执行,也可能分支执行。因此需要增加选择下一条指令地址和跳转地址的多选器。 指令译码阶段 译码得到a,b,offset(同上),从RF中取出a号和b号寄存器的数据,将offset先左移两位再作符号扩展处理(相对寻址方式),再同PC4(自增4后的指令地址)相加得到BPC。这里需要给扩展单元增加功能,还需新增跳转地址计算单元(NPC)。 指令执行阶段 将a和b相减得到res信号,res=((a-b)==0)。只需要再ALU上增加新的功能,无需新增硬件单元。 存储器访问阶段 无需DM参与。无需新增硬件单元。 写回阶段 无需写回。
这里的设计就有不同方案,比如取指令阶段的多选器就可以不要,而是把PC4直接作为NPC的可能输出(实质上就是将多选器放在NPC内部),直接将NPC的输出作为下一条指令地址。
在我们找出beq的所需硬件时,实际上就找出了大部分分支指令的所需硬件,比如bne、blez、bltz、bgez、bgtz等,区别在于需要修改ALU使其能支持这些指令所需信号。
其余结构根据指令迭代添加。
3、构建数据通路
数据通路的构建也就是将所有指令都执行一次的过程,主要可以分为两步:连线、增加多选器。
3.1 硬件连接
上面我们在寻找功能硬件的过程中,实际上已经把所有指令在脑中的CPU执行了一遍,现在要做的就是回顾指令的执行过程,随着指令的执行,连接这些功能硬件。
在连接的过程中也注意模块化设计,先连接下层模块的电路以组成上层模块,再将上层模块进行连接组成CPU。
3.2 增加多选器
在连接的过程中,一般会遇到多条线路连接到一个端口,比如:
ALU的B/A端口,即ALU的其中一个操作数来源可以是寄存器中数据(addu),也可以是扩展后的立即数(ori);RF的A3端口,即要写回寄存器的编号来源可以是指令码的rt字段(lw),也可以是指令码的rd字段(addu),甚至可能是常数(jal);RF的WD3端口,即要写回寄存器的内容可以是ALU计算结果(add),也可以是DM中取出的数据(lw)。
看起来多选器的增加很麻烦,但实际上是你在遍历执行指令的过程中会自然而然考虑添加的。
3.3 再谈模块化
你可能会感觉,这样又要连接下层模块,又要连接顶层模块,不是徒增烦恼吗?直接连接不好吗?
确实,在指令不多的时候,模块化确实显得很“多余”,但模块化的优势——良好扩展性,在增加指令时能得到鲜明的体现。因为模块化通过分层的思想,大大降低了每层的设计难度和连接难度,也更加利于debug。
4、控制器的设计
我们上面已经完成了搭建CPU的一部分:数据通路和功能硬件。现在我们来完成控制器的设计。
4.1 why控制器
前面考虑硬件的时候,我们忽略了控制信号,控制信号对CPU的正常运行至关重要:最简单的例子,ALU执行的操作就需要控制信号来控制;其他的还有多选器的选择信号,扩展单元控制信号等等。控制信号就由控制器产生,控制器就是一个逻辑电路,其真值表就是指令以及部分运算结果到控制信号的映射。
4.2 如何设计
控制器的设计很简单,只需要列出真值表,再依据此画出逻辑电路。指令码和控制信号之间的映射双射到真值表,真值表双射到控制器的逻辑电路。
这里的真值表的输入为指令码,在遍历指令时,按照类别的顺序遍历,遍历时写出该指令所需要的所有控制信号。
在列完真值表后开始着手构造控制器,为了简化逻辑电路,采取两个策略:其一,采取二级译码;其二,构建逻辑电路时,与或门电路,与门得到指令,或门得到控制信号。
4.2.1 二级译码
二级译码是指将ALU的控制器独立成模块,主控制器处理其他控制信号,主控制器和ALU控制器共同组成控制器。
二级译码存在的原因是因为R型指令的[31:26],即op字段为000000,它们的ALU的控制信号需要funct以及其他可能字段决定,但是其余大部分指令的ALU控制信号可仅通过op字段决定。采用二级译码,大部分指令的控制信号的原像只需要op字段,仅当op为000000时,指令的(ALU)控制信号需要额外的字段决定。
二级译码的具体实现:设一中间信号aluctrl,当它为特定值时,ALU控制信号由funct再次译码决定,否则aluctrl就是ALU控制信号。主控制器的输出信号之一为aluctrl,该信号作为ALU控制器的输入产生最终的ALU控制信号aluop。

4.2.2 与或门
与或门设计逻辑电路的思路更加清晰,本质是在分层:先通过op字段判断指令(类别),再通过指令来决定控制信号。本质上与或门就是“书写”最小项表达式。
与门决定指令

或门决定控制信号
