Computer System II Lab 6 :综合实验

引言:

在之前的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 环境配置

  • riscv-unknwon-elf-gcc安装

sudo apt install gcc-riscv-unknown-elf

  • Makefile的更改

将ZJGG给的Makefile中的--march=rv64i-zicsr改为--march=rv64imafd

更改/testcode/Makefile中的编译命令,调用endian.py脚本使得生成的.hex文件满足小端格式:

  • 安装新版本verilator

按照实验指导git clone之后执行以下命令:

1
2
3
4
5
6
7
8
cd verilator
git pull# Make sure git repository is up-to-dategit tag# See what versions exist#git checkout master# Use development branch (e.g. recent bug fixes)#git checkout stable# Use most recent stable release#git checkout vlversion # Switch to specified release version
autoconf
# Create ./configure script
# Configure and create Makefile
./configure
# Build Verilator itself (if error, try just 'make')make -j ~nproc~sudo make install
make -j `nproc`

2.3 硬件代码调整

2.3.1 v->sv代码调整

由于本实验中ZJGG给的代码全是.sv,在某些方面, .sv的代码规范和.v并不相同。比如.sv不允许相互连接的两条线(或寄存器和线,寄存器的寄存器)的位宽不相等。

所以我们之前写的所有不等位宽赋值的语句都需要进行调整(才知道原来自己代码写得这么不规范)

例如:

1
2
assign cosim_br_taken = npc_sel_EXE; // old 
assign cosim_br_taken = {3'b0, npc_sel_EXE}; // new

其次,所有包含结构的文件必须为.sv文件,这个仿真倒不要求,但是上板综合的时候会报错的。那就把所有的.v改成.sv文件就好了

1
EXEMEM.v -> EXEMEM.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_cpuCircular combinational logic,想了个办法,能不能让这个值通过EXEMEM寄存器,即在下个时钟周期传给MEM。毕竟(我认为)解决这种问题最好的办法就是给这个环路加一个时序逻辑。

取出rdata_mem后把它塞给EXEMEM阶段间寄存器,等下个时钟周期再取出,塞给MEM进行处理

1
2
3
4
5
6
7
EXEMEM EXEMEM1(
....
.Ram_to_Path_temp(rdata_mem), // input
....
.Ram_to_Path(rdata_mem_delay_1), // output
....
);

与此同时,整个流水线都要进行额外的一拍stall。

在取内存的时候MEM_stall恒为1,整个流水线此时处于停滞的状态。那么我们现在只需要让MEM_stall再延续一拍,MEMWB,EXEMEMrdata_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中提供了打印字符,接受字符,设置时间的函数。

再看下我们的软件,也是直接调用printkputc,其中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
a0 = a1 // a0 = 1 a1 = 2
a1 = a0 // a0 = 2 a1 = 2
// expect: a0 = 2 a1 = 1

之后运行仿真的时候会因为寄存器传参不对卡死.....请教了一下大神,用了另外一种内联汇编的写法:

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
// CSR Handler
7'b1110011: begin
we_reg = 1;
case(fun3)
3'b001: begin
immgen_op = R;
alu_asel = 2'b01; // register
alu_bsel = 2'b00; // 0
alu_op = ADD; // 0 + register
wb_sel = 2'b01;
end
3'b010:begin
immgen_op = R;
alu_asel = 2'b01; // register
alu_bsel = 2'b00; // 0
alu_op = ADD; // 0 + register
wb_sel = 2'b01;
end
3'b011: begin
immgen_op = R;
alu_asel = 2'b01; // register
alu_bsel = 2'b00; // 0
alu_op = ADD; // 0 + register
wb_sel = 2'b01;
end
3'b101:begin
immgen_op = 3'b111;
alu_asel = 2'b00; //0
alu_bsel = 2'b10; // imm
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; // mret, sret
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阶段才能写回。所以为了避免在这个过程中发生一些不可描述乱码七糟的事情,我们直接设置:

  • alu_bsel = 0 && alu_op = ADD 使得EXE阶段的运算结果ALU_resultrs + 0 = rs

  • wb_sel=2'b01保证在写回阶段选择写回ALU_result

这样就可以保证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_id
.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}), //csr_ret信号
.except_commit(except_WB), // 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; // 发生异常的pc
logic [63:0] ecause; // 发生异常的原因
logic [63:0] etval; // 发生异常的值(指令)
} ExceptPack;

endpackage
`endif

可以看到,IDExpcetExamine的执行逻辑分为以下三步:

  1. 接受外部ID阶段的指令,pc,及stall和flush的信号
  2. 将收到的指令传入InstExamine检查是否为异常指令,并将信号打包输出出来。
  3. 如果发生异常,将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}; //恒置0
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接收到传过来的异常信号包,输出的信号会有如下变化:

  • switch_mode置1,切换特权态

  • priv如果原来为2'b11即M态变为2'b10S态,如果是S态切换为M态

  • pc_csr输出异常处理的pc

当发生异常时,我们通常需要做这几件事:

  1. pc_IF更改为pc_csr
  2. 由于我们的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;

// inst <= inst_reg;
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;
// inst_temp <= inst_next_temp;
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信号触发时钟中断

对于时钟中断的处理方法和异常类似,只不过这次的信号传递是外部给的。

  • switch_mode置1,切换特权态

  • priv如果原来为2'b11即M态变为2'b10S态,如果是S态切换为M态

  • pc_csr输出异常处理的pc

这里我们需要注意,因为时钟中断不一定在哪个阶段产生,也就是说假如现在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; // 定义一个寄存器 fake_inst_if,并初始化为 0

assign if_stall = (if_open == 1'b1) | switch_mode; // 根据 if_open 和 switch_mode 的值来确定是否产生流水线停顿
assign mem_stall = (mem_open == 1'b1) ? 1 : 0; // 根据 mem_open 的值来确定是否产生内存访问停顿
assign inst = (inst_open) ? (((pc - 4 >> 2) % 2 == 0) ? inst_keep[31:0] : inst_keep[63:32]) : 0; // 根据 inst_open 的值选择需要输出的指令数据
assign rdata_cpu = (rdata_open) ? rdata_mem : 0; // 根据 rdata_open 的值选择需要输出的数据

always @(posedge clk or negedge rstn) begin
if (!rstn) begin
// 在复位期间的操作
...
end else begin
// 复位结束后的操作
case (state)
// 状态机的各个状态
2'b00: begin
if (keep)begin
// 如果 keep 为真,则执行相关操作
...
end else if (wen_cpu | ren_cpu) begin
// 如果 wen_cpu 或 ren_cpu 为真,则执行相关操作
...
end else if (if_request) begin
// 如果 if_request 为真,则执行相关操作
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
// 如果 valid_mem 为真,则执行相关操作
...
end else if (!valid_mem) begin
// 如果 valid_mem 为假,则执行相关操作
mem_open <= mem_open;
end
end
2'b10: begin
if (valid_mem) begin
// 如果 valid_mem 为真,则执行相关操作
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; // 保存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。

本实验处理的时钟中断过程中有两个关键点:

  1. 在时钟中断发生时触发exception interrupt #7,由S态切换到M态,执行sbi_mti_handler

  1. 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的架构和Regs基本一致,即寄存器的读出是组合电路,而寄存器的写入是时序电路。

即,寄存器的读取是瞬发的,而寄存器的写入需要stall一拍(上升沿写回)

  • 在我的设计中,由于我把CSRModule当做另一个Reg去处理,实际上没有引入额外的stall(与Reg交叉写入,共用一个stall)。

  • 可以利用forward技术来减少stall。我们可以注意到,实际上一个值在IDEXE阶段从Reg中读取出来要传到WB才能进行写入。由于RV64特权指令通常不需要进行很复杂的运算,也就用不到EXE,MEM阶段的功能,把CSR_rdataREG_rdata一直传递到最后才写入实际上是一种时间的浪费。

我们可以在IDEXE阶段读出CSR和Reg的值的同时进行交叉写入,这样只要在这里stall一拍就可以完成写回(上升沿写回)。如果是下降沿写回那就一拍都不用stall,上升沿取出,下降沿写回即可。