引言

  • 通过ISA和状态机模型, 你已经了解处理器在概念上如何工作
  • 同时你也已经学习了数字逻辑电路的基础知识

 

现在是时候用数字电路实现一个CPU了!

只有一条指令的CPU

根据ISA规范设计CPU

先明确指令集sISA的细节:

  • PC位宽为4位, 初值为0
  • GPR有4个, 位宽均为8位
  • 支持如下3条指令
 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): 执行一条指令的步骤

  1. 取指(fetch): 根据当前PC, 在存储器中找到一条指令
  2. 译码(decode): 看这条指令具体是什么指令, 操作数是哪些
    • li为例, 操作数需要看立即数是多少, 需要写入哪个目的寄存器
  3. 执行(execute): 对操作数进行处理, 必要时更新指定的目的寄存器
  4. 更新PC: 让PC指向下一条指令

 

实现CPU = 用数字电路实现上述的每个步骤

取指

  • 首先需要一个PC寄存器
    • 对应数字电路的寄存器
  • 然后需要根据PC在存储器中找到一条指令
    • 需要实现存储器

存储器 = 可寻址的存储单元的集合

和寄存器相比, 存储器还支持寻址

  • 存储器中的内容按顺序进行排布
  • 给出一个地址, 存储器可以读出该地址对应的内容

可将存储器看成一个由比特构成的矩阵

  • 每一行是一个存储字(word)
    • 地址 = 行编号, 行的数量 = 存储器的深度(depth)
  • 一行可存储多位数据, 数据位宽 = 存储器的宽度(width)

 

通常用深度x宽度表示存储器的规格

  • 例: 深度为2, 宽度为3的存储器
地址 存储字
0 \(b_{(0, 2)}b_{(0, 1)}b_{(0, 0)}\)
1 \(b_{(1, 2)}b_{(1, 1)}b_{(1, 0)}\)

存储器又分只读存储器(ROM)和随机访问存储器(RAM), 后者可读可写

  • sISA的3条指令执行时都不会访问存储器, 只有取指操作需要从存储器中读出指令, 因此可采用ROM

ROM的结构

  • 左上方的译码器又称地址译码器
  • 和地址译码器输出相连的导线称为字线(word line)
    • 每条字线对应一个存储字
  • 和或门输出相连的导线称为位线(bit line)
    • 每条位线对应存储字的一位
  • ROM中存储的信息通过硬连线方式连接到逻辑0逻辑1

ROM的工作原理

给定地址addr, 读出ROM中相应存储字的工作过程如下:

  1. 地址译码器将输入的地址转换成一组独热码
  2. 由于独热码中只有一位有效, 故只有地址addr对应的字线有效, 使得该行中存放的信息可以通过与门
    • 其余行因字线无效, 存放的信息均被与门过滤为0
  3. 被选中的存储字的每一位经过或门传输到位线, 向存储器外部输出

从另一个角度理解ROM

地址译码器, 与门和或门, 在功能上共同构成了一个多路选择器

ROM的读操作也可以看作是从多个存储字中选择一个

  • 地址addr相当于多路选择器的选择端
  • 所有存储字相当于多路选择器的数据端

 

ROM从功能上也可看作是数据端为常数的多路选择器

译码

译码: 根据指令的编码识别指令的功能

  • 在ISA的状态机上执行指令时, 本质上是属于人工进行译码操作
    • 通过指令的汇编表示直接识别其功能, 如li r0, 10
    • 或给出指令的二进制表示, 如10001010, 查阅指令表了解其功能
  • 现在需要用电路来实现这一操作
    • 需要在二进制层面对指令进行译码

 

译码的工作进一步分为操作码译码和操作数译码

  • 操作码译码: 根据指令的操作码来识别指令的功能
    • 目前只需要实现一条li指令, 无需进行
  • 操作数译码: 从指令的编码中识别出相应的操作数
    • 只需要解析出li指令中的rdimm字段
      • 一些电路层次上的位抽取操作

执行

li指令的功能: 将立即数imm写入rd寄存器中

  • 需要考虑如何实现ISA的GPR

 

GPR通常包含多个寄存器, 一次访问通常只访问其中的几个寄存器

  • GPR需要支持寻址
    • GPR的电路本质也是一个存储器
  • 不过GPR需要作为目的寄存器被指令写入
    • 是一个支持写入的存储器, 即RAM

