构建五级流水线——lab6

本文最后更新于 2026年6月9日 11:35:18

注:bonus已完成缺页异常和mtimecmp的中断处理程序

一、Lab6背景

在lab6中,我们会对异常和中断的类型进行扩展,从而支持更多的中断和异常。 ### 1. 异常 对于异常,新加入指令地址不对齐、数据地址不对齐、非法指令这三种。根据wiki提示,这些异常需要做这些事:

1
2
3
4
5
6
7
8
1. `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` 后再清除流水线。
可以看出,对于Lab5中的ecall,我们也完整实现了上面的1,2,4~8的所有步骤,唯一需要改动的就是mcause,即异常原因。因此,Lab5中的处理方式可以极大程度上进行复用。

2. 中断

中断可以复用异常的处理逻辑,但区别在于中断可以在任何时间发生,与时钟周期无关。这也使得中断不需要实时处理,可以稍微延后。根据wiki介绍,只有在满足以下两个条件时才应该处理中断:

1
2
- (1) **中断是否启用**:【如果当前是 M Mode,要求 `mstatus.mie=1`】 或者 【当前不是 M Mode】 
- (2) `mip[i]=1``mie[i]=1`
且本lab只考虑刚收到一个中断信号时的中断处理。 因此,中断的实施思路是实时检测当前是否有中断信号;若有,则产生中断元信息,并判断当时是否适合处理中断;若否,等待直到适合处理中断时再去处理。区别在于中断不跟着流水线流动,因为它无法确定什么时候可以处理中断。 > 实际上,我们会在适合处理中断时(除了上两条,其余信号在第三部分详述)打包发送中断信号开始传,并把下一个pc记录为mepc,当处理完后就能继续处理下一条指令。

TODO > 注:实际上不存在优先级,因为ecall是主动调用,非法指令无法识别,指令不对齐会被取指阶段识别,剩下的是数据不对齐) TODO 实际上每个时钟周期都检测一遍也是对的 ## 二、异常 ### 1. 异常指令的抽象化 我没有将所有异常指令都抽象成一个固定的枚举类型,而是在每个异常可能触发的阶段识别它们,并直接完成异常元信息的写入,并向下传递。 ### 2. 分别处理每一种异常 指令地址不对齐和数据地址不对齐都是CPU内部进行计算后发现的,来自ex阶段;非法指令在decode阶段判断;ecall指令也在decode阶段判断。可以看出,这些指令产生的阶段完全不同,且有所重复。如果仍像Lab5那样统一在wb阶段利用trap_unit完成trap_valid,trap_cause和trap_tval的判断,需要重新判断属于四种异常中的哪一种,导致逻辑上的重复。因此,本Lab将会采取判断出异常的同阶段内直接完成exc_validexc_causeexc_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
100
decoder.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
19
core.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
接着,要将这一元信息继续向下传递。这里我顺便调整了if_id的透传逻辑,只对真正会产生副作用的信号进行特殊处理 > 会产生副作用的信号指的就是如果不清零,即使valid=0仍然无法控制下游模块进行运转从而影响流水线正常运行的信号,包括写寄存器、读写内存、三种跳转指令、改动csr寄存器、乘法、xret。同理,在id_ex中也沿用这种透传方式:

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
id_ex.sv
input u1 exc_valid_in,
input u64 exc_cause_in, exc_tval_in,
output u1 exc_valid_out,
output u64 exc_cause_out, exc_tval_out,
always_ff @(posedge clk) begin
if (reset) begin
exc_valid_out <= valid_in && exc_valid_in;
exc_cause_out <= (valid_in && exc_valid_in) ? exc_cause_in : 64'b0;
exc_tval_out <= (valid_in && exc_valid_in) ? exc_tval_in : 64'b0;
end
end else if (enable) begin
if (valid_in) begin
if (exc_valid_in) 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;
end else begin
write_regs_out <= write_regs_in;
mem_read_out <= mem_read_in;
mem_write_out <= mem_write_in;
is_csr_out <= is_csr_in;
is_mul_out <= is_mul_in;
xret_type_out <= xret_type_in;
end
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;
end
end
end

