在 GitHub 阅读源代码:bazingaterry/Mipu
一个 16bit 的精简指令集的 CPU 简介
CPU 的状态转移图,只有当 Enable = 1 && start = 1 时,CPU 才会运行。
如图为此次设计的 CPU 架构图,为五级流水线,分为 IF、ID、EX、MEM、WB 五个阶段。 i_addr 为指令内存的地址,i_datain 为指令内存根据地址取出的指令。d_addr 为数据内存的地址, d_dataout 为写入到数据内存的数据,d_datain 为数据内存根据地址取出的数据,d_we 为写入使能 信号,高电平代表写入,低电平为读取。
操作指令通过 id_ir、ex_ir、mem_ir、wb_ir 一级一级传下去。
IF:计算下一条指令的地址 PC,并从存储器读取当 前 PC 指向的指令,写入 id_ir。若是跳转指令,从 mem_ir 和 flag 判断是否需要跳转,从 reg_C 读取地址;其余 指令均是顺序执行,即每一周期 PC + 1。而在本次设计 中,对 JUMP 指令做了优化,JUMP 指令在 ID 阶段便 可以进行无条件跳转,所以 id_ir 也回接入回 MUX 中。
ID:对指令进行译码,从通用寄存器中取出源操作 数,并根据指令将数据传给 reg_A 和 reg_B。其中 reg_A 的数据来自通用寄存器,reg_B 的数据来自指令 (立即数)或是通用寄存器。若是 STORE 指令,则将数据内存写入地址传给 smdr。
EX:若当指令是运算类指令时执行运算,reg_A 和 reg_B 传入 ALU 模 块,并且输出 ALUo 给 reg_C 并处理 ZF、NF、CF,若当指令是转移类指令时进行有效地址计算并将地址传到 PC。其中 ALU 是组合逻辑,根据指令计算结果。若是 STORE 指令,则将 smdr 传给 smdr1。
MEM:若是 STORE 指令或者是 LOAD 指令则 从内存储存或读取数据。数据内存的时钟必须快于 CPU 时钟,即在 CPU 看来数据内存是可以瞬间读写的,否则流水线需要多加一个流程。
WB:将 reg_C1 数据写回到对应的通用寄存器,reg_C1 来自 reg_C 或者是数据内存。
Hazard
hazard 的产生下面用 add 指令和 load 指令举例:
- Data hazards
- Control hazards
Data hazards
不涉及到 LOAD 指令的 hazard
add gr0 gr1 gr2
add gr4 gr0 gr1
最简单的例子,当第二行指令执行到 ID 阶段,第一行代码才执行到 EX 阶段,此时第一行代码的运算结果还没有写入到 gr0,但是第二行代码会从 gr0 读取,这样数据就出错了。
解决办法:加判断直接从 ALUo 读数据。
同样地,其他的情况也可以这样解决。这里有一个小坑就是,判断从哪里读取是有顺序的,一般是从最近的指令开始判断,发现 hazard 就直接 data forward,不要判断下去了。
涉及到 LOAD 的 hazard
load gr0 gr1 val
add gr4 gr0 gr1
按着上面的思路,还是需要 data forward。结果发现,add 到 ID 的阶段,load 才到 ex 阶段,还未从内存读出数据,没法 forward。
解决办法:在 IF 阶段,发现这类冲突,pc 不要往下跳,保持不动,id_ir 全为 0,等同于插入一个 nop 指令。即变为 load nop add,下一时钟周期直接从 d_datain 读取。
同样地,这里也需要注意刚才提到的判断顺序。即先判断 ex 再判断 mem、wb。
Control hazards
JUMP 指令
无脑直接跳,没坑没难度。
其余跳转指令
其余跳转指令需要 EX 结束,得到运算结果,才能决定需不需要跳转,此时跳转指令已经在 MEM 阶段。此时 PC 按顺序取址,仍有两个指令进入了 ID 和 EX 阶段,如果发生跳转,这两条指令仍然会在流水线继续运行,这样可能会修改到寄存器或者数据内存的值。
解决办法有两种:
- 确定需要跳转时,将 ID 和 EX 里面的指清零掉,即改为 NOP 指令。例如即将 ADD -> ADD -> BN 改为 NOP -> NOP -> BN。由于未到 MEN 和 WB 阶段,无用的指令是不会影响数据的。
- IF 读取到可能需要跳转的指令的时,IF 模块忽略 i_datain 读取到的指令,强制在流水线插入两个 NOP,PC 不动。若跳转指令进行结束 EX 阶段,运算结果需要跳转,则 PC 跳转,流水线继续运行;若不需要跳转,则 PC 继续 +1,此时流水线插入了三个 NOP。
- 分支预测
第一种方法是还没有上 Jun Wang 大大的课之前想出来的,第二种方法是 Jun Wang 大大课堂上介绍的,第三种方法是 Jun Wang 大大让我们开开眼界的。
言归正传,第三种方法效率是最高的,它需要一次读取多条指令,但对整体架构改动太大,超出讨论(能力)范围。对比前两种方法,当都需要跳转的时候,效率是一样的,因为都是跳转后插进了三个 NOP。但是当不需要跳转的时候,第一种方法就有效率优势了,因为没有插进去 NOP,第二种仍然插进去三个 NOP。
同样地,这里也会有优先级的问题,在 IF 判断跳转的时候,如果 MEM 里面的 JMPR、BNZ 等需要判断的跳转指令需要跳,id_ir 里面的 JUMP 也需要跳,此时 MEM 的优先级比 ID 高。因为 JUMP 是在后面,如果前面的跳转指令需要跳转,JUMP 就不需要运行。
Instruction Memory 和 Data Memory
在跑测试的时候,instruction memory 可以写成 ROM 的形式,为逻辑电路。data memory 由于要进行读写操作,所以要设计成时序逻辑。而且读取和写入都是一个是时钟内完成的,所以 data memory 的时钟频率要比 CPU 的要高,否则读写需要增加一级流水线。