RAM的读操作

  • 存储单元采用带使能端的D触发器搭建
  • 给定地址addr, 读出存储器中的相应内容
    • 工作原理和ROM类似

RAM的写操作相关的结构

  • 给定地址addr和数据D, 将数据D写入存储器中的相应位置
  • 并非所有时刻都需要进行写入操作, 还需要一个写使能信号EN, 指示当前是否需要写入
    • 在执行bner0指令时, 就不需要写入GPR

写入RAM的工作原理

  • 数据D通过位线将每一位分别连接到每一个存储字中相应位的D
  • 将每一行的字线分别连接到对应存储器中相应的EN
    • EN有效时, 只有地址addr对应的字线有效, 其余行无效
  • 被字线选中的存储字将更新为数据D, 从而完成写入操作

RAM的完整结构

  • 单端口RAM(single port RAM): 同一时刻只用一个地址访问一个存储字
  • 多端口RAM(multi-port RAM): 同一时刻可用多个地址访问多个存储字
    • 多端口RAM的多个端口可以同时工作, 需要添加额外的电路逻辑

更新PC

对PC寄存器加1即可

  • 可通过数字电路中的计数器来实现

支持数列求和的简单处理器

添加add指令

取指 - 和li一样

操作码译码

  • 需要检查指令的opcode来确定是什么指令
  • 识别输入的取值 - 用n选1译码器!
  • opcode有2位, 用2-4译码器
    • 输出的独热码可以指示当前指令类型, 这种译码器称为指令译码器
    • 这组独热码通常作为控制信号, 控制CPU中的其他电路如何工作

操作数译码

  • add指令除了写入rd寄存器, 还需要读出rs1rs2作为源操作数
  • 需要为GPR添加两个读端口, 添加后GPR有以下信号
    • 第1个读端口: raddr1(读地址), rdata1(读数据)
    • 第2个读端口: raddr2, rdata2
    • 写端口: waddr(写地址), wdata(写数据), wen(写使能), clk(时钟)

添加add指令(2)

执行

  • 用加法器实现加法操作
  • 写入GPR时, wdata信号已经被li指令占用
    • 由于opcode编码的唯一性, 一条指令不可能既是li, 又是add
    • 可以采用多路选择器, 根据指令的类别选择写入GPR的数据
      • 当前是add指令, 就选择加法器的结果作为写入数据
      • 当前是li指令, 就选择li指令的立即数作为写入数据

 

更新PC - 和li一样

添加bner0指令

取指 - 和li一样

操作码译码 - 可复用指令译码器

操作数译码

  • 除了指令中的rs2addr, 还有一个隐含的R[0]
    • 0作为GPR的raddr1的输入
  • 不过这个信号已经被add指令的rs1占用
    • 同样可以通过多路选择器解决问题

执行 - 用比较器实现比较两数是否相等的操作

更新PC - 若比较结果不相等, 则跳转

  • 当前指令为bner0, 且比较结果不相等, 则将PC更新为addr
  • 否则, 将PC更新为PC+1
  • 同样可以通过多路选择器来实现

重新审视CPU设计

添加指令的过程:

  1. 分析指令的预期行为
  2. 然后根据指令的行为, 在数据流动的方向上依次添加所需的部件
    • 数据在CPU中流动的路径和路径上的相关部件, 称为CPU的数据通路(data path)
      • 包括GPR, 加法器, 存储器, 比较器等
  3. 当多条指令的数据通路出现冲突时, 需要引入一些额外的电路, 来控制数据如何流动
    • 使得每条指令都能完成符合ISA规范的操作
    • 这些额外的电路属于CPU中的控制逻辑
    • 决定控制逻辑行为的信号称为控制信号

控制信号的设计

指令 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电路进行状态转移

功能完备的迷你RISC-V处理器

sISA的局限性

虽然sCPU可以计算1+2+...+10, 但无法运行更复杂的程序

因为sISA过于简单

  • PC寄存器的位宽只有4位
    • 程序最多只能包含16条指令
  • GPR的位宽只有8位
    • 无法表示大于255的数据
  • 指令的功能有限
    • 无法进行减法操作, 更不用说乘法和除法

 

