基于STM32+iCE40的电赛训练平台完成RISC-V软核的移植
这是一个在iCE40UP5K中移植的一个RISC-V软核:FemtoRV,并根据核心板情况做出修改,实现RV32I基础指令集,通过RISC-V指令兼容性测试,能够用流水灯的方式点亮核心板上的LED。
标签
FPGA
2023寒假在家练
RISC-V软核
pei
更新2023-03-28
福州大学
1584

项目需求:

  1. 设计或移植一款RISC-V软核;
  2. 能够用流水灯的方式点亮核心板上的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/绘制流程图:

FqZPDWn-vNAcKUzZ2Ydel6SeZ_0V

二、项目介绍:

这是一个在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中输入汇编即可实时得到机器码,同时还可以进行仿真:

FhW0urKjcvnCidi4IV3f8lBCyigA

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的资源占用报告:

Ftchl9Er4BwVAKUbSvONos7C6_ev

五、指令测试:

在官方测试文件中,类似“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情况如下:

Frx_bc9kM4PIImFoEhMjXfH93U0c

功能展示:

因为暂时没有完成GPIO模块,所以将LED直接与一个寄存器绑定,如绑定至x22(s6)寄存器(所绑定的寄存器可以任意设置,只要改变对应寄存器值即可改变灯的状态);由于硬件中引脚输出1时灯灭,输出0时灯暗,所以加入leds令LED恢复正逻辑,便于流水灯展示与调试。

wire [3:0] leds;
assign LED = ~leds;
assign leds = RegisterBank[22];

用流水灯的方式点亮核心板上的LED(请移步视频看流水灯效果)。

遇到的主要难题及解决方法:

  1. FemtoRV使用串口下载程序,而这块核心板的串口被STM32占用:通过初始化内存将程序直接读取到工程文件中;
  2. Radiant综合速度慢:使用开源工具Apio;
  3. Apio中没有对应核心板的配置文件:根据其他iCE40UP5K板子的配置文件修改;
  4. Apio生成的是bin文件,而这块核心板上的iCE40只能上传rbt格式:使用John Huang与moo两位大佬开发的bin转rbt脚本https://github.com/375432636/bin2rbt

未来的计划:

  1. 改进Load、Store指令;
  2. 在现在的RV32I基础上添加RV32M指令集(有点困难);
  3. 实现SPI功能并与核心板上的STM32通信;
  4. 尝试使用刚刚发现的SpinalHDL高效生成Verilog。

参考资料整理:

以下是我在学习FPGA与RISC-V过程中使用的资料与开发工具:

  1. Verilog学习与刷题平台 - HDLBits:https://hdlbits.01xz.net/wiki/Main_Page
  2. FPGA学习与Apio开源工具使用视频教程:https://www.youtube.com/watch?v=lLg1AgA2Xoo&t=11s
  3. 极简的RISC-V CPU - FemtoRV:https://github.com/BrunoLevy/learn-fpga
  4. 五脏俱全的RISC-V处理器核 - tinyriscv:https://gitee.com/liangkangnan/tinyriscv
  5. CPU结构的简单入门 - 《CPU自制入门》:https://book.douban.com/subject/25780703/
  6. 速度很快的综合布线开源工具 - Apio:https://github.com/FPGAwars/apio
  7. 将RISC-V汇编语言转换为机器语言 - Ripes:https://github.com/mortbopet/Ripes
附件下载
risc_v_practice.zip
工程文件夹
risc_v_practice.rbt
可直接上传的rbt文件
团队介绍
福州大学 电气工程及其自动化专业 何祥培
团队成员
何祥培
福州大学 电气工程及其自动化专业
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号