前面我们选好了要实现的指令集,并且了解了每个指令的功能(传送门:RISC-V处理器的设计与实现(一)—— 基本指令集_Patarw_Li的博客-CSDN博客),接下来我们就可以开始设计cpu了。当然我们不可能一上来就写代码,首先我们要把cpu的结构、工作流程了解清楚,然后再开始代码的编写。
一、CPU和计算机的关系
说到计算机,那就不得不提大名鼎鼎的冯诺依曼了:
约翰·冯·诺依曼(John von Neumann,1903年12月28日-1957年2月8日),美籍匈牙利数学家,计算机科学家,物理学家,是20世纪最重要的数学家之一。冯·诺依曼是罗兰大学数学博士,是现代计算机,博弈论,核武器和生化武器等领域内的科学全才之一,被后人称为“现代计算机之父”、“博弈论之父”。
而现在的计算机大多都遵循冯诺依曼结构:
冯诺依曼结构包括如下4个部分:
- CPU,即中央处理器,是一台计算机的运算核心和控制核心。其功能主要是解释计算机指令以及处理计算机软件中的数据。CPU由运算器、控制器、寄存器、高速缓存及实现它们之间联系的数据、控制及状态的总线构成
- 存储器,分为外存和内存, 用于存储数据(使用二进制方式存储)
- 输入设备,用户给计算机发号施令的设备
- 输出设备,计算机个用户汇报结果的设备
其中最核心的部分也就是CPU了,CPU又名中央处理器(Central Processing Unit,简称CPU),它由ALU(算术逻辑单元)和CU(控制单元)两部分组成:
- ALU 算术逻辑单元(Arithmetic logical Unit):是中央处理器(CPU)的执行单元,是所有中央处理器的核心组成部分,由"And Gate"(与门) 和"Or Gate"(或门)构成的算术逻辑单元,主要功能是进行二位元的算术运算,如加减乘(不包括整数除法)。基本上,在所有现代CPU体系结构中,二进制都以补码的形式来表示。
- CU 控制单元(Control Unit):负责程序的流程管理。控制单元是整个 CPU 的指挥控制中心,对协调整个计算机有序工作极为重要。
此外还包括一些寄存器,如PC(程序计数器) 、IR(指令寄存器)和一些通用寄存器等等。
二、CPU的工作流程
CPU执行分为5个阶段,取指阶段(IF)、译码阶段(ID)、执行阶段(EX)、访存阶段(MEM)、写回阶段(WB):
一、取指令阶段
取指令(Instruction Fetch,IF)阶段是根据PC的值,将一条指令从主存中取到指令寄存器的过程。程序计数器PC中的数值,用来指示当前指令在主存中的位置。当一条指令被取出后,PC中的数值将根据指令字长度而自动递增(如32位指令为PC+4)。
二、指令译码阶段
取出指令后,计算机进入指令译码(Instruction Decode,ID)阶段。在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。
三、执行指令阶段
在取指令和指令译码阶段之后,接着进入执行指令(Execute,EX)阶段。此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。为此,CPU的不同部分被连接起来,以执行所需的操作。
四、访存取数阶段
根据指令需要,有可能要访问主存,读取操作数,这样就进入了访存取数(Memory,MEM)阶段。此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。
五、结果写回阶段
作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”。写回的地方一般是寄存器或者内存,其中写回寄存器的情况最多,方便之后的指令使用。在有些情况下,结果数据也可被写入相对较慢、但较廉价且容量较大的主存。许多指令还会改变程序状态字寄存器中标志位的状态,这些标志位标识着不同的操作结果,可被用来影响程序的动作。
因为每个阶段的执行部件是分时复用的,所以可以用流水线的方式执行指令,下面是一个五级指令流水线:
三、CPU的架构设计
在了解上面这些内容后我们就可以开始CPU的框架设计了,为了简单,我们只设计三级流水线,写回和访存都放在执行阶段完成,下面是模块框架图:
1、取指阶段
取指阶段负责从rom中取出指令,传给译码阶段。其中pc模块输出需要取出的指令的地址pc_out,将指令地址传给rom后,rom寻址到对应指令再回传给if_id模块。if_id模块的作用是将指令和指令地址延后一拍传给译码模块ID_UNIT,延后一拍的目的是为了让系统以流水线的形式工作。
module IF_UNIT(
input wire clk ,
input wire rst_n ,
input wire hold_flag ,
input wire jump_flag ,
input wire[`INST_REG_DATA] jump_addr ,
output wire[`INST_DATA_BUS] ins_o , // 指令
output wire[`INST_ADDR_BUS] ins_addr_o , // 指令地址
output wire[`INST_ADDR_BUS] pc_o , // 传给rom的指令地址
input wire[`INST_DATA_BUS] ins_i // rom根据地址读出来指令
);
wire[`INST_ADDR_BUS] pc;
assign pc_o = pc;
// PC寄存器模块例化
pc u_pc(
.clk (clk) ,
.rst_n (rst_n),
.jump_flag (jump_flag),
.jump_addr (jump_addr),
.pc_out (pc)
);
// 指令寄存器模块例化
if_id u_if_id(
.clk (clk),
.rst_n (rst_n),
.hold_flag (hold_flag),
.ins_i (ins_i),
.ins_addr_i (pc),
.ins_o (ins_o),
.ins_addr_o (ins_addr_o)
);
endmodule
2、译码阶段
译码阶段负责对指令进行译码。id模块从指令中读出寄存器写回地址rd,拼接立即数imm,从寄存器单元(RF_UNIT)中取出操作数R1、R2,然后将这些通过id_ex模块延后一拍传给执行模块EX_UNIT。
module ID_UNIT(
input wire clk ,
input wire rst_n ,
input wire hold_flag ,
//从IF模块传来的指令和指令地址
input wire[`INST_DATA_BUS] ins_i ,
input wire[`INST_ADDR_BUS] ins_addr_i ,
// 传给RF模块的地址,用于读取数据
output wire[`INST_REG_ADDR] reg1_rd_addr_o ,
output wire[`INST_REG_ADDR] reg2_rd_addr_o ,
// 根据传给RF模块地址读到的数据
input wire[`INST_REG_DATA] reg1_rd_data_i ,
input wire[`INST_REG_DATA] reg2_rd_data_i ,
output wire[`INST_DATA_BUS] ins_o ,
output wire[`INST_ADDR_BUS] ins_addr_o ,
// 将读到的寄存器数据传给EX模块
output wire[`INST_REG_DATA] reg1_rd_data_o ,
output wire[`INST_REG_DATA] reg2_rd_data_o ,
// 写寄存器地址
output wire[`INST_REG_ADDR] reg_wr_addr_o ,
// 立即数
output wire[`INST_REG_DATA] imm_o
);
wire[`INST_REG_ADDR] reg_wr_addr;
wire[`INST_REG_DATA] imm;
// 指令译码模块例化
id u_id(
.clk (clk),
.rst_n (rst_n),
.ins_i (ins_i),
.ins_addr_i (ins_addr_i),
.reg1_rd_addr_o (reg1_rd_addr_o),
.reg2_rd_addr_o (reg2_rd_addr_o),
.reg_wr_addr_o (reg_wr_addr),
.imm_o (imm)
);
// 将传给EX单元的内容打一拍
id_ex u_id_ex(
.clk (clk),
.rst_n (rst_n),
.hold_flag (hold_flag),
.ins_i (ins_i),
.ins_addr_i (ins_addr_i),
.reg1_rd_data_i (reg1_rd_data_i),
.reg2_rd_data_i (reg2_rd_data_i),
.reg_wr_addr_i (reg_wr_addr),
.imm_i (imm),
.ins_o (ins_o),
.ins_addr_o (ins_addr_o),
.reg1_rd_data_o (reg1_rd_data_o),
.reg2_rd_data_o (reg2_rd_data_o),
.reg_wr_addr_o (reg_wr_addr_o),
.imm_o (imm_o)
);
endmodule
3、执行阶段
执行阶段负责指令的执行、访存以及数据的回写。alu模块对传入的两个操作数进行计算,通过 alu_op_code来判断计算类型;cu负责发出各种控制信号,比如寄存器堆写使能信号、ram写使能信号、流水线暂停信号,等等。
module EX_UNIT(
input wire clk ,
input wire rst_n ,
input wire[`INST_DATA_BUS] ins_i ,
input wire[`INST_ADDR_BUS] ins_addr_i ,
input wire[`INST_REG_DATA] reg1_rd_data_i ,
input wire[`INST_REG_DATA] reg2_rd_data_i ,
input wire[`INST_REG_ADDR] reg_wr_addr_i ,
input wire[`INST_REG_DATA] imm_i ,
output wire reg_wr_en_o ,
output wire[`INST_REG_ADDR] reg_wr_addr_o ,
output wire[`INST_REG_DATA] reg_wr_data_o ,
output wire jump_flag ,
output wire[`INST_REG_DATA] jump_addr ,
output wire hold_flag ,
// 内存相关引脚(ram)
input wire[`INST_DATA_BUS] mem_rd_data_i ,
output wire mem_wr_en_o ,
output wire[`INST_ADDR_BUS] mem_wd_addr_o ,
output reg[`INST_DATA_BUS] mem_wr_data_o
);
wire[`INST_REG_DATA] alu_res;
wire[3:0] alu_op_code;
wire pc_flag;
wire pc_imm_flag;
wire imm_flag;
wire pc_4_flag;
wire alu_zero_flag;
wire alu_sign_flag;
wire alu_overflow_flag;
wire load_ins_flag;
wire[`INST_REG_DATA] alu_data1_i;
wire[`INST_REG_DATA] alu_data2_i;
reg[`INST_DATA_BUS] mem_rd_data;
wire[2:0] funct3;
assign funct3 = ins_i[14:12];
// 访存相关
assign mem_wd_addr_o = alu_res;
always @ (*) begin
case(funct3)
`INS_SB: begin
case(mem_wd_addr_o[1:0])
2'b00: begin
mem_wr_data_o = {mem_rd_data_i[31:8],reg2_rd_data_i[7:0]};
end
2'b01: begin
mem_wr_data_o = {mem_rd_data_i[31:16],reg2_rd_data_i[7:0],mem_rd_data_i[7:0]};
end
2'b10: begin
mem_wr_data_o = {mem_rd_data_i[31:24],reg2_rd_data_i[7:0],mem_rd_data_i[15:0]};
end
2'b11: begin
mem_wr_data_o = {reg2_rd_data_i[7:0],mem_rd_data_i[23:0]};
end
endcase
end
`INS_SH: begin
if(mem_wd_addr_o[1:0] == 2'b00) begin
mem_wr_data_o = {mem_rd_data_i[31:16],reg2_rd_data_i[15:0]};
end
else begin
mem_wr_data_o = {reg2_rd_data_i[15:0],mem_rd_data_i[15:0]};
end
end
`INS_SW: begin
mem_wr_data_o = reg2_rd_data_i;
end
default: begin
mem_wr_data_o = reg2_rd_data_i;
end
endcase
end
always @ (*) begin
case(funct3)
`INS_LB: begin
case(mem_wd_addr_o[1:0])
2'b00: begin
mem_rd_data = {{24{mem_rd_data_i[7]}}, mem_rd_data_i[7:0]};
end
2'b01: begin
mem_rd_data = {{24{mem_rd_data_i[15]}}, mem_rd_data_i[15:8]};
end
2'b10: begin
mem_rd_data = {{24{mem_rd_data_i[23]}}, mem_rd_data_i[23:16]};
end
2'b11: begin
mem_rd_data = {{24{mem_rd_data_i[31]}}, mem_rd_data_i[31:24]};
end
endcase
end
`INS_LH: begin
if(mem_wd_addr_o[1:0] == 2'b00) begin
mem_rd_data = {{16{mem_rd_data_i[15]}}, mem_rd_data_i[15:0]};
end
else begin
mem_rd_data = {{16{mem_rd_data_i[31]}}, mem_rd_data_i[31:16]};
end
end
`INS_LW: begin
mem_rd_data = mem_rd_data_i;
end
`INS_LBU: begin
case(mem_wd_addr_o[1:0])
2'b00: begin
mem_rd_data = {{24{1'b0}}, mem_rd_data_i[7:0]};
end
2'b01: begin
mem_rd_data = {{24{1'b0}}, mem_rd_data_i[15:8]};
end
2'b10: begin
mem_rd_data = {{24{1'b0}}, mem_rd_data_i[23:16]};
end
2'b11: begin
mem_rd_data = {{24{1'b0}}, mem_rd_data_i[31:24]};
end
endcase
end
`INS_LHU: begin
if(mem_wd_addr_o[1:0] == 2'b00) begin
mem_rd_data = {{16{1'b0}}, mem_rd_data_i[15:0]};
end
else begin
mem_rd_data = {{16{1'b0}}, mem_rd_data_i[31:16]};
end
end
default: begin
mem_rd_data = mem_rd_data_i;
end
endcase
end
//选择输入的是pc值还是寄存器数据
assign alu_data1_i = (pc_flag) ? ins_addr_i : reg1_rd_data_i;
//选择输入的是立即数还是寄存器数据
assign alu_data2_i = (imm_flag) ? imm_i : reg2_rd_data_i;
assign reg_wr_addr_o = reg_wr_addr_i;
assign reg_wr_data_o = (load_ins_flag) ? mem_rd_data : ((pc_4_flag) ? (ins_addr_i + 4'd4) : alu_res);
assign jump_addr = (pc_imm_flag) ? ($signed(ins_addr_i) + $signed(imm_i)) : alu_res;
// 控制单元例化
cu u_cu(
.ins_i (ins_i),
.ins_addr_i (ins_addr_i),
.alu_zero_flag (alu_zero_flag),
.alu_sign_flag (alu_sign_flag),
.alu_overflow_flag (alu_overflow_flag),
.alu_op_code (alu_op_code),
.imm_flag (imm_flag),
.pc_flag (pc_flag),
.pc_4_flag (pc_4_flag),
.pc_imm_flag (pc_imm_flag),
.jump_flag (jump_flag),
.hold_flag (hold_flag),
.load_ins_flag (load_ins_flag),
.reg_wr_en_o (reg_wr_en_o),
.mem_wr_en_o (mem_wr_en_o)
);
// 运算单元例化
alu u_alu(
.alu_data1_i (alu_data1_i),
.alu_data2_i (alu_data2_i),
.alu_op_code (alu_op_code),
.alu_data_o (alu_res),
.alu_zero_flag (alu_zero_flag),
.alu_sign_flag (alu_sign_flag),
.alu_overflow_flag (alu_overflow_flag)
);
endmodule
上面只是每个模块的上层代码,具体代码请到我的gitee上下载:cpu_prj: 一个基于RISC-V指令集的CPU实现
四、仿真流程
本项目的cpu目前实现了37条指令,目前可以跑一些基本的c语言代码。
首先下载gnu工具链:百度网盘 请输入提取码,提取码:uk0w
然后配置ripes,点击edit->setting->compiller,选择compiller path为gnu工具链的地址(\bin\riscv-none-embed-gcc):
下面的编译选项要改成:-nostdlib -march=rv32i -mabi=ilp32
下面没有报错就可以点击ok了。
下面在ripes写一个简单的求和c程序,其中point指针是为了将结果sum放到ram的地址为0x00000000的地方,方便我们查看结果:
int main(){
int n = 5;
int sum = 0;
for (int i = 1; i <= n; ++i) {
sum = sum + i;
}
int* point;
point = (int*) 0x00000000;
*point = sum;
return 0;
}
点击锤子开始编译执行,可以看到右边正在执行:
将指令切换到二进制形式:
然后将右边的二进制指令复制到 \rtl\perips\instructions.txt 下:
保存之后用vivado打开项目,然后进行仿真:
将_ram添加到波形中:
然后开始仿真,可以看到成功计算出结果15:
至此,代码的仿真结束,大家用其他代码来测试也可以,不过无法实现乘除法(因为还没写这些指令,后面我会加上去的)。
之后我会继续研究如何把自己写的riscv处理器移植到开发板上,并且烧录程序进行验证。