要运行更多程序, 就需要换一个ISA

迷你RISC-V指令集

我们提出一个迷你RISC-V指令集minirv

  • PC初值为0
  • GPR数量与RV32E中定义的GPR数量一致
  • 支持如下8条指令: add, addi, lui, lw, lbu, sw, sb, jalr
  • 其他的ISA细节与RV32I相同

 

我们在编译层次研发了一些黑科技

  • 可以将其他常用的34条RISC-V指令编译到上述8条指令
    • 也即, 用上述8条指令实现其他34条指令的功能
  • 不必实现完整的42条常用RISC-V指令, 也能让CPU运行更复杂的程序!

实现addi指令

取指

  • 存储器位宽在ISA层次和电路层次都存在
    • 在ISA层次, PC寄存器是以\(w_{ISA}\)为单位寻址
    • 如果电路层次的存储器位宽\(w_{circuit} \ne w_{ISA}\), 则不能直接用PC直接进行寻址

操作码译码

  • 指令数量少, 编码稀疏, 用译码器反而不方便
  • 用比较器更合适

操作数译码

  • 立即数需要符号扩展
  • 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\)

  • \(a_{k-1}=0\), 扩展前后的真值显然一致
  • \(a_{k-1}=1\), 按补码方式加权展开, 有

\[\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}\]

  • 易见\(k=2\)时, 上述结论成立
  • 根据数学归纳法, 可证命题成立

实现更多指令

  • jalr
    • bner0更简单, 跳转目标作为新PC的数据选择端
    • 其他和addi类似
  • add
    • 和sISA的add类似
  • lui
    • 和sISA的li类似, 只是立即数的格式不同

 

四条访存指令中有两条store指令, 需要额外添加RAM

  • lwlbu和取指类似
    • lbu还需要对读出数据进行零扩展
  • swsb需要写入内存
    • 还需要设置RAM的写使能和字节写使能

添加图形显示功能

  • 在Logisim中添加一个RGB Video组件
    • 端口包括时钟, 复位, 写使能, X坐标, Y坐标, 待写入的像素数据
  • 处理器如何访问外设, 属于ISA规范的一部分
    • 在RISC-V中, CPU通过内存映射I/O方式来访问外设
    • 根据访存地址的范围来决定CPU访问的是内存还是外设
  • RGB Video组件的屏幕大小是256x256
    • 1个像素占4B, 整个屏幕的像素数据大小为256x256x4B=256KB
    • 需要划分一段连续的地址区域, 如[0x20000000, 0x20040000)
      • 访存指令的目标地址落在这个区域, 相应指令将访问RGB Video

  • 实现内存映射I/O的关键 - 添加一个地址译码器
    • 输入访存地址, 输出两个控制信号isVGAisMem
    • 通过两个控制信号控制相应组件的访问行为

具体细节需要自己动手体会并理解

课程的讲解只能让你相信这个处理器能设计出来

  • 如果不去动手做, 讲得再详细也没用
  • 如果要去动手做, 自己把这些细节处理好, 才是真正学会了设计处理器
    • 具体的细节指导参考讲义

迈向现代化的处理器设计

Logisim的缺陷1 - 设计繁琐

  • 拖动组件和连线确实有设计电路的感觉
    • 但当设计的规模增加后, 这些操作将会变得繁琐

 

  • minirv处理器只有8条指令
  • 完整的RV32I有42条指令
  • 要实现一个可以启动现代操作系统的RISC-V处理器, 需要实现上百条指令
  • 现代的Intel和ARM这些成熟的商业处理器, 需要支持上千条指令
    • 光是ISA手册就有几千页
    • 芯片的晶体管数量更是到达几百亿的量级

Logisim的缺陷2 - 仿真速度慢

  • Logisim的仿真效率本身就不高
    • 随着设计规模变得复杂, 仿真速度也会因为组件数量增多而越来越慢
  • 虽然minirv原则上可以实现常用RISC-V指令的所有功能
    • 但需要把其他指令翻译成行为等价的几条甚至几十条minirv指令
      • 对于一个程序, 编译到minirv的执行效率将会降低数倍甚至数十倍

 

目前通过执行程序在RGB Video组件中显示一张256x256的图片, 都要花费1~2小时

  • 即使minirv具备运行超级玛丽游戏的潜能, 游戏体验也会因为仿真效率过低而变得难以忍受

