从零构建前的准备
设计思路
理论知识已经准备好了,接下来该开始实践了。
如何从零开始写一个CPU?
首先要明确ISA架构:我们需要支持哪些指令?RV32的拓展很多很多,比如MIAFD等等。我们可以实现一个“精简版”的RV32I指令集CPU——连里面的ecall和ebreak都不实现。只要满足基础的38条(甚至只支持lw/sw,不考虑非对齐访存),就能运行80%的C语言程序。至于Zicsr扩展和M扩展等,在构建完毕后再考虑。
然后,是微架构设计:是做单周期还是多周期,亦或者流水线?是做顺序还是乱序?单发射还是多发射?饭要一口口吃,步子迈太大了容易扯着蛋。因此,做一个经典的五级流水线、顺序单发射的CPU即可。
然后,保持一个清醒的头脑。即使是一个最简单的RV32ICPU,也需要不下于10个模块。所有的代码加起来可能连3000行都不到,但是其中的连线无比复杂。如果不在写之前就想好,很容易莫名其妙地丢掉一条连线、一个端口,然后看波形debug半天。
时刻要记住:切不可“只见树木、不见森林”——不建议一个一个模块写,而是创建好文件后问自己:
- 这个模块的功能是什么?
- 这个模块是组合逻辑电路还是时序逻辑电路?
- 这个模块需要哪些控制信号?
- 这个模块需要输出什么数据?输出给谁?
边考虑这些问题边创建文件,然后,类似参考书上的指导,构建数据通路。可以不写出来,但要想清楚,哪些数据会在寄存器之间流动,并被传递到下一级流水线。等每个文件的大体框架搭建完毕,再从易到难、从组合逻辑到时序逻辑完成。一级一级流水线向前推进,写完某一级后继续完成两级之间的流水线寄存器设计。
参考资料
xuanhao44 - HITSZ-miniRVCPU
开发环境
参考 VSCode配置Verilog开发环境 即可。
这里安利一下一个超级棒的的VSCode插件:Digital IDE
内部集成了一件模块测试、代码高亮与格式化、快速跳转等。虽然生成模块图的功能有些BUG,但是依旧非常强,瑕不掩瑜!
现在写Verilog都用它了。绝赞!
取指 IF级 设计
IF级取出指令,因此模块很少,只要实现PC程序计数器和IROM即可。
程序计数器 PC.sv
显然,程序计数器是时序逻辑模块。这里将PC+4的计算也合并到PC模块内。
程序计数器要干什么?既要计算出下一时刻的PC,还要考虑是否发生分支跳转——这里要接受来自EX级的跳转发生信号,因为B-Type类指令到EX级才能得出结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| module PC ( input logic clk, input logic rst_n, input logic keep_pc, input logic branch_op, input logic [31:0] branch_target, output logic [31:0] pc_if, output logic [31:0] pc4_if );
logic [31:0] npc;
always_comb begin pc4_if = pc_if + 4; npc = branch_op ? branch_target : pc4_if; end
always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) pc_if <= 0; else if (keep_pc) pc_if <= pc_if; else pc_if <= npc; end
endmodule
|
指令存储器 IROM.sv
IROM即存放程序代码的Flash,只读,不依赖任何时钟信号,因此是组合逻辑模块。通常来说,IROM(指令存储器)会被放在译码级,但不自行封装时,我们也会给CPU接出instr和pc到厂商的 IROM IP核。为了方便思考,此处放在IF级即可。为了便于仿真,这里自己实现一个支持加载hex文件的IROM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| `timescale 1ns / 1ps
module IROM ( input wire [13:0] a, output reg [31:0] spo );
reg [31:0] rom_data[16384];
initial begin
integer i; string rom_file;
for (i = 0; i < 16384; i = i + 1) begin rom_data[i] = 32'h0d000721; end
if (1) begin rom_file = "user/data/hex/jalr.hex" ; $readmemh(rom_file, rom_data); $display("IROM: Loaded instructions from %s", rom_file); end else begin $display("IROM: Loading default test program"); load_default_program(); end end
always_comb begin spo = rom_data[a]; end
task automatic load_default_program; begin $display("IROM: Default test program loaded"); rom_data[0] = 32'h00500093; rom_data[1] = 32'h00300113; rom_data[2] = 32'h002081b3; rom_data[3] = 32'h40208233; rom_data[4] = 32'h0020f2b3; rom_data[5] = 32'h0020e333; rom_data[6] = 32'h002093b3; rom_data[7] = 32'h0020c433; end endtask
endmodule
|
hex文件加载方式可以选择手动指定路径,也可以选择编译时传入,后者配合Makefile文件很方便,不过我选择DIDE的一键仿真。
IF/ID级流水线寄存器 PR_IF_IF.sv
终于到了第一道坎:两级之间的流水线寄存器。
流水线寄存器起到一个承上启下的作用,在两个阶段之间保存数据,使得指令可以连续执行。
都寄存器了,那肯定是时序逻辑模块了。
我们需要哪些信号?
首先是PC和PC+4,其中PC+4因为分支跳转和jal/jalr指令的需求,要一直送到EX级。接着是当前的指令,要送入下一级的译码器。
还有什么?我们肯定需要处理流水线的竞争冒险,而不能简单打一拍——就算打一拍,也要有相应的控制信号。因此,还需要flush和stall这两个控制信号,来控制IF级是否冲刷、是否停顿。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| `include "./include/defines.svh"
module PR_IF_ID ( input logic clk, input logic rst_n, input logic flush, input logic stall, input logic [31:0] pc_if_i, input logic [31:0] pc4_if_i, input logic [31:0] instr_if_i, output logic [31:0] pc_id_o, output logic [31:0] pc4_id_o, output logic [31:0] instr_id_o, input logic instr_valid_if_i, output logic instr_valid_id_o );
always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin pc_id_o <= 32'b0; pc4_id_o <= 32'b0; instr_id_o <= 32'b0; instr_valid_id_o <= 1'b0; end else if (flush) begin pc_id_o <= 32'b0; pc4_id_o <= 32'b0; instr_id_o <= 32'b0; instr_valid_id_o <= 1'b0; end else if (stall) begin pc_id_o <= pc_id_o; pc4_id_o <= pc4_id_o; instr_id_o <= instr_id_o; instr_valid_id_o <= instr_valid_id_o; end else begin pc_id_o <= pc_if_i; pc4_id_o <= pc4_if_i; instr_id_o <= instr_if_i; instr_valid_id_o <= instr_valid_if_i; end end
endmodule
|
至此,IF级完毕。