要求:

在此之前, 我们需要再次明确需要实现的指令集sISA的细节. 和上一小节相比, 此处还约定了一些寄存器的位宽:

  • PC位宽为4位, 初值为0
  • GPR有4个, 位宽均为8位
  • 支持如下3条指令
1
2
3
4
5
6
7
8
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]则跳转
+----+----------+-----+

最多执行16条指令,不含benr0
现有情况,支持8位int,最大数为256

我们将这个用数字电路实现的sISA指令集的CPU称为sCPU. 要实现sCPU, 我们需要用数字电路实现sISA中的每一个概念. 为了简单起见, 我们先从最简单的li指令开始考虑, 也即, 先实现一个只支持li指令的sCPU

只有一套指令的sCPU

我们先从ISA的状态机模型回顾执行一条li指令的具体过程. 事实上, 无论是执行什么指令, 其步骤都是类似的, 有一个叫”指令周期”(instruction cycle)的概念专门描述这些步骤:

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

因此, 我们的目标就是用数字电路实现上述过程的每一个步骤.听起来很有意思。

取指

存储器和寄存器都可以存储信息,但存储器还支持寻址(addressing),也即,存储器中的内容按顺序进行排布,给出一个地址,存储器可以读出该地址对应的内容。

从功能上划分, 存储器可以分别只读存储器(Read-Only Memory, ROM) 和随机访问存储器(Random Access Memory, RAM), 前者不支持写入, 而后者支持. 对于sISA来说, 因为3条指令都不会访问存储器, 只有取指操作需要从存储器中读出指令, 因此这里可以采用ROM.

一个2x3的ROM的结构如下图所示. 左上方的译码器又称”地址译码器”. 和地址译码器输出相连的导线称为”字线”(word line), 每条字线对应一个存储字. 和或门输出相连的导线称为”位线”(bit line), 每条位线对应存储字的一位.
image.png
给定addr地址后,通过编码器的作用,最后输出独热编码,最后只有一路会被点亮,经过与门的过滤作用后,其它门的信息被过滤为0,在最后一个门电路处,不会得到输出,只有被选中的存储字,最后会得到输出。

在事实上,图中的电路构成了一个3位的2选1选择器,因此ROM的操作,同时也是在N个选择器中,最后选择一个作为最终数据进行读取。

通过多路选择器实现一个ROM, 并在其中存放数列求和的指令序列, 然后通过PC寄存器取出指令. 你需要根据你的理解来确定ROM的规格.

我们决定采用8位的数字寄存器,8位指令的ROM。其中左上角是一个累加的PC。
image.png

译码

由于电路中存在的是二进制表示的指令,所以我们要在二进制层面,对其进行译码。目前而言,我们要实现的只是一条li指令,所以我们只用默认为是1条指令,然后对于操作数的译码,我们只用抽取其中某些位进行运算。

li的指令的功能是将立即数imm写入rd寄存器,一次我们需要考虑如何实现ISA的GPR。GPR包含着指令寻址和数据写入功能,不难发现这实际上是一个RAM

我们加入一个写呢能信号,用于标注是否该进行写入,所以,我们可以总结一个RAM的工作状态如下:

  • 当使能信号为1,addr输入位的地方,可以接受信号的寻址。
  • 不难发现,这是一个但端口的RAM,即为在同一时刻只能够通过一个地址访问其中的一个存储子,称为单端口RAM
  • 同时,译码器也可以加入,最后实现一个addr和encoder可以同时控制写入信号和读取信号
    image.png

    在寄存器的基础上搭建一个RAM, 从而实现GPR的写入功能. 你需要根据你的理解来确定RAM的规格.

根据要求,我们的GPR有4个寄存器,每一个寄存器存储八位的数据。
image.png

更新PC

PC寄存器+1,至少在没有benr0的情况下的确是这个样子的

实现仅仅支持li指令的sCPU
根据上文, 用数字电路实现li的指令周期涉及的各个部件, 并将它们连接起来. 实现后, 尝试让sCPU执行数列求和程序中的前几条li指令, 并观察电路中GPR的状态是否与ISA的状态一致.

我们来分析支持li组件的CPU需要的几个元素:

  • 一个PC
  • 一个ROM进行存储,以及提前写好的数据
  • 对ROM传递的数据的拆分与分线

我们可以暂时不管指令的12位,对于34位我们传递进入RAM的addr中,然后进行补0到8位,实现的结果如下所示:
image.png
虽然我们最终似乎实现了一个支持单一指令的sCPU,但是我们也感觉到,我们手搓的CPU在调试的过程中似乎没有那么的容易,我们在划分位数和最后的寻址环节,是发生了一些错误的,我们需要对它进行一些改进,使之符合使用的规范。

实现完整的sCPU

接下来考虑如何实现add指令,对于译码,我们对于add的opcode字段,如果是00,则是add指令,如果是10,则是li指令。最适合的电路就是译码器!

考虑到opcode只有两位,我们可以使用一个2-4的译码器,它输出的独热码可以指示当前的指令属于何种指令