看起来仍然很绕。观察到可以将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
45
core.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
19
core.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
2
3
4
ex.sv
output u1 inst_should_jump,
assign invalid_addr = inst_should_jump && (jump_addr[1:0] != 2'b00);
assign should_jump_valid = inst_should_jump && ~invalid_addr;

4)数据地址异常

写地址异常和取地址异常对应的cause码不同,需要分开处理。原理同指令地址异常,根据对应的长度确定应该看后几位的数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ex.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
21
core.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
end
以及更新接口命名
1
2
3
core.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_validexc_causeexc_tval

1
2
3
4
5
6
7
8
9
10
11
12
13
trap_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;
从wiki中我们可以得知,区分中断和异常的cause只看最高位是否为1:如果是1,则代表是中断,否则是异常。当且仅当trap不是异常,且当前特权级别不是M,且medeleg中允许当前cause交给S模式处理,下一步即将跳转的特权级别会成为S;否则成为M。

4)在trap_unit和csr_file中写入数据:

  • 在core中根据三类中断对应的mip位数定义mask:csr_external_mip
  • mip需要实时反应中断状态,因此暴露在外的mip接口需要实时与csr_external_mip反应的中断保持一致;对于CPU直接读写的mip,通过mip_next和mip_reg实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
core.sv
assign csr_external_mip = (swint ? (64'b1 << 3) : 64'b0) |
(trint ? (64'b1 << 7) : 64'b0) |
(exint ? (64'b1 << 11) : 64'b0);

csr_file.sv
input u64 external_mip,
u64 mip_next;
always_comb begin
mip_next = mip_reg;
mip = mip_next | external_mip;
if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_MIP: mip_next = csr_write_data & MIP_MASK;
CSR_SIP: mip_next = (mip_reg & ~S_INTERRUPT_MASK) | (csr_write_data & S_INTERRUPT_MASK & MIP_MASK);
mip = mip_next | external_mip;
always_ff @(posedge clk) begin
else begin
mip_reg <= mip_next;

三、中断

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
    2
    interrupt_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
    12
    interrupt_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
      4
      core.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
    20
    interrupt_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
    4
    core.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
    8
    core.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=0W=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 == 1X == 1 - PPN[1] != 0,或 - PPN[0] != 0

或: - level == L1 - R == 1X == 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
6
mypkg.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
30
dbus_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
6
mmu.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
12
mmu.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
11
mmu.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
32
mmu.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
18
mmu.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
19
mmu.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
14
mmu.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
21
mmu.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根据当前正在等待返回的请求来源判断。由于总线请求可能跨多个周期,不能用当前输入的ireqdreq直接判断,而应该使用仲裁器内部已经保存的saved_select_is_dbus

首先在dbus_arbiter.sv中接收MMU输出的异常信息,并额外输出两个归属信号:

1
2
3
dbus_arbiter.sv
input u1 exc_valid,
output u1 pc_exc, mem_exc

其中pc_exc表示本次MMU异常来自取指请求,后续应该送入if_idmem_exc表示本次MMU异常来自读写请求,后续应该送入mem_wb。判断逻辑如下:

1
2
3
dbus_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
3
core.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_excmem_exc

1
2
3
4
5
6
7
core.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
12
if_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
6
core.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
19
mem_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
6
core.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提供的测试框架中,mtimemtimecmp是两个通过MMIO访问的64位寄存器。其中mtime位于0x3800bff8,表示当前计时器的计数值;mtimecmp位于0x38004000,表示下一次时钟中断触发的比较值。

测试框架会不断递增mtime,并在mtime > mtimecmp时拉高时钟中断信号trint。CPU收到这个外部输入后,会在CSR中设置对应的mip.MTIP位。若同时满足mie.MTIE=1mstatus.MIE=1,CPU就会进入机器模式时钟中断,跳转到mtvec指定的中断入口。

因此,软件侧处理时钟中断的关键是:初始化mtimecmp,打开mie.MTIEmstatus.MIE,在中断处理程序中确认mcause为机器时钟中断,然后重新写入一个更大的mtimecmp。这样本次中断会被清除,之后等mtime再次超过新的比较值时,下一次时钟中断会再次触发。

2. 汇编代码

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
    .option norvc
.section .text
.globl _start

.equ MTIME, 0x3800bff8
.equ MTIMECMP, 0x38004000
.equ UART_TX, 0x40600004
.equ UART_STATUS, 0x40600008
.equ TIMER_INTERVAL, 5000

_start:
la sp, stack_top

la t0, trap_vector
csrw mtvec, t0

li t0, 0
li t1, MTIME
sd t0, 0(t1)

li t0, TIMER_INTERVAL
li t1, MTIMECMP
sd t0, 0(t1)

li t0, 0x80
csrs mie, t0

li t0, 0x8
csrs mstatus, t0

main_loop:
j main_loop

trap_vector:
addi sp, sp, -128
sd ra, 0(sp)
sd a0, 8(sp)
sd a1, 16(sp)
sd a2, 24(sp)
sd a3, 32(sp)
sd a4, 40(sp)
sd a5, 48(sp)
sd a6, 56(sp)
sd a7, 64(sp)
sd t0, 72(sp)
sd t1, 80(sp)
sd t2, 88(sp)
sd t3, 96(sp)
sd t4, 104(sp)
sd t5, 112(sp)
sd t6, 120(sp)

csrr t0, mcause
bgez t0, trap_done
andi t1, t0, 0xff
li t2, 7
bne t1, t2, trap_done

la a0, timer_msg
call puts

li t0, MTIME
ld t1, 0(t0)
li t2, TIMER_INTERVAL
add t1, t1, t2
li t0, MTIMECMP
sd t1, 0(t0)

trap_done:
ld ra, 0(sp)
ld a0, 8(sp)
ld a1, 16(sp)
ld a2, 24(sp)
ld a3, 32(sp)
ld a4, 40(sp)
ld a5, 48(sp)
ld a6, 56(sp)
ld a7, 64(sp)
ld t0, 72(sp)
ld t1, 80(sp)
ld t2, 88(sp)
ld t3, 96(sp)
ld t4, 104(sp)
ld t5, 112(sp)
ld t6, 120(sp)
addi sp, sp, 128
mret

puts:
mv t3, a0

puts_loop:
lbu t4, 0(t3)
beqz t4, puts_ret
mv a0, t4
call putchar
addi t3, t3, 1
j puts_loop

puts_ret:
ret

putchar:
li t0, '\n'
bne a0, t0, putchar_wait
addi sp, sp, -16
sd ra, 0(sp)
sd a0, 8(sp)
li a0, '\r'
call putchar
ld a0, 8(sp)
ld ra, 0(sp)
addi sp, sp, 16

putchar_wait:
li t0, UART_STATUS
lbu t1, 0(t0)
andi t1, t1, 8
bnez t1, putchar_wait

li t0, UART_TX
sb a0, 0(t0)
ret

.section .rodata
timer_msg:
.asciz "[lab6 bonus] machine timer interrupt, reset mtimecmp\n"

.section .bss
.align 12
stack:
.space 4096
stack_top:

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返回到被中断的位置继续执行。由于主程序一直在循环等待,所以可以周期性看到时钟中断打印内容。

五、通过截图

缺页异常已经完成,经过验证不会触发额外报错,最终输出与不包含缺页异常时的输出一致。 通过截图


构建五级流水线——lab6
https://travellingsheep.github.io/2026/05/29/riscv/构建五级流水线——lab6/
作者
trs62
发布于
2026年5月29日
许可协议