用Verilog搭出RISC-V架构单周期CPU
本帖最后由 魏定国 于 2023-2-21 14:27 编辑单周期CPU设计目录
一、前言(一些废话)
二、知识预备
三、整体构造图及开发板型号
四、将CPU工作分解
4.1取指(IF)
4.1.1 PC模块
4.1.2 NPC模块
4.1.3 IROM模块
4.2译码(ID)
4.2.1 CU模块
4.2.2 RF模块
4.2.3 SEXT模块
4.3执行(EXE)
4.3.1 ALU模块
4.4访存(MEM)
4.4.1 DRAM模块
4.5写回(WB)
4.5.1 WB模块
4.6显示(DISPLAY)
4.6.1 display模块
4.7各模块逻辑类型总结
五、各模块关键代码
5.1取指(IF)
5.1.1 PC模块
4.1.2 NPC模块
5.1.3 IROM模块
5.2译码(ID)
5.2.1 CU模块(仅列出R型指令部分)
5.2.2 RF模块
5.2.3 SEXT模块(仅列出I型指令立即数的符号扩展)
5.3执行(EXE)
5.3.1 ALU模块(仅列出一种操作)
5.4访存(MEM)
5.4.1 DRAM模块
5.5写回(WB)
5.5.1 WB模块
5.6显示(DISPLAY)(略)
六、实际运行结果
七、结语
一、前言(一些废话)
封面与内容无关!侵权必删!
这是笔者学校暑期学期的必修课,难度并不高(仅追求及格的话),但相当耗时,对于在硬件设计方面完全不感冒的同学更是一场避不开的折磨,所以笔者想通过自己的成果和被折磨经验来帮助有此方面需求的同学,或是对于CPU设计有浓厚兴趣的人。
这是笔者在CSDN上第一篇博客,若有不足请多海涵
若有源码需求或对本文章有所见解者,请不吝赐教
二、知识预备
如果在编写Verilog这类硬件编程语言时出现没有思路等现象时,可尝试反思对于设计所要求的理论知识是否牢固。
编写CPU的预备知识如下:
[*]对数字逻辑设计有一定了解或更深层次的学习
[*]对硬件原理有较好的基础并且能加以运用
[*]对《计算机组成原理》理论至少要有所涉猎
[*]对《计算机组成原理》教材(国内外皆可)中的CPU工作原理比较熟悉,并且能够自行构思其完整工作过程
三、整体构造图及开发板型号
[*]本设计基于开发板为Xilinx的xc7a100tfgg484系列
四、将CPU工作分解
4.1取指(IF)
4.1.1 PC模块
我们知道CPU要取出指令,所依靠的计数器就是PC,通过PC的值pc,CPU可确定取指令的位置,所以此模块负责传输pc值。
4.1.2 NPC模块
在单周期模型中,PC需要在每个时钟上升沿到来之际进行相应更新,我们可以把这个逻辑单元单独拿出来,向PC模块传输下一个pc的值npc,同时此模块也接受一些后序模块数据,因为这些数据可能影响到如何选择npc的正确值。
module NPC(
input imm ,//符号扩展结果
input rD1 ,//寄存器的第一输出
input pc_sel,//来自CU单元,用于分支和跳转指令
input npc_op,//同pc_sel来源相同
input pc ,
outputnpc
);
//npc_op为0时pc+4,为1时有跳转(pc_sel为0时pc + imm,为1时pc = rD1 + imm)
assign npc = npc_op ? (pc_sel ? rD1 + imm : pc + imm) : pc + 4;
endmodule 4.1.3 IROM模块
该模块使用vivado平台提供的IP核实现,通过预先存入16进制指令序列,并且接受PC模块的pc值作为地址寻址取指令,并将取出的指令inst传送至后序模块(inst可以说是CPU最重要的变量了,没有inst一切工作都不可能进行)。
4.2译码(ID)
4.2.1 CU模块
控制单元(control unit)的简称,CU是CPU的“大脑”,负责解析从IROM模块传来的inst指令,并且生成各类控制信号,用于调遣其余部件进行工作。
4.2.2 RF模块
寄存器堆(register file)的简称,寄存器堆是CPU负责高速存储运算结果以及一些常用数据的存储设备,有读\写两种功能,为正确读写数据并且尽可能使CPU实际性能提示,采用组合逻辑读,时序逻辑写操作。
4.2.3 SEXT模块
符号扩展单元(sign extend),负责对risv-v指令的“可能”立即数位置所表示的立即数进行符号扩展为32位宽的数据,该行为受CU单元的sext_op信号所控制。
4.3执行(EXE)
4.3.1 ALU模块
ALU是算数逻辑单元,负责CPU内部绝大部分运算任务(所以主频的提高关键跟ALU性能有着偌大的关系),接受前面IF阶段和ID阶段的各类信号以及数据输入,通过CU单元的控制信号选择第一、第二运算数以及运算类型。
4.4访存(MEM)
4.4.1 DRAM模块
DRAM模块是使用vivado平台提供的IP核搭建的模拟主存,读写接口会有所提示。
4.5写回(WB)
4.5.1 WB模块
其实单周期CPU中的写回模块就是一个多路选择器,通过CU单元的控制信号wd_sel决定写回数据的选择,备选的数据有:ALU运算结果,pc值,符号扩展结果imm,主存读取输出ramdout。
4.6显示(DISPLAY)
4.6.1 display模块
此模块是为了实际下板而设立的,主要负责控制开发板上的LED灯,拨码开关和数码管。
4.7各模块逻辑类型总结
[*]组合逻辑:NPC, IROM, SEXT, CU, ALU, WB
[*]时序逻辑:PC, DRAM, DISPLAY
[*]混合逻辑:RF(逻辑读,时序写)
五、各模块关键代码
5.1取指(IF)
5.1.1 PC模块
module PC(
input clk ,//CPU时钟
input rst ,//复位信号,高电平有效
input npc ,//NPC模块的npc值
outputreg pc //PC模块的输出值pc
);
always @(posedge clk, posedge rst)
begin
if(rst) pc <= 32'h0ffff_fffc;//方便复位之后的第一个pc将为全0
else pc <= npc; //不复位时将npc作为pc的新值
end
endmodule 5.1.2 NPC模块
module NPC(
input imm ,//符号扩展结果
input rD1 ,//寄存器的第一输出
input pc_sel,//来自CU单元,用于分支和跳转指令
input npc_op,//同pc_sel来源相同
input pc ,
outputnpc
);
//npc_op为0时pc+4,为1时有跳转(pc_sel为0时pc + imm,为1时pc = rD1 + imm)
assign npc = npc_op ? (pc_sel ? rD1 + imm : pc + imm) : pc + 4;
endmodule 5.1.3 IROM模块
module inst_mem(
input addr,//是pc信号的部分截取,可以思考一下为什么只需要2~15bit位?
outputinst //32位risv-v架构的指令
);
//program为vivado对应IP核的实例化,大小为16384*32bit
program U0_irom
(
.a(addr),
.spo(inst )
); 5.2译码(ID)
5.2.1 CU模块(仅列出R型指令部分)
module CU(
input fun7 ,//接口是inst
input [ 2:0]fun3 ,//inst
input [ 6:0]opcode,//inst
input [ 1:0]b_flag,//branch的大小判断结果
//outputreg debug_wb_ena,//debug信号
outputreg npc_op,//用于NPC模块确认npc值
outputreg pc_sel,//同样用于确认npc值
outputreg rf_we ,//寄存器堆的写使能
outputreg [ 1:0]wd_sel,//写回模块的选择信号
outputreg [ 2:0]sext_op ,//符号扩展单元的操作选择信号
outputreg [ 3:0]alu_op,//ALU单元的操作选择信号
outputreg alua_sel,//ALU单元的第一操作数选择信号
outputreg alub_sel,//ALU单元的第二操作数选择信号
outputreg branch,//分支信号,分支、跳转指令的标志
outputreg dram_wr//主存的写使能
);
//以下带`的如`R,`DFT均为宏定义的常量
always @(*)
begin
case(opcode)
`R ://R-type
begin
//debug_wb_ena = 1;
npc_op = 0 ;
pc_sel = 0 ;
sext_op= `DFT;
rf_we = 1 ;
wd_sel = 2'b00 ;
alua_sel = 0 ;
alub_sel = 0 ;
branch = 0 ;
dram_wr= 0 ;
case(fun3)
3'b000:
case(fun7)
1'b0: alu_op = `ADD ;//add
1'b1: alu_op = `SUB ;//sub
default:alu_op = `DFT ;
endcase
3'b001: alu_op = `LSHIFT ;//sll
3'b010: alu_op = `CMP ;//slt
3'b011: alu_op = `UCMP ;//sltu
3'b100: alu_op = `XOR ;//xor
3'b101:
case(fun7)
1'b0: alu_op = `RSHIFT ;//srl
1'b1: alu_op = `ARSHIFT;//sra
default:alu_op = `DFT ;
endcase
3'b110: alu_op = `OR ;//or
3'b111: alu_op = `AND ;//and
default: alu_op = `DFT ;
endcase
end
default://unknown-type
begin
/*...*/
end
endcase
end
endmodule
5.2.2 RF模块
module RF(
input clk ,
input rst ,
input [ 4:0]rR1_addr,//寄存器堆第一输出的地址
input [ 4:0]rR2_addr,//寄存器堆第二输出的地址
input [ 4:0]wR ,//写寄存器堆的寄存器号
input rf_we ,
input wD ,//写寄存器堆的写回数据
outputreg rD1 ,
outputreg rD2
);
reg rf ; //用二维信号定义了寄存器堆,实际上只有31个,x0恒为0不需要定义
always @(*)
begin
rD1 <= rR1_addr ? rf : 32'h0;
rD2 <= rR2_addr ? rf : 32'h0;
end
//写
always @(posedge clk, posedge rst)
begin
if(rst)
begin
/*...全部置零操作*/
end
else if(rf_we)
begin
rf <= wR ? wD : 32'h0;
end
else;
end
endmodule
5.2.3 SEXT模块(仅列出I型指令立即数的符号扩展)
module SEXT(
input inst ,
input [ 2:0] sext_op ,
outputreg imm
);
always @(*)
begin
case(sext_op)
`I_ext://I-type
if(inst) imm = {20'h0fffff, inst};
else imm = {20'h000000, inst};
default: imm = 32'b0;
endcase
end
endmodule
5.3执行(EXE)
5.3.1 ALU模块(仅列出一种操作)
module ALU(
input rD1 ,
input pc ,
input rD2 ,
input imm ,
input [ 3:0]alu_op,
input alub_sel,
input alua_sel,
input branch,
outputreg [ 1:0]b_flag,
outputreg alu_c //ALU单元的运算结果
);
wire alu_a; //第一操作数
wire alu_b; //第二操作数
reg compare; //比较计算结果
assignalu_a = alua_sel ? pc : rD1; //1:pc,0:rD1
assignalu_b = alub_sel ? imm : rD2;//1:imm,0:rD2
always @(*)
begin
if(branch)
begin//risc-v参与运算的数均为补码形式,通过减法结果符号位可判断大小
case(alu_op)
`SUB://此情况均为有符号数比较
begin
compare = alu_a+~alu_b + 1;//用a - b来判断大小关系
if(compare) b_flag = `LESS ;//a < b
else
if(compare) b_flag = `MORE ;//a > b
else b_flag = `EQUAL;//a = b
end
/*...*/
default: b_flag = `DFT ;
endcase
end
else
case(alu_op)//至少9种操作
/*...*/
endcase
end
endmodule 5.4访存(MEM)
5.4.1 DRAM模块
module data_mem(
input clk ,
input addr ,//alu单元运算结果就是地址
input dram_wr ,
input din ,//输入数据就是RF的输出信号rD2
output ramdout
);
wire ram_clk = ~clk;// 因为芯片的固有延迟,DRAM的地址线来不及在时钟上升沿准备好,
// 使得时钟上升沿数据读出有误。所以采用反相时钟,使得读出数据比地址准备好要晚大约半个时钟,
// 从而能够获得正确的数据。
wire addr_tmp = addr - 16'h4000;
dram U_dram
(
.clk (ram_clk) , // input wire clka
.a (addr_tmp) , // input wire addra
.spo (ramdout) , // output wire douta
.we (dram_wr) , // input wire wea
.d (din) // input wire dina
);
endmodule
5.5写回(WB)
5.5.1 WB模块
module WB(
input alu_c ,
input pc ,
input ramdout ,
input imm ,
input [ 1:0]wd_sel,
outputreg wD
);
always @(*)
begin
case(wd_sel)
2'b00:wD = alu_c;
2'b01:wD = ramdout;
2'b10:wD = pc+4 ;
2'b11:wD = imm ;
default:wD = 32'h0;
endcase
end
endmodule
5.6显示(DISPLAY)(略)
六、实际运行结果
测试汇编代码:(包含算数逻辑运算,访存,跳转)
图1.3.1图1.3.2
图1.3.2中所执行的指令为lui x1,0x21212至jal start部分,其中bge的条件被满足会分支跳转,jal不会被执行,下一条指令将是sw x3,0(x0),寄存器x1在第一条指令后变为0x21212000,第二条指令后变为0x21212121;x2的值在第三条指令后值为0xffffffe1;x3在第四条指令后变为0x42424242,即x1内容左移1位的结果;x4在第五条指令后变为0x10909090,即为x1内容算术右移1位后的结果;同时第六条分支指令bge的b_flag结果为2,即表示x3>x2,进行跳转;目前为止所有指令均按照预期进行。
图1.3.3
图1.3.3展示从sw x3,0(x0)到lw x4,0(x0)部分,可见在执行完xor指令后,x1, x2, x3寄存器的值全部清零,ramdout的值变为x3的值,表示指令sw执行成功
图1.3.4
图1.3.4展示最后一条指令执行结果,x4在短暂清零后获得了存入内存的x3的值,至此,所有指令均成功执行。
七、结语
构思这项工程时苦于自己近乎弱智的资料检索能力以及本届要求进一步严格化,只能“白手起家”从零开始搭建框架、实现模块、拼接连线以及逐步Debug,实际上板更是在若干次修改代码,写bitstream的噩梦循环中度过的,但总而言之对于设计硬件能力而言,确实有了长足的进步和质的提升。
最后附上一句激励我坚持完整个暑期学期的话语,伟人的言语总是充满力量与希望,在人心中种下信念的火种。
一步一步走下去
一点一滴做下去
世间事最怕的就是认真
——教员完
给力
页:
[1]