添加add指令:
根据上文, 在sCPU中添加add指令. 实现后, 尝试让sCPU继续执行数列求和程序中的几条add指令, 并观察电路中GPR的状态是否与ISA的状态一致.

我们需要能够封装住GPR。为了便宜我们的设计,我们需要对于GPR进行封装,我们需要完成的指令有:

  • add指令,需要至少能够同时读取两个数据,载入一个数据,需要写使能,写地址,写数据
  • li指令,需要写地址,写数据,写使能

所以我们需要的端口有:

  • 第一个读取端口:raddr1,rdata1
  • 第二个读取端口:raddr2,rdata2
  • 写端口:waddr wdata wen clk
    共有八个端口,我们需要对其进行封装

image.png
好的,我们再来分析

  • li信号,需要提供写使能信号,写数据,和数据本身
  • add信号
    • 需要raddr1和raddr2输入
    • 需要读取rdata1和rdata2,做加法,写入到wdata
    • 根据wdata和waddr,更新寄存器
      image.png
      image.png

添加bner0指令

根据上文, 在sCPU中添加bner0指令. 实现后, 尝试让sCPU执行完整的数列求和程序, 如果你的实现正确, 你应该能看到PC最终为7, 且在某GPR中存放求和结果55.

最后是bner0指令. 为了识别bner0指令, 我们可以复用指令译码器的功能. 至于操作数, 除了指令中的rs2addr, 还有一个隐含的R[0]. 由于bner0指令中rs2字段的位置和add指令中rs2字段的位置一样, 因此可以复用add指令中读出rs2寄存器的逻辑. 但bner0还需要读出R[0], 因此可以把0作为GPR的raddr1端口的输入. 不过这个端口已经被add指令的rs1占用, 但也同样可以通过多路选择器的解决问题.

读出源操作数后, bner0指令需要比较两数是否相等, 这可以通过比较器来实现. 若比较结果不相等, 需要将PC更新为addr字段. 换句话说, 只有当前指令为bner0指令, 且比较结果不相等, 才将PC更新为addr字段, 其余情况应将PC更新为PC加1. 同样地, 我们可以借助多路选择器对PC寄存器的输入端进行选择.

最后, bner0指令不会写入GPR, 因此需要将GPR的wen置为无效.

所以我们对于benr0指令进行设计,内容需要包括

  • PC的输入端,多路选择器
  • 比较器,结果为01判断,然后默认的读取字符,一个是0,一个等待输入。
  • wen使能端不开放,可以直接不给,wen默认为没有
  • 多路选择器的使能端,唯一由11通路决定,只有在11通路有效时,才可以进行使用。

其01位,代表要比较的地址,2-5位代表PC写入的addr的地址,6-7位代表11指令,而对于地址位的控制读取,我们仍然需要一个多路选择器,用于选择可以通过的信号。我们看到,在之前的指令中,01位时通过raddr2的,所以我们只需要在23位,加一个多路选择器和raddr1即可完成这个逻辑。

image.png

我们在这里加入了benr0指令,运用到两个多路选择器,一个比较器,和一个与门,在与是否等于的信号进行与门运算后,作为最后PC更新的控制信号

我们增加几个输出端口,对这个结构进行一个调试.我们加入了data数据的实时的去,以及PC段的值的显示
image.png
我们的CPU可以成功实现1-10的累加,输出结果为rdata2所示,值为55,和实际的结果一致。
最后的benr0指令非常有意思,执行完了以后就让这个命令卡在这一个命令了,方便我们进行调试,查看当前的寄存器的实时值。PC最终的结果为7

和数列求和电路进行对比

在学习数字电路时, 有一道必做题要求你通过寄存器和加法器, 计算出1+2+...+10的结果. 现在你用sCPU完成了同样的计算, 尝试对比两个方案各有什么优点和缺点.

优点:指令的可变性,可以通过外部的指令输入,灵活的进行运算,而之前学的寄存器和加法器,属于i硬编码,功能单一。使用了循环的思想,有效利用。可拓展性非常牛逼。
缺点:硬件使用,而且多个指令之间的选择与兼容,的确非常非常的复杂

计算10以内的奇数的和

实际上并没有很难,我们只需要把上述的rdata3改成2,r0改成1就可以顺利地完成这个命题。对于存储程序,其输入的编码,唯一决定输出的结果

添加out指令

实际上我们可以寻找一个小bug,因为其中的01位是固定的输出位,所以我们只用把rdata2给接入输出就欧克了,实在还有对out有要求,比如说没有out指令时不进行输出的话,我们可以把rdata2和00做一个选择器,这样的结果就是。可以在出现out指令的时候完成输出。数码管就不复制过来了,整体还是非常的复杂的。

能够实现一条让10个数相加的指令吗

显然不行。
从程序角度:指令的长度会很长,8位完全不够
从ISA的角度,ISA会有繁琐的冗余
从CPU的角度,加法器由于只有两个输入,做加法,还有一个cin,1和1和1总体还是在两位以内,不然就会出现两位进位,三位进位的极端情况,严重影响到了计算的效率

小结

自此,我们顺利的完成了F5,道阻且长,行则将至