Why do?
为什么要做这么个东西?
本人正在准备秋招,复习408的计组时,看到冯诺依曼架构(运算器、控制器、存储器和输入输出),突发奇想,学了这么久计算机,自己能不能做一个计算机呢?于是网上搜了一下,使用FPGA就可以实现自制CPU,正好手头有一个正点原子的开发板,于是开启了这个自己动手写CPU的探索之旅。总而言之,兴趣使然。如果你也和我一样,对FPGA感兴趣,对CPU实现感兴趣,欢迎交流讨论。
What do?
做这么个东西需要做些什么?
首先CPU分为CISC和RISC架构,CISC的指令很复杂,不利于初学者直接上手,因此,我们从RISC入手,选择MIPS架构实现CPU的设计。RISC架构规定必须要使用流水线设计,分为5个阶段,取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB),尽管大多数指令不需要访存,因为RISC架构使用了很多通用寄存器,操作数大多保存在寄存器中。整个MIPS架构包含了很多指令,但是,我们只需要先实现一条,然后再去举一反三,一步步完善我们的CPU。在此之前,我们必须先实现一个五级流水线。
How do?
怎么做?
实现五级流水主要是明白每一级都在干什么?级间的关系是什么?本人通过仿真测试总结了一下,五级流水分四个时钟周期完成,每一级的实现都是通过组合逻辑实现,不需要时钟控制;而级间的数据传输需要时钟同步,这样既能保证速度也能保证功能。也就是说我们需要设计这些源文件,IF(取指阶段),IF_ID(取值阶段结果送到译码阶段)、ID(译码阶段)、ID_EX(译码阶段的结果送到执行阶段)、EX(执行阶段)、EX_MEM(执行阶段的结果送到访存阶段)、MEM(访存阶段)、MEM_WB(访存阶段的结果写回)。
下面直接给出具体实现,都是一些基本的语法,主要是逻辑。
//取指阶段IF 除复位外,每个时钟都从指令寄存器ROM中获取指令 module pc( input clk, input rst, output reg [`InstAddrBus] pc, output reg pc_en ); always @ (posedge clk) begin if (rst) begin pc_en <= 1'd0; //复位时,指令寄存器禁用 end else begin pc_en <= 1'd1; end end always @ (posedge clk) begin if (pc_en == 1'd0) begin pc <= 32'd0; end else begin pc <= pc + 32'd4; //指令寄存器使能时,每个时钟周期取一个指令 end end endmodule
//IF_ID阶段 取出指令后,下一拍发送到译码阶段 module if_id( input clk, input rst, input [`InstAddrBus] if_pc, //该值由pc_reg模块输出 input [`InstDataBus] if_inst, //该值由rom指令寄存器输出,只有在pc信号使能时才有效 output reg [`InstAddrBus] id_pc, output reg [`InstDataBus] id_inst ); always @ (posedge clk) begin if (rst) begin id_pc <= 32'd0; id_inst <= 32'd0; end else begin id_pc <= if_pc; id_inst <= if_inst; end end endmodule
//译码阶段 对if_id传入的指令进行译码,分离出操作数和操作码 module id( input rst, input [`InstAddrBus] pc, input [`InstDataBus] inst, //读通用寄存器 读取 input [`RegDataBus] reg1_data, input [`RegDataBus] reg2_data, output reg reg1_rden, output reg reg2_rden, output reg [`RegAddrBus] reg1_addr, //源操作数1的地址 output reg [`RegAddrBus] reg2_addr, //源操作数2的地址 //送到ex阶段的值 output reg [`RegDataBus] reg1, //源操作数1 32b output reg [`RegDataBus] reg2, //源操作数2 32b output reg reg_wb, //写回目的寄存器标志 output reg [`RegAddrBus] reg_wb_addr,//写回目的寄存器地址 output reg [`AluOpBus] aluop //操作码 ); /* ORI指令 31:26 25:21 20:16 15:0 op rs rt imm */ wire [5:0] op = inst[31:26]; //从指令中获取操作码 高6位 reg [`RegDataBus] imm; //立即数 always @ (*) begin if (rst) begin reg1_rden <= 1'd0; reg2_rden <= 1'd0; reg1_addr <= 5'd0; reg2_addr <= 5'd0; imm <= 32'd0; reg_wb <= 1'd0; reg_wb_addr <= 5'd0; aluop <= 6'd0; end else begin reg1_rden <= 1'd0; reg2_rden <= 1'd0; reg1_addr <= inst[25:21]; //默认从指令中读取操作数1地址 reg2_addr <= inst[20:16]; //默认从指令中读取操作数2地址 imm <= 32'd0; reg_wb <= 1'd0; reg_wb_addr <= inst[15:11]; //默认结果地址寄存器rd aluop <= inst[31:26]; //高6位为操作码 //操作类型 case (op) `EXE_ORI: begin //或指令 rs寄存器值是操作数1,imm是操作数2,结果放到rt寄存器 reg1_rden <= 1'd1; //需要读取操作数1 rs寄存器的值 reg2_rden <= 1'd0; //不需要读取操作数2 rt寄存器值, imm <= {16'h0, inst[15:0]}; reg_wb <= 1'd1; reg_wb_addr <= inst[20:16]; end endcase end end always @ (*) begin if (rst) begin reg1 <= 32'd0; end else if (reg1_rden == 1'd1) begin //从通用寄存器获取操作数 reg1 <= reg1_data; end else if (reg1_rden == 1'd0) begin //从指令中获取操作数 reg1 <= imm; end else begin reg1 <= 32'd0; end end always @ (*) begin if (rst) begin reg2 <= 32'd0; end else if (reg2_rden == 1'd1) begin //从通用寄存器获取操作数 reg2 <= reg2_data; end else if (reg2_rden == 1'd0) begin //从指令中获取操作数 reg2 <= imm; end else begin reg2 <= 32'd0; end end endmodule
//通用寄存器组,用来给出操作数以及写回结果 module id_reg( input clk, input rst, //读端口 input reg1_rden, input [`RegAddrBus] reg1_addr, output reg [`RegDataBus] reg1_data, input reg2_rden, input [`RegAddrBus] reg2_addr, output reg [`RegDataBus] reg2_data, //写端口 input wr_en, input [`RegAddrBus] wr_addr, input [`RegDataBus] wr_data ); reg [`RegDataBus] regs [0:31]; //定义32个32位的寄存器 always @ (*) begin if (rst) begin reg1_data <= 32'd0; end else if (reg1_addr == 5'd0) begin //0号寄存器恒为0 reg1_data <= 32'd0; end else if (reg1_rden) begin reg1_data <= regs[reg1_addr]; end else begin reg1_data <= 32'd0; end end always @ (*) begin if (rst) begin reg2_data <= 32'd0; end else if (reg2_addr == 5'd0) begin //0号寄存器恒为0 reg2_data <= 32'd0; end else if (reg2_rden) begin reg2_data <= regs[reg2_addr]; end else begin reg2_data <= 32'd0; end end always @ (posedge clk) begin //复位时不写,写0寄存器不允许 if (!rst && (wr_en == 1'd1) && (wr_addr != 5'd0) ) begin regs[wr_addr] <= wr_data; end end endmodule
//将译码阶段的操作数、运算类型、写目的寄存器等信息,下一拍发送到流水线执行阶段 module id_ex( input clk, input rst, //id阶段信号 input [`AluOpBus] id_aluop, input [`RegDataBus] id_reg1, input [`RegDataBus] id_reg2, input id_reg_wb, input [`RegAddrBus] id_reg_wb_addr, //ex阶段信号 output reg [`AluOpBus] ex_aluop, output reg [`RegDataBus] ex_reg1, output reg [`RegDataBus] ex_reg2, output reg ex_reg_wb, output reg [`RegAddrBus] ex_reg_wb_addr ); always @(posedge clk) begin if (rst) begin ex_aluop <= 6'd0; ex_reg1 <= 32'd0; ex_reg2 <= 32'd0; ex_reg_wb <= 1'd0; ex_reg_wb_addr <= 5'd0; end else begin ex_aluop <= id_aluop; ex_reg1 <= id_reg1; ex_reg2 <= id_reg2; ex_reg_wb <= id_reg_wb; ex_reg_wb_addr <= id_reg_wb_addr; end end endmodule
//执行阶段,根据译码阶段得到的操作码和操作数进行运算,得到结果 module ex( input rst, input [`AluOpBus] aluop, input [`RegDataBus] reg1, input [`RegDataBus] reg2, input reg_wb_i, input [`RegAddrBus] reg_wb_addr_i, output reg reg_wb_o, output reg [`RegAddrBus] reg_wb_addr_o, output reg [`RegDataBus] reg_wb_data //写回数据到目的寄存器 ); always @ (*) begin if (rst) begin reg_wb_o <= 1'd0; reg_wb_addr_o <= 5'd0; reg_wb_data <= 32'd0; end else begin reg_wb_o <= reg_wb_i; reg_wb_addr_o <= reg_wb_addr_i; case (aluop) `EXE_ORI: begin reg_wb_data <= reg1 | reg2; end endcase end end endmodule
//将执行阶段的运算结果 在下一拍时发送到流水线的访存阶段 module ex_mem( input clk, input rst, input [`RegDataBus] ex_reg_wb_data, input ex_reg_wb, input [`RegAddrBus] ex_reg_wb_addr, output reg [`RegDataBus] mem_reg_wb_data, output reg mem_reg_wb, output reg [`RegAddrBus] mem_reg_wb_addr ); always @ (posedge clk) begin if (rst) begin mem_reg_wb <= 1'd0; mem_reg_wb_addr <= 5'd0; mem_reg_wb_data <= 32'd0; end else begin mem_reg_wb <= ex_reg_wb; mem_reg_wb_addr <= ex_reg_wb_addr; mem_reg_wb_data <= ex_reg_wb_data; end end endmodule
//访存阶段 如果不访存就相当于线 传输数据 module mem( input rst, input reg_wb_i, input [`RegAddrBus] reg_wb_addr_i, input [`RegDataBus] reg_wb_data_i, output reg reg_wb_o, output reg [`RegAddrBus] reg_wb_addr_o, output reg [`RegDataBus] reg_wb_data_o ); always @ (*) begin if (rst) begin reg_wb_o <= 1'd0; reg_wb_addr_o <= 5'd0; reg_wb_data_o <= 32'd0; end else begin reg_wb_o <= reg_wb_i; reg_wb_addr_o <= reg_wb_addr_i; reg_wb_data_o <= reg_wb_data_i; end end endmodule
//将访存阶段的结果 下一拍传递到回写阶段 只需要将输出端口连接到目的寄存器端口即可 module mem_wb( input clk, input rst, input mem_reg_wb, input [`RegAddrBus] mem_reg_wb_addr, input [`RegDataBus] mem_reg_wb_data, output reg wb_reg, output reg [`RegAddrBus] wb_reg_addr, output reg [`RegDataBus] wb_reg_data ); always @ (posedge clk) begin if (rst) begin wb_reg <= 1'd0; wb_reg_addr <= 5'd0; wb_reg_data <= 32'd0; end else begin wb_reg <= mem_reg_wb; wb_reg_addr <= mem_reg_wb_addr; wb_reg_data <= mem_reg_wb_data; end end endmodule
以上代码就是实现五级流水线的全部代码;但是光这些代码还不能实现仿真功能,下一章介绍一个完整的MIPS_SOPC,该设计可以通过仿真测试查看我们设计的流水线是否正常工作。这也是我们一步步设计CPU的关键核心步骤。
另外,安利一本电子书<<自己动手写CPU>>(该书已绝版,网上找个电子版吧QAQ),本文即根据这本书一步步实现CPU的设计,内容很棒,但是一定要动手!
自己动手写CPU_step1_五级流水线设计
于 2024-08-19 20:38:44 首次发布