写一个 CPU
这是计算机组成课的实验项目,当然,也是学 CS 底层绕不过去的项目。我曾经试过一个 RISC-V 流水线 CPU 的实践,就跟着 Patterson 教授经典的教材 Computer Organization and Design RISC-V Edition,可惜当时动力不是十分充足,做的东西有点烂尾。我觉得到了需要做作业的时候,我大概就有动力了。
可惜 XJTU 的计组不出意料地食古不化,使用 MIPS。无所谓了,反正得写吧。做好的项目已经开源了,如果有小伙伴需要一点参考,那可以看看我是怎么组织这些 Verilog 代码的:
Github Repository: HeliumCPUv2-MIPS32
文件目录描述
1 | . # 根目录,包含 Makefile 等文件 |
准备环境
老师提供的是一个古早版本的 Modelsim,在 Win7 安装才不会出 bug 的那种。说实话,我不想用。有更多的好用或免费的解决方案放在这里呢:
笔者使用的是 Icarus Verilog,它比较简单;Verilator 也非常不错,可以使用 C++ 写 Testbench。如果想要 IDE,或者买了赛灵思的板子,那推荐使用 Verilator,也是免费的。
Icarus Verilog 是命令行工具,配合 VSCode 使用很香。VSCode 还有查看波形的插件 WaveTrace,但超过 8 个波形要收费,如果有需求可以使用 GTKWave。至于怎么安装这些环境,以及怎么跑通我的代码,在 Github 的仓库中有说明。
一步一步来
小目标
事实上略有点一步登天,但是直接支持多一点指令,能够提前想好奇怪的数据通路,比先实现一个小的指令集再大修大改要优雅。考虑支持下面的指令:
1 | `R_TYPE: (18) |
Verilog 的宏就需要使用符号 ` 开头,所以这也是我在
defines.v
中说明的方法。
MIPS-32 的指令集分为三种:R 型,I 型和 J 型。R 型指令一般有
op_code
,rs
,rt
,rd
,funct
这么几个有用的字段(具体的请参照指令集手册)。I 型顾名思义,没有
rd
和 funct
,取而代之的是 16 位的立即数
imm
。至于 J
型,除了
op_code
,剩下的所有位都是目的地址。
现在设计解码器和控制器为时尚早,只是了解一下有什么东西会进来,可以先从简单的开始。
先参照一下别人设计的数据通路图(比如书上的),或者我的:
嗯,只是为了看看有什么元件,然后一个一个写。
各个模块
ALU
事实上,要是不实现乘法,ALU 是最简单的部件,只需要 Case 语句即可,剩下的操作 Verilog 都直接提供了。
ALU MUX
要想想为什么设计成这样:ALU 的两个 OP 可以有一些不同的来源,在我这里,它们可以是 PC(为了 JALR, JAL);可以是立即数;可以是寄存器值。
REGFILE
就是很多寄存器存储的地方,顺便解决一下上升沿写入,读出的问题。为了方便,我的读出甚至是组合逻辑。
MEM
又称 Memory Access,做的主要工作是处理读字、读半字、读字节的琐事,剩下的交给“内存”就好了。
PC
除了每回合自增 4,这个 PC 模块还需要处理取指令,以及跳转。
IMEM & DMEM
由于是仿真,不需要接上 FPGA 跑,就和寄存器堆差不多实现就可以了。
DECODE & CONTROL
这是最难解决的一部分,也是最能体现设计的一部分,你需要根据指令生成控制信号,耐心些吧。
WB
事实上只是一个 MUX,控制把什么写回寄存器堆。
COUNTER
是一个计数器,用来产生各个 Stage 的时钟信号。
连线
我建议每写一个模块,就把线连出来,这样,写完的时候,线也连好了,几乎就可以测试了!
高级的版本
上面只是实现了一个单周期的 CPU,每五个周期做完一条指令。但是做的时候就考虑的流水,所以写模块的时候已经为直接用于流水优化了。
我做了多周期和五级流水,可以前往 Github 看看通路和结果!