Logisim的缺陷3 - 调试困难

只要在设计过程中不小心连错了一根线, 处理器运行程序的结果就可能不符合预期

 

  • 如果程序规模不大, 还能逐条指令地检查处理器的执行状态是否与ISA的状态一致
  • 但对于1+2+...+100的求和程序, 就已经要执行6000多条指令
  • 显示logo的程序甚至要执行628000条指令
    • 要找到哪一条指令的执行不符合预期, 是非常困难的
  • 要运行超级玛丽, 执行的指令数量将会是一个天文数字!
    • 要在这么多的指令中找到错误, 简直比大海捞针还难!

现代的处理器设计方式

这些缺陷说明, 使用Logisim这种手工连线的方式设计处理器, 并不是一种扩展性良好的方案

 

事实上, 现代的处理器设计流程主要采用代码开发的方式

  • 通过硬件描述语言(Hardware Description Language, HDL)描述硬件组件之间如何连接, 来给出处理器的逻辑结构
  • 完成代码开发后, 需要通过仿真工具来检查代码所给出的逻辑结构是否符合预期
  • 还需要通过EDA工具将代码转变成版图
    • 就像编译器将C代码转变成指令序列

现代的处理器设计流程

现代的处理器设计流程(2)

在现代处理器设计流程中需要解决的一部分问题:

  • 架构设计: 给定一个新特性, 如何给出一个设计方案, 将其分解成合适的硬件模块来实现它?
    • 可能是添加新指令等来自ISA规范的功能
    • 也可能是处理器层次上的功能优化方案
  • 逻辑设计: 有了设计方案, 如何通过HDL在电路层次将设计方案中的硬件模块实现出来?
  • 功能验证: 如何验证HDL所描述的电路满足新特性所期望的功能?
  • 性能验证: 如何保证处理器的性能符合预期?
  • 电路评估: 如何评估并优化处理器的频率, 面积和功耗等指标?
  • 物理设计: 如何将HDL代码转变成可流片的版图?
  • 性能优化: 如何发现并定位处理器中性能瓶颈, 并设计出相应的优化方案?

处理器设计 != HDL编码

HDL编码只是逻辑设计一个环节的工作

  • 整个流程有非常多环节

 

事实上, 处理器设计的过程中的很多环节都与软件相关, 因为:

  1. 处理器离开了软件就无法工作
    • 处理器的工作就是不断执行指令, 这些指令就是软件
    • 要评估一个处理器, 就是看软件在这个处理器上运行得对不对, 运行得好不好

 

需要理解程序如何在处理器上运行:

  • C语言是如何生成RISC-V指令序列的?
  • 如何用C语言开发一个能在屏幕上显示logo的程序?
  • 如何开发更多的程序在处理器上运行?

处理器设计 != HDL编码(2)

  1. 要完成处理器设计流程的各步骤, 就需要各种工具和基础设施的支撑
    • 而这些工具和基础设施的本质也是软件
    • 尤其是那些与处理器功能紧密相关的工具, 它们对相关步骤的开展起着重要的作用
      • 如指令集模拟器, 功能模拟器, 差分测试方法等

 

  1. HDL代码虽然描述的对象是硬件, 但它本身作为代码, 也是一种软件
    • 既然是代码, 就需要使用合适的软件技术对其进行管理, 维护, 测试, 评估和优化
    • 尤其是随着代码规模的增大, 这些问题也会显得越来越重要
      • 软件工程领域已经针对这些问题研究数十年, 有很多可以借鉴的地方

 

要设计出一个好的处理器, 就需要重视软件在其中发挥的作用

总结

处理器设计

  • 处理器设计 = 用数字电路实现指令周期
    • 取指, 译码, 执行, 更新PC

 

  • 实现指令的过程
    • 分析指令的预期行为
    • 设计数据通路, 添加相关部件(GPR, 加法器, 存储器, 比较器等)
    • 设计控制信号, 用于控制使能信号和数据选择器的选择信号
      • 最终使得每条指令都能完成符合ISA规范的操作

 

CPU执行程序 = 用程序编译出的指令序列控制CPU电路进行状态转移

 

纸上得来终觉浅, 绝知此事要躬行