
现在是时候用数字电路实现一个CPU了!
先明确指令集sISA的细节:
0 7 6 5 4 3 2 1 0
+----+----+-----+-----+
| 00 | rd | rs1 | rs2 | R[rd]=R[rs1]+R[rs2] add指令, 寄存器相加
+----+----+-----+-----+
| 10 | rd | imm | R[rd]=imm li指令, 装入立即数, 高位补0
+----+----+-----+-----+
| 11 | addr | rs2 | if (R[0]!=R[rs2]) PC=addr bner0指令, 若不等于R[0]则跳转
+----+----------+-----+和之前相比, 还约定了一些寄存器的位宽
将这个用数字电路实现的sISA指令集的CPU称为sCPU
li指令的sCPU回顾在ISA层次执行指令的过程: 无论是执行什么指令, 步骤都是类似的
指令周期(instruction cycle): 执行一条指令的步骤
li为例, 操作数需要看立即数是多少,
需要写入哪个目的寄存器
实现CPU = 用数字电路实现上述的每个步骤
和寄存器相比, 存储器还支持寻址
可将存储器看成一个由比特构成的矩阵
通常用深度x宽度表示存储器的规格
| 地址 | 存储字 |
|---|---|
| 0 | \(b_{(0, 2)}b_{(0, 1)}b_{(0, 0)}\) |
| 1 | \(b_{(1, 2)}b_{(1, 1)}b_{(1, 0)}\) |
存储器又分只读存储器(ROM)和随机访问存储器(RAM), 后者可读可写

地址译码器字线(word line)
位线(bit line)
硬连线方式连接到逻辑0或逻辑1
给定地址addr, 读出ROM中相应存储字的工作过程如下:
addr对应的字线有效,
使得该行中存放的信息可以通过与门
0
地址译码器, 与门和或门, 在功能上共同构成了一个多路选择器
ROM的读操作也可以看作是从多个存储字中选择一个
addr相当于多路选择器的选择端
ROM从功能上也可看作是数据端为常数的多路选择器

译码: 根据指令的编码识别指令的功能
li r0, 1010001010,
查阅指令表了解其功能
译码的工作进一步分为操作码译码和操作数译码
li指令, 无需进行li指令中的rd和imm字段
li指令的功能:
将立即数imm写入rd寄存器中
GPR通常包含多个寄存器, 一次访问通常只访问其中的几个寄存器

addr, 读出存储器中的相应内容

