引言:
在之前的Lab中,我们已经分别进行了硬件系统(体系结构)和软件系统(操作系统)的学习;本次实验,我们将让之前lab5编写的操作系统运行在我们自行编写的微架构上,将硬件系统和软件系统充分结合起来,真正做到软硬件的协同开发。(2023SYS2实验指导)
1 实验目的
2 实验准备
2.1 文件结构
本实验需要将lab5中的软件和lab2中的硬件结合起来,所以在完全完成实验之后的文件结构图如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 ├── Makefile ├── ip ├── sim │ ├── Axi_lite_DDR.sv │ ├── cosim.v │ ├── dpi.cpp │ └── testbench.sv ├── submit │ ├── ALU.sv│ ├── Axi_interface.sv │ ├── Axi_lite_Core.sv │ ├── Axi_lite_Displayer.sv │ ├── Axi_lite_Hub.sv │ ├── Axi_lite_RAM.sv │ ├── Axi_lite_Timer.sv │ ├── Axi_lite_Uart.sv │ ├── Bomb.v │ ├── CSRModule.sv│ ├── CSRStruct.vh │ ├── Control.v │ ├── Core.sv │ ├── Core2MMIO_FSM.v │ ├── Core2Mem_FSM.v │ ├── CoreAxi_lite.sv │ ├── Cosim_MMIO.sv │ ├── DIsplayer.sv │ ├── Define.vh│ ├── EXE.v │ ├── EXEMEM.sv │ ├── Examine.v │ ├── ExceptReg.sv │ ├── ExceptStruct.vh │ ├── Forward.v │ ├── ID.v │ ├── IDEXE.sv │ ├── IDExceptExamine.sv │ ├── IF.v │ ├── IFID.v │ ├── ImmGen.v │ ├── InstExamine.sv │ ├── MEM.v│ ├── MEMWB.sv │ ├── MMIOStruct.vh │ ├── MemAxi_lite.sv │ ├── Memmap.v │ ├── Mux.v │ ├── PipelineCPU.sv │ ├── RAM.sv │ ├── RaceControl.v │ ├── RegStruct.vh │ ├── Regs.sv │ ├── Timer.sv │ ├── WB.v │ └── uart.sv ├── syn │ ├── Axi_lite_DDR.sv │ ├── Axi_lite_DDR_sim.sv │ ├── DDR_Ctrl.sv │ ├── DebugModule.sv │ ├── DebugStruct.vh │ ├── VRAM.v │ ├── font_table.v │ ├── io.sv │ ├── mig.ucf │ ├── mig_a.prj │ ├── top.sv │ ├── top.xdc │ └── vram.hex ├── tcl │ └── vivado.tcl ├── testcode │ ├── Makefile │ ├── bootload │ │ ├── Makefile│ │ ├── bootload.S │ │ ├── link.ld│ │ ├── load_binary.c │ │ ├── load_binary.h │ ├── compress_elf.py │ ├── dummy │ │ ├── dummy.hex │ ├── endian.py │ ├── kernel │ │ ├── Makefile │ │ ├── arch │ │ │ └── riscv │ │ │ ├── Makefile │ │ │ ├── include │ │ │ │ ├── defs.h │ │ │ │ ├── mm.h │ │ │ │ ├── proc.h │ │ │ │ └── sbi.h │ │ │ └── kernel│ │ │ ├── Makefile │ │ │ ├── clock.c │ │ │ ├── clock.h │ │ │ ├── entry.S│ │ │ ├── head.S │ │ │ ├── mm.c │ │ │ ├── proc.c │ │ │ ├── sbi.c │ │ │ ├── trap.c│ │ │ └── vmlinux.lds │ │ ├── include │ │ │ ├── defs.h │ │ │ ├── math.h │ │ │ ├── mm.h │ │ │ ├── print.h │ │ │ ├── printk.h │ │ │ ├── proc.h │ │ │ ├── rand.h │ │ │ ├── sbi.h │ │ │ ├── stddef.h│ │ │ ├── string.h │ │ │ └── types.h │ │ ├── init │ │ │ ├── Makefile │ │ │ ├── main.c │ │ │ └── test.c │ │ └── lib │ │ ├── Makefile │ │ ├── math.c │ │ ├── printk.c │ │ ├── rand.c │ │ └── string.c │ ├── link.ld │ ├── mini_sbi │ │ ├── Makefile │ │ ├── def.h │ │ ├── mcsr.h │ │ ├── sbi_entry.S │ │ ├── sbi_head.S │ │ ├── sbi_trap.c │ │ ├── sbi_trap.h │ │ └── uart.c│ ├── rom │ │ ├── Makefile │ │ ├── link.ld │ │ ├── rom.S │ │ └── vmlinux │ └── testcase.ld └── tree.txt
此处的ip为系统一的lab5中所用的ip,需要搬过来供仿真使用。
2.2 环境配置
sudo apt install gcc-riscv-unknown-elf
将ZJGG给的Makefile中的--march=rv64i-zicsr
改为--march=rv64imafd
更改/testcode/Makefile
中的编译命令,调用endian.py脚本使得生成的.hex文件满足小端格式:
按照实验指导git clone
之后执行以下命令:
1 2 3 4 5 6 7 8 cd verilatorgit pull autoconf ./configure make -j `nproc `
2.3 硬件代码调整
2.3.1 v->sv
代码调整
由于本实验中ZJGG给的代码全是.sv,在某些方面, .sv的代码规范和.v并不相同。比如.sv不允许相互连接的两条线(或寄存器和线,寄存器的寄存器)的位宽不相等。
所以我们之前写的所有不等位宽赋值的语句都需要进行调整(才知道原来自己代码写得这么不规范)
例如:
1 2 assign cosim_br_taken = npc_sel_EXE; assign cosim_br_taken = {3'b0 , npc_sel_EXE};
其次,所有包含结构的文件必须为.sv文件,这个仿真倒不要求,但是上板综合的时候会报错的。那就把所有的.v改成.sv文件就好了
2.3.2 访存的指令前递问题
这是个大问题,在Lab2的代码中,我们将访问内存的操作进行了前递,即本应在MEM阶段传地址的操作被我们前递到了EXE阶段进行,这样可以省略访问内存时的一拍stall。
但本实验中助教在FSM外围引入了如下的逻辑:
MEMMAP中:
1 2 3 4 5 6 7 8 9 10 wire is_mem= (address_cpu<(`ROM_BASE+`ROM_LEN))| ((`BUFFER_BASE<=address_cpu)& (address_cpu<(`BUFFER_BASE+`BUFFER_LEN)))| ((`MEM_BASE<=address_cpu)& (address_cpu<(`MEM_BASE+`MEM_LEN))); wire is_mmio=(`MTIME_BASE==address_cpu)| (`MTIMECMP_BASE==address_cpu)| (`DISP_BASE==address_cpu)| ((`UART_BASE==address_cpu)&((`UART_BASE+`UART_LEN)==address_cpu));
好好好,他这么一连不要紧,把address_cpu(EXE阶段产生)和is_mem连上了。由于我使用了指令前递,所以本该和MEM的线被改成了和EXE的线,直接在EXE外围给我把电路干成圈了,报巨错,贼心累.......
没办法,秉持着打死不改助教代码的原则, 只能把我的指令前递改掉了....
看了看报错,发现是我从FSM里取出的数据直接塞给了MEM的问题,这样就会造成address_cpu-->Ram_to_Path-->is_mem-->address_cpu
的Circular combinational logic
,想了个办法,能不能让这个值通过EXEMEM寄存器,即在下个时钟周期传给MEM。毕竟(我认为)解决这种问题最好的办法就是给这个环路加一个时序逻辑。
取出rdata_mem
后把它塞给EXEMEM阶段间寄存器,等下个时钟周期再取出,塞给MEM进行处理
1 2 3 4 5 6 7 EXEMEM EXEMEM1( .... .Ram_to_Path_temp (rdata_mem), .... .Ram_to_Path (rdata_mem_delay_1), .... );
与此同时,整个流水线都要进行额外的一拍stall。
在取内存的时候MEM_stall
恒为1,整个流水线此时处于停滞的状态。那么我们现在只需要让MEM_stall
再延续一拍,MEMWB
,EXEMEM
等rdata_mem
的传递。
如何将MEM_stall
延续一拍:
1 2 3 4 5 6 7 8 reg MEM_stall_delay_1 = 0 ;always @(posedge clk ) begin MEM_stall_delay_1 <= MEM_stall; end .... assign EXEMEM_stall = MEM_stall | (MEM_stall_delay_1 && we_MEM_EXE);.... assign MEMWB_flush = MEM_stall | (MEM_stall_delay_1 && we_MEM_EXE) | switch_mode;
同时在EXEMEM
中给rdata_mem
开一个后门,即stall
的时候只有我能走,其他信号不许动:
1 2 3 4 5 6 7 8 9 10 11 12 always @(posedge clk ) begin if (rst|flush) begin ...... Ram_to_Path <= 64'b0 ; ...... end else if (~stall)begin ...... end Ram_to_Path <= Ram_to_Path_temp; end
问题完美解决~爱死时序逻辑了(bushi)
2.4 软件代码调整
由于本实验中我们使用助教提供的mini-sbi
,但在/lab6/testcode
目录下make编译的时候,会显示对sbi_*
函数进行了重复定义。原因是助教的mini_sbi
中提供了打印字符,接受字符,设置时间的函数。
再看下我们的软件,也是直接调用printk
,putc
,其中putc
的定义如下:
1 2 3 void putc (char c) { sbi_ecall(SBI_PUTCHAR, 0 , c, 0 , 0 , 0 , 0 , 0 ); }
嗯,既然直接调ecall
...也就是说我们的sbi_*
在lab5就已经不需要了,那直接把我们写的两个函数注释掉就好了。
其次,我们之前写的sbi_ecall
也有问题,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __asm__ volatile ( "mv a7, %[ext]\n" "mv a6, %[fid]\n" "mv a0, %[arg0]\n" "mv a1, %[arg1]\n" "mv a2, %[arg2]\n" "mv a3, %[arg3]\n" "mv a4, %[arg4]\n" "mv a5, %[arg5]\n" "ecall\n" "mv %[err], a0\n" "mv %[value], a1\n" :[err]"=r" (err),[value]"=r" (value) :[ext]"r" (ext),[fid]"r" (fid),[arg0]"r" (arg0),[arg1]"r" (arg1),[arg2]"r" (arg2),[arg3]"r" (arg3),[arg4]"r" (arg4),[arg5]"r" (arg5) :"memory" ) ;
因为这些arg参数都是通过寄存器传递的,所以在这种顺序赋值的情况下,很可能出现这种情况
之后运行仿真的时候会因为寄存器传参不对卡死.....请教了一下大神,用了另外一种内联汇编的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct sbiret ret ;register uint64 a0 asm ("a0" ) = (uint64)(arg0);register uint64 a1 asm ("a1" ) = (uint64)(arg1);register uint64 a2 asm ("a2" ) = (uint64)(arg2);register uint64 a3 asm ("a3" ) = (uint64)(arg3);register uint64 a4 asm ("a4" ) = (uint64)(arg4);register uint64 a5 asm ("a5" ) = (uint64)(arg5);register uint64 a6 asm ("a6" ) = (uint64)(fid);register uint64 a7 asm ("a7" ) = (uint64)(ext);asm volatile ( "ecall" : "+r" (a0), "+r" (a1) : "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7) : "memory" ) ;ret.error = a0; ret.value = a1; return ret;
这样编译器编译的时候会自动规避上面的那种错误情况,使得寄存器可以正确传参。
再次,由于我们的硬件还不支持mul div rem
的指令,所以要把软件中所有乘除余换成助教给的函数
1 2 3 int int_mul () ;int int_div () ;int int_mod () ;
现在我们的环境配置就基本完成了,在make
编译的时候会先编译/testcode
下面的*.c
和*.S
,在将编译出来的.elf文件转化成.hex,把.hex放到硬件上面跑。
3 实验步骤
### 3.1 RV64特权指令与CSRModule
由于本实验需要我们的处理器可以处理CSR特权级指令,所以需要对特权指令进行识别和解码,并通过CSRModule进行寄存器的读取和写入:
3.1.2 RV64特权指令解码
观察到RV64特权级指令的op_code比较统一,均为:1110011
所以可以根据op_code,在Control
对指令解码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 7'b1110011 : begin we_reg = 1 ; case (fun3) 3'b001 : begin immgen_op = R; alu_asel = 2'b01 ; alu_bsel = 2'b00 ; alu_op = ADD; wb_sel = 2'b01 ; end 3'b010 :begin immgen_op = R; alu_asel = 2'b01 ; alu_bsel = 2'b00 ; alu_op = ADD; wb_sel = 2'b01 ; end 3'b011 : begin immgen_op = R; alu_asel = 2'b01 ; alu_bsel = 2'b00 ; alu_op = ADD; wb_sel = 2'b01 ; end 3'b101 :begin immgen_op = 3'b111 ; alu_asel = 2'b00 ; alu_bsel = 2'b10 ; wb_sel = 2'b01 ; alu_op = ADD; end 3'b110 : begin immgen_op = 3'b111 ; alu_asel = 2'b00 ; alu_bsel = 2'b10 ; wb_sel = 2'b01 ; alu_op = ADD; end 3'b111 : begin immgen_op = 3'b111 ; alu_asel = 2'b00 ; alu_bsel = 2'b10 ; wb_sel = 2'b01 ; alu_op = ADD; end 3'b000 : begin npc_sel = 1 ; end ............ assign CSR_if = (op_code == 7'b1110011 && inst[19 :15 ] != 5'b0 ) ? 1 : 0 ;
由于指令的解码在ID阶段进行,读取特权寄存器的操作也在ID和EXE之间搞定,所以我们必须需要这几种情况
3.1.2.1 只读不写 CSRR
0x0000000080200024 (0x104022f3) csrr t0, sie
这条指令的意思是将sie中的值读出来放到t0寄存器中。这条指令是条伪指令,其实他的全写是csrrw t0, sie, x0
指令展开
000100000100 00000 010 00101 1110011
sie rs fun3 rd op_code
很显然我们不能让x0写到sie中,所以需要对是否写入CSR寄存器进行额外的判断。输出一个信号,保证在写CSR的时候rs不是0
assign CSR_if = (op_code == 7'b1110011 && inst[19:15] != 5'b0) ? 1 : 0;
3.1.2.2 只写不读 CSRW
0x000000008020002c (0x10429073) csrw sie, t0
这条指令的意思是说把t0的值写入CSR寄存器。显然他也是条伪指令,全写是csrrw x0, sie, t0
如果按照正常的解码的话,是把sie写入x0寄存器,但在Reg.sv
中阻止了对x0寄存器的任何写入操作,保证x0寄存器的值为0,所以这种情况就不用管了,reg_write
置0置1无所谓。
3.1.2.3 又读又写 CSRRW
0x0000000080000050 (0x34011173) csrrw sp, mscratch, sp
实际上就是对sp和mscratch两个寄存器做数值的交换,这里直接让两个写使能打开,互相读取互相交换即可。
其次,对于这三种情况而言,由于取寄存器的操作在ID和EXE之间进行,之后取出的两个值(CSR和reg)还要在Pipeline中走一轮儿到WB阶段才能写回。所以为了避免在这个过程中发生一些不可描述乱码七糟的事情,我们直接设置:
这样就可以保证RV64特权指令的正确执行啦~
3.1.2 CSRModule
根据实验指导,CSRModule要放在ID和EXE之间,和Reg的位置相同,完成特权指令的提取
根据提示链接CSRModule与Core的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CSRModule CSR( .clk (clk), .rst (!rstn), .csr_we_wb (CSR_if_WB), .csr_addr_wb (inst_WB[31 :20 ]), .csr_val_wb (Ram_to_Path), .csr_addr_id (inst_ID[31 :20 ]), .csr_val_id (CSR_value_ID), .pc_wb (pc_WB), .valid_wb (valid_WB), .time_int (time_int), .csr_ret ({mret , sret}), .except_commit (except_WB), .priv (priv), .switch_mode (switch_mode), .pc_csr (pc_csr_ID), .cosim_interrupt (cosim_interrupt), .cosim_cause (cosim_cause), .cosim_csr_info (cosim_csr_info) );
其中mret和sret两条特权指令的指令码是一定的,所以这个信号可以单独判断一下
1 2 assign mret = (inst_WB == 32'h30200073 );assign sret = (inst_WB == 32'h10200073 );
3.2 IDExceptExamine 异常检测
本实验要求我们不仅要处理时钟中断,还要处理指令异常,幸运的是在本实验中只有一条异常需要处理,就是ecall
指令
需要注意虽然这个...助教起的名字叫IDExceptExamine
...但是...后面细说吧(小狗摇头)
观察IDExceptExamine的内部结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 `include "ExceptStruct.vh" module IDExceptExamine( input clk, input rst, input stall, input flush, input [63 :0 ] pc_id, input [1 :0 ] priv, input [31 :0 ] inst_id, input valid_id, input ExceptStruct::ExceptPack except_id, output ExceptStruct::ExceptPack except_exe, output except_happen_id ); import ExceptStruct::ExceptPack; ExceptPack except_new; ExceptPack except; InstExamine instexmaine( .PC_i (pc_id), .priv_i (priv), .inst_i (inst_id), .valid_i (valid_id), .except_o (except_new) ); assign except=except_id.except ?except_id:except_new; assign except_happen_id=except_new.except &~except_id.except ; ExceptReg exceptreg( .clk (clk), .rst (rst), .stall (stall), .flush (flush), .except_i (except), .except_o (except_exe) ); endmodule
结构体ExceptReg如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 `ifndef __EXCEPT_STRUCT__ `define __EXCEPT_STRUCT__ package ExceptStruct; typedef struct { logic except; logic [63 :0 ] epc; logic [63 :0 ] ecause; logic [63 :0 ] etval; } ExceptPack; endpackage `endif
可以看到,IDExpcetExamine的执行逻辑分为以下三步:
接受外部ID阶段的指令,pc,及stall和flush的信号
将收到的指令传入InstExamine
检查是否为异常指令,并将信号打包输出出来。
如果发生异常,将except_happen_id置1
打包输出之后的信号要通过阶段间寄存器传递,传到WB之后塞给CSRModule,CSRModule就会根据ExceptReg输出一些信号。
1 2 3 4 ExceptStruct::ExceptPack except_ID='{except: 1'b0 , epc:64'b0 , ecause:64'b0 , etval: 64'b0 }; ExceptStruct::ExceptPack except_EXE; ExceptStruct::ExceptPack except_MEM; ExceptStruct::ExceptPack except_WB;
这里绝对!!绝对!!绝对!!绝对不能直接在IDEXE阶段就把except_EXE塞给CSRModule,后面时钟中断会出大问题。
3.3 异常处理
首先明确RV64的特权模式:
异常主要有这几种情况:
非法指令:在低特权态执行了高特权态才能执行的指令,或操作了高特权态才能访问的寄存器,比如在S态下rdtime a0
就会因为权限不够陷入异常处理。或者输入的指令是条空指令,错误的指令,也会触发非法异常。
ecall
异常:调用ecall时处理器会判定此条指令为异常,并传递此时的异常信息(通过sbi_ecal),根据mcause
的值不同去执行不同的异常处理操作。比如S态ecall调用陷入M态
exception trap_supervisor_ecall, epc 0x0000000080200050
ebreak
异常:在本实验中涉及不到,不提~
具体如何判断这条指令是否发生异常的逻辑ZJGG已经帮我们写好了,不用管~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 `include "Define.vh" `include "ExceptStruct.vh" module InstExamine ( input [63 :0 ] PC_i, input [1 :0 ] priv_i, input [31 :0 ] inst_i, input valid_i, output ExceptStruct::ExceptPack except_o ); wire is_ecall=inst_i==`ECALL; wire is_ebreak=inst_i==`EBREAK; wire is_illegal=(inst_i[1 :0 ]!=2'b11 )&valid_i; wire [63 :0 ] ecall_code [3 :0 ]; assign ecall_code[0 ]=`U_CALL; assign ecall_code[1 ]=`S_CALL; assign ecall_code[2 ]=`H_CALL; assign ecall_code[3 ]=`M_CALL; assign except_o.except =is_ebreak|is_ecall|is_illegal; assign except_o.epc =PC_i; assign except_o.ecause =is_ebreak?`BREAKPOINT: is_ecall?ecall_code[priv_i]: is_illegal?`ILLEAGAL_INST: 64'h0 ; assign except_o.etval =is_illegal?{32'b0 ,inst_i}:64'h0 ; endmodule
重点:当CSRModule接收到传过来的异常信号包,输出的信号会有如下变化:
当发生异常时,我们通常需要做这几件事:
将pc_IF
更改为pc_csr
。
由于我们的CSRModule是放在最后处理异常的,所以需要将所有阶段间寄存器flush
掉。
其余的事情ZJGG已经帮我们写好了,比如传递CSR的信息,切换特权态等等
IF中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 always @(posedge clk or negedge rstn) begin if (!rstn) begin pc <= 0 ; end else if (switch_mode) begin pc <= pc_csr; pc_next <= pc_csr + 4 ; end else if (br_taken) begin pc <= pc_in; pc_next <= pc_in + 4 ; end else if (~pc_stall)begin valid <= 1 ; pc <= pc_next; pc_next <= pc_next + 4 ; end end
FSM中:
1 2 3 4 assign IFID_flush = (predict_flush | IF_stall)&~IFID_stall | switch_mode;assign IDEXE_flush = ~IDEXE_stall & predict_flush | switch_mode;assign EXEMEM_flush = ~EXEMEM_stall & (data_race_if | predict_flush & IF_stall) | switch_mode;assign MEMWB_flush = MEM_stall | (delay_open == 1 && MEM_stall_delay_1 && we_MEM_EXE) | switch_mode;
3.4 中断响应
本实验中只需要我们去处理时钟中断
在软件中我们已经设置了第一次的时钟中断时间到达时,Core外围就会给Core输入time_int
信号触发时钟中断
对于时钟中断的处理方法和异常类似,只不过这次的信号传递是外部给的。
这里我们需要注意,因为时钟中断不一定在哪个阶段产生,也就是说假如现在FSM在取指令,突然发生了时钟中断pc变为pc_csr,那么下面一条从MEM中取出的指令的pc是old_pc,并不是跳转之后的。
这种情况下我们需要对此条指令进行舍弃。
FSM中的逻辑更改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 reg fake_inst_if = 0 ; assign if_stall = (if_open == 1'b1 ) | switch_mode; assign mem_stall = (mem_open == 1'b1 ) ? 1 : 0 ; assign inst = (inst_open) ? (((pc - 4 >> 2 ) % 2 == 0 ) ? inst_keep[31 :0 ] : inst_keep[63 :32 ]) : 0 ; assign rdata_cpu = (rdata_open) ? rdata_mem : 0 ; always @(posedge clk or negedge rstn) begin if (!rstn) begin ... end else begin case (state) 2'b00 : begin if (keep)begin ... end else if (wen_cpu | ren_cpu) begin ... end else if (if_request) begin state <= 2'b10 ; if_open <= 1 ; wmask_mem <= 8'b11111111 ; mem_open <= 0 ; address_mem <= pc; ren_mem <= 1 ; wen_mem <= 0 ; end end 2'b01 : begin if (valid_mem) begin ... end else if (!valid_mem) begin mem_open <= mem_open; end end 2'b10 : begin if (valid_mem) begin if (!fake_inst_if) begin if_open <= 0 ; if (~keep) mem_open <= 0 ; inst_keep <= rdata_mem; ren_mem <= 0 ; wen_mem <= 0 ; state <= 2'b00 ; inst_open <= 1 ; end else begin fake_inst_if <= 0 ; state <= 2'b11 ; ren_mem <= 0 ; wen_mem <= 0 ; end end else if (if_request == 1 ) begin fake_inst_if <= 1 ; address_mem <= pc; end else begin ... end end 2'b11 : begin state <= 2'b10 ; if_open <= 1 ; wmask_mem <= 8'b11111111 ; mem_open <= 0 ; address_mem <= pc; ren_mem <= 1 ; wen_mem <= 0 ; end endcase end end endmodule
在FSM正在取指令时,假如这时valid_mem
没有返回,却又接收到了if_request
,此时fake_inst_if
,传进来的,表示现在取的指令是假的需要舍弃。
当valid_mem
信号返回时,状态机不按照正常的状态跳转,而是跳转到另外一个状态2'b11
区别于2'b00
的状态,2'b11
继续保持if_open
信号开启即if_stall
。由于该信号的存在,刚刚取出来的fake_inst
只会停留于IF
阶段传不下去,这样就跳过了这条指令的执行。 之后,状态机由2'b11
再次跳转到2'b10
执行取指令的操作,这次取回来的就是中断函数的pc对应的inst
了。
同时在Core里面加这么一句:
1 assign if_request = ~IF_stall | switch_mode ;
这样就能在时钟中断触发时及时改变FSM中的pc了。
3.4.* 关于时钟中断的触发问题
这里想解释一下为什么之前强调不能把except_EXE
取出来直接怼给CSRModule
,而要一路传到except_WB
,然后s塞给CSRModule。
本实验处理的时钟中断过程中有两个关键点:
在时钟中断发生时触发exception interrupt #7
,由S态切换到M态,执行sbi_mti_handler
在sbi_mti_handler
函数结束运行,即mret
时触发exception interrupt #5
使程序返回S态并陷入S态的异常处理函数traps
假如我们把except_EXE
直接怼给CSRModule,会发第二个中断无法触发。
观察波形:
发现switch_mode,pc_csr,s_trap
均成功载入,但是中断无法触发,导致框架认为我们跳转的地址是不正确的。
后来发现是指令对应错了。
当s_trap
为1时,只有当valid_WB
为0才能触发S态的中断。但很明显在错误的波形中,s_trap
所对应的指令是m_ret
并不是j pc + 0x0
,导致我们不可能让这条指令valid为0 ,也不可能触发中断。
所以按照助教的代码逻辑,一定要一路传到except_WB
,然后塞给CSRModule。
4 实验结果
3.1仿真验证
make
指令仿真,一路到底
make 2>log.txt
可以在屏幕上打印字符
make board_sim
一路到底~
3.2 上板验证
按照实验指导配置IP核,把build
文件夹整个移到项目目录下,烧录bit
流文件,上板发现寄了。
用vivado跑个仿真试试:
后来问助教gg才知道要去魔改rand函数不然就会寄,那我直接注释掉就好了(笑)
再次烧录上板:
Pass~
5 Questions
5.1 使用 putc 函数输出一个字符 'a' 前后需要发生几次特权态切换,请将切换的状态和切换的原因一一列举出来。
使用putc
输出一个字符共进行两次特权态的切换
第一次ecall
触发异常,从S态切换到M态
第二次sbi_console_putchar
运行结束mret
,从M态切换到S态
5.2 如果流水线的 IF、ID、MEM 阶段都检测到了异常发生,应该选择那个流水级的异常作为 trap_handler 处理的异常?请说说为什么这么选择。
应选择MEM
阶段的异常进行处理
因except_regs
要一路传递到WB阶段才能进入CSRModule,CSRModule才能传出异常处理的信号给Core,将所有阶段间寄存器flush
掉。所以应该以第一条遇到的异常指令为准,后面的指令没有参考价值。
5.3 CSR 寄存器的读写操作如 csrrw、csrrwi 会不会引入新的 stall?如果会,在你的实现中引入了哪些 stall?可以用 forward 技术来减少这部分 stall 吗?
即,寄存器的读取是瞬发的,而寄存器的写入需要stall一拍(上升沿写回)
在我的设计中,由于我把CSRModule当做另一个Reg去处理,实际上没有引入额外的stall(与Reg交叉写入,共用一个stall)。
可以利用forward技术来减少stall。我们可以注意到,实际上一个值在IDEXE阶段从Reg中读取出来要传到WB才能进行写入。由于RV64特权指令通常不需要进行很复杂的运算,也就用不到EXE,MEM阶段的功能 ,把CSR_rdata
和REG_rdata
一直传递到最后才写入实际上是一种时间的浪费。
我们可以在IDEXE阶段读出CSR和Reg的值的同时进行交叉写入,这样只要在这里stall一拍就可以完成写回(上升沿写回)。如果是下降沿写回那就一拍都不用stall,上升沿取出,下降沿写回即可。