现在是时候用数字电路实现一个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, 10
10001010
,
查阅指令表了解其功能
译码的工作进一步分为操作码译码和操作数译码
li
指令, 无需进行li
指令中的rd
和imm
字段
li
指令的功能:
将立即数imm
写入rd
寄存器中
GPR通常包含多个寄存器, 一次访问通常只访问其中的几个寄存器
addr
, 读出存储器中的相应内容
addr
和数据D
,
将数据D
写入存储器中的相应位置EN
,
指示当前是否需要写入
bner0
指令时, 就不需要写入GPRD
通过位线将每一位分别连接到每一个存储字中相应位的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
, rdata2
waddr
(写地址), wdata
(写数据),
wen
(写使能), clk
(时钟)执行
wdata
信号已经被li
指令占用
opcode
编码的唯一性,
一条指令不可能既是li
, 又是add
add
指令, 就选择加法器的结果作为写入数据li
指令,
就选择li
指令的立即数作为写入数据
更新PC - 和li
一样
取指 - 和li
一样
操作码译码 - 可复用指令译码器
操作数译码
rs2
和addr
,
还有一个隐含的R[0]
0
作为GPR的raddr1
的输入add
指令的rs1
占用
执行 - 用比较器实现比较两数是否相等的操作
更新PC - 若比较结果不相等, 则跳转
bner0
, 且比较结果不相等,
则将PC更新为addr
PC+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的正确性
这些控制信号的取值与指令的类型相关
CPU执行程序 = 用程序编译出的指令序列控制CPU电路进行状态转移
虽然sCPU可以计算1+2+...+10
, 但无法运行更复杂的程序
因为sISA过于简单
要运行更多程序, 就需要换一个ISA
我们提出一个迷你RISC-V
指令集minirv
0
add
, addi
,
lui
, lw
, lbu
, sw
,
sb
, jalr
我们在编译层次研发了一些黑科技
取指
操作码译码
操作数译码
R[0]
和sISA不同, 需要考虑执行 - 加法器
更新PC - 指令编码的位宽与sISA不同, 需要考虑如何更新
假设有\(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}\]
jalr
bner0
更简单, 跳转目标作为新PC的数据选择端addi
类似add
add
类似lui
li
类似, 只是立即数的格式不同
四条访存指令中有两条store指令, 需要额外添加RAM
lw
和lbu
和取指类似
lbu
还需要对读出数据进行零扩展sw
和sb
需要写入内存
RGB Video
组件
内存映射I/O
方式来访问外设RGB Video
组件的屏幕大小是256x256
256x256x4B=256KB
[0x20000000, 0x20040000)
RGB Video
内存映射I/O
的关键 - 添加一个地址译码器
isVGA
和isMem
课程的讲解只能让你相信这个处理器能设计出来
目前通过执行程序在RGB Video
组件中显示一张256x256
的图片,
都要花费1~2小时
只要在设计过程中不小心连错了一根线, 处理器运行程序的结果就可能不符合预期
1+2+...+100
的求和程序,
就已经要执行6000多条指令这些缺陷说明, 使用Logisim这种手工连线的方式设计处理器, 并不是一种扩展性良好的方案
事实上, 现代的处理器设计流程主要采用代码开发的方式
在现代处理器设计流程中需要解决的一部分问题:
HDL编码只是逻辑设计一个环节的工作
事实上, 处理器设计的过程中的很多环节都与软件相关, 因为:
需要理解程序如何在处理器上运行:
要设计出一个好的处理器, 就需要重视软件在其中发挥的作用
CPU执行程序 = 用程序编译出的指令序列控制CPU电路进行状态转移
纸上得来终觉浅, 绝知此事要躬行