addr和数据D,
将数据D写入存储器中的相应位置EN,
指示当前是否需要写入
bner0指令时, 就不需要写入GPR
D通过位线将每一位分别连接到每一个存储字中相应位的D端EN端
EN有效时, 只有地址addr对应的字线有效,
其余行无效D, 从而完成写入操作
单端口RAM(single port RAM):
同一时刻只用一个地址访问一个存储字多端口RAM(multi-port RAM):
同一时刻可用多个地址访问多个存储字
对PC寄存器加1即可
取指 - 和li一样
操作码译码
opcode来确定是什么指令opcode有2位, 用2-4译码器
指令译码器操作数译码
add指令除了写入rd寄存器,
还需要读出rs1和rs2作为源操作数raddr1(读地址),
rdata1(读数据)raddr2, rdata2waddr(写地址), wdata(写数据),
wen(写使能), clk(时钟)执行
wdata信号已经被li指令占用
opcode编码的唯一性,
一条指令不可能既是li, 又是addadd指令, 就选择加法器的结果作为写入数据li指令,
就选择li指令的立即数作为写入数据
更新PC - 和li一样
取指 - 和li一样
操作码译码 - 可复用指令译码器
操作数译码
rs2和addr,
还有一个隐含的R[0]
0作为GPR的raddr1的输入add指令的rs1占用
执行 - 用比较器实现比较两数是否相等的操作
更新PC - 若比较结果不相等, 则跳转
bner0, 且比较结果不相等,
则将PC更新为addrPC+1添加指令的过程:
| 指令 | wdata的选择 |
wen |
raddr1的选择 |
更新PC的选择 |
|---|---|---|---|---|
add |
加法器的输出 | 1 |
指令的rs1 |
PC + 1 |
li |
立即数 | 1 |
X |
PC + 1 |
bner0 |
X |
0 |
0 |
指令的addr |
X表示无关项(don’t care), 选择什么都可以
bner0指令不会写入GPR
wdata选择什么, 都不影响sCPU的正确性
这些控制信号的取值与指令的类型相关
+-------------------------------------------------------------+
| | +---------------> +-----+ |
V | | +--> | Cmp | --+
+----+ +-----+ | +---------+ | +-----+ | +-----+
| PC | --> | ROM | --> | Decoder | -+-> | GPR | -+
+----+ +-----+ | +---------+ | +-----+ | +-----+
instruction | ^ ^ +--> | Add | --+
| | | | +-----+ |
left | right +-----+ +-------------------+一个观察: 把取出的指令作为边界, 将sCPU划分为左右两侧
结论: 在CPU上执行程序 = 用程序编译出的指令序列控制CPU电路进行状态转移
sCPU已经可以计算1+2+...+10
但由于sISA的局限性, 很难运行更复杂的程序
li指令只能装入0~15的立即数
li指令和add指令进行累加192, 就要通过类似以下的指令序列来完成bner0指令的跳转目标范围只有0~15为了缓解上述问题,
可以对li和bner0的功能进行增强
7 6 5 4 3 2 1 0
+----+----+-----+-----+
| 10 | rd | s | imm | R[rd]=imm << (s << 1)
+----+----+-----+-----+
| 11 | offset | rs2 | if (R[0]!=R[rs2]) PC=PC+sign_ext(offset)
+----+----------+-----+li指令通过s和imm的组合来表示一个更大的立即数
<<表示左移运算符s表示将2位的imm放置在8位数据中的哪一段位置 7 6 5 4 3 2 1 0
+-----+-----+-----+-----+
| 00 | 00 | 00 | imm | s=00
+-----+-----+-----+-----+
| 00 | 00 | imm | 00 | s=01
+-----+-----+-----+-----+
| 00 | imm | 00 | 00 | s=10
+-----+-----+-----+-----+
| imm | 00 | 00 | 00 | s=11
+-----+-----+-----+-----+
装入192 = 0b11000000
为了缓解上述问题,
可以对li和bner0的功能进行增强
7 6 5 4 3 2 1 0
+----+----+-----+-----+
| 10 | rd | s | imm | R[rd]=imm << (s << 1)
+----+----+-----+-----+
| 11 | offset | rs2 | if (R[0]!=R[rs2]) PC=PC+sign_ext(offset)
+----+----------+-----+bner0通过offset字段来表示跳转目标相对于PC的偏移量
offset字段可以表示负数
-8~+7范围内的位置取指, 操作码译码, 执行, 更新PC, 都和之前相同
s字段看成一个选择信号, 来从4种立即数当中选择一种
imm放置在某一段位置的情况 7 6 5 4 3 2 1 0
+----+----------+-----+
| 11 | offset | rs2 | if (R[0]!=R[rs2]) PC=PC+sign_ext(offset)
+----+----------+-----+offset字段相加
PC+1
7 6 5 4 3 2 1 0
+----+----------+-----+
| 11 | offset | rs2 | if (R[0]!=R[rs2]) PC=PC+sign_ext(offset)
+----+----------+-----+
offset字段只有4位
offset扩展成与PC位宽一致的数据
扩展方式通常有两种
li指令要求对立即数进行零扩展由于offset字段要支持负数, 因此需要采用补码表示
0b1111(-1)进行零扩展后,
得到0b01111(15)offset的真值不变, 需要进行符号扩展假设有\(k\)位二进制数\(A=\mathrm{0b}a_{k-1}a_{k-2}\dots a_1a_0\)
将其符号扩展至\(k+1\)位, 得到\(A^\prime=\mathrm{0b}a_{k-1}a_{k-1}a_{k-2}\dots a_1a_0\)
\[\begin{array}{ll} A^\prime&=\displaystyle -2^{k}+\sum_{i=0}^{k-1}2^ia_i=-2^k+2^{k-1}a_{k-1}+\sum_{i=0}^{k-2}2^ia_i \\ &=\displaystyle -2^k+2^{k-1}+\sum_{i=0}^{k-2}2^ia_i=-2^{k-1}+\sum_{i=0}^{k-2}2^ia_i=A \\ \end{array}\]
之前要检查sCPU运行数列求和程序的结果, 就要直接观察寄存器的值
sISA的操作码还有一个槽位
io, 来让sCPU支持输入输出功能 7 6 5 4 3 2 1 0
+----+----+---+-------+
| 01 | rd |i/o| idx | R[rd]<=>dev[idx]
+----+----+---+-------+
io指令根据方向位i/o进行输入或输出:
i/o=0时, 表示输入操作
idx的设备中读出8位数据,
输入到R[rd]中i/o=1时, 表示输出操作
R[rd]中的8位数据,
输出到编号为idx的设备中讨论输入输出的数据通路:
io指令输出时,
需要将指令中的rd字段作为源寄存器的编号读出raddr3和rdata3)并不值得io指令并不使用rs1字段和rs2字段来访问GPRrd字段作为GPR其中一个读端口的地址
io指令输入时,
需要将相应设备的值写入到rd寄存器中wdata和wen即可idx有3位, 每种方向可分别关联8种设备
idx相同时,
输入和输出可以关联不同的设备(也可以相同)
sCPU对设备编号作如下关联:
i/o位 |
设备编号idx |
含义 |
|---|---|---|
1 |
0b000 |
输出到LED |
1 |
0b001 |
输出到七段数码管 |
0 |
0b000 |
从拨码开关输入 |
0 |
0b001 |
从按钮输入 |
通常不会从LED输入, 也不会输出到拨码开关
0b000对于输出设备,
我们希望输出设备的状态在执行io指令后保持不变
io指令中指定的GPR连接到LED中
io指令,
LED的亮灭状态也会随之改变io指令将数据写入到这个输出数据寄存器
输入设备的状态是用户设置的, 不会受到处理器内部状态的影响
还需要编写程序, 把设备的功能用起来

只要在设计过程中不小心连错了一根线, 处理器运行程序的结果就可能不符合预期
这些缺陷说明, 使用Logisim这种手工连线的方式设计处理器, 并不是一种扩展性良好的方案
事实上, 现代的处理器设计流程主要采用代码开发的方式

在现代处理器设计流程中需要解决的一部分问题:
HDL编码只是逻辑设计一个环节的工作
事实上, 处理器设计的过程中的很多环节都与软件相关, 因为:
需要理解程序如何在处理器上运行:
要设计出一个好的处理器, 就需要重视软件在其中发挥的作用
CPU执行程序 = 用程序编译出的指令序列控制CPU电路进行状态转移
纸上得来终觉浅, 绝知此事要躬行