项目需求:
-
设计或移植一款RISC-V软核;
-
能够用流水灯的方式点亮核心板上的LED。
硬件介绍:
本项目使用了硬禾学堂的基于STM32+iCE40的电赛训练平台,而移植RISC-V软核主要使用了其上的iCE40UP5K芯片:https://www.eetree.cn/project/detail/7;芯片的介绍和数据手册:https://www.latticesemi.com/en/Products/FPGAandCPLD/iCE40UltraPlus;所移植的FemtoRV:https://github.com/BrunoLevy/learn-fpga。
项目描述:
一、程序流程图:
使用https://app.diagrams.net/绘制流程图:
二、项目介绍:
这是一个在iCE40UP5K中移植的RISC-V软核:FemtoRV,并根据核心板情况做出修改,实现RV32I基础指令集,通过RISC-V指令兼容性测试,能够用流水灯的方式点亮核心板上的LED。
三、设计思路:
1、整体设计
FemtoRV使用了多个module实现,但为了学习其结构以及加深自己的理解,我使用一个module整合所有的软核模块(可以这么做的原因是多周期CPU可以不需要多个模块来完成数据与信号的传递);同时完整版FemtoRV的许多设计都十分地简洁巧妙,但我使用了相当一部分FemtoRV教程中的代码:这些代码虽然不够优美且需要消耗更多的LUT,但更加符合直观感受,更适合初学者阅读。
2、程序的写入
因为这款核心板上的串口被MCU占用,所以在FPGA上的RISC-V软核无法像大多数同类项目一样通过串口先采用先下载软核,再上传程序,所以需要将软核与其中的程序一同上传。首先使用开源工具Ripes将RISC-V的汇编代码转换成十六进制机器语言,再将其保存在一个txt文件中,利用$readmemh读取至Memory。
initial begin
$readmemh("led_demo.txt",MEM);
end
在Ripes中输入汇编即可实时得到机器码,同时还可以进行仿真:
3、Decoder
在FemtoRV的设计中,译码部分使用列举的方式直接获得对指令类别的判断标志(isXxx)与各指令立即数(Ximm),并在后续使用中通过三目运算符简洁优雅地获取对应值:
//opcode
wire isALUreg = (instr[6:2] == 5'b01100);
wire isALUimm = (instr[6:2] == 5'b00100);
wire isBranch = (instr[6:2] == 5'b11000);
wire isJALR = (instr[6:2] == 5'b11001);
wire isJAL = (instr[6:2] == 5'b11011);
wire isAUIPC = (instr[6:2] == 5'b00101);
wire isLUI = (instr[6:2] == 5'b01101);
wire isLoad = (instr[6:2] == 5'b00000);
wire isStore = (instr[6:2] == 5'b01000);
wire otherInstr= (instr[6:2] == 5'b11100)||(instr[6:2] == 5'b00011);
//立即数
wire [31:0] Uimm={ instr[31], instr[30:12], {12{1'b0}}};
wire [31:0] Iimm={{21{instr[31]}}, instr[30:20]};
wire [31:0] Simm={{21{instr[31]}}, instr[30:25],instr[11:7]};
wire [31:0] Bimm={{20{instr[31]}}, instr[7], instr[30:25],instr[11:8],1'b0};
wire [31:0] Jimm={{12{instr[31]}}, instr[19:12],instr[20], instr[30:21],1'b0};
//寄存器地址
wire [4:0] rs1Id = instr[19:15];
wire [4:0] rs2Id = instr[24:20];
wire [4:0] rdId = instr[11:7];
//funct
wire [2:0] funct3 = instr[14:12];
wire funct7 = instr[30];
在RV32I中,opcode的最低两位都是1,所以在判断指令类型的时候只需取2到6位;一些复杂的RISC-V会使用符号位扩展(extend)完成立即数的补全,但FemtoRV使用了直接将各类立即数解码并扩展的方式;funct7只在第5位(即指令的第30位)有区别,所以译码时只需取出此位作为funct7,用于区分ADD与SUB、SRL与SRA、SRLI与SRAI。
4、ALU
ALU负责R、I型指令rd值的确定。这些指令主要依靠funct3区分,个别需要在确认完funct3后再利用funct7确认细分种类。
wire [31:0] aluIn1 = rs1;
wire [31:0] aluIn2 = isALUreg ? rs2 : Iimm;
reg [31:0] aluOut;
wire [4:0] shamt = isALUreg ? rs2[4:0] : instr[24:20]; // shift amount
always @(*) begin
case(funct3)
3'b000: aluOut = (funct7 & instr[5]) ? (aluIn1 - aluIn2) : (aluIn1 + aluIn2);
3'b001: aluOut = aluIn1 << shamt;
3'b010: aluOut = ($signed(aluIn1) < $signed(aluIn2));
3'b011: aluOut = (aluIn1 < aluIn2);
3'b100: aluOut = (aluIn1 ^ aluIn2);
3'b101: aluOut = funct7 ? ($signed(aluIn1) >>> shamt) : ($signed(aluIn1) >> shamt);
3'b110: aluOut = (aluIn1 | aluIn2);
3'b111: aluOut = (aluIn1 & aluIn2);
default: aluOut = {32{1'b0}};
endcase
end
5、Branch
与ALU大同小异,通过funct3指向的指令确认是否进行分支跳转:isBranch确认是否进入Branch的判断,而takeBranch是最终是否进行跳转的标志。
同样是更改PC,但J型指令没有涉及比较运算,所以只需体现在以及nextPC(对下一个PC的选择)及RegWrite(寄存器写回)中。
reg takeBranch;
always @(*) begin
case(funct3)
3'b000: takeBranch = (rs1 == rs2);
3'b001: takeBranch = (rs1 != rs2);
3'b100: takeBranch = ($signed(rs1) < $signed(rs2));
3'b101: takeBranch = ($signed(rs1) >= $signed(rs2));
3'b110: takeBranch = (rs1 < rs2);
3'b111: takeBranch = (rs1 >= rs2);
default: takeBranch = 1'b0;
endcase
end
6、RegWrite
来自R、I、J、U指令的寄存器的写入。这里是我认为FemtoRV十分巧妙的地方,它大量地使用了三目运算符,将原本可能需要用到条件判断的地方替换成可读性更高的方式。regWriteData是回写的数值,regWriteEn是回写的标志,在状态机中的EXECUTE状态用于判断是否回写。
assign regWriteData = (isJAL || isJALR) ? (PC + 4) :
(isLUI) ? Uimm :
(isAUIPC) ? (PC + Uimm) :
aluOut;
assign regWriteEn = (isALUreg ||
isALUimm ||
isJAL ||
isJALR ||
isLUI ||
isAUIPC)
;
7、LoadStore
处理Load、Store指令。Load与Store中都有对字(w)、半字(h)、字节(b),而RISC-V中funct3前两位为10、01、00,Load中无符号位扩展指令的funct3第2位为1;对字的操作直接使用RegisterBank地址,对半字的操作可以是RegisterBank中的两个位置(左右对齐),而对字节的操作可为四个位置(类比半字),据此可同样使用三目运算符起到条件判断的作用。
assign mem_L_S_Addr = isStore ? (rs1+Simm) : (rs1+Iimm);
wire mem_byteAccess = funct3[1:0] == 2'b00;
wire mem_halfwordAccess = funct3[1:0] == 2'b01;
//Load
wire [15:0] LOAD_halfword =
mem_L_S_Addr[1] ? memReadData[31:16] : memReadData[15:0];
wire [7:0] LOAD_byte =
mem_L_S_Addr[0] ? LOAD_halfword[15:8] : LOAD_halfword[7:0];
wire LOAD_sign = !funct3[2] & (mem_byteAccess ? LOAD_byte[7] : LOAD_halfword[15]);
wire [31:0] memLoadData =
mem_byteAccess ? {{24{LOAD_sign}}, LOAD_byte} :
mem_halfwordAccess ? {{16{LOAD_sign}}, LOAD_halfword} :
memReadData ;
//Store
wire [31:0] memStoreData;
assign memStoreData[ 7: 0] = rs2[7:0];
assign memStoreData[15: 8] = mem_L_S_Addr[0] ? rs2[7:0] : rs2[15: 8];
assign memStoreData[23:16] = mem_L_S_Addr[1] ? rs2[7:0] : rs2[23:16];
assign memStoreData[31:24] = mem_L_S_Addr[0] ? rs2[7:0] :
mem_L_S_Addr[1] ? rs2[15:8] : rs2[31:24];
wire [3:0] memStoreMask =
mem_byteAccess ?
(mem_L_S_Addr[1] ?
(mem_L_S_Addr[0] ? 4'b1000 : 4'b0100) :
(mem_L_S_Addr[0] ? 4'b0010 : 4'b0001)
) :
mem_halfwordAccess ?
(mem_L_S_Addr[1] ? 4'b1100 : 4'b0011) :
4'b1111;
8、PC
在B、J、正常+4三种情况中将相应值赋给nextPC。
wire [31:0] nextPC = (isBranch && takeBranch) ? PC+Bimm :
isJAL ? PC+Jimm :
isJALR ? rs1+Iimm :
PC+4;
9、StateMachine
状态机实现多周期CPU中状态的切换,我根据FemtoRV以及自己的想法将其分为六种状态:FETCH_INSTR:取指,并将状态切换为FETCH_REGS;FETCH_REGS:赋值rs1、rs2,避免竞争,并将状态切换为EXECUTE ;EXECUTE:执行,完成PC值与寄存器回写,并根据isStore、isLoad值(即指令是否为L、S)进入BEFORE_LOAD、MEM_STORE或回到FETCH_INSTR;BEFORE_LOAD:取出Load指令所需的内存,并将状态切换为MEM_LOAD;MEM_LOAD:将前一状态的内存值经LoadStore处理后赋值给相应寄存器,并将状态切换回FETCH_INSTR;MEM_STORE:完成Store指令,并将状态切换回FETCH_INSTR(Load、Store部分存在一些时序方面的小问题,但FemtoRV原版不存在这样的问题,由于流水灯的实现不需要用到这两个指令,所以用这样的状态机仍能完成需求)。
localparam FETCH_INSTR = 0;
localparam FETCH_REGS = 1;
localparam EXECUTE = 2;
localparam BEFORE_LOAD = 3;
localparam MEM_LOAD = 4;
localparam MEM_STORE = 5;
reg [2:0] state = FETCH_INSTR;
always @(posedge CLK) begin
if(!RST) begin
PC <= 0;
state <= FETCH_INSTR;
end else begin
case(state)
FETCH_INSTR: begin
instr <= MEM[PC[31:2]];
state <= FETCH_REGS;
end
FETCH_REGS: begin
rs1 <= RegisterBank[rs1Id];
rs2 <= RegisterBank[rs2Id];
state <= EXECUTE;
end
EXECUTE: begin
if(!otherInstr) begin
PC <= nextPC;
end
if(regWriteEn && rdId != 0) begin
RegisterBank[rdId] <= regWriteData;
end
state <= isStore ? MEM_STORE :
isLoad ? BEFORE_LOAD :
FETCH_INSTR;
end
BEFORE_LOAD: begin
memReadData <= MEM[mem_L_S_Addr[31:2]];
state <= MEM_LOAD;
end
MEM_LOAD: begin
RegisterBank[rdId] <= memLoadData;
state <= FETCH_INSTR;
end
MEM_STORE: begin
if(memStoreMask[0]) MEM[mem_L_S_Addr[31:2]][ 7:0 ] <= memStoreData[ 7:0 ];
if(memStoreMask[1]) MEM[mem_L_S_Addr[31:2]][15:8 ] <= memStoreData[15:8 ];
if(memStoreMask[2]) MEM[mem_L_S_Addr[31:2]][23:16] <= memStoreData[23:16];
if(memStoreMask[3]) MEM[mem_L_S_Addr[31:2]][31:24] <= memStoreData[31:24];
state <= FETCH_INSTR;
end
endcase
end
end
四、FPGA资源占用报告:
让Apio输出完整的日志,得到iCE40的资源占用报告:
五、指令测试:
在官方测试文件中,类似“rv32ui-p-add.txt”的txt格式文件为测试指令的十六进制形式,其中寄存器x3(gp)为存储当前进行的test数量,x26(s10)表示测试结束,x27(s11)为1时表示测试通过(pass),为0时表示测试失败(fail),所以我将寄存器x26、x27与LED[1]、LED[2]绑定,通过实际运行的结果验证其通过了指令测试。在全部测试后可知,运行所有测试文件后LED[1]、LED[2]均亮起,说明符合RISC-V的设计要求(测试文件及测试中需要修改的LED与寄存器的绑定已打包至附件中)。LED绑定代码:
assign leds = {1'b0,1'b0,RegisterBank[26][0],RegisterBank[27][0]};
测试成功的LED情况如下:
功能展示:
因为暂时没有完成GPIO模块,所以将LED直接与一个寄存器绑定,如绑定至x22(s6)寄存器(所绑定的寄存器可以任意设置,只要改变对应寄存器值即可改变灯的状态);由于硬件中引脚输出1时灯灭,输出0时灯暗,所以加入leds令LED恢复正逻辑,便于流水灯展示与调试。
wire [3:0] leds;
assign LED = ~leds;
assign leds = RegisterBank[22];
用流水灯的方式点亮核心板上的LED(请移步视频看流水灯效果)。
遇到的主要难题及解决方法:
- FemtoRV使用串口下载程序,而这块核心板的串口被STM32占用:通过初始化内存将程序直接读取到工程文件中;
- Radiant综合速度慢:使用开源工具Apio;
- Apio中没有对应核心板的配置文件:根据其他iCE40UP5K板子的配置文件修改;
- Apio生成的是bin文件,而这块核心板上的iCE40只能上传rbt格式:使用John Huang与moo两位大佬开发的bin转rbt脚本https://github.com/375432636/bin2rbt
未来的计划:
- 改进Load、Store指令;
- 在现在的RV32I基础上添加RV32M指令集(有点困难);
- 实现SPI功能并与核心板上的STM32通信;
- 尝试使用刚刚发现的SpinalHDL高效生成Verilog。
参考资料整理:
以下是我在学习FPGA与RISC-V过程中使用的资料与开发工具:
- Verilog学习与刷题平台 - HDLBits:https://hdlbits.01xz.net/wiki/Main_Page
- FPGA学习与Apio开源工具使用视频教程:https://www.youtube.com/watch?v=lLg1AgA2Xoo&t=11s
- 极简的RISC-V CPU - FemtoRV:https://github.com/BrunoLevy/learn-fpga
- 五脏俱全的RISC-V处理器核 - tinyriscv:https://gitee.com/liangkangnan/tinyriscv
- CPU结构的简单入门 - 《CPU自制入门》:https://book.douban.com/subject/25780703/
- 速度很快的综合布线开源工具 - Apio:https://github.com/FPGAwars/apio
- 将RISC-V汇编语言转换为机器语言 - Ripes:https://github.com/mortbopet/Ripes