Single Cycle CPU RISCV-V
2023 北京理工大学 计算机组成与体系结构 课程大作业 : Lab 2
实验二:单周期CPU设计
-
掌握RISCV指令格式与编码,掌握RISC-V汇编转换成机器代码的方法。
-
掌握单周期CPU的数据通路与控制器设计方法。
-
掌握硬件描述语言与EDA工具。
-
掌握基本测试与调试方法。
-
硬件描述语言Verilog HDL
-
Vivado版本:2019.2
-
代码编辑器 Visual Studio code
-
RISC-V仿真器:RARS下载地址https://github.com/TheThirdOne/rars
-
RARS需要配置Java环境,java版本:16.0.2
-
操作系统:windows 10。
-
设计并实现单周期处理器,支持包含RISC-V RV32I整数指令(LW,SW,ADD,SUB, ORI,BEQ)在内的指令子集等。
-
测试程序:完成2后在该cpu上实现对5个整数的排序,仿真测试结果与RARS中运行结果对比正确。
-
乐学提交实验报告、工程源代码和测试程序。
首先确定指令集指令条数、指令格式及编码。本次实验**使用11条RISC-V指令.
按照模块化方式设计数据通路,数据通路如图所示。本次实验设计的单周期CPU共分为12个模块,分别为PC,NPC,Instruction Memory,Control Unit,Instruction Decoder,Extend Unit,Register File,ALU,Data Memory和三个MUX模块。
根据数据通路设计结果,设计控制信号。ALUASrc为多路选择器选择信号,1选择pc作为输出,0选择寄存器1数据作为输出。ALUBSrc为多路选择器选择信号,1选择扩展立即数作为输出,0选择寄存器2数据作为输出。MemtoReg为多路选择器选择信号,选择ALU、内存数据、扩展立即数作为输出。RegWr为寄存器堆写使能信号。MemWrite为内存写使能信号。nPC_sel为NPC控制信号,选择下一条指令的地址为pc+4或xrs1+imm或pc+imm。ExtOP为符号位扩展模块选择信号。ALUctr为ALU运算指令选择信号。
top.v中为顶层控制模块,负责连接CPU各个模块,将输出送入相应模块。
module top(
input wire clk,
input wire rst
);
Control Unit负责生成控制信号。CU根据输入的操作码、指令类型等,生成控制信号。共有八种控制信号,分别为nPC_sel、RegWr、ExtOp、ALUASrc、ALUBSrc、ALUctr、MemWr、MemtoReg。
CU模块部分代码如下所示。
case(opcode)
`INST_R: //add、sub
begin
nPC_sel = `NPC_NEXT;
//RegDst = 0;
RegWr = 1;
ExtOp = `EXT_OP_DEFAULT;
ALUAsrc = 0;
ALUBsrc = 0;
MemWr = 0;
MemtoReg = `MemtoReg_ALU;
case(fun3)
//add sub
3'b000:
case(fun7)
1'b0: ALUctr = `ALU_CTR_ADD;
1'b1: ALUctr = `ALU_CTR_SUB;
endcase
endcase
end
PC模块负责根据NPC模块输出,生成下一条指令。
PC模块部分代码如下所示。
always @(posedge clk, posedge rst)
begin
//if(rst) pc <= 32'h0ffff_fffc; //方便复位之后的第一个pc+4将为全0
if(rst) pc <= 32'h0000_0000;
else pc <= npc; //不复位时将npc作为pc的新值
end
NPC模块负责根据nPC_sel生成下一条指令。nPC_sel为00时下一条指令为pc+4;nPC_sel为为10时下一条指令为pc + imm立即数;nPC_sel为11时下一条指令为reg1_data + imm立即数。
NPC模块部分代码如下所示。
module NPC(
input [31:0] ext_imm ,//符号扩展结果
input [31:0] reg1_data ,//寄存器的第一输出
input [`NPC_OP_LENGTH - 1:0] nPC_sel ,//来自CU单元,用于分支和跳转指令
input [31:0] pc ,
output [31:0] npc
);
//00时pc+4,10时pc + imm,11时pc = reg1_data + imm
assign npc= nPC_sel[0] ? (nPC_sel[1] ? reg1_data + ext_imm : pc + ext_imm) : pc + 4;
endmodule
INST_MEMORY为指令存储器,存储程序的指令。编写RISC-V代码,使用RARS生成二进制指令文件。使用vivado中的Block Memory Generator IP核作为指令存储器,将二进制文件导入IP核中完成初始化。
INST_MEMORY模块部分代码如下所示。
module INST_MEMORY(
input [15:2] addr,//是pc信号的部分截取
input clk,
output [31:0] inst //32位risv-v架构的指令
);
//IROM
instruction_memory_ip IROM (
.clka(clk), // input wire clka
.addra(addr[15:2]), // input wire [15:2] addra
.douta(inst) // output wire [31 : 0] douta
);
endmodule
Register_File为寄存器堆,可以进行寄存器读写。RISC-V有32个整数寄存器,用二维信号定义了寄存器堆,实际上只有31个,x0恒为0不需要定义。
Register_File模块部分代码如下所示。
reg [31:0] rf [1:31]; //用二维信号定义了寄存器堆,实际上只有31个,x0恒为0不需要定义
//读
always @(*)
begin
reg1_data <= reg1_addr ? rf[reg1_addr] : 32'h0;
reg2_data <= reg2_addr ? rf[reg2_addr] : 32'h0;
end
//写
always @(posedge clk, posedge rst)
begin
if(rst)
begin
/*...全部置零操作*/
end
else if(RegWr)
begin
rf[wr_reg_addr] <= wr_reg_addr ? write_data : 32'h0;
end
else;
end
EXTEND为立即数扩展模块。在CU中根据指令类型生成立即数扩展信号ExtOp,EXTEND模块根据ExtOp,输出符号位扩展后的32位立即数。
EXTEND模块部分代码如下所示。
case(ExtOp)
`EXT_OP_DEFAULT: ext_imm = 32'b0;
//符号位扩展
`EXT_OP_I_12:
if(inst_imm[31]) ext_imm = {20'h0fffff, inst_imm[31:20]}; //31为符号位
else ext_imm = {20'h000000, inst_imm[31:20]};
`EXT_OP_I_6:
//shamt[5]=0时,指令才有效
if(inst_imm[21]) ext_imm = 32'h0;
else ext_imm = {26'h0, inst_imm[25:20]};
`EXT_OP_S:
if(inst_imm[11]) ext_imm = {20'h0fffff, inst_imm[11:7], inst_imm[31:25]};
else ext_imm = {20'h000000, inst_imm[11:7], inst_imm[31:25]};
`EXT_OP_U_20:
if(inst_imm[31]) ext_imm = {12'hfff, inst_imm[31:12]};
else ext_imm = {12'h000, inst_imm[31:12]};
`EXT_OP_J:
if(inst_imm[30]) ext_imm = {12'hfff, inst_imm[30:21], inst_imm[20], inst_imm[19:12], inst_imm[31]};
else ext_imm = {12'h000, inst_imm[30:21], inst_imm[20], inst_imm[19:12], inst_imm[31]};
endcase
ALU_MUX有ALUA_MUX、ALUB_MUX两种多路选择器,分别对ALU的两个输入数据进行选择。ALUA_MUX选择pc或busA的数据,ALUB_MUX选择扩展后的立即数或者busB的数据。
ALU_MUX模块部分代码如下所示。
//ALUA
assign alu_in_1 = ALUAsrc ? pc : busA; //1:pc,0:busA
//ALUB
assign alu_in_2 = ALUBsrc ? ext_imm : busB; //1:ext_imm,0:busB
DATA_MEMORY为内存模块,使用vivado中的Distributed Memory Generator IP核,作为随机读写存储器可读可写。
DATA_MEMORY模块部分代码如下所示。
data_memory_ip DRAM (
.a(addr[9 : 0]), // input wire [9 : 0] a //这里可能有问题
.d(write_mem_data), // input wire [31 : 0] d
.clk(clk), // input wire clk
.we(MemWr), // input wire we
.spo(read_mem_data) // output wire [31 : 0] spo
);
WB_MUX为写回多路选择器,选择写入的数据为ALU计算结果或内存数据或扩展后的立即数,默认值为0。
WB_MUX模块部分代码如下所示。
always @(*)
begin
case(MemtoReg)
`MemtoReg_ALU: write_data = alu_result;
`MemtoReg_MEM: write_data = read_mem_data;
`MemtoReg_EXT: write_data = ext_imm;
`MemtoReg_DEFAULT: write_data = 32'h0 ;
endcase
end
使用RISC-V编写汇编代码,完成对5个整数进行冒泡排序的功能。汇编代码如下所示。
.data
v:
.word 8,3,6,9,1
.text
sort:
addi sp,sp,-20
sw ra,16(sp)
sw s7,12(sp)
sw s6,8(sp)
sw s4,4(sp)
sw s3,0(sp)
addi a1,x0,5 #n=5
addi s3,x0,0 #i=0
la a0,v #a0=v的address
mv s5,a0 #s5=v
mv s6,a1 #s6=n
loop1:
bge s3,s6,exit1 #i>=n,goto exit1
addi s4,s3,-1 #else,j=i-1
loop2:
blt s4,x0,exit2 #j<0,goto exit2
slli t0,s4,2 #t0=j*4,word
add t0,s5,t0 #t0=v+j*4
lw t1,0(t0) #t1=v[j]
lw t2,4(t0) #t2=v[j+1]
ble t1,t2,exit2 #if v[j]<=v[j+1].goto exit2
mv a0,s5 #else,a0=v,用于swap
mv a1,s4 #a1=j,用于swap
jal ra,swap
addi s4,s4,-1 #j=j-1
j loop2
exit2:
addi s3,s3,1 #i=i+1
j loop1
exit1:
lw s3,0(sp)
lw s4,4(sp)
lw s6,8(sp)
lw s7,12(sp)
lw ra,16(sp)
addi sp,sp,20
jal x0,exit
swap:
slli t0,a1,2
add t0,a0,t0 #v+k
lw t1,0(t0) #t1=v[k]
lw t2,4(t0) #t2=v[k+1]
sw t2,0(t0) #v[k]=t2
sw t1,4(t0) #v[k+1]=t0
jalr x0,0(ra) #回去
exit:
配置Java环境,使用RISC-V仿真器RARS对汇编代码进行测试。从地址0x10010000开始依次存放5个从小到大排序后的整数。代码运行结果正确,符合预期效果。
编写testbench进行仿真测试,以ns为时间尺度进行仿真测试。由于有顶层top模块对所有模块进行控制,只需生成时钟clk信号和复位rst信号。
testbench部分代码如下所示。
module testbench();
reg clk;
reg rst;
top My_top(.clk(clk), .rst(rst));
initial begin
rst = 1;
clk = 0;
#30 rst = 0;
//#500 $stop;
end
always
#20 clk = ~clk;
endmodule