构建五级流水线——lab6
本文最后更新于 2026年6月9日 11:35:18
注:bonus已完成缺页异常和mtimecmp的中断处理程序
一、Lab6背景
在lab6中,我们会对异常和中断的类型进行扩展,从而支持更多的中断和异常。 ### 1. 异常 对于异常,新加入指令地址不对齐、数据地址不对齐、非法指令这三种。根据wiki提示,这些异常需要做这些事: 1
2
3
4
5
6
7
81. `mepc` ← `pc`
2. `next_pc` ← `mtvec`
3. `mcause[63]` ← 0 表示异常(而不是中断),`mcause[62:0]` ← 对应的异常类型
4. `mstatus.mpie` ← `mstatus.mie`
5. `mstatus.mie` ← 0
6. `mstatus.mpp` ← mode
7. `mode` ← 2'b11
8. 清除流水线。取消当周期发起的 `dreq.valid`。已发起的 `dreq` 保留,等到 `data_ok` 后再清除流水线。
2. 中断
中断可以复用异常的处理逻辑,但区别在于中断可以在任何时间发生,与时钟周期无关。这也使得中断不需要实时处理,可以稍微延后。根据wiki介绍,只有在满足以下两个条件时才应该处理中断: 1
2- (1) **中断是否启用**:【如果当前是 M Mode,要求 `mstatus.mie=1`】 或者 【当前不是 M Mode】
- (2) `mip[i]=1` 且 `mie[i]=1`
TODO > 注:实际上不存在优先级,因为ecall是主动调用,非法指令无法识别,指令不对齐会被取指阶段识别,剩下的是数据不对齐) TODO 实际上每个时钟周期都检测一遍也是对的 ## 二、异常 ### 1. 异常指令的抽象化 我没有将所有异常指令都抽象成一个固定的枚举类型,而是在每个异常可能触发的阶段识别它们,并直接完成异常元信息的写入,并向下传递。 ### 2. 分别处理每一种异常 指令地址不对齐和数据地址不对齐都是CPU内部进行计算后发现的,来自ex阶段;非法指令在decode阶段判断;ecall指令也在decode阶段判断。可以看出,这些指令产生的阶段完全不同,且有所重复。如果仍像Lab5那样统一在wb阶段利用trap_unit完成trap_valid,trap_cause和trap_tval的判断,需要重新判断属于四种异常中的哪一种,导致逻辑上的重复。因此,本Lab将会采取判断出异常的同阶段内直接完成exc_valid、exc_cause 和 exc_tval这三种元信息的写入。
需要注意,trap=exception+interruption,所以针对trap_unit和csr_file这类包含异常和中断的模式,仍然采用trap_*的命名,但对于前几个阶段将会通过exc_*和interruption_*的方式来区分异常和中断。
1)非法指令异常的识别
在decoder中根据opcode的值,采用白名单的方式核对funct3、funct7的值,产出是否是有效指令的布尔值is_valid_inst:如果不在白名单中,设置is_valid_inst=0,否则为1。需要注意,有效指令除了包括Lab中已经实现的指令,还需要包括lab5测试中出现的sfence.vma这一指令,否则测试会无法通过。 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
100decoder.sv
always_comb begin
is_valid_inst = 1'b0;
case (opcode)
7'b0110111, // lui
7'b0010111, // auipc
7'b1101111: begin // jal
is_valid_inst = 1'b1;
end
7'b1100111: begin // jalr, funct3 must be 000
is_valid_inst = (funct3 == 3'b000);
end
7'b1100011: begin // branch
case (funct3)
3'b000, // beq
3'b001, // bne
3'b100, // blt
3'b101, // bge
3'b110, // bltu
3'b111: is_valid_inst = 1'b1; // bgeu
default: is_valid_inst = 1'b0;
endcase
end
7'b0000011: begin // load
case (funct3)
3'b000, // lb
3'b001, // lh
3'b010, // lw
3'b011, // ld
3'b100, // lbu
3'b101, // lhu
3'b110: is_valid_inst = 1'b1; // lwu
default: is_valid_inst = 1'b0;
endcase
end
7'b0100011: begin // store
case (funct3)
3'b000, // sb
3'b001, // sh
3'b010, // sw
3'b011: is_valid_inst = 1'b1; // sd
default: is_valid_inst = 1'b0;
endcase
end
7'b0010011: begin // immediate ALU
case (funct3)
3'b000, // addi
3'b010, // slti
3'b011, // sltiu
3'b100, // xori
3'b110, // ori
3'b111: is_valid_inst = 1'b1; // andi
3'b001: is_valid_inst = (inst[31:26] == 6'b000000); // slli
3'b101: is_valid_inst = (inst[31:26] == 6'b000000) || (inst[31:26] == 6'b010000); // srli/srai
default: is_valid_inst = 1'b0;
endcase
end
7'b0110011: begin // register ALU
case (funct7)
7'b0000000: is_valid_inst = 1'b1; // add/sll/slt/sltu/xor/srl/or/and
7'b0100000: is_valid_inst = (funct3 == 3'b000) || (funct3 == 3'b101); // sub/sra
7'b0000001: is_valid_inst = (funct3 == 3'b000); // mul
default: is_valid_inst = 1'b0;
endcase
end
7'b0011011: begin // immediate word ALU
case (funct3)
3'b000: is_valid_inst = 1'b1; // addiw
3'b001: is_valid_inst = (funct7 == 7'b0000000); // slliw
3'b101: is_valid_inst = (funct7 == 7'b0000000) || (funct7 == 7'b0100000); // srliw/sraiw
default: is_valid_inst = 1'b0;
endcase
end
7'b0111011: begin // register word ALU
case (funct7)
7'b0000000: is_valid_inst = (funct3 == 3'b000) || (funct3 == 3'b001) || (funct3 == 3'b101); // addw/sllw/srlw
7'b0100000: is_valid_inst = (funct3 == 3'b000) || (funct3 == 3'b101); // subw/sraw
7'b0000001: is_valid_inst = (funct3 == 3'b000); // mulw
default: is_valid_inst = 1'b0;
endcase
end
7'b1110011: begin // system
case (funct3)
3'b000: is_valid_inst = is_ecall ||
((inst[31:25] == 7'b0001001) && (inst[11:7] == 5'b00000)) ||
(xret_type != XRET_NONE); // ecall/sfence.vma/mret/sret
3'b001, // csrrw
3'b010, // csrrs
3'b011, // csrrc
3'b101, // csrrwi
3'b110, // csrrsi
3'b111: is_valid_inst = 1'b1; // csrrci
default: is_valid_inst = 1'b0;
endcase
end
default: begin
is_valid_inst = 1'b0;
end
endcase
end
2)非法指令异常与ecall的异常信号
由于二者都发生在decoder阶段,我们一起处理,最后在core中判断是否处于这两种情况,并针对两种情况写入异常元信息。
在赋值过程中,需要优先保留if_id传下来的异常信息(这个阶段产生的信号会在后续报告中详述),接着分开处理非法指令和ecall指令即可。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19core.sv
always_comb begin
id_exc_valid = if_id_valid && (if_id_exc_valid || ~decoder_is_valid_inst || decoder_is_ecall);
id_exc_cause = 64'b0;
id_exc_tval = 64'b0;
if (if_id_valid && if_id_exc_valid) begin
id_exc_cause = if_id_exc_cause;
id_exc_tval = if_id_exc_tval;
end else if (if_id_valid && ~decoder_is_valid_inst) begin
id_exc_cause = 64'd2;
id_exc_tval = {32'b0, if_id_inst};
end else if (if_id_valid && decoder_is_ecall) begin
case (current_priv)
PRIV_U: id_exc_cause = 64'd8;
PRIV_S: id_exc_cause = 64'd9;
default: id_exc_cause = 64'd11;
endcase
end
end
1 | |
看起来仍然很绕。观察到可以将exc_valid提高倒core层面提前过滤,也就是说在id_ex中,如果valid,则一定满足exc_valid=0。最终改成如下形式: 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
45core.sv
id_ex u_id_ex(
.is_jal_in (decoder_is_jal && ~id_exc_valid),
.is_jalr_in (decoder_is_jalr && ~id_exc_valid),
.is_branch_in (decoder_is_branch && ~id_exc_valid),
.write_regs_in (decoder_write_regs && ~id_exc_valid),
.mem_read_in (decoder_mem_read && ~id_exc_valid),
.mem_write_in (decoder_mem_write && ~id_exc_valid),
.is_mul_in (decoder_is_mul && ~id_exc_valid),
);
id_ex.sv
always_ff @(posedge clk) begin
if (reset) begin
;
end else if (enable) begin
if (valid_in) begin
write_regs_out <= write_regs_in;
mem_read_out <= mem_read_in;
mem_write_out <= mem_write_in;
is_jal_out <= is_jal_in;
is_jalr_out <= is_jalr_in;
is_branch_out <= is_branch_in;
is_csr_out <= is_csr_in;
is_mul_out <= is_mul_in;
xret_type_out <= xret_type_in;
exc_valid_out <= exc_valid_in;
exc_cause_out <= exc_cause_in;
exc_tval_out <= exc_tval_in;
end else begin
write_regs_out <= 1'b0; // 不许写寄存器
mem_read_out <= 1'b0; // 不许读内存
mem_write_out <= 1'b0; // 不许写内存
is_jal_out <= 1'b0;
is_jalr_out <= 1'b0;
is_branch_out <= 1'b0;
is_csr_out <= 1'b0;
is_mul_out <= 1'b0;
xret_type_out <= XRET_NONE;
exc_valid_out <= 1'b0;
exc_cause_out <= 64'b0;
exc_tval_out <= 64'b0;
end
end
end
在core中写入异常元信息 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19core.sv
always_comb begin
id_exc_valid = if_id_valid && (if_id_exc_valid || ~decoder_is_valid_inst || decoder_is_ecall);
id_exc_cause = 64'b0;
id_exc_tval = 64'b0;
if (if_id_valid && if_id_exc_valid) begin
id_exc_cause = if_id_exc_cause;
id_exc_tval = if_id_exc_tval;
end else if (if_id_valid && ~decoder_is_valid_inst) begin
id_exc_cause = 64'd2;
id_exc_tval = {32'b0, if_id_inst};
end else if (if_id_valid && decoder_is_ecall) begin
case (current_priv)
PRIV_U: id_exc_cause = 64'd8;
PRIV_S: id_exc_cause = 64'd9;
default: id_exc_cause = 64'd11;
endcase
end
end
3)指令地址异常
由于pc本身是32位的,因此每个pc的末两位都必须为0。若否,则触发指令地址异常。 我们将之前计算是否要进行跳转的should_jump指令重命名为inst_should_jump,而额外新建一个输出信号should_jump_valid用于最终指导pc是否真正要跳转的信号,以及是否触发指令地址异常的布尔信号invalid_addr:
1 | |
4)数据地址异常
写地址异常和取地址异常对应的cause码不同,需要分开处理。原理同指令地址异常,根据对应的长度确定应该看后几位的数值 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15ex.sv
output u1 invalid_load_addr,
output u1 invalid_store_addr,
u1 data_addr_misaligned;
always_comb begin
case (msize)
MSIZE1: data_addr_misaligned = 1'b0;
MSIZE2: data_addr_misaligned = alu_result[0];
MSIZE4: data_addr_misaligned = (alu_result[1:0] != 2'b00);
MSIZE8: data_addr_misaligned = (alu_result[2:0] != 3'b000);
default: data_addr_misaligned = 1'b0;
endcase
end
assign invalid_load_addr = mem_read && data_addr_misaligned;
assign invalid_store_addr = mem_write && data_addr_misaligned;
在core中写入异常元信息 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21core.sv
always_comb begin
ex_exc_valid = id_ex_exc_valid;
ex_exc_cause = id_ex_exc_cause;
ex_exc_tval = id_ex_exc_tval;
if (id_ex_valid && ~id_ex_exc_valid) begin
if (ex_invalid_addr) begin
ex_exc_valid = 1'b1;
ex_exc_cause = 64'd0;
ex_exc_tval = ex_jump_addr;
end else if (ex_invalid_load_addr) begin
ex_exc_valid = 1'b1;
ex_exc_cause = 64'd4;
ex_exc_tval = ex_alu_result;
end else if (ex_invalid_store_addr) begin
ex_exc_valid = 1'b1;
ex_exc_cause = 64'd6;
ex_exc_tval = ex_alu_result;
end
end
end1
2
3core.sv
assign ex_should_jump_real = (id_ex_valid && id_ex_is_csr) ? 1'b1 : ex_should_jump_valid;
assign trap_kill_mem = mem_wb_valid && (mem_wb_exc_valid || (mem_wb_xret_type != XRET_NONE));
3. 传递信号,并进入后续的异常处理逻辑
将信号传递至ex:ex_mem与id_ex逻辑完全一致,这里略去不表。 在mem_wb阶段时,只剩下三个异常元信息exc_valid、exc_cause 和 exc_tval。 1
2
3
4
5
6
7
8
9
10
11
12
13trap_unit.sv
input u1 mem_wb_exc_valid,
input u64 mem_wb_exc_cause,
input u64 mem_wb_exc_tval,
u1 trap_is_interrupt;
assign trap_is_interrupt = mem_wb_exc_cause[63];
assign trap_valid = commit_fire && mem_wb_exc_valid;
assign trap_epc = mem_wb_pc;
assign trap_cause = mem_wb_exc_cause;
assign trap_tval = mem_wb_exc_tval;
assign trap_from_priv = current_priv;
assign trap_target_priv = (!trap_is_interrupt && (current_priv != PRIV_M) && csr_medeleg[trap_cause[5:0]]) ? PRIV_S : PRIV_M;
4)在trap_unit和csr_file中写入数据:
- 在core中根据三类中断对应的mip位数定义mask:csr_external_mip
- mip需要实时反应中断状态,因此暴露在外的mip接口需要实时与csr_external_mip反应的中断保持一致;对于CPU直接读写的mip,通过mip_next和mip_reg实现
1 | |
三、中断
1. 实现
1)完成interrupt_unit
首先判断当前能否接收interrupt,即 1
- (1) **中断是否启用**:【如果当前是 M Mode,要求 `mstatus.mie=1`】 或者 【当前不是 M Mode】
其次,按照swint-trint-exint的顺序,分别判断各自的mip和mie,只有都符合时,才进一步处理,即 1
- (2) `mip[i]=1` 且 `mie[i]=1`
在本模块中,我们在可以处理中断时打断正常的流水线,让interrupt_unit接管if_id寄存器,并在处理中断时阻塞上游pc取指。这样一来,处理完中断后,pc重新enable开始流动,自然可以继续处理下一条pc。同时,由于lab中的要求较为宽松,我们让中断随着流水线流到mem_wb阶段再进行处理。
2)从core中拼出这些信号,并接入interrupt_unit:
首先判断是否启用中断
1
2interrupt_unit.sv
assign global_interrupt_enable = (current_priv != PRIV_M) || mstatus[MSTATUS_MIE_BIT];接着分别判断swint,trint,exint,先看mip对应位置有无pending的中断,再看mie是否允许处理他们,最终生成对应的信号can_take,顺序我们在always块里面处理
1
2
3
4
5
6
7
8
9
10
11
12interrupt_unit.sv
assign msip_pending = mip[MSIP_BIT];
assign mtip_pending = mip[MTIP_BIT];
assign meip_pending = mip[MEIP_BIT];
assign msip_enabled = mie[MSIP_BIT];
assign mtip_enabled = mie[MTIP_BIT];
assign meip_enabled = mie[MEIP_BIT];
assign msip_can_take = msip_pending && msip_enabled;
assign mtip_can_take = mtip_pending && mtip_enabled;
assign meip_can_take = meip_pending && meip_enabled;需要注意接受新中断的逻辑:除了前两个步骤判断出来的global_interrupt_enable,我们还需要确认流水线适合接收新的中断:
- 首先,如果pc本身处在中断状态,我们最好不要打断它。
- 其次,pc不能等待跳转(pending),或者准备跳转(should)
1
2
3
4core.sv
assign interrupt_unit_can_accept = ctrl_pc_enable &&
!pc_pending_jump &&
!pc_should_jump;
最后,在always块中依次处理中断元信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20interrupt_unit.sv
always_comb begin
interrupt_valid = 1'b0;
interrupt_pc = cur_pc;
interrupt_cause = 64'b0;
interrupt_tval = 64'b0;
if (!reset && can_accept_interrupt && global_interrupt_enable) begin
if (meip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd11};
end else if (msip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd3};
end else if (mtip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd7};
end
end
end
3)接管pc和if_id
- 如果发生中断,需要阻塞pc模块
1
2
3
4core.sv
pc u_pc(
.enable (ctrl_pc_enable && fetch_iresp.data_ok && !interrupt_unit_interrupt_valid),
); - 如果中断有效,if_id直接接收此中断信息。在这里,我们将中断视作一条空指令32’h00000013:
1
2
3
4
5
6
7
8core.sv
assign if_id_valid_in = interrupt_unit_interrupt_valid ? 1'b1 :
(fetch_iresp.data_ok && ~ctrl_if_id_flush);
assign if_id_pc_in = interrupt_unit_interrupt_valid ? interrupt_unit_interrupt_pc : pc_cur;
assign if_id_inst_in = interrupt_unit_interrupt_valid ? 32'h00000013 : fetch_iresp.data;
assign if_id_exc_valid_in = interrupt_unit_interrupt_valid;
assign if_id_exc_cause_in = interrupt_unit_interrupt_valid ? interrupt_unit_interrupt_cause : 64'b0;
assign if_id_exc_tval_in = interrupt_unit_interrupt_valid ? interrupt_unit_interrupt_tval : 64'b0;
2. 信号传递与处理
由于中断相较于异常只有mcause最高位上的区别,其他的元信息完全一致。因此这部分的逻辑完全等同异常。
四、bonus
1. MMU的缺页异常
1)背景信息
在上一个Lab,我们已经知道页表的56位中,低10位是符号位。这个Lab正是通过检测这些位的值来判断是否触发缺页异常 | 位号 | 名字(缩写 + 全称) | 含义 | |—:|—|—| | bit 0 | V = Valid | 有效位;为 1 表示该 PTE 有效,为 0 表示无效 | | bit 1 | R = Read | 可读权限;为 1 表示该页可读 | | bit 2 | W = Write | 可写权限;为 1 表示该页可写 | | bit 3 | X = Execute | 可执行权限;为 1 表示该页可用于取指执行 | | bit 4 | U = User | 用户态访问权限;为 1 表示 U-mode 可访问 | | bit 5 | G = Global | 全局映射;通常用于跨地址空间共享的映射 | | bit 6 | A = Accessed | 访问位;表示该页是否被访问过 | | bit 7 | D = Dirty | 脏位;表示该页是否被写入过 | | bit 9:8 | RSW = Reserved for Software | 保留给软件使用;硬件地址翻译通常不解释 | | bit 53:10 | PPN = Physical Page Number | 物理页号;指向下一级页表或最终物理页 | | bit 63:54 | Reserved | 保留位;通常应为 0 |
缺页异常的mcause由原始访问类型决定:取指失败对应12,读失败对应13,写失败对应15。无论是哪一种缺页异常,mtval都应写入触发异常的虚拟地址。所有判断都可以放在MMU内部完成,一旦判断出错就结束本次访问,不再继续发出后续访存请求,只输出异常元信息并顺着流水线传到mem_wb统一提交。
以下这些都属于缺页异常:
Sv39虚拟地址不合法
判断标准: - vaddr[63:39] != {25{vaddr[38]}}
原因:vaddr[38]是Sv39有效虚拟地址的符号扩展位,如果高位没有按它扩展,说明该虚拟地址不属于合法的Sv39地址空间。
异常元信息: - exc_valid = 1 - exc_cause = 取指时64'd12,load时64'd13,store时64'd15 - exc_tval = 取指时pc,load/store时vaddr
PTE无效
判断标准: - V == 0
或: - R == 0 - W == 1
原因:V表示PTE是否有效,V=0说明这一项不存在;R=0且W=1是RISC-V规定的非法PTE编码,因为页面不允许出现不可读但可写的状态。
异常元信息: - exc_valid = 1 - exc_cause = 取指时64'd12,load时64'd13,store时64'd15 - exc_tval = 取指时pc,load/store时vaddr
PTE保留位非法
判断标准: - pte[63:54] != 10'b0
原因:pte[63:54]是当前实现中没有使用的保留位,如果不为0,硬件不能把这个PTE解释为合法页表项。
异常元信息: - exc_valid = 1 - exc_cause = 取指时64'd12,load时64'd13,store时64'd15 - exc_tval = 取指时pc,load/store时vaddr
最低级页表仍不是叶子项
判断标准: - level == L0 - R == 0 - X == 0
原因:R表示可读权限,X表示可执行权限,二者都为0时该PTE不是叶子项;但L0已经没有下一级页表可以继续查找,因此必须触发缺页异常。
异常元信息: - exc_valid = 1 - exc_cause = 取指时64'd12,load时64'd13,store时64'd15 - exc_tval = 取指时pc,load/store时vaddr
巨页物理页号未对齐
判断标准: - level == L2 - R == 1或X == 1 - PPN[1] != 0,或 - PPN[0] != 0
或: - level == L1 - R == 1或X == 1 - PPN[0] != 0
原因:PPN表示物理页号,L2叶子项映射1GiB巨页时低两级PPN必须为0,L1叶子项映射2MiB巨页时最低一级PPN必须为0,否则物理页起始地址没有按巨页大小对齐。
异常元信息: - exc_valid = 1 - exc_cause = 取指时64'd12,load时64'd13,store时64'd15 - exc_tval = 取指时pc,load/store时vaddr
访问类型权限不满足
判断标准: - access_type == instruction - X == 0
或: - access_type == load - R == 0
或: - access_type == store - W == 0
原因:X表示可执行权限,取指时必须为1;R表示可读权限,load时必须为1;W表示可写权限,store时必须为1。
异常元信息: - exc_valid = 1 - exc_cause = 取指权限失败时64'd12,load权限失败时64'd13,store权限失败时64'd15 - exc_tval = 取指时pc,load/store时vaddr
当前特权级与U位不匹配
判断标准: - current_priv == PRIV_U - U == 0
或: - current_priv == PRIV_S - U == 1
原因:U表示用户态页面,U=1代表U-mode可访问,U=0代表该页不是用户页;在当前Lab的简化实现中,U-mode只能访问U=1的页,S-mode只访问U=0的页。
异常元信息: - exc_valid = 1 - exc_cause = 取指时64'd12,load时64'd13,store时64'd15 - exc_tval = 取指时pc,load/store时vaddr
A/D位不满足
判断标准: - A == 0
或: - access_type == store - D == 0
原因:A表示页面是否被访问过,任意访问都要求它为1;D表示页面是否被写入过,store访问还要求它为1,否则应通过缺页异常交给软件处理。
异常元信息: - exc_valid = 1 - exc_cause = 取指时64'd12,load时64'd13,store时64'd15 - exc_tval = 取指时pc,load/store时vaddr
2)具体实施
缺页异常需要根据当前地址的作用分别处理,具体可分成取指、读数据和写数据三类。一旦触发异常,直接中断请求,并根据读、写、取指中的一种返回异常信息。
定义三类信号
首先在mypkg.sv中增加MMU请求类型,用来区分当前翻译的是取指、读数据还是写数据: 1
2
3
4
5
6mypkg.sv
typedef enum u2 {
MMU_REQ_INST = 2'd0,
MMU_REQ_LOAD = 2'd1,
MMU_REQ_STORE = 2'd2
} mmu_access_t;
dbus_arbiter
接着在dbus_arbiter.sv中给仲裁后的请求打上类型标签。如果dbus有效,若stobe全零,表示当前是读数据;否则是写数据;否则再看ibus是否有效,如果是则全部为取指部分。同Lab5一样,总线请求可能持续多个周期,因此仲裁器还要保存本次请求的类型: 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
30dbus_arbiter.sv
output mmu_access_t oreq_access_type;
mmu_access_t selected_access_type, saved_access_type;
always_comb begin
selected_req = '0;
selected_access_type = MMU_REQ_LOAD;
if (dreq.valid) begin
selected_req = dreq;
selected_access_type = (dreq.strobe == 8'b0) ? MMU_REQ_LOAD : MMU_REQ_STORE;
end else if (ireq.valid) begin
selected_req.valid = ireq.valid;
selected_req.addr = ireq.addr;
selected_req.size = MSIZE4;
selected_req.strobe = 8'b0;
selected_req.data = 64'b0;
selected_access_type = MMU_REQ_INST;
end
end
assign oreq_access_type = busy ? saved_access_type : selected_access_type;
always_ff @(posedge clk) begin
if (reset) begin
saved_access_type <= MMU_REQ_LOAD;
end else if (dreq.valid || ireq.valid) begin
saved_access_type <= selected_access_type;
end
end
MMU中集中处理缺页异常
然后在mmu.sv中增加请求类型输入和异常元信息输出,每一种异常按照规则对应写入异常元信息即可: 1
2
3
4
5
6mmu.sv
input mmu_access_t req_access_type,
output u1 exc_valid,
output u64 exc_cause,
output u64 exc_tval
MMU内部需要新增一个MMU_FAULT状态,并保存当前请求的访问类型、原始虚拟地址和异常元信息: 1
2
3
4
5
6
7
8
9
10
11
12mmu.sv
typedef enum u3 {
MMU_FAULT = 3'd6
} mmu_state_t;
mmu_access_t saved_access_type;
priv_t saved_priv;
u64 saved_vaddr, fault_cause, fault_tval;
u64 request_fault_cause, saved_fault_cause;
u1 pte_is_leaf, pte_reserved;
u1 req_bad_vaddr, pte_invalid, pte_not_leaf_at_l0, pte_superpage_misaligned, pte_priv_fault, pte_access_fault, pte_ad_fault, pte_leaf_fault, pte_fault;//八种异常分别对应一个布尔值
首先检查虚拟地址是否合法,并拆出PTE中的权限位: 1
2
3
4
5
6
7
8
9
10
11mmu.sv
u1 pte_v, pte_r, pte_w, pte_x, pte_u, pte_a, pte_d;
assign req_bad_vaddr = core_req.addr[63:39] != {25{core_req.addr[38]}};
assign pte_v = dresp.data[0];
assign pte_r = dresp.data[1];
assign pte_w = dresp.data[2];
assign pte_x = dresp.data[3];
assign pte_u = dresp.data[4];
assign pte_a = dresp.data[6];
assign pte_d = dresp.data[7];
接着根据PTE位判断所有缺页异常条件: 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
32mmu.sv
assign pte_is_leaf = pte_r || pte_x;
assign pte_reserved = dresp.data[63:54] != 10'b0;
assign pte_invalid = !pte_v || (!pte_r && pte_w) || pte_reserved;
assign pte_not_leaf_at_l0 = (state == MMU_L0) && !pte_is_leaf;
assign pte_superpage_misaligned = pte_is_leaf &&
(((state == MMU_L2) && (dresp.data[27:10] != 18'b0)) ||
((state == MMU_L1) && (dresp.data[18:10] != 9'b0)));
assign pte_priv_fault = ((saved_priv == PRIV_U) && !pte_u) ||
((saved_priv == PRIV_S) && pte_u);
always_comb begin
pte_access_fault = 1'b0;
unique case (saved_access_type)
MMU_REQ_INST: pte_access_fault = !pte_x;
MMU_REQ_LOAD: pte_access_fault = !pte_r;
MMU_REQ_STORE: pte_access_fault = !pte_w;
default: pte_access_fault = 1'b0;
endcase
end
assign pte_ad_fault = !pte_a || ((saved_access_type == MMU_REQ_STORE) && !pte_d);
assign pte_leaf_fault = pte_is_leaf && (pte_priv_fault || pte_access_fault || pte_ad_fault);
assign pte_fault = pte_invalid || pte_not_leaf_at_l0 || pte_superpage_misaligned || pte_leaf_fault;
异常可能发生在任意一次翻译过程中,因此同样需要保存access_type,并在不同阶段分别根据req与saved获取cause值。读、写、取指三种访问对应不同的权限位,对应不同的exc_cause: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18mmu.sv
always_comb begin
unique case (req_access_type)
MMU_REQ_INST: request_fault_cause = 64'd12;
MMU_REQ_LOAD: request_fault_cause = 64'd13;
MMU_REQ_STORE: request_fault_cause = 64'd15;
default: request_fault_cause = 64'd13;
endcase
end
always_comb begin
unique case (saved_access_type)
MMU_REQ_INST: saved_fault_cause = 64'd12;
MMU_REQ_LOAD: saved_fault_cause = 64'd13;
MMU_REQ_STORE: saved_fault_cause = 64'd15;
default: saved_fault_cause = 64'd13;
endcase
end
如果刚进入MMU时虚拟地址本身就不合法,则不再访问页表,直接进入缺页异常状态: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19mmu.sv
always_ff @(posedge clk) begin
case (state)
MMU_IDLE: begin
if (core_req.valid) begin
saved_access_type <= req_access_type;
saved_priv <= current_priv;
saved_vaddr <= core_req.addr;
if (translation_enabled) begin
if (req_bad_vaddr) begin
state <= MMU_FAULT;
fault_cause <= request_fault_cause;
fault_tval <= core_req.addr;
end
end
end
end
endcase
end
如果查页表过程中发现PTE不合法,则结束本次翻译,记录异常元信息: 1
2
3
4
5
6
7
8
9
10
11
12
13
14mmu.sv
always_ff @(posedge clk) begin
case (state)
MMU_L2, MMU_L1, MMU_L0: begin
if (dresp.data_ok) begin
if (pte_fault) begin
state <= MMU_FAULT;
fault_cause <= saved_fault_cause;
fault_tval <= saved_vaddr;
end
end
end
endcase
end
最后,MMU_FAULT状态对外输出异常元信息,同时返回data_ok结束当前访问: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21mmu.sv
always_comb begin
case (state)
MMU_FAULT: begin
core_resp.addr_ok = 1'b1;
core_resp.data_ok = 1'b1;
core_resp.data = 64'b0;
exc_valid = 1'b1;
exc_cause = fault_cause;
exc_tval = fault_tval;
end
endcase
end
always_ff @(posedge clk) begin
case (state)
MMU_FAULT: begin
state <= MMU_IDLE;
end
endcase
end
处理信号传递
MMU只输出一组异常元信息,具体属于取指异常还是访存异常由dbus_arbiter根据当前正在等待返回的请求来源判断。由于总线请求可能跨多个周期,不能用当前输入的ireq或dreq直接判断,而应该使用仲裁器内部已经保存的saved_select_is_dbus。
首先在dbus_arbiter.sv中接收MMU输出的异常信息,并额外输出两个归属信号: 1
2
3dbus_arbiter.sv
input u1 exc_valid,
output u1 pc_exc, mem_exc
其中pc_exc表示本次MMU异常来自取指请求,后续应该送入if_id;mem_exc表示本次MMU异常来自读写请求,后续应该送入mem_wb。判断逻辑如下: 1
2
3dbus_arbiter.sv
assign pc_exc = busy && oresp.data_ok && exc_valid && !saved_select_is_dbus;
assign mem_exc = busy && oresp.data_ok && exc_valid && saved_select_is_dbus;
在core.sv中,新增两个信号接收仲裁器拆分后的异常归属;异常原因和异常地址仍然直接共用MMU输出,不经过dbus_arbiter转发: 1
2
3core.sv
u1 mmu_exc_valid, dbus_arbiter_pc_exc, dbus_arbiter_mem_exc;
u64 mmu_exc_cause, mmu_exc_tval;
对应地,在DBusArbiter实例化时只传入mmu_exc_valid,并接出pc_exc与mem_exc: 1
2
3
4
5
6
7core.sv
DBusArbiter u_dbus_arbiter(
.exc_valid (mmu_exc_valid),
.pc_exc (dbus_arbiter_pc_exc),
.mem_exc (dbus_arbiter_mem_exc)
);
取指缺页异常发生时,需要让if_id接收一条空指令,同时携带MMU输出的异常元信息继续向后传递: 1
2
3
4
5
6
7
8
9
10
11
12if_id.sv
input u1 pc_exc,
input u64 mmu_exc_cause, mmu_exc_tval,
end else if (enable) begin
inst_out <= pc_exc ? 32'h00000013 : inst_in;
exc_valid_out <= valid_in && (exc_valid_in || pc_exc);
exc_cause_out <= exc_valid_in ? exc_cause_in :
(pc_exc ? mmu_exc_cause : 64'b0);
exc_tval_out <= exc_valid_in ? exc_tval_in :
(pc_exc ? mmu_exc_tval : 64'b0);
end
在core.sv中,将取指异常归属信号和MMU异常元信息接入if_id: 1
2
3
4
5
6core.sv
if_id u_if_id(
.pc_exc (dbus_arbiter_pc_exc),
.mmu_exc_cause(mmu_exc_cause),
.mmu_exc_tval(mmu_exc_tval)
);
读写缺页异常发生时,异常属于当前MEM阶段的指令,因此在mem_wb中接收mem_exc,并使用MMU异常元信息覆盖传入的异常信息: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19mem_wb.sv
input u1 mem_exc,
input u64 mmu_exc_cause, mmu_exc_tval,
if (valid_in) begin
write_regs_out <= write_regs_in && !mem_exc;
is_mmio_out <= is_mmio_in && !mem_exc;
is_csr_out <= is_csr_in && !mem_exc;
csr_should_write_out <= csr_should_write_in && !mem_exc;
exc_valid_out <= exc_valid_in || mem_exc;
exc_cause_out <= exc_valid_in ? exc_cause_in :
(mem_exc ? mmu_exc_cause : 64'b0);
exc_tval_out <= exc_valid_in ? exc_tval_in :
(mem_exc ? mmu_exc_tval : 64'b0);
xret_type_out <= xret_type_in;
end else begin
write_regs_out <= 1'b0;
is_mmio_out <= 1'b0;
end
最后,在core.sv中将访存异常归属信号和MMU异常元信息接入mem_wb: 1
2
3
4
5
6core.sv
mem_wb u_mem_wb(
.mem_exc (dbus_arbiter_mem_exc),
.mmu_exc_cause (mmu_exc_cause),
.mmu_exc_tval (mmu_exc_tval)
);
2. 编写中断处理程序
1. mtime与mtimecmp的作用
在difftest提供的测试框架中,mtime和mtimecmp是两个通过MMIO访问的64位寄存器。其中mtime位于0x3800bff8,表示当前计时器的计数值;mtimecmp位于0x38004000,表示下一次时钟中断触发的比较值。
测试框架会不断递增mtime,并在mtime > mtimecmp时拉高时钟中断信号trint。CPU收到这个外部输入后,会在CSR中设置对应的mip.MTIP位。若同时满足mie.MTIE=1和mstatus.MIE=1,CPU就会进入机器模式时钟中断,跳转到mtvec指定的中断入口。
因此,软件侧处理时钟中断的关键是:初始化mtimecmp,打开mie.MTIE和mstatus.MIE,在中断处理程序中确认mcause为机器时钟中断,然后重新写入一个更大的mtimecmp。这样本次中断会被清除,之后等mtime再次超过新的比较值时,下一次时钟中断会再次触发。
2. 汇编代码
1 | |
3. 汇编代码思路
程序启动后,首先初始化栈指针,并把mtvec设置为trap_vector,这样发生异常或中断时CPU会跳转到这个入口。接着将mtime清零,并把mtimecmp设置为TIMER_INTERVAL,用于制造第一次时钟中断。
随后程序通过csrs mie, 0x80打开mie.MTIE,通过csrs mstatus, 0x8打开全局中断使能mstatus.MIE。主程序本身不做其他工作,只在main_loop中自旋等待中断。这里没有使用wfi,因为当前CPU没有实现该指令,普通跳转循环更适合本实验环境。
进入trap_vector后,程序先保存会被中断处理过程使用到的寄存器,防止破坏主程序现场。然后读取mcause:如果mcause最高位为0,说明这是异常而不是中断;如果低8位不是7,说明它不是机器时钟中断。这两种情况都直接恢复现场并执行mret返回。
确认是机器时钟中断后,程序调用puts输出字符串。puts逐字节读取字符串,并调用putchar把字符写到UART的MMIO地址0x40600004。输出完成后,程序读取当前mtime,计算mtime + TIMER_INTERVAL,并写回mtimecmp。由于新的mtimecmp大于当前mtime,时钟中断信号会被撤销;等mtime继续增长并再次超过新的mtimecmp时,下一次中断会重新触发。
最后,中断处理程序从栈中恢复所有保存过的寄存器,释放栈空间,并执行mret返回到被中断的位置继续执行。由于主程序一直在循环等待,所以可以周期性看到时钟中断打印内容。
五、通过截图
缺页异常已经完成,经过验证不会触发额外报错,最终输出与不包含缺页异常时的输出一致。 