构建五级流水线——labplus

本文最后更新于 2026年9月5日 14:13:38

Lab +

之前已完成的 Bonus

属于 Bonus 列表 评定方式 附注
Lab 4 简述一下此次 lab 中各个 csr 寄存器的作用 Lab 4 报告 在 Lab 4 作业中已经提交,用红字标出了
Lab 4 思考为什么一定要刷新流水线? Lab 4 报告 在 Lab 4 作业中已经提交,用红字标出了
Lab 4 实现 csr.sv 给定的所有寄存器,包括那些以 s 开头的寄存器(如 stvec 等等) Lab 4 报告 / Lab + 报告 在 Lab 4 作业中已经提交,用红字标出了,由于篇幅原因没有将代码改动写出。在 Lab + 中详细给出涉及到的代码改动
Lab 5 MMU 支持巨页 Lab 5 报告 / Lab + 报告 在 Lab 5 报告的 Bonus 分区写了如何实现,但受限于篇幅删除了代码部分,本次 Lab 将给出完整的 Lab 5 报告
Lab 5 实现 MMU 的缺页异常 Lab 5 报告 / Lab + 报告 根据 Wiki,实际上是 Lab 6 的 Bonus,因此我在 Lab 6 的作业中完成了,完整的实验报告已经在本次作业中提交了
Lab 5 实现 S 模式 Lab 5 报告 / Lab + 报告 在 Lab 5 报告的 Bonus 分区写了如何实现,但受限于篇幅删除了代码部分,本次 Lab 将给出完整的 Lab 5 报告
Lab 6 编写中断处理程序,在时钟中断时打印一些内容 Lab 6 报告 完整的实验报告已经在本次作业中提交了

注:Wiki 上 Lab 5 的 Bonus 是实现 S 模式和巨页,Lab 6 的 Bonus 是实现 MMU 的缺页异常和编写中断处理程序。缺页异常我是在 Lab 6 中提交的

本次完成的 Bonus

属于 Bonus 列表 评定方式
任意 Lab 完成 乘除法 lab1-extra
Lab 6 思考时钟中断为什么使用 MMIO 计时器 Lab 6 报告
Lab + PMP、U 模式、特权拦截 Lab + 报告
Lab + 简单性能优化——分支预测与 I-Cache Lab + 报告
Lab + 原子指令 Lab + 报告
Lab + 运行 xv6,可以进入用户终端,尝试上板,但小板子资源不够,无法通过 implementation Lab + 报告

“思考时钟中断为什么使用 MMIO 计时器”这个 Bonus 在 Wiki 的“特权架构”页面上,做 Lab 6 的时候没看到,在 Lab + 中补上。

通过 make test-labplus-4 还要求完成 U 模式以及完成特权拦截(特权等级不够或者非法操作),这两个我在完成 PMP 的过程中顺便完成了。这里我实现的是针对 test-labplus-4 的最小特权拦截(特权等级不够、未实现 CSR,写入只读 CSR)。

Lab 4:实现以 s 开头的寄存器 - 代码部分

一、S-mode 相关寄存器

这一类寄存器包括 stvec、sscratch、sepc、scause、stval、sie、sip。其中 stvec、sscratch、sepc、scause、stval 在 csr_file 中新增真实寄存器;sie 和 sip 不单独建寄存器,而是分别从 mie、mip 中用 S_INTERRUPT_MASK 提取监管者模式相关位。

首先,csr_file 新增 S-mode CSR 输出端口、内部寄存器和 sie/sip 使用的 mask。

1
2
3
4
5
csr_file.sv
output u64 stvec, sscratch, sepc, scause, stval

u64 stvec_reg, sscratch_reg, sepc_reg, scause_reg, stval_reg;
localparam u64 S_INTERRUPT_MASK = 64'h222;

组合输出中把新增寄存器值导出,使同一拍写入时可以通过 write-first 旁路被外部和 Difftest 看到。

1
2
3
4
5
6
7
8
csr_file.sv
always_comb begin
stvec = stvec_reg;
sscratch = sscratch_reg;
sepc = sepc_reg;
scause = scause_reg;
stval = stval_reg;
end

写 CSR 时,sip 只替换 mip 中监管者中断位,sie 只替换 mie 中监管者中断位;stvec 复用 mtvec 的 mask,其余 S-mode CSR 直接写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csr_file.sv
always_comb begin
if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_SIP: mip = (mip_reg & ~S_INTERRUPT_MASK) | (csr_write_data & S_INTERRUPT_MASK & MIP_MASK);
CSR_SIE: mie = (mie_reg & ~S_INTERRUPT_MASK) | (csr_write_data & S_INTERRUPT_MASK);
CSR_STVEC: stvec = csr_write_data & MTVEC_MASK;
CSR_SSCRATCH: sscratch = csr_write_data;
CSR_SEPC: sepc = csr_write_data;
CSR_SCAUSE: scause = csr_write_data;
CSR_STVAL: stval = csr_write_data;
default: begin
end
endcase
end
end

读 CSR 时,sip 和 sie 分别返回 mip/mie 的监管者中断位,其他 S-mode CSR 返回对应寄存器的可见值。

1
2
3
4
5
6
7
8
9
10
11
12
13
csr_file.sv
always_comb begin
unique case (csr_read_addr)
CSR_SIP: csr_read_data = mip & S_INTERRUPT_MASK;
CSR_SIE: csr_read_data = mie & S_INTERRUPT_MASK;
CSR_STVEC: csr_read_data = stvec;
CSR_SSCRATCH: csr_read_data = sscratch;
CSR_SEPC: csr_read_data = sepc;
CSR_SCAUSE: csr_read_data = scause;
CSR_STVAL: csr_read_data = stval;
default: csr_read_data = 64'b0;
endcase
end

时序逻辑中给新增寄存器补上 reset 清零和正常更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csr_file.sv
always_ff @(posedge clk) begin
if (reset) begin
stvec_reg <= 64'b0;
sscratch_reg <= 64'b0;
sepc_reg <= 64'b0;
scause_reg <= 64'b0;
stval_reg <= 64'b0;
end else begin
stvec_reg <= stvec;
sscratch_reg <= sscratch;
sepc_reg <= sepc;
scause_reg <= scause;
stval_reg <= stval;
end
end

顶层 core 新增 S-mode CSR 连线,并把这些输出接到 csr_file 实例上。

1
2
core.sv
u64 csr_stvec, csr_sscratch, csr_sepc, csr_scause, csr_stval;
1
2
3
4
5
6
7
8
core.sv
csr_file u_csr_file(
.stvec (csr_stvec),
.sscratch (csr_sscratch),
.sepc (csr_sepc),
.scause (csr_scause),
.stval (csr_stval)
);

Difftest 原先这些 S-mode CSR 端口接常量 0, Lab 4 Bonus 中改为接入真实 CSR 状态。

1
2
3
4
5
6
7
8
core.sv
DifftestCSRState DifftestCSRState(
.sepc (csr_sepc),
.stval (csr_stval),
.stvec (csr_stvec),
.scause (csr_scause),
.sscratch (csr_sscratch)
);

二、委托寄存器

第二类是 medeleg 和 mideleg。它们用于记录哪些异常或中断可以委托给监管者模式处理。 Lab 4 中只实现 CSR 读写和 Difftest 接线,真正根据委托结果进入 S-mode 的 trap 逻辑在后续 Lab 5 才补上。

csr_file 新增两个输出端口和两个内部寄存器。

1
2
3
4
csr_file.sv
output u64 medeleg, mideleg

u64 medeleg_reg, mideleg_reg;

组合输出中导出 medeleg/mideleg 当前值。

1
2
3
4
5
csr_file.sv
always_comb begin
medeleg = medeleg_reg;
mideleg = mideleg_reg;
end

写入时分别按照 MEDELEG_MASK 和 MIDELEG_MASK 保留合法位。

1
2
3
4
5
6
7
8
9
10
11
csr_file.sv
always_comb begin
if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_MEDELEG: medeleg = csr_write_data & MEDELEG_MASK;
CSR_MIDELEG: mideleg = csr_write_data & MIDELEG_MASK;
default: begin
end
endcase
end
end

读 CSR 时返回这两个委托寄存器。

1
2
3
4
5
6
7
8
csr_file.sv
always_comb begin
unique case (csr_read_addr)
CSR_MEDELEG: csr_read_data = medeleg;
CSR_MIDELEG: csr_read_data = mideleg;
default: csr_read_data = 64'b0;
endcase
end

时序逻辑中同样补上 reset 和更新。

1
2
3
4
5
6
7
8
9
10
csr_file.sv
always_ff @(posedge clk) begin
if (reset) begin
medeleg_reg <= 64'b0;
mideleg_reg <= 64'b0;
end else begin
medeleg_reg <= medeleg;
mideleg_reg <= mideleg;
end
end

顶层增加 medeleg/mideleg 连线,并接入 csr_file 和 DifftestCSRState。

1
2
core.sv
u64 csr_medeleg, csr_mideleg;
1
2
3
4
5
core.sv
csr_file u_csr_file(
.medeleg (csr_medeleg),
.mideleg (csr_mideleg)
);
1
2
3
4
5
core.sv
DifftestCSRState DifftestCSRState(
.mideleg (csr_mideleg),
.medeleg (csr_medeleg)
);

三、PMP 寄存器

第三类是 pmpcfg0 和 pmpaddr0。在 Lab 4 Bonus 阶段,这两个寄存器只是在 csr_file 内部补齐读写;当时没有真正接入 PMP 权限检查,也没有 Difftest 端口需要连接。

csr_file 内部新增 pmpcfg0/pmpaddr0 寄存器和组合可见值。

1
2
3
csr_file.sv
u64 pmpcfg0_reg, pmpaddr0_reg;
u64 pmpcfg0, pmpaddr0;

组合输出中把内部寄存器值送到临时可见值。

1
2
3
4
5
csr_file.sv
always_comb begin
pmpcfg0 = pmpcfg0_reg;
pmpaddr0 = pmpaddr0_reg;
end

写 CSR 时直接更新 pmpcfg0 和 pmpaddr0 的可见值。

1
2
3
4
5
6
7
8
9
10
11
csr_file.sv
always_comb begin
if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_PMPCFG0: pmpcfg0 = csr_write_data;
CSR_PMPADDR0: pmpaddr0 = csr_write_data;
default: begin
end
endcase
end
end

读 CSR 时返回 pmpcfg0/pmpaddr0。

1
2
3
4
5
6
7
8
csr_file.sv
always_comb begin
unique case (csr_read_addr)
CSR_PMPCFG0: csr_read_data = pmpcfg0;
CSR_PMPADDR0: csr_read_data = pmpaddr0;
default: csr_read_data = 64'b0;
endcase
end

时序逻辑中补上 reset 和正常更新。

1
2
3
4
5
6
7
8
9
10
csr_file.sv
always_ff @(posedge clk) begin
if (reset) begin
pmpcfg0_reg <= 64'b0;
pmpaddr0_reg <= 64'b0;
end else begin
pmpcfg0_reg <= pmpcfg0;
pmpaddr0_reg <= pmpaddr0;
end
end

Lab 5:MMU 支持巨页,以及 S 模式

注:已完成两个Bonus 任务,并且通过了袁神的测试

一、Lab 5 背景

在这个 Lab 中,我们需要真正处理异常和中断,需要完成 MRET、ECALL 和 MMU(支持 Sv39 页表)。 说了这么老半天,到底什么是异常,什么是中断? 具体的细节会在实现时说明,根据资料,大致可以这样归类:

  • 异常:运行中的程序自己出了问题(系统内部产生),如缺页异常,除零异常
  • 中断:程序外的外部事件对运行中的程序造成了影响(系统外部产生),如键盘输入 Ctrl+C,关机

处理异常时,需要遵循精确异常的宗旨,即异常指令之前的所有指令必须走完它们的生命周期,而异常指令之后的所有指令不能对计算机(包括内存、通用寄存器、CSR 寄存器、PC、特权级别等)造成任何影响。这听上去非常合理,毕竟这样才方便我们专门针对异常进行处理:发现是异常指令时,直接清空整条流水线即可。

然而并不是所有指令都只在 WB 阶段才对系统发生作用,比如对于 store 指令,会在 MEM 阶段将数值写入内存:

  • 当 trap 指令走到 mem_wb 时,在同一拍,store 指令处在 ex_mem
  • 这一拍中,trap_unit 发现需要清空流水线,但是下一拍才能将这个信息广播出去
  • 然而,同一拍中 MEM 就会将数据写入内存,导致流水线清空不完全
  • 同理,dbus 访存也需要被阻止。

实际上,下一拍传递到 mem_wb 的任何信号都需要被 flush 掉,无论它是否是 store 指令。否则这条指令依然会被 commit 导致报错。在第四板块中会详细处理。 > 一条指令被视作 commit 的标准是什么呢?这就需要将 commit_fire 这个点拿出来重新审视一番了。在前几个 Lab 中,这个布尔值用来向 Difftest 说明当前是否进行一次提交,拆成自然语言就是当前允许 mem_wb 信号有效且正常流动。也就是一条指令只要经过 mem_wb 这个门槛,进入了只会写入寄存器的 WB 阶段,就绝对不会再出现任何报错了,过完 WB 阶段,它就彻底被执行完毕了。

1
assign commit_fire = mem_wb_valid && ctrl_mem_wb_enable;

因此,我们需要让两条异常指令 ECALL 和 MRET 一路走到 WB 阶段之后再更改 CSR、特权级别、PC 等。否则,需要刷新流水线时,它们已经造成的状态改变难以被恢复。

Part I —— ECALL 与 MRET 指令

二、准备工作——新建枚举类型

为了表示新增的特权级别,在 mypkg 中新建枚举类型:

1
2
3
4
5
typedef enum u2 {
PRIV_U = 2'b00,
PRIV_S = 2'b01,
PRIV_M = 2'b11
} priv_t;

还记得 Lab 4 中的 MSTATUS 寄存器吗?在 Lab 4 中,我们提到 MSTATUS “包含一系列状态和控制位”,本次 Lab 中,我们将针对其中四位进行更改: MIE 和 MPIE 共同用于控制 CPU 是否响应异常;MPRV 用于核对特权级别;MPP 是两位字段,决定 trap 发生前的特权级别,便于处理完异常后还原至原有的特权级别。

1
2
3
4
5
6
7
8
9
mypkg.sv
typedef enum logic [5:0] {
MSTATUS_MIE_BIT = 6'd3,
MSTATUS_MPIE_BIT = 6'd7,
MSTATUS_MPRV_BIT = 6'd17
} mstatus_bit_t;

localparam int unsigned MSTATUS_MPP_LOW = 11;
localparam int unsigned MSTATUS_MPP_HIGH = 12;

三、先搞定 ECALL 与 MRET 指令

1. 这两条指令都是什么?

MRET (M return)一般出现在异常处理完成之后,让 CPU 退回到异常开始之前的状态(包括特权级别、中断状态和 PC 指针)。它单独调用,不涉及任何寄存器读写操作,除了开头结尾的 funct 和 opcode,其余部分都是零,机器码为 32'h30200073。

ECALL(environment call)指令用于触发系统调用,是程序特权级别不够时让系统在更高级别下代为完成的请示。它也是单独调用,具有固定二进制码 32'h00000073。调用 ECALL 前,程序会通过应用 程序二进制接口(ABI) 约定将系统调用号和参数存入特定寄存器中。

具体这两套指令要做什么,在 Wiki 上写的很详细。这里假设只实现 U 和 M 模式,S 模式作为 bonus 放在后面实现。

2. 识别、传递、实例化

由于机器码固定,直接在 decoder 中筛选即可:

1
2
3
output u1 is_ecall, is_mret;
assign is_ecall = (inst == 32'h00000073);
assign is_mret = (inst == 32'h30200073);

在 id_ex、ex_mem、mem_wb 中传递它们,同样为了防止意外识别成异常信号,只在 valid 时传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id_ex,ex_mem,mem_wb_reg.sv
input u1 is_trap_in, is_mret_in;
output u1 is_trap_out, is_mret_out;

always_ff @(posedge clk) begin
if (reset) begin
is_trap_out <= 1'b0;
is_mret_out <= 1'b0;
end else if (enable) begin
if (valid_in) begin
is_trap_out <= is_trap_in;
is_mret_out <= is_mret_in;
end else begin
is_trap_out <= 1'b0;
is_mret_out <= 1'b0;
end
end
end

最后在 core 中实例化它们:

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
core.sv
u1 decoder_is_ecall, decoder_is_mret;
u1 id_ex_is_ecall, id_ex_is_mret;
u1 ex_mem_is_ecall, ex_mem_is_mret;
u1 mem_wb_is_ecall, mem_wb_is_mret;

decoder u_decoder(
.is_ecall (decoder_is_ecall),
.is_mret (decoder_is_mret)
);

id_ex u_id_ex(
.is_trap_in (decoder_is_ecall),
.is_mret_in (decoder_is_mret),
.is_trap_out (id_ex_is_ecall),
.is_mret_out (id_ex_is_mret)
);

ex_mem u_ex_mem(
.is_trap_in (id_ex_is_ecall),
.is_mret_in (id_ex_is_mret),
.is_trap_out (ex_mem_is_ecall),
.is_mret_out (ex_mem_is_mret)
);

mem_wb u_mem_wb(
.is_trap_in (ex_mem_is_ecall),
.is_mret_in (ex_mem_is_mret),
.is_trap_out (mem_wb_is_ecall),
.is_mret_out (mem_wb_is_mret)
);

3. trap 信号控制单元

这两条指令的流水线控制与之前不同,我们在 utils 文件夹中新建 trap_unit 进行处理。它们都涉及到 MCAUSE、MEPC、MTVAL、priv 四个量:

① ECALL 指令:主动记录异常发生时的系统状态,导致特权级别升高。根据 Wiki 界面,我们需要完成如下事项:
1
2
3
4
5
当处理异常的时候,你应该:
保存当前 `PC` 到 `MEPC`
设置 `CSR`:`MCAUSE` 参考表格,我们这里是 8 或 11。
设置 `CSR`:`MPIE` 设置为原来的 `MIE`,`MIE` 设置为 0。`MPP` 设置为当前(执行之前)的特权级别。
特权级别设置为 `M`

其中,“跳转到 MTVEC,冲刷流水线”这一条将在第四板块详解,其他条目如下:

  • 首先,判断当前是否是一条有效的 ECALL 指令(为兼容后续 Lab 更加广泛的异常,直接用 trap 命名):valid 信号用于告知当前是否是一条有效的 trap 指令,由 commit_fire 和 is_trap 共同决定
1
2
trap_unit.sv
assign trap_valid = commit_fire && mem_wb_is_trap;
  • 保存当前 PC 至 MEPC:发生异常时的 PC 就是 ECALL 指令,等到实际调用时再将 PC+4
1
2
trap_unit.sv
assign trap_mepc = mem_wb_pc;
1
2
3
4
csr_file.sv
if (trap_commit) begin
mepc = trap_mepc;
end
  • 设置 MCAUSE:由于此 CPU 简化了特权级别,只涉及 U 和 M 两种,根据规则规定,如果 ECALL 发生时当前处在 User 级别,则原因编号为 8,M 对应 11
1
2
trap_unit.sv
assign trap_cause = (current_priv == PRIV_U) ? 64'd8 : 64'd11;
  • 设置 MPIE、MIE:
1
2
3
4
5
csr_file.sv
if (trap_commit) begin
mstatus[MSTATUS_MPIE_BIT] = mstatus_reg[MSTATUS_MIE_BIT];
mstatus[MSTATUS_MIE_BIT] = 1'b0;
end
  • 最后记录当前的特权状态,并存入 MPP
1
2
trap_unit.sv
assign trap_from_priv = current_priv;
1
2
3
4
csr_file.sv
if (trap_commit) begin
mstatus[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW] = trap_from_priv;
end
  • 特权级别设置为 M
1
2
3
4
core.sv
else if (trap_unit_trap_valid) begin
current_priv <= PRIV_M;
end
  • 额外地,设置 MTVAL。尽管当前 ECALL 指令不需要记录 MTVAL,但在 Lab 6 中处理其他异常时就需要记录这个值了
1
2
3
trap_unit.sv
assign trap_tval = 64'b0;
assign trap_from_priv = current_priv;
1
2
3
4
csr_file.sv
if (trap_commit) begin
mtval = trap_mtval;
end
② MRET 指令:负责在异常处理完成之后将状态进行还原,导致特权级别降低

需要注意,当一个异常发出时,CPU 所处的状态可能并不是最高权限的 M 模式,但是在实际处理异常时,CPU 会被强制切换成 M 模式,通过更高的访问权限来尝试解决问题。但异常如果包含恶意,通过 M 模式把重要的信息泄露出去就危险了。

因此,我们需要记录异常发生时处在的权限等级(用 MSTATUS 中的 MPP 位进行记录),并设置 MPRV=1 表示处理时需要伪装自己是 MPP 中的权限等级进行处理。 > 实际执行中我们采用防御性编程,只要异常发出时的权限不是 M,就让 MPRV=0。毕竟权限为 M 不代表着一定要伪装成 MPP 中的值。

根据 RISC-V 中的要求:

1
An MRET or SRET instruction is used to return from a trap in M-mode or S-mode respectively. When executing an xRET instruction, supposing xPP holds the value y, xIE is set to xPIE; the privilege mode is changed to y; xPIE is set to 1; and xPP is set to the least-privileged supported mode (U if U-mode is implemented, else M). If y≠M, xRET also sets MPRV=0.

也就是说 MRET 要做以下几件事:

  • 将 MIE 还原为 MPIE 中存下来的旧值,MPIE 设置为 1,
1
2
3
4
5
csr_file.sv
else if (mret_commit) begin
mstatus[MSTATUS_MIE_BIT] = mstatus_reg[MSTATUS_MPIE_BIT];
mstatus[MSTATUS_MPIE_BIT] = 1'b1;
end
  • 将当前的 MPP 值 mret_previous_priv 提取出来,存入 current_priv
1
2
3
trap_unit.sv
assign mret_valid = commit_fire && mem_wb_is_mret;
assign mret_previous_priv = priv_t'(csr_mstatus[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW]);
1
2
3
4
5
6
core.sv
always_ff @(posedge clk)
else if (trap_unit_mret_valid) begin
current_priv <= trap_unit_mret_previous_priv;
end
end

这里需要注意,csr_file 本身做成了向外暴露组合逻辑接口,而内部的时序逻辑接口是晚一拍再写入的。这本身没有错,因为作为一个触发器,它必须满足时序逻辑。那么对应的,current_priv 本身也必须做成组合+时序的模式,并将这个组合接口暴露给 Difftest。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
core.sv
priv_t current_priv, current_priv_comb;
always_comb begin
current_priv_comb = current_priv;
if (reset) begin
current_priv_comb = PRIV_M;
end else if (trap_unit_trap_valid) begin
current_priv_comb = PRIV_M;
end else if (trap_unit_mret_valid) begin
current_priv_comb = csr_mstatus_reg_mpp;
end
end
always_ff @(posedge clk) begin
if (reset) begin
current_priv <= PRIV_M;
end else begin
current_priv <= current_priv_comb;
end
end
DifftestCSRState DifftestCSRState(
.priviledgeMode (current_priv_comb),
);
  • 此时记录旧权限的 MPP 已经废弃了,因此将其设置为最低权限的 U 模式,确保 MRET 指令结束后旧权限一定为 U;同时用 csr_file 内部旧的 mstatus_reg.MPP 判断返回目标是否为 M,若否,则将 MPRV 设置为 0。这里不把 trap_unit 的 mret_previous_priv 再接回 csr_file,避免 csr_mstatus 和 mstatus 更新逻辑之间形成组合环。
1
2
3
4
5
csr_file.sv
mstatus[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW] = PRIV_U;
if (mstatus_reg[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW] != PRIV_M) begin
mstatus[MSTATUS_MPRV_BIT] = 1'b0;
end
③ 最后,在 core 中实例化并连线。

首先引入 trap_unit,并声明 trap_unit 和 csr_file 之间需要连接的信号:

1
2
3
4
5
core.sv
`include "src/utils/trap_unit.sv"
u1 trap_unit_trap_valid, trap_unit_mret_valid;
u64 trap_unit_trap_mepc, trap_unit_trap_cause, trap_unit_trap_tval;
priv_t trap_unit_trap_from_priv, csr_mstatus_reg_mpp;

接着,在 core 中更新 current_priv:

1
2
3
4
5
6
7
8
9
always_ff @(posedge clk) begin
if (reset) begin
current_priv <= PRIV_M;
end else if (trap_unit_trap_valid) begin
current_priv <= PRIV_M;
end else if (trap_unit_mret_valid) begin
current_priv <= csr_mstatus_reg_mpp;
end
end

然后,将 trap_unit 产生的 trap 和 MRET 信息接到 csr_file,这里只展示新增信号:

1
2
3
4
5
6
7
8
9
10
csr_file u_csr_file(
// ...
.trap_commit (trap_unit_trap_valid),
.trap_mepc (trap_unit_trap_mepc),
.trap_mcause (trap_unit_trap_cause),
.trap_mtval (trap_unit_trap_tval),
.trap_from_priv (trap_unit_trap_from_priv),
.mret_commit (trap_unit_mret_valid),
.mstatus_reg_mpp (csr_mstatus_reg_mpp)
);

最后实例化 trap_unit,把 WB 阶段的 ECALL/MRET 标记、当前特权级和相关 CSR 接进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trap_unit u_trap_unit(
.commit_fire (commit_fire),
.mem_wb_is_ecall (mem_wb_is_trap),
.mem_wb_is_mret (mem_wb_is_mret),
.mem_wb_pc (mem_wb_pc),
.current_priv (current_priv),
.csr_mepc (csr_mepc),
.csr_mtvec (csr_mtvec),
.trap_valid (trap_unit_trap_valid),
.trap_mepc (trap_unit_trap_mepc),
.trap_cause (trap_unit_trap_cause),
.trap_tval (trap_unit_trap_tval),
.trap_from_priv (trap_unit_trap_from_priv),
.mret_valid (trap_unit_mret_valid)
);

四、让 PC 和 flush 指令受到异常指令的控制

1. 前一拍不是 ECALL/MRET

这一步紧接上一步,将 PC 重定向和 flush 真正落实到流水线中。 是否要跳转实例化为 redirect_valid,只要 trap 或者 MRET 其中一个生效就进行跳转 异常引起的 PC 跳转在代码中实例化为 redirect_pc,对于 ECALL 指令,需要跳转到 CPU 专用异常处理地址 MTVEC,MRET 则是要返回异常出现的地址 MEPC

1
2
3
4
5
6
7
8
9
module trap_unit
(
output u1 redirect_valid,
output u64 redirect_pc
);

assign redirect_valid = trap_valid || mret_valid;
assign redirect_pc = trap_valid ? csr_mtvec : csr_mepc;
endmodule

对应地,在 ctrl 模块中引入 redirect_valid,以应用最新的 flush 信号。由于 mem_wb 信号为当前处理阶段,不能 flush,其他三个寄存器都需要 flush

1
2
3
4
5
6
7
8
9
10
11
12
module ctrl
(
input u1 ex_mem_read, should_jump, commit_flush, ex_is_mul, ex_mul_done
);

assign if_id_flush = (global_enable && (should_jump || commit_flush) || pending_jump);

assign id_ex_flush = (global_enable && (should_jump || commit_flush) || pending_jump) |
(global_enable && load_use_stall);

assign ex_mem_flush = (global_enable && commit_flush) || (global_enable && mul_stall);
endmodule

最后在 core 中进行实例化,注意异常的 PC 跳转优先级最高,因此在原有 Lab 3 的跳转逻辑上加上 trap 的判断,并连入 pc 模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
u1  trap_unit_redirect_valid, pc_should_jump;
u64 trap_unit_redirect_pc, pc_jump_addr;

assign pc_should_jump = trap_unit_redirect_valid ? 1'b1 : ex_should_jump_real;
assign pc_jump_addr = trap_unit_redirect_valid ? trap_unit_redirect_pc : ex_jump_addr_real;

pc u_pc(
.should_jump(pc_should_jump),
.jump_addr (pc_jump_addr)
);

trap_unit u_trap_unit(
.redirect_valid (trap_unit_redirect_valid),
.redirect_pc (trap_unit_redirect_pc)
);

ctrl u_ctrl(
.should_jump (pc_should_jump),
.commit_flush (trap_unit_redirect_valid)
);

2. 前一拍是 ECALL/MRET

无论任何在 MRET/ECALL 后的指令,都需要防止它们提交,也就是需要引入 mem_wb_flush。需要注意这个 flush 不能和前几个 flush 一样用 commit_flush 决定,因为 commit_flush 来自 redirect_valid,而这个信号在 ECALL 指令的下一拍才产生,已经来不及拦截 MEM 阶段的行为了,因此作出如下修改:

首先是通过 mem_wb 的值拦截 store 和 dbus,在 core 中新增 trap_kill_mem,表示需要拦截这次 MEM 访存行为,并接入 ex_mem;对于 dbus,直接修改 valid 值;同时,直接拦截 mem_wb 的 valid 信号,这样就不用管当前信号是否是 store

1
2
3
4
5
6
7
core.sv
u1 trap_kill_mem;
assign trap_kill_mem = mem_wb_valid && (mem_wb_is_ecall || mem_wb_is_mret);
assign dreq.valid = mem_dreq_valid && ~trap_kill_mem;
mem_wb u_mem_wb(
.valid_in (ex_mem_valid && ~trap_kill_mem),
);

五、连接 Difftest

最后检查 DifftestCSRState。current_priv 已经是 2 位的 RISC-V 特权级编码,因此可以直接接到 priviledgeMode。同时,ECALL/MRET 修改过的 MSTATUS、MEPC、MCAUSE、MTVAL 和 MTVEC 都需要从 csr_file 的输出接入 Difftest。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DifftestCSRState DifftestCSRState(
.priviledgeMode (current_priv_comb),
.mstatus (csr_mstatus),
.mepc (csr_mepc),
.mtval (csr_mtval),
.mtvec (csr_mtvec),
.mcause (csr_mcause),
.satp (csr_satp),
.mip (csr_mip),
.mie (csr_mie),
.mscratch (csr_mscratch),
.mideleg (csr_mideleg),
.medeleg (csr_medeleg)
);

Part II —— MMU

一、基本概念

1. Page、Page Table

页(page)指的是固定大小的内存块,也是内存管理的最小基本单位。页表(page table)是一个页表项(Page Table Entry, PTE)数组,每个 PTE 对应一个虚拟页。

2. Virtual Memory、Physical Memory

我们知道,一个程序需要内存才能运行。对于 S 级和 U 级的程序,直接让它们见到真实的物理地址是不安全的,它们只能拿到虚拟地址(Virtual Address, VA)。在执行时,CPU 需要把虚拟地址翻译成真实物理地址(Physical Address, PA),而这个翻译过程就会用到页表 page table。

存储页表根地址和分页模式的是 satp 寄存器。对于本 Lab 实现的 Sv39 页表,satp 的构成如下所示(摘自 Lab Wiki):

1
2
3
4
63       60 59                  44 43                                0
---------------------------------------------------------------------
| MODE | ASID | PPN |
---------------------------------------------------------------------

其中,最高 4 位是分页模式 MODE,取值为 8 时代表使用 Sv39 页表模式,取值为 0 时代表不进行页表翻译。PPN 的全称是 Physical Page Number,指的是真实物理页号。

3. MMU(Memory Management Unit)

整个翻译过程由 CPU 中的 MMU 模块完成。CSAPP 中文版第三版图 9-2 清晰展示了 MMU 扮演的职责:

4. 核心计算

1) 地址系统

我们采用 64 位地址系统,其中真实物理地址仅占据低 56 位,高 8 位全部为 0。 > 2^56B=64PB=64*10^6GB,系统不会具有这么夸张的内存大小 > ##### 2) 位数划分原因

  • 页大小为 4KB,这也意味着任何一个页起始地址的低 12 位都必须为 0。整个翻译过程中只记录 56 位中剩下的 44 位记录 PPN 的部分。
  • PTE 大小为 8B。因此一页能够容纳的 PTE 数量 = 4KB/8B=512=2^9
3) 如何进行翻译

我们实现的是 Sv39 三级页表,这里的 39=9*3+12。三个 9 对应三个 VPN(Virtual Page Number),指的是我们要找的 PTE 处在某一页的第几个;最后的十二位是 Page Offset,是真实地址的偏移量。 每个 PTE 的高 10 位是保留位,必须为 0;中间的 44 位是 PPN,左移 10 位后即可得到下一层页表的根地址。 递归查询到第三层后,将第三层 PTE 中的 PPN 左移 12 位,再拼上页内偏移量 offset,开头补上 8 个 0,就得到了最终的真实物理地址。

三、实施思路

在前几个 Lab,CPU 处于“裸机”状态,所有输入输出都是物理真实地址。而本次 Lab 中的所有地址都需要通过 MMU 模块,因为 ibus 和 dbus 的地址都有可能需要翻译。而 MMU 本身需要访问真实物理地址,常规的 ibus 返回的 data 宽度只有 32 位,无法使用。助教提供了两种方法。其中方法二将 MMU 放在 CBus 外侧,已经处在 CPU 外部,因而需要更改 SimTop 和 VTop,难以直接获取 CSR 寄存器中存储的 satp 值,较为混乱。这里采用方法一。这里 MMU 的输出还需要 DBusToCBus 进行一层额外的转换: 对于外部总线接口,它根本看不到一条指令是 ibus 还是 dbus,所有的请求都通过 CBus 返回。因此,在 CBus 之前设置 MMU,可以继续保持 CBus 负责全部请求的架构。相对地,需要复用已有的 dbus 通路,利用其 64 位宽度的特性整合 ibus 和 dbus 的所有请求,并统一发送给 MMU。

1. DBusArbiter

把 ibus 和 dbus 整合在一起,就涉及到了优先级问题。 这里类似 ctrl 中的做法,如果有 dbus,就优先处理 dbus;没有 dbus 再去处理 ibus。这是因为 mem 阶段的访存需求没有结束时,不能引入新的上游需求,否则会导致堵塞。

可得主循环逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dbus_arbiter.sv
always_ff @(posedge clk) begin
if (reset) begin
busy <= 1'b0;
saved_select_is_dbus <= 1'b0;
saved_req <= '0;
saved_ibus_upper_word <= 1'b0;
end else if (busy) begin
if (oresp.data_ok) begin
busy <= 1'b0;
end
end else if (dreq.valid || ireq.valid) begin
busy <= 1'b1;
saved_select_is_dbus <= dreq.valid;
saved_req <= selected_req;
saved_ibus_upper_word <= ireq.addr[2];
end
end
  • 如果有 reset,直接清零
  • 一旦 oresp 准备就绪,busy 在下一拍变回 0
  • 如果有 ireq 或者 dreq,意味着 CBus 被占用,设置布尔值 busy 来记录,dbus 为高优先级
  • 为防止输入变化导致 selected 发生变化,当时钟上升沿来临时,将 selected_req 压入 saved_req,保存当前正在处理的需求,实现触发器逻辑
  • 对于 ibus,由于只用到 32 位,需要选择 64 位的前半或者后半。地址是字节编码的,也就是无论前半后半,地址的最后两位一定都是 0,我们看第 2 位(0 开始计数)的奇偶性即可
  • 如果不再 busy,则当前总线没有请求。这时不需要手动清零 saved,因为下一次请求来临时会读取当拍变动的 selected 值。如果这次请求仍然跨拍,saved 会被自动更新为 selected 值。

用组合逻辑,将 selected_req 与外部输入连接起来。对于 dbus,直接传递 dbus_req_t 即可;而 ibus 相当于一个 32 位不写入数据的写指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dbus_arbiter.sv
always_comb begin
selected_req = '0;

if (dreq.valid) begin
selected_req = dreq;
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;
end
end

最终输出通过 busy 来决定:空闲时直接连 selected_req,繁忙时连接 saved_req。

1
2
dbus_arbiter.sv
assign oreq = busy ? saved_req : selected_req;

对于上游返回的输出,同输入一样,如果是 dbus 直接用,ibus 需要特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
always_comb begin
dresp = '0;
iresp = '0;

if (busy && saved_select_is_dbus) begin
dresp = oresp;
end else if (busy) begin
iresp.addr_ok = oresp.addr_ok;
iresp.data_ok = oresp.data_ok;
iresp.data = saved_ibus_upper_word ? oresp.data[63:32] : oresp.data[31:0];
end
end

现在 ireq 变成了无效请求,取指和访存都统一通过 core 中的 dbus 输入输出端口。因此,core 中强制 ireq = 0,并新增 fetch_ireq、fetch_iresp 端口接回 pc、if_id;同时新增 mem_dreq 和 mem_dresp,接回 mem。最后将这四个新增端口接入 ctrl 和 dbus_arbiter,完成原先的 flush 控制和新增的 arbiter 选择模块,由 arbiter 统一输出 dreq,并接收 dresp。

2. MMU

接下来,MMU 会接管 dresp 和 dreq,经过处理之后再通过 core 向外输出。 先不管 bonus 的事情,直接分成三个阶段 L2、L1、L0,以及空闲、透传和计算完毕,定义六个 MMU 状态,并实例化:

1
2
3
4
5
6
7
8
9
10
11
mmu.sv
typedef enum u3 {
MMU_IDLE = 3'd0,
MMU_DIRECT = 3'd1,
MMU_L2 = 3'd2,
MMU_L1 = 3'd3,
MMU_L0 = 3'd4,
MMU_ACCESS = 3'd5
} mmu_state_t;

mmu_state_t state;

类似 dbus_arbiter,MMU 本身也是一个状态机,需要记录当前状态,防止数值变动。尤其是原始 satp 的低 12 位作为 offset,要一直留到最后一阶段:

1
2
mmu.sv
dbus_req_t saved_req;

主循环之外,我们定义一些列辅助变量:

  • 根据当前优先级和 satp 高 4 位的 mode 决定是否要启用三级翻译,得到布尔值 translation_enabled。
  • l{i}_page_base 则是每一轮的基地址中间变量,l0_addr 是最终翻译出来的地址,也作为中间变量;l2_page_base 直接由 satp 获得基地址,l1_page_base、l0_page_base 则是根据每一轮外部返回的 data 得到基地址;l0_paddr 则是根据最后一轮外部返回数据与原先 satp 的低 12 位拼接得到物理地址。
  • 定义 page_index 和 page_base,用于每一阶段保存基地址和 satp 中每一轮的索引值。
  • 所需页表项的物理地址用 pte_addr,其计算方式为 页表基地址[索引] = 页表基地址 + 索引 * 8。
  • L2、L1、L0 三个阶段并不是真实的下游请求,我们用 pte_req 来指代这三个阶段发送出去的请求,同时直接用组合逻辑拼出 pte_addr 的 valid、addr、size、strobe、data。此逻辑与 mem 一样,实际上是发送一个读请求,因此请求有效,地址为计算出的 pte_addr,size 为 8B,掩码全 0 表示不写入数据,data 为 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
mmu.sv
u1 translation_enabled;
assign translation_enabled = (current_priv != PRIV_M) && (satp[63:60] == 4'd8);

u64 l2_page_base, l1_page_base, l0_page_base, l0_paddr;
assign l2_page_base = {8'b0, satp[43:0], 12'b0};
assign l1_page_base = {8'b0, dresp.data[53:10], 12'b0};
assign l0_page_base = {8'b0, dresp.data[53:10], 12'b0};
assign l0_paddr = {8'b0, dresp.data[53:10], saved_req.addr[11:0]};

u64 page_base;
u9 page_index;
always_comb begin
case (state)
MMU_L2: page_index = saved_req.addr[38:30];
MMU_L1: page_index = saved_req.addr[29:21];
MMU_L0: page_index = saved_req.addr[20:12];
default: page_index = 9'b0;
endcase
end

u64 pte_addr;
assign pte_addr = page_base + {52'b0, page_index, 3'b0};

dbus_req_t pte_req;
assign pte_req.valid = 1'b1;
assign pte_req.addr = pte_addr;
assign pte_req.size = MSIZE8;
assign pte_req.strobe = 8'b0;
assign pte_req.data = 64'b0;

接着定义每一轮发出与接收的请求。这里接收来自 core 的 core_req,向上游输出 core_resp;接收上游的 dreq,向下游发放 dresp:

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
mmu.sv
input dbus_req_t core_req,
input dbus_resp_t dresp,
output dbus_resp_t core_resp,
output dbus_req_t dreq,

always_comb begin
dreq = '0;
core_resp = '0;

case (state)
MMU_IDLE: begin
dreq = '0;
core_resp = '0;
end
MMU_DIRECT: begin
dreq = saved_req;
core_resp = dresp;
end
MMU_L2, MMU_L1, MMU_L0: begin
dreq.valid = 1'b1;
dreq.addr = pte_addr;
dreq.size = MSIZE8;
dreq.strobe = 8'b0;
dreq.data = 64'b0;
end
MMU_ACCESS: begin
dreq = saved_req;
core_resp = dresp;
end
default: begin
dreq = '0;
core_resp = '0;
end
endcase
end

即默认状态下为 0;如果判断需要直传,则将下游请求给到输出端口;三级翻译过程中,填写 dreq 并使其有效;翻译完成时将最终结果发给上游,并接收上游回复传给下游。

主循环如下:

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
mmu.sv
always_ff @(posedge clk) begin
if (reset) begin
state <= MMU_IDLE;
saved_req <= '0;
page_base <= 64'b0;
end else begin
case (state)
MMU_IDLE: begin
if (core_req.valid) begin
saved_req <= core_req;
if (translation_enabled) begin
state <= MMU_L2;
page_base <= l2_page_base;
end else begin
state <= MMU_DIRECT;
end
end
end
MMU_DIRECT: begin
if (dresp.data_ok) begin
state <= MMU_IDLE;
end
end
MMU_L2: begin
if (dresp.data_ok) begin
state <= MMU_L1;
page_base <= l1_page_base;
end
end
MMU_L1: begin
if (dresp.data_ok) begin
state <= MMU_L0;
page_base <= l0_page_base;
end
end
MMU_L0: begin
if (dresp.data_ok) begin
state <= MMU_ACCESS;
saved_req.addr <= l0_paddr;
end
end
MMU_ACCESS: begin
if (dresp.data_ok) begin
state <= MMU_IDLE;
end
end
default: begin
state <= MMU_IDLE;
end
endcase
end
end

首先 reset 直接重置 MMU 状态;空闲时,如果下游请求有效,则保存下游请求;如果下游有效且需要开启翻译,则转到 L2 状态,并计算 L2 基地址作为基地址;若下游有效且不需要开启翻译,则转到 DIRECT 状态;L2、L1、L0 同理,算完后将状态还原为空闲。

整个过程不需要引入额外的 stall,因为上游数据没返回的时候,ctrl 模块会正常执行 stall 指令

TODO 改接线名字

四、最终输出

五、总结

我认为,整个 CPU 可以看作“表”、“里”两部分,其中“表”部分是经由外界(Difftest)检查的接口,它们需要保持时序逻辑以满足实时性;“里”部分则是完整的 CPU,它采用时序逻辑,在下一拍才会真正更新自己的值。整个 CPU 的运行与“表”部分无关,但为了通过测试不得不采取时序+组合的双重逻辑。

Part III —— Bonus

一、巨页

1. 巨页的性质

三次翻译需要与内存交互三次,带来巨大的延迟。为了保证流水线的效率,我们允许提前终止查找,减少翻译次数。这使得原本用于索引下级页表的 VPN 位自动并入 offset,从而成倍增大页的大小,这就是巨页。受限于硬件电路,在 Sv39 中,我们只能分别增加 9 位与 18 位,每次扩大 2^9 次方,最终得到 2MB 和 1GB 的巨页。

这带来两个好处:首先是翻译次数减少,与内存交互次数减少,从而提升了查找效率,减少延迟;第二是充分利用 TLB(Translation Lookaside Buffer,快表,本 Lab 中尚未实现),通过提升 TLB 命中率减少重新查找的次数。

相对地,页大小增大会导致内存碎片化:一个小程序完全用不到2MB甚至更大的内存,但一次性只能分配这么大,带来了更加严重的内存浪费;同时,清理一个巨页的开销也更大,也影响了其灵活性。一般只在大内存需求的特定环境下使用巨页。

需要注意,本次 Lab 中假设三层查找一定对应一个正确的地址,假设 flag 位一定正确。现有的流水线尚不支持额外的异常处理。

2. 巨页的原理

三种页大小对应的拼接如下

1
2
3
L2 叶子: {8'b0, pte[53:28], va[29:0]}  // 1GB 页
L1 叶子: {8'b0, pte[53:19], va[20:0]} // 2MB 页
L0 叶子: {8'b0, pte[53:10], va[11:0]} // 4KB 页

我们如何判断当前 PPN 是否是叶子节点呢?这时我们就需要用到 PTE 的 flag 位了。根据规定,如果 flag 的低 1 和 3 位中有任何一位是 1,则意味着我们走到了翻译的终点。

3. 改动

我们需要额外记录一次和两次翻译得到的物理地址,并按照各自的拼接方式连线:

1
2
3
4
mmu.sv
u64 l1_paddr, l0_paddr;
assign l2_paddr = {8'b0, dresp.data[53:28], saved_req.addr[29:0]};
assign l1_paddr = {8'b0, dresp.data[53:19], saved_req.addr[20:0]};

同时,我们还要额外判断当前是否走到叶子节点,引入布尔值 pte_is_leaf 进行记录:

1
2
3
mmu.sv
u1 pte_is_leaf;
assign pte_is_leaf = dresp.data[1] || dresp.data[3];

对应地修改主循环的逻辑,如果是叶子节点,直接进入 ACCESS 阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mmu.sv
always_ff @(posedge clk) begin
end else begin
case (state)
MMU_L2: begin
if (dresp.data_ok) begin
if (pte_is_leaf) begin
state <= MMU_ACCESS;
saved_req.addr <= l2_paddr;
end
end
end
MMU_L1: begin
if (dresp.data_ok) begin
if (pte_is_leaf) begin
state <= MMU_ACCESS;
saved_req.addr <= l1_paddr;
end
end
end

4. 结果

可以看到,IPC 确实提升了不少,流水线的效率确实提升了。

二、S 模式

1. 背景介绍与复用思路

S 模式是什么,为什么我们要实现它?这里我们需要指导 M、S、U 模式分别处理什么任务 根据 Lab Wiki 上的表述:

1
2
3
M 模式是机器模式,权限最高,通常是固件或者最底层的管理代码在跑。M 模式有 CPU 完整的访问权,且是 CPU 必选项。
S 模式是监管者模式,一般是操作系统内核所在。
U 模式是用户模式,通常是用户代码运行的地方。

因此,不是所有异常都非得交给 M 模式处理,在 CPU 中,我们通过 medeleg 和 mideleg 两个寄存器来控制特权级别的跳转。

从上一个 Lab 中 sstatus 就是 mstatus 的子集就可以看出,M 模式和 S 模式的许多操作是类似的。因此,我们可以将 mret 和 sret 直接抽象为 xret 指令,实现高效的复用逻辑。

2. 具体实施

1)新增枚举类型和 mstatus 位数,并在 decoder 中识别新指令

根据资料,mstatus 寄存器中与 S 指令有关的是这几位:

1
2
3
4
5
6
mypkg.sv
typedef enum logic [5:0] {
MSTATUS_SIE_BIT = 6'd1,
MSTATUS_SPIE_BIT = 6'd5,
MSTATUS_SPP_BIT = 6'd8,
} mstatus_bit_t;

将它们定义出来,方便后面调用

接着,我们定义抽象化的 xret 枚举类型

1
2
3
4
5
6
mypkg.sv
typedef enum u2 {
XRET_NONE = 2'd0,
XRET_M = 2'd1,
XRET_S = 2'd2
} xret_t;

在 decoder 中,我们去除原本的 is_mret 输出端口,统一替换成 xret_type,确定 ret 类型后打包成 xret_t 类型输出:

1
2
3
4
5
6
7
decoder.sv
-output u1 is_ecall, is_mret,
+output u1 is_ecall,
+output xret_t xret_type,
-assign is_mret = (inst == 32'h30200073);
+assign xret_type = (inst == 32'h30200073) ? XRET_M :
+ (inst == 32'h10200073) ? XRET_S : XRET_NONE;
2)将新增类型一路传递到 mem_wb 寄存器

同样为了满足“精确异常”,需要在 WB 阶段进行判断,因此修改原有的 mret 接口,改成 xret_type 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id_ex.sv
- input u1 is_mret_in,
+ input xret_t xret_type_in,
- output u1 is_mret_out,
+ output xret_t xret_type_out,
always_ff @(posedge clk) begin
if (reset) begin
- is_mret_out <= 1'b0;
+ xret_type_out <= XRET_NONE;
end else if (enable) begin
if (valid_in) begin
- is_mret_out <= is_mret_in;
+ xret_type_out <= xret_type_in;
end else begin
- is_mret_out <= 1'b0;
+ xret_type_out <= XRET_NONE;
end
end
end
3)trap_unit

在这里,我们需要兼容 S 模式的操作:

  • 删去 mret 判断逻辑,改为用 xret 代替,这涉及到 sepc、stvec 的引入,对应到 redirect 信号的相关判断
  • 对于 mcause,可以直接用 trap_cause = {60'b0, 1'b1, 1'b0, current_priv}; 进行接线,因为 MCAUSE 正好等于四种特权级别对应的数值加 8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trap_unit.sv
- input u1 mem_wb_is_mret,
+ input xret_t mem_wb_xret_type,
+ input u64 csr_sepc,
+ input u64 csr_stvec,
- output u1 mret_valid,
+ output u1 xret_valid,

- assign trap_cause = (current_priv == PRIV_U) ? 64'd8 : 64'd11;
+ assign trap_cause = {60'b0, 1'b1, 1'b0, current_priv};

- assign mret_valid = commit_fire && mem_wb_is_mret;
+ assign xret_valid = commit_fire && (mem_wb_xret_type != XRET_NONE);

- assign redirect_valid = trap_valid || mret_valid;
+ assign redirect_valid = trap_valid || xret_valid;

为满足职责分离,并减小性能开销,部分非 M 模式触发的异常应该直接在 S 模式下处理。这涉及到了 medeleg 寄存器,根据要求,当非 M 状态下出现异常,若 medeleg[mcause]=1,则应该在 S 模式下处理,因此需要重新考虑跳转的目标 PC:

1
2
3
4
5
6
trap_unit.sv
input u64 csr_medeleg,
output priv_t trap_target_priv,
assign trap_target_priv = ((current_priv != PRIV_M) && csr_medeleg[trap_cause[5:0]]) ? PRIV_S : PRIV_M;
assign redirect_pc = trap_valid ? ((trap_target_priv == PRIV_S) ? csr_stvec : csr_mtvec) :
((mem_wb_xret_type == XRET_S) ? csr_sepc : csr_mepc);

期中 medeleg 取后四位即可,这里为适配 64 位的寄存器本身,直接截取后六位。效果是一样的

4)完成 S 模式下 CSR 寄存器的操作

同样地,我们先用 xret 取代 mret 的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csr_file.sv
- input u1 mret_commit,
+ input u1 xret_commit,
+ input xret_t xret_type,
- output priv_t mstatus_reg_mpp,
+ output priv_t xret_return_priv,
- assign mstatus_reg_mpp = priv_t'(mstatus_reg[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW]);
+ assign xret_return_priv = (xret_type == XRET_M) ?
+ priv_t'(mstatus_reg[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW]) :
+ (mstatus_reg[MSTATUS_SPP_BIT] ? PRIV_S : PRIV_U);
- if (trap_commit) begin
+ if (trap_commit && (trap_target_priv == PRIV_M)) begin
- end else if (mret_commit) begin
- mstatus[MSTATUS_MIE_BIT] = mstatus_reg[MSTATUS_MPIE_BIT];
- mstatus[MSTATUS_MPIE_BIT] = 1'b1;
- mstatus[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW] = PRIV_U;
- if (mstatus_reg[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW] != PRIV_M) begin
- mstatus[MSTATUS_MPRV_BIT] = 1'b0;
- end

输入以及 if-else 判断逻辑我们略去不提。对于 xret 指令要返回的特权级别,如果是 mret,复用之前的逻辑;如果不是,需要根据 mstatus_reg 中记录 SPP 的位来判断返回至哪个特权级别。

接着,我们处理 S 模式新增逻辑: mstatus 的处理思路与 M 模式完全一致,sepc、scause、stval 直接填写输入的 trap_* 的值。

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
//处理trap
input priv_t trap_target_priv,
if (trap_commit && (trap_target_priv == PRIV_M)) begin
//m模式,不用改动
end else if (trap_commit && (trap_target_priv == PRIV_S)) begin
//完全仿照m模式
sepc = trap_epc;
scause = trap_cause;
stval = trap_tval;
mstatus[MSTATUS_SPIE_BIT] = mstatus_reg[MSTATUS_SIE_BIT];
mstatus[MSTATUS_SIE_BIT] = 1'b0;
mstatus[MSTATUS_SPP_BIT] = (trap_from_priv == PRIV_S);
//处理ret
end else if (xret_commit) begin
unique case (xret_type)
XRET_M: begin
mstatus[MSTATUS_MIE_BIT] = mstatus_reg[MSTATUS_MPIE_BIT];
mstatus[MSTATUS_MPIE_BIT] = 1'b1;
mstatus[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW] = PRIV_U;
if (mstatus_reg[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW] != PRIV_M) begin
mstatus[MSTATUS_MPRV_BIT] = 1'b0;
end
end
XRET_S: begin
mstatus[MSTATUS_SIE_BIT] = mstatus_reg[MSTATUS_SPIE_BIT];
mstatus[MSTATUS_SPIE_BIT] = 1'b1;
mstatus[MSTATUS_SPP_BIT] = 1'b0;
mstatus[MSTATUS_MPRV_BIT] = 1'b0;
end
default: begin
end
endcase
end

针对新增 S 模式,写入寄存器的 mask 也需要对应调整:medeleg 需要暴露 U 和 S 模式的位以供写入;sstatus 需要暴露 SIE、SPIE、SPP 三位以供修改。

1
2
3
4
5
6
7
8
9
10
11
12
csr_file.sv
output u64 medeleg_reg_value,
assign medeleg_reg_value = medeleg_reg;
localparam u64 MEDELEG_WRITABLE_MASK = (64'b1 << 8) | (64'b1 << 9);
localparam u64 SSTATUS_LOCAL_MASK = SSTATUS_MASK |
(64'b1 << MSTATUS_SIE_BIT) |
(64'b1 << MSTATUS_SPIE_BIT) |
(64'b1 << MSTATUS_SPP_BIT);
CSR_SSTATUS: mstatus = (mstatus_reg & ~(SSTATUS_LOCAL_MASK & MSTATUS_MASK)) |
(csr_write_data & SSTATUS_LOCAL_MASK & MSTATUS_MASK);
CSR_MEDELEG: medeleg = csr_write_data & MEDELEG_WRITABLE_MASK;
sstatus = mstatus & SSTATUS_LOCAL_MASK;
5)最后在 core 中进行实例化

略去纯模块化接口。我们需要注意 current_priv_comb 的赋值逻辑,以及 trap_kill_mem 的判断逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
core.sv
always_comb begin
current_priv_comb = current_priv;
if (reset) begin
current_priv_comb = PRIV_M;
end else if (trap_unit_trap_valid) begin
current_priv_comb = trap_unit_trap_target_priv;
end else if (trap_unit_xret_valid) begin
current_priv_comb = csr_xret_return_priv;
end
end

assign trap_kill_mem = mem_wb_valid && (mem_wb_is_trap || (mem_wb_xret_type != XRET_NONE));

三、完成 Bonus 之后的最终输出

在vscode中: 在串口调试助手上: 已通过袁神的测试

X、常见错误:组合环

组合逻辑被视作实时变化,因此一系列组合逻辑一旦形成环,编译时会直接报错。解决方法就是打破这个环,用一个时序逻辑代替它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
verilator --lint-only -sv -UVERILATOR -Wall -Wno-IMPORTSTAR -Wno-STMTDLY -Wno-declfilename -Wno-UNUSED -Wno-EOFNEWLINE vsrc/include/config.sv vsrc/include/common.sv vsrc/include/csr.sv vsrc/src/utils/mypkg.sv vsrc/src/utils/dbus_arbiter.sv vsrc/src/utils/mmu.sv vsrc/src/utils/alu.sv vsrc/src/utils/multiplier.sv vsrc/src/basics/pc.sv vsrc/src/basics/decoder.sv vsrc/src/utils/regfile.sv vsrc/src/utils/csr_file.sv vsrc/src/utils/csr_cal.sv vsrc/src/utils/trap_unit.sv vsrc/src/utils/forward.sv vsrc/src/basics/ex.sv vsrc/src/basics/mem.sv vsrc/src/basics/wb.sv vsrc/src/utils/ctrl.sv vsrc/src/regs/if_id.sv vsrc/src/regs/id_ex.sv vsrc/src/regs/ex_mem.sv vsrc/src/regs/mem_wb.sv vsrc/src/core.sv

%Warning-UNOPTFLAT: vsrc/src/core.sv:78:6: Signal unoptimizable: Circular combinational logic: 'core.csr_file_write_enable'
78 | u1 csr_file_write_enable;
| ^~~~~~~~~~~~~~~~~~~~~
... For warning description see https://verilator.org/warn/UNOPTFLAT?v=5.032
... Use "/* verilator lint_off UNOPTFLAT */" and lint_on around source to disable this message.
vsrc/src/core.sv:78:6: Example path: core.csr_file_write_enable
vsrc/src/utils/csr_file.sv:47:5: Example path: ALWAYS
vsrc/src/core.sv:82:64: Example path: core.csr_satp
vsrc/src/utils/mmu.sv:40:32: Example path: ASSIGNW
vsrc/src/utils/mmu.sv:38:8: Example path: core.u_mmu.translation_enabled
vsrc/src/utils/mmu.sv:57:5: Example path: ALWAYS
vsrc/src/core.sv:50:14: Example path: core.mmu_core_resp
vsrc/src/utils/dbus_arbiter.sv:43:5: Example path: ALWAYS
vsrc/src/core.sv:48:14: Example path: core.mem_dresp
vsrc/src/utils/ctrl.sv:62:26: Example path: ASSIGNW
vsrc/src/core.sv:100:59: Example path: core.ctrl_ex_mem_enable
vsrc/src/core.sv:226:21: Example path: ASSIGNW
vsrc/src/core.sv:194:6: Example path: core.commit_fire
vsrc/src/core.sv:229:31: Example path: ASSIGNW
vsrc/src/core.sv:78:6: Example path: core.csr_file_write_enable
%Error: Exiting due to 1 warning(s)

参考文献

这些资料对于我搞清楚这个 Lab 到底在干什么很有帮助:

  1. https://www.cnblogs.com/LuoboLiam/p/13411709.html
  2. https://foxsen.github.io/archbase/%E6%8C%87%E4%BB%A4%E6%B5%81%E6%B0%B4%E7%BA%BF.html#sec-precise-exception
  3. https://zhuanlan.zhihu.com/p/561136976 感谢大佬!

乘除法

一、Lab 1 背景介绍

在 Lab 1 中,我们完成了基本的加减法运算,在 Bonus 部分,我们将会实现乘除法。为什么乘除法不能像加减法那样在 alu 中用 * 和 / 来简单表示呢?这里我们需要对 CPU 频率和周期数的计算有更深入的了解:

在 CPU 中,每当时钟信号来临时,寄存器的值会被输入到各个模块中进行运算,等待其中的组合逻辑运算完成并将数据存入寄存器之后才能进入下一个时钟周期。这意味着,时钟周期受限于关键路径,即时钟周期必须大于等于两个寄存器之间延迟最长的组合逻辑。对于加法和减法,使用超前进位加法器可以在\(O(lgn)\)时间内完成计算,但乘除法复杂度高很多,强行放在一个时钟周期内完成会导致时钟频率大大下降,严重拖慢 CPU 的运行速率。

为了确保 CPU 的速度,我们需要将乘除法分散在多个周期内完成,也就是本次 Lab 的主题。

二、Lab 1 目标

在 Lab 1 Bonus 中,我们需要完成以下指令:

  1. mul,mulw:
1
mul x3, x1, x2  # 将x1*x2的低64位结果写入x3

这个指令会截断最终结果,只保留后 64 位,相当于对\(2^{64}\)进行取模运算。而 $X_{unsigned} = X_{signed} + 2^{64} $ ,因此 mul 指令对于有/无符号数得到的计算结果完全相同,不需要加以区分。

  1. div, divu, divw,divuw,rem, remu,remw,remuw:
  • div:求商
  • rem:求余数
1
2
3
4
5
6
7
8
9
li x1, 13
li x2, -5
# 有符号操作
div x3, x1, x2 # x3 = 13 / -5 = -2 (向零舍入)
rem x4, x1, x2 # x4 = 13 % -5 = 3 (余数符号同被除数 x1)
# 无符号操作
# 此时 x2 的 -5 会被当作一个极大的无符号正数 (0xFFFFFFFB)
divu x5, x1, x2 # x5 = 13 / 0xFFFFFFFB = 0
remu x6, x1, x2 # x6 = 13 % 0xFFFFFFFB = 13

注:带 w 后缀的是32 位版本的指令,只处理操作数的低 32 位,最后再把32 位结果符号扩展成64 位写回寄存器,divuw 和 remuw 也一样进行符号扩展。

注:先截断操作数是必要操作,尤其在除法中,如果高32 位非空,不进行截断往往会带来错误的结果

  1. 边界条件 除数为0时,根据规定:
  • div/divu/divw/divuw:返回全1(对应有符号数的-1和无符号数的最大值)
  • rem/remu:返回被除数,以满足 被除数 = 除数 × 商 + 余数;remw/remuw:先将被除数截断至 32 位,再进行符号扩展

最小负数/(-1)=最小负数: > 补码中,有符号数的负数比整数多一位,原先结果为正,但超出表示范围,只能溢出,最终结果等于最小负数;余数仍然是0

三、乘法

本lab中,我采用移位乘法,思路非常接近快速幂: > 对于一个x位乘法A*B,进行x轮操作,每轮操作将A左移一位,再将B右移一位,每次根据B的最低位判断是否要加上A,最终汇报结果

这里的乘法器是一个状态机,需要存储当前的状态,后续状态也与当前状态有关。

乘法器共有三种状态:IDLE,BUSY,DONE,分别表示空闲,正在进行运算和运算完成。

  • IDLE 表示乘法器可以接收 start 信号开始计算,将操作数加载进来
  • BUSY 是乘法器正在被占用
  • DONE 用于指示当前拍已经完成计算,可以让下一条指令进来了,下一拍再次变回 IDLE 完成循环。

具体实现如下:

1. 加入新的指令

首先,我们需要先为两条乘法指令在 mypkg 中定义枚举类型:

1
2
3
4
5
6
mypkg.sv
typedef enum logic [1:0] {
MUL_NONE = 2'd0,
MUL_MUL = 2'd1,
MUL_MULW = 2'd2
} mul_t;

2. 接着完成乘法器主体部分

在 utils 中新建 multiplier。作为一个时序逻辑,它需要接受 clk 信号和 reset 信号。同时,decoder 将会提供各类操作信号(start,use_w,rs1_num,rs2_num),其中 start 是新出现的符号,表示乘法器需要开始运算了。最终,乘法器要将内部状态暴露出去,输出 busy 和 done,以及计算结果 result。

1
2
3
4
5
6
7
8
9
10
11
12
multiplier.sv
module multiplier
import common::*;
(
input logic clk,
input logic reset,
input u1 start,use_w,
input u64 rs1_num,rs2_num,

output u1 busy,done,
output u64 result
);

接着定义内部状态 state,acc 表示accumulator,用于存放中间运算结果,multiplicand 存放被乘数rs1,multiplier_num 存放乘数rs2。count 用于计数,round则是循环上限。busy 和 done 信号通过内部状态导出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
multiplier.sv
typedef enum logic [1:0] {
MUL_IDLE = 2'd0,
MUL_BUSY = 2'd1,
MUL_DONE = 2'd2
} state_t;

state_t state;
u7 count, rounds;
u64 acc, multiplicand, multiplier_num;
u64 addend, acc_next;

assign busy = (state == MUL_BUSY);
assign done = (state == MUL_DONE);

然后是主体循环部分,假设每个时钟上升沿到来进行一轮运算,利用 count 进行计数。addend 是每轮乘法的加数,根据当前乘数尾数决定是否增加被乘数,acc_next 是下一步计算结果,利用历史结果 acc 加上 addend 得到。

之后根据内部状态决定如何运行:start 会让 IDLE 转为 BUSY,并加载数据;BUSY 一共round-1轮,每次将 acc 更新为 acc_next,改变乘数、被乘数;当进行round-1轮之后,更新状态为 DONE,同时直接将 acc_next 的数据拿出来交给结果;一轮之后将 DONE 还原为 IDLE。

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
multiplier.sv
assign addend = multiplier_num[0] ? multiplicand : 64'b0;
assign acc_next = acc + addend;

always_ff @(posedge clk) begin
if (reset) begin
state <= MUL_IDLE;
count <= 7'b0;
rounds <= 7'b0;
acc <= 64'b0;
multiplicand <= 64'b0;
multiplier_num <= 64'b0;
result <= 64'b0;
end else begin
case (state)
MUL_IDLE: begin
if (start) begin
state <= MUL_BUSY;
count <= 7'b0;
rounds <= use_w ? 7'd32 : 7'd64;
acc <= 64'b0;
multiplicand <= use_w ? {32'b0, rs1_num[31:0]} : rs1_num;
multiplier_num <= use_w ? {32'b0, rs2_num[31:0]} : rs2_num;
end
end
MUL_BUSY: begin
acc <= acc_next;
multiplicand <= multiplicand << 1;
multiplier_num <= multiplier_num >> 1;
count <= count + 7'd1;

if (count == (rounds - 7'd1)) begin
state <= MUL_DONE;
result <= use_w ? {{32{acc_next[31]}}, acc_next[31:0]} : acc_next;
end
end
MUL_DONE: begin
state <= MUL_IDLE;
end
default: begin
state <= MUL_IDLE;
end
endcase
end
end

3. 在 decoder 中输出对应信号

乘法信号 is_mul 需要额外根据 funct7 和 funct3 判断,以与普通的寄存器操作区分开

1
2
decoder.sv
assign is_mul = ((is_alu_r || is_alu_rw) && (funct7 == 7'b0000001) && (funct3 == 3'b000));

并在下面判断 mul_op

1
2
3
4
5
6
7
8
9
10
11
12
13
decoder.sv
else if (is_alu_r) begin
if (funct7 == 7'b0000001) begin
case(funct3)
3'b000: mul_op = MUL_MUL;
default: op = ALU_WRONG;
endcase
else if (is_alu_rw) begin
if (funct7 == 7'b0000001) begin
case(funct3)
3'b000: mul_op = MUL_MULW;
default: op = ALU_WRONG;
endcase

4. 将 is_mul 和 mul_op 从 id_ex 传递给 ex

代码整体比较常规,这里掠过 需要注意 bubble 时要将 is_mul 清零,防止意外启动乘法器

5. 将 multiplier 包装进 ex 模块

ex 模块需要额外接收 clk 和 reset 信号,decoder 解析出来的 mul_op,is_mul 信号转交给 multiplier,并将乘法器状态 mul_done 向外输出。

1
2
3
4
5
6
ex.sv
input logic clk,
input u1 reset,
input mul_t mul_op,
input u1 is_mul,
output u1 mul_done,

在内部,需要将普通 alu 信号与乘法器结果区分开,于是将原先的 alu_result 重命名为 alu_result_normal;定义 mul_busy 导出乘法器状态,mul_use_w 用于指导乘法器是否进行32 位计算;定义 mul_start 用于指导 multiplier 何时开始计算

1
2
ex.sv
mul_start = is_mul && ~mul_busy;

这里只需要判断是乘法指令,且当前乘法器不在运行即可,DONE 状态根本不理会 start 信号 最终根据当前指令是否是乘法指令来选择 ex 输出

1
2
ex.sv
assign alu_result = is_mul ? mul_result : alu_result_normal;

6. 完成乘法器的流水线控制

乘法器在计算时,ex 阶段不能流入新的指令,因此 pc,if_id,id_ex 全都要 stall;而 ex_mem 需要塞气泡,代表这是无效指令。待乘法器由 BUSY 转为 DONE 阶段时,stall 取消,让新的指令流进来;bubble 取消,让乘法器的结果流出去。 而当一条乘法指令进来时,乘法器从 IDLE 转为 BUSY,这一拍 ex_mem 会根据 mul_result 将答案传出去,导致错误。为了防止这一行为,我们应该在识别出当前指令是乘法指令时就进行拦截,且这一拦截优先级高于 DONE 信号的判断

1
2
ctrl.sv
assign mul_stall = ex_is_mul && ~ex_mul_done;

接着把这个信号接入:

1
2
3
4
ctrl.sv
assign pc_enable = global_enable && ~load_use_stall && ~ibus_stall && ~mul_stall;
assign if_id_enable = global_enable && ~load_use_stall && ~mul_stall;
assign id_ex_enable = global_enable && ~mul_stall;

最后是 ex_mem 专属的 flush:

1
2
ctrl.sv
assign ex_mem_flush = global_enable && mul_stall;

7. 在 core 中完成接线和实例化

这次接线比较清晰,略去代码部分

四、除法

整体思路可以复用,具体实施过程中采用移位除法,复用乘法的 stall 逻辑。

1. 在 mypkg 中定义新的除法枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
mypkg.sv
typedef enum logic [3:0] {
DIV_NONE = 4'd0,
DIV_DIV = 4'd1,
DIV_DIVU = 4'd2,
DIV_REM = 4'd3,
DIV_REMU = 4'd4,
DIV_DIVW = 4'd5,
DIV_DIVUW = 4'd6,
DIV_REMW = 4'd7,
DIV_REMUW = 4'd8
} div_t;

2. 完成除法模块 divide

为了复用除法模块,我们的目标是完成一个只负责无符号运算,兼容32 位与64 位运算及除零的 divide 模块。有符号运算会先在 ex 中进行预处理,这里按下不表。

1)核心逻辑与伪代码:从被除数的高位向低位依次处理。若余数大于除数,则减去除数,商对应位数上变成1。

1
2
3
4
5
6
7
8
rem = 0
quot = dividend
for i in range(rounds):
rem = {rem[62:0], quot[63]}
quot = {quot[62:0], 1'b0}
if rem >= divisor:
rem = rem - divisor
quot[i] = 1

2)代码如下:

初始化(reset 略去了):
  • 根据是否是32 位运算决定最多进行多少轮移位
  • 看情况将除数、被除数截取到32 位
  • rounds 表示轮数,count 表示进行了几轮
  • 如果被除数是0,直接进入边界处理(结束计算、商全1、余数为被除数)
  • 否则进入 BUSY 模式开始计算:
  • 32 位运算时,将被除数低 32 位放在商的高32 位,后续全部为0,而不是低 32 位。

这样做是合理的:因为32 位运算只进行32轮,因此从高到低的32 位内必须把除法解决掉

  • 余数为0
  • 定义 use_w,divisor 的reg寄存器,用于保存输入内容;定义 remainder 的reg寄存器,用于保存中间结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
divide.sv
assign normalized_dividend = use_w ? {32'b0, dividend[31:0]} : dividend;
assign normalized_divisor = use_w ? {32'b0, divisor[31:0]} : divisor;
DIV_IDLE: begin
if (start) begin
count <= 7'b0;
rounds <= use_w ? 7'd32 : 7'd64;
use_w_reg <= use_w;
divisor_reg <= normalized_divisor;
remainder_reg <= 65'b0;
if (normalized_divisor == 64'b0) begin
state <= DIV_DONE;
quotient <= 64'hffff_ffff_ffff_ffff;
remainder <= normalized_dividend;
end else begin
state <= DIV_BUSY;
quotient <= use_w ? {dividend[31:0], 32'b0} : dividend;
remainder <= 64'b0;
end
end
end
运行:
  • 商左移1位,接上0;商的最高位接到余数后面
  • 若商大于余数:减去余数,商最后一位为1
  • remainder_shift 需要在常规 remainder 的基础上额外继承商的一位,因此定义成65位寄存器。由循环不变量知 remainder[65]=1不会造成溢出:
  • 上一轮结束后 remainder_reg < divisor。开始时 remainder_reg 是0,因此开始时成立。
  • 本轮:remainder_shift = remainder_reg * 2 + next_bit,所以 remainder_shift < 2 * divisor
  • 当 remainder_shift >= divisor 时:remainder_next = remainder_shift - divisor。因此 remainder_next < divisor。因此 remainder_next 一定能放进64 位
  • remainder_next 和 divisor_ext 需要参与 remainder_shift 的运算,所以都定义成65位寄存器
  • 如果只剩1轮,下次直接变回 DIV_DONE,防止多一轮等待
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
divide.sv
assign remainder_shift = {remainder_reg[63:0], quotient[63]};
assign quotient_shift = {quotient[62:0], 1'b0};
assign divisor_ext = {1'b0, divisor_reg};

always_comb begin
if (remainder_shift >= divisor_ext) begin
remainder_next = remainder_shift - divisor_ext;
quotient_next = quotient_shift | 64'd1;
end else begin
remainder_next = remainder_shift;
quotient_next = quotient_shift;
end
end

DIV_BUSY: begin
count <= count + 7'd1;
quotient <= quotient_next;
remainder_reg <= remainder_next;

if (count == (rounds - 7'd1)) begin
state <= DIV_DONE;
quotient <= quotient_result;
remainder <= remainder_result;
end
end
状态接手
  • 向外展示除法器状态
  • 如果使用32 位,则商和余数都截取低 32 位,高位全部为0
1
2
3
4
5
divide.sv
assign busy = (state == DIV_BUSY);
assign done = (state == DIV_DONE);
assign quotient_result = use_w_reg ? {32'b0, quotient_next[31:0]} : quotient_next;
assign remainder_result = use_w_reg ? {32'b0, remainder_next[31:0]} : remainder_next[63:0];

3. 在 ex 阶段处理除法器相关内容

divide 模块只负责无符号除法,所以在 ex 阶段需要先把RISC-V除法指令转换成无符号除法器能处理的形式。这里主要完成四件事:

  • 根据 div_op 判断是否是32 位运算、是否是有符号运算、最终返回商还是余数
  • 对有符号除法先取绝对值,送入无符号 divide
  • 处理除0和最小负数除以-1这两个边界条件
  • 在多周期过程中锁存修正结果需要的信息,避免 stall 期间前递数据变化影响最终结果

首先在 ex 模块端口中加入除法控制信号,并定义除法器需要的中间变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ex.sv
input div_t div_op,
input u1 is_mul, is_div, mem_read, mem_write,

output u1 mul_done, div_done,

u64 alu_result_normal,mul_result,div_result;
u64 div_dividend,div_divisor,div_quotient,div_remainder;
u64 div_quotient_signed,div_remainder_signed,div_result_pre;
u64 div_zero_quotient,div_zero_remainder,div_overflow_quotient;
u64 div_original_dividend_reg;
u1 div_busy,div_start,div_use_w,div_is_signed,div_is_rem;
u1 div_rs1_sign,div_rs2_sign,div_quotient_neg;
u1 div_by_zero,div_overflow;
u1 div_use_w_reg,div_is_rem_reg,div_rs1_sign_reg,div_quotient_neg_reg;
u1 div_by_zero_reg,div_overflow_reg;

随后根据 div_op 拆分指令语义。div_use_w 决定除法器执行32轮还是64轮;div_is_signed 决定是否需要对操作数取绝对值;div_is_rem 决定最终选择商还是余数。对于有符号除法,送入 divide 前需要将负数转为正数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ex.sv
assign div_start = is_div && ~div_busy;
assign div_use_w = (div_op == DIV_DIVW) || (div_op == DIV_DIVUW) ||
(div_op == DIV_REMW) || (div_op == DIV_REMUW);
assign div_is_signed = (div_op == DIV_DIV) || (div_op == DIV_REM) ||
(div_op == DIV_DIVW) || (div_op == DIV_REMW);
assign div_is_rem = (div_op == DIV_REM) || (div_op == DIV_REMU) ||
(div_op == DIV_REMW) || (div_op == DIV_REMUW);

assign div_rs1_sign = div_is_signed && (div_use_w ? forward_rs1_val[31] : forward_rs1_val[63]);
assign div_rs2_sign = div_is_signed && (div_use_w ? forward_rs2_val[31] : forward_rs2_val[63]);
assign div_quotient_neg = div_rs1_sign ^ div_rs2_sign;

assign div_dividend = div_is_signed ?
(div_use_w ?
{32'b0, (div_rs1_sign ? (~forward_rs1_val[31:0] + 32'd1) : forward_rs1_val[31:0])} :
(div_rs1_sign ? (~forward_rs1_val + 64'd1) : forward_rs1_val)) :
(div_use_w ? {32'b0, forward_rs1_val[31:0]} : forward_rs1_val);
assign div_divisor = div_is_signed ?
(div_use_w ?
{32'b0, (div_rs2_sign ? (~forward_rs2_val[31:0] + 32'd1) : forward_rs2_val[31:0])} :
(div_rs2_sign ? (~forward_rs2_val + 64'd1) : forward_rs2_val)) :
(div_use_w ? {32'b0, forward_rs2_val[31:0]} : forward_rs2_val);

除法的边界条件也在 ex 中统一处理。除数为0时,商为全1,余数为原被除数;有符号最小负数除以-1时,商保持最小负数,余数为0。由于除法器会运行多个周期,所以这些信息在 div_start 时锁存下来,保证 DONE 周期使用的仍然是同一条除法指令的属性。

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
ex.sv
assign div_by_zero = div_use_w ? (forward_rs2_val[31:0] == 32'b0) : (forward_rs2_val == 64'b0);
assign div_overflow = div_is_signed &&
((div_use_w && (forward_rs1_val[31:0] == 32'h8000_0000) &&
(forward_rs2_val[31:0] == 32'hffff_ffff)) ||
(!div_use_w && (forward_rs1_val == 64'h8000_0000_0000_0000) &&
(forward_rs2_val == 64'hffff_ffff_ffff_ffff)));

always_ff @(posedge clk) begin
if (reset) begin
div_original_dividend_reg <= 64'b0;
div_use_w_reg <= 1'b0;
div_is_rem_reg <= 1'b0;
div_rs1_sign_reg <= 1'b0;
div_quotient_neg_reg <= 1'b0;
div_by_zero_reg <= 1'b0;
div_overflow_reg <= 1'b0;
end else if (div_start) begin
div_original_dividend_reg <= forward_rs1_val;
div_use_w_reg <= div_use_w;
div_is_rem_reg <= div_is_rem;
div_rs1_sign_reg <= div_rs1_sign;
div_quotient_neg_reg <= div_quotient_neg;
div_by_zero_reg <= div_by_zero;
div_overflow_reg <= div_overflow;
end
end

当 divide 给出无符号商和余数后,ex 再根据前面锁存的信息修正符号并选择最终结果。对于所有w后缀指令,最终都取低 32 位并符号扩展到64 位。

1
2
3
4
5
6
7
8
9
10
ex.sv
assign div_quotient_signed = div_quotient_neg_reg ? (~div_quotient + 64'd1) : div_quotient;
assign div_remainder_signed = div_rs1_sign_reg ? (~div_remainder + 64'd1) : div_remainder;
assign div_zero_quotient = div_use_w_reg ? {32'b0, 32'hffff_ffff} : 64'hffff_ffff_ffff_ffff;
assign div_zero_remainder = div_use_w_reg ? {32'b0, div_original_dividend_reg[31:0]} : div_original_dividend_reg;
assign div_overflow_quotient = div_use_w_reg ? {32'b0, 32'h8000_0000} : 64'h8000_0000_0000_0000;
assign div_result_pre = div_by_zero_reg ? (div_is_rem_reg ? div_zero_remainder : div_zero_quotient) :
div_overflow_reg ? (div_is_rem_reg ? 64'b0 : div_overflow_quotient) :
div_is_rem_reg ? div_remainder_signed : div_quotient_signed;
assign div_result = div_use_w_reg ? {{32{div_result_pre[31]}}, div_result_pre[31:0]} : div_result_pre;

最后实例化 divide 模块,并在 ex 阶段输出结果时让除法优先于乘法和普通 ALU 结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ex.sv
divide u_divide(
.clk (clk),
.reset (reset),
.start (div_start),
.use_w (div_use_w),
.dividend (div_dividend),
.divisor (div_divisor),
.busy (div_busy),
.done (div_done),
.quotient (div_quotient),
.remainder (div_remainder)
);

assign alu_result = is_div ? div_result :
is_mul ? mul_result : alu_result_normal;

4. ctrl 阶段复用乘法器逻辑

除法和乘法一样是多周期执行单元,所以流水线控制可以直接复用原先乘法的 stall 策略:当 EX 阶段存在除法指令且 div_done 还没有拉高时,冻结 PC/IF_ID/ID_EX,同时向 EX/MEM 注入气泡;等 DONE 周期到来后,stall 取消,除法结果进入后级。

为了避免为乘法和除法分别写两套控制,ctrl 中将它们合并成 multi_cycle_stall:

1
2
3
4
5
6
7
ctrl.sv
input u1 ex_mem_read, should_jump, commit_flush, ex_is_mul, ex_mul_done, ex_is_div, ex_div_done,

logic mul_stall, div_stall, multi_cycle_stall;
assign mul_stall = ex_is_mul && ~ex_mul_done;
assign div_stall = ex_is_div && ~ex_div_done;
assign multi_cycle_stall = mul_stall || div_stall;

之后原先使用 mul_stall 的位置统一改为 multi_cycle_stall。这样乘法和除法都遵循相同的流水线行为。

1
2
3
4
5
6
ctrl.sv
assign pc_enable = global_enable && ~load_use_stall && ~ibus_stall && ~multi_cycle_stall;
assign if_id_enable = global_enable && ~load_use_stall && ~multi_cycle_stall;
assign id_ex_enable = global_enable && ~multi_cycle_stall;

assign ex_mem_flush = (global_enable && commit_flush) || (global_enable && multi_cycle_stall);

5. 信号传递复用乘法器传递逻辑

除法信号的传递链路和乘法保持一致:decoder 产生 is_div/div_op,id_ex 寄存这两个信号,最后在 core 中分别送到 ex 执行单元和 ctrl 控制单元。

首先在 decoder 中增加除法输出信号。is_div 只识别 funct7=0000001 且 funct3 为 100/101/110/111 的M扩展指令,避免和普通寄存器 ALU 指令混淆。

1
2
3
4
5
6
7
8
9
10
11
12
decoder.sv
output u1 is_mul,
output u1 is_div,
output u1 is_ecall,
output u1 is_valid_inst,
output xret_t xret_type,
output mul_t mul_op,
output div_t div_op

assign is_div = ((is_alu_r || is_alu_rw) && (funct7 == 7'b0000001) &&
((funct3 == 3'b100) || (funct3 == 3'b101) ||
(funct3 == 3'b110) || (funct3 == 3'b111)));

合法指令判断中也要放开除法和取余对应的 funct3,否则这些指令会被非法指令异常截断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
decoder.sv
7'b0110011: begin // register ALU
case (funct7)
7'b0000001: is_valid_inst = (funct3 == 3'b000) ||
(funct3 == 3'b100) ||
(funct3 == 3'b101) ||
(funct3 == 3'b110) ||
(funct3 == 3'b111); // mul/div/rem
endcase
end

7'b0111011: begin // register word ALU
case (funct7)
7'b0000001: is_valid_inst = (funct3 == 3'b000) ||
(funct3 == 3'b100) ||
(funct3 == 3'b101) ||
(funct3 == 3'b110) ||
(funct3 == 3'b111); // mulw/divw/remw
endcase
end

随后根据 funct3 生成具体的 div_op。普通64 位指令和w后缀指令使用同一套 funct3 编码,只是枚举值不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
decoder.sv
else if (is_alu_r) begin
if (funct7 == 7'b0000001) begin
case(funct3)
3'b100: div_op = DIV_DIV;
3'b101: div_op = DIV_DIVU;
3'b110: div_op = DIV_REM;
3'b111: div_op = DIV_REMU;
endcase
end
end

else if (is_alu_rw) begin
if (funct7 == 7'b0000001) begin
case(funct3)
3'b100: div_op = DIV_DIVW;
3'b101: div_op = DIV_DIVUW;
3'b110: div_op = DIV_REMW;
3'b111: div_op = DIV_REMUW;
endcase
end
end

id_ex 中加入 is_div/div_op 输入输出。和 is_mul 一样,is_div 属于控制信号,只有 valid_in 有效时才允许传递;bubble 时必须清零,防止无效指令误启动除法器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id_ex.sv
input u1 is_mul_in, is_div_in,
input mul_t mul_op_in,
input div_t div_op_in,

output u1 is_mul_out, is_div_out,
output mul_t mul_op_out,
output div_t div_op_out

always_ff @(posedge clk) begin
if (reset) begin
is_div_out <= 1'b0;
div_op_out <= DIV_NONE;
end else if (enable) begin
div_op_out <= div_op_in;

if (valid_in) begin
is_div_out <= is_div_in;
end else begin
is_div_out <= 1'b0;
end
end
end

在 core 中先声明 decoder 侧和 ID/EX 侧的除法信号,再把 decoder 实例、ID/EX 实例、EX 实例和 ctrl 实例串起来。

1
2
3
4
5
6
7
8
core.sv
u1 decoder_is_mul, decoder_is_div;
mul_t decoder_mul_op;
div_t decoder_div_op;

u1 id_ex_is_mul, id_ex_is_div;
mul_t id_ex_mul_op;
div_t id_ex_div_op;

decoder 实例输出 decoder_is_div/decoder_div_op:

1
2
3
4
5
6
7
core.sv
decoder u_decoder(
.is_mul (decoder_is_mul),
.is_div (decoder_is_div),
.mul_op (decoder_mul_op),
.div_op (decoder_div_op)
);

ID/EX 实例负责把 decoder 阶段的除法控制信号送到 EX 阶段。异常指令不应该启动除法器,所以 is_div_in 和 is_mul_in 一样要与 ~id_exc_valid 相与。

1
2
3
4
5
6
7
8
9
10
11
12
core.sv
id_ex u_id_ex(
.is_mul_in (decoder_is_mul && ~id_exc_valid),
.is_div_in (decoder_is_div && ~id_exc_valid),
.mul_op_in (decoder_mul_op),
.div_op_in (decoder_div_op),

.is_mul_out (id_ex_is_mul),
.is_div_out (id_ex_is_div),
.mul_op_out (id_ex_mul_op),
.div_op_out (id_ex_div_op)
);

最后,id_ex_is_div/id_ex_div_op 送入 ex 真正启动除法器,ex_div_done 再回到 ctrl 参与多周期 stall 判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
core.sv
ctrl u_ctrl(
.ex_is_mul (id_ex_is_mul),
.ex_mul_done (ex_mul_done),
.ex_is_div (id_ex_is_div),
.ex_div_done (ex_div_done)
);

ex u_ex(
.mul_op (id_ex_mul_op),
.div_op (id_ex_div_op),
.is_mul (id_ex_is_mul),
.is_div (id_ex_is_div),
.mul_done (ex_mul_done),
.div_done (ex_div_done)
);

五、通过截图

思考时钟中断为什么使用 MMIO 计时器

因为现代的 CPU 往往是多核的,如果将计时器内置于 CPU 内部,多核之间的时间难以同步。相对地,使用 MMIO 计时器可以在外部设定一个全局统一的时间模块,保证整个系统在时间上的一致性。 同时,CPU 一旦因为省电而进入休眠状态会停止运行。如果 mtime 是 CPU 内部的 CSR 寄存器,它会停止计数,导致系统无法记录时间。相对地,MMIO 寄存器不会因为 CPU 的断电停止计时。 并且,如果将计时器放在 CPU 内部,会导致频繁的寄存器值比较以及冲刷流水线,降低 CPU 的运行效率。

而 mcycle 记录的是 CPU 的运行周期,与 CPU 的当前运行频率有关,无法衡量真实世界的时间,因此无法使用 mycycle 进行比较。

PMP

一、PMP 的背景原理

1. 组成

PMP 分成 pmpcfg 和 pmpaddr。根据 csr_file 的内容可知,本 Lab 要求实现的是单 pmpcfg + 单 pmpaddr 的组合。

2. pmpaddr

是地址右移 2 位后的值。实际处理过程中需要左移两位。

3. pmpcfg

单个 PMP 条目(entry)的 pmpcfg 一共 8 位,在本实现中只使用 pmpcfg0[7:0],也就是 entry 0。分别代表以下含义:

位 名称 含义
0 R 允许读
1 W 允许写
2 X 允许取指执行
4:3 A 地址匹配模式
7 L 锁定位

其中,A 位最为重要,它决定了 CPU 应该如何处理 pmpaddr 中的值。根据 pmpcfg[4:3] 的值,一共分成 4 类:

1)A=0

表示不启用 PMP 表项,不匹配任何地址

2)A=1

表示Top of range:

  • 若当前 PMP 不是第0个,匹配 \((pmpaddr_{i-1} << 2) \le y < (pmpaddr_i << 2)\)
  • 若当前 PMP 是第0个,匹配 \(0 \le y < (pmpaddr_0 << 2)\)

3)A=2

表示Naturally aligned four-byte region,表示从 \((pmpaddr << 2)\) 开始的4字节(包含)

4)A=3

表示Naturally aligned power-of-two region,\(\ge\) 8 bytes 这时会从低位开始寻找连续1的个数,设找到n个,则表示从 (pmpaddr & ~((1 << x) - 1)) << 2 开始的\(2^{n+3}\)个字节

二、PMP 的具体实施

1. pmp_checker

这个模块用于判断地址是否匹配,输入物理地址、访问大小、访问类型、当前特权级和 PMP 配置,输出本次访问是否允许以及对应的 access fault cause。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pmp_checker.sv
module pmp_checker
import common::*;
import my_pkg::*;
(
input u64 paddr,
input msize_t access_size,
input mmu_access_t access_type,
input priv_t current_priv,
input u64 pmpcfg0, pmpaddr0,

output u1 allow,
output u64 fault_cause
);

pmpcfg0[7:0] 被拆分成权限位、地址匹配模式和锁定位。PMP 拒绝访问时按照原始访问类型产生 access fault:取指为 1,load 为 5,store 为 7。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pmp_checker.sv
always_comb begin
unique case (access_type)
MMU_REQ_INST: access_fault_cause = 64'd1;
MMU_REQ_LOAD: access_fault_cause = 64'd5;
MMU_REQ_STORE: access_fault_cause = 64'd7;
default: access_fault_cause = 64'd5;
endcase
end

assign cfg0 = pmpcfg0[7:0];
assign pmp_r = cfg0[0];
assign pmp_w = cfg0[1];
assign pmp_x = cfg0[2];
assign pmp_a = cfg0[4:3];
assign pmp_l = cfg0[7];

定义辅助函数msize_to_bytes,将枚举类型对应回具体数值

1
2
3
4
5
6
7
8
9
10
11
12
pmp_checker.sv
function automatic u64 msize_to_bytes(input msize_t size);
begin
unique case (size)
MSIZE1: msize_to_bytes = 64'd1;
MSIZE2: msize_to_bytes = 64'd2;
MSIZE4: msize_to_bytes = 64'd4;
MSIZE8: msize_to_bytes = 64'd8;
default: msize_to_bytes = 64'd8;
endcase
end
endfunction

定义辅助函数count_low_ones,检查从最后一位开始的连续1的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pmp_checker.sv
function automatic u7 count_low_ones(input u64 value);
u7 count;
u1 done;
begin
count = 7'b0;
done = 1'b0;
for (int unsigned i = 0; i < 64; i++) begin
if (!done) begin
if (value[i]) begin
count = count + 7'd1;
end else begin
done = 1'b1;
end
end
end
count_low_ones = count;
end
endfunction

根据 A 位的值具体对应规则。需要注意边界条件:

  • TOR时pmpaddr不能为0。当pmpaddr=0时,对应[0,0),区间为空。但由于数值溢出的原因,计算时反而会匹配[0,0xffff_ffff_ffff_ffff),导致错误
  • 当pmpaddr全为1时,会左移64 位,属于不规范操作。因此单独处理
  • 连续1的个数>=61位时,左移位数>=64,同样单独处理,解释为匹配所有地址
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
pmp_checker.sv
always_comb begin
napot_ones = count_low_ones(pmpaddr0);
napot_shift = napot_ones + 7'd3;
napot_mask = 64'b0;
napot_bytes = 64'b0;
range_start = 64'b0;
range_end = 64'b0;
range_valid = 1'b0;

unique case (pmp_a)
2'b00: begin
range_valid = 1'b0;
end
2'b01: begin
range_start = 64'b0;
range_end = (pmpaddr0 << 2) - 64'd1;
range_valid = (pmpaddr0 != 64'b0);
end
2'b10: begin
range_start = pmpaddr0 << 2;
range_end = (pmpaddr0 << 2) + 64'd3;
range_valid = 1'b1;
end
2'b11: begin
if (napot_ones >= 7'd64) begin
napot_mask = {64{1'b1}};
end else begin
napot_mask = (64'b1 << napot_ones) - 64'd1;
end

if (napot_shift >= 7'd64) begin
range_start = 64'b0;
range_end = {64{1'b1}};
end else begin
napot_bytes = 64'b1 << napot_shift;
range_start = (pmpaddr0 & ~napot_mask) << 2;
range_end = range_start + napot_bytes - 64'd1;
end
range_valid = 1'b1;
end
default: begin
range_valid = 1'b0;
end
endcase
end

权限判断遵循 M/S/U 的差异:L=0 时 M 模式不受限制;S/U 模式必须匹配 entry 并满足对应 R/W/X 权限;未匹配时 S/U 拒绝、M 模式允许。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pmp_checker.sv
always_comb begin
unique case (access_type)
MMU_REQ_INST: perm_allow = pmp_x;
MMU_REQ_LOAD: perm_allow = pmp_r;
MMU_REQ_STORE: perm_allow = pmp_w;
default: perm_allow = 1'b0;
endcase
end

always_comb begin
if ((current_priv == PRIV_M) && !pmp_l) begin
allow = 1'b1;
end else if (!addr_match) begin
allow = (current_priv == PRIV_M);
end else begin
allow = perm_allow;
end
end

assign fault_cause = allow ? 64'b0 : access_fault_cause;

为了避免只检查起始地址导致跨界访问漏判,checker 先根据访存大小计算 [paddr, access_last]。只有完整访问范围落入 PMP 区间,才认为地址匹配。

1
2
3
4
pmp_checker.sv
assign access_bytes = msize_to_bytes(access_size);
assign access_last = paddr + access_bytes - 64'd1;
assign access_overflow = access_last < paddr;

最终判断式如下

1
2
3
4
5
pmp_checker.sv
assign addr_match = range_valid &&
!access_overflow &&
(paddr >= range_start) &&
(access_last <= range_end);

2. csr_file

CSR 文件把 pmpcfg0 和 pmpaddr0 作为输出暴露给顶层,并实现 L 位锁定语义。锁定后对 pmpcfg0 和 pmpaddr0 的后续写入都会被忽略,直到 reset。

1
2
3
csr_file.sv
output u64 stvec, sscratch, sepc, scause, stval, medeleg, mideleg,
output u64 pmpcfg0, pmpaddr0

由于只实现 pmpcfg0,因此截取低8位,且5、6位保持为0

1
2
3
4
5
6
7
8
9
csr_file.sv
function automatic u64 legalize_pmpcfg0(input u64 value);
begin
legalize_pmpcfg0 = {56'b0, value[7], 2'b0, value[4:0]};
end
endfunction

pmpcfg0_reg <= (csr_write_enable && (csr_write_addr == CSR_PMPCFG0) && !pmpcfg0_reg[7]) ?legalize_pmpcfg0(csr_write_data) : pmpcfg0_reg;
pmpaddr0_reg <= (csr_write_enable && (csr_write_addr == CSR_PMPADDR0) && !pmpcfg0_reg[7]) ?csr_write_data : pmpaddr0_reg;

3. core

就是普通的连线:加入 pmp_checker include;声明从 CSR 文件接出的 PMP 配置线;把配置线分别接到 csr_file 和 MMU。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
core.sv
`include "src/utils/pmp_checker.sv"
u64 csr_pmpcfg0, csr_pmpaddr0;
csr_file u_csr_file(
.mideleg (csr_mideleg),
.pmpcfg0 (csr_pmpcfg0),
.pmpaddr0 (csr_pmpaddr0)
);

MMU u_mmu(
.current_priv (current_priv),
.satp (csr_satp),
.pmpcfg0 (csr_pmpcfg0),
.pmpaddr0 (csr_pmpaddr0),
.core_req (mmu_core_req)
);

4. MMU

每一次访存请求都需要经过 pmp_checker 的判断,如果判断地址不可用,直接报错。 具体实施如下:

mmu.sv 的接口新增 pmpcfg0/pmpaddr0 输入,并新增 PMP 检查结果与 access fault cause 信号。saved_access_fault_cause 用于页表 walk 被 PMP 拒绝时按原始访问类型报告 access fault。

1
2
3
4
5
6
7
8
mmu.sv
input u64 pmpcfg0, pmpaddr0,

u64 request_fault_cause, saved_fault_cause, saved_access_fault_cause;
u64 req_pmp_fault_cause, pte_pmp_fault_cause, l2_pmp_fault_cause, l1_pmp_fault_cause, l0_pmp_fault_cause;
u1 req_pmp_allow, pte_pmp_allow, l2_pmp_allow, l1_pmp_allow, l0_pmp_allow;

`UNUSED_OK({pte_pmp_fault_cause})

mmu.sv 中一共例化五个 pmp_checker:原始请求、页表 PTE 读取、以及 L2/L1/L0 三级页表叶子命中后的最终物理地址检查。

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
mmu.sv
pmp_checker u_req_pmp_checker(
.paddr (core_req.addr),
.access_size (core_req.size),
.access_type (req_access_type),
.current_priv (current_priv),
.pmpcfg0 (pmpcfg0),
.pmpaddr0 (pmpaddr0),

.allow (req_pmp_allow),
.fault_cause (req_pmp_fault_cause)
);

pmp_checker u_pte_pmp_checker(
.paddr (pte_addr),
.access_size (MSIZE8),
.access_type (MMU_REQ_LOAD),
.current_priv (saved_priv),
.pmpcfg0 (pmpcfg0),
.pmpaddr0 (pmpaddr0),

.allow (pte_pmp_allow),
.fault_cause (pte_pmp_fault_cause)
);

pmp_checker u_l2_pmp_checker(
.paddr (l2_paddr),
.access_size (saved_req.size),
.access_type (saved_access_type),
.current_priv (saved_priv),
.pmpcfg0 (pmpcfg0),
.pmpaddr0 (pmpaddr0),

.allow (l2_pmp_allow),
.fault_cause (l2_pmp_fault_cause)
);

pmp_checker u_l1_pmp_checker(
.paddr (l1_paddr),
.access_size (saved_req.size),
.access_type (saved_access_type),
.current_priv (saved_priv),
.pmpcfg0 (pmpcfg0),
.pmpaddr0 (pmpaddr0),

.allow (l1_pmp_allow),
.fault_cause (l1_pmp_fault_cause)
);

pmp_checker u_l0_pmp_checker(
.paddr (l0_paddr),
.access_size (saved_req.size),
.access_type (saved_access_type),
.current_priv (saved_priv),
.pmpcfg0 (pmpcfg0),
.pmpaddr0 (pmpaddr0),

.allow (l0_pmp_allow),
.fault_cause (l0_pmp_fault_cause)
);

saved_access_fault_cause 用于把页表 walk 期间的 PMP 拒绝转换成原始取指、load、store 对应的 access fault。

1
2
3
4
5
6
7
8
9
mmu.sv
always_comb begin
unique case (saved_access_type)
MMU_REQ_INST: saved_access_fault_cause = 64'd1;
MMU_REQ_LOAD: saved_access_fault_cause = 64'd5;
MMU_REQ_STORE: saved_access_fault_cause = 64'd7;
default: saved_access_fault_cause = 64'd5;
endcase
end

页表 walk 发出 PTE 读取请求前增加 pte_pmp_allow 门控,避免在 PMP 已拒绝的情况下继续访问总线。

1
2
3
4
5
6
mmu.sv
MMU_L2, MMU_L1, MMU_L0: begin
if (pte_pmp_allow) begin
dreq = pte_req;
end
end

未开启分页时,MMU_IDLE 在进入 MMU_DIRECT 前检查原始物理地址。PMP 拒绝时直接进入 MMU_FAULT。

1
2
3
4
5
6
7
8
mmu.sv
end else if (!req_pmp_allow) begin
state <= MMU_FAULT;
fault_cause <= req_pmp_fault_cause;
fault_tval <= core_req.addr;
end else begin
state <= MMU_DIRECT;
end

MMU_L2 状态先检查 PTE 读取地址是否被 PMP 允许,再在 L2 叶子 PTE 命中时检查 l2_paddr。允许后才更新 saved_req.addr。

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
mmu.sv
MMU_L2: begin
if (!pte_pmp_allow) begin
state <= MMU_FAULT;
fault_cause <= saved_access_fault_cause;
fault_tval <= saved_vaddr;
end else if (dresp.data_ok) begin
if (pte_fault) begin
state <= MMU_FAULT;
fault_cause <= saved_fault_cause;
fault_tval <= saved_vaddr;
end else if (pte_is_leaf) begin
if (!l2_pmp_allow) begin
state <= MMU_FAULT;
fault_cause <= l2_pmp_fault_cause;
fault_tval <= saved_vaddr;
end else begin
state <= MMU_ACCESS;
saved_req.addr <= l2_paddr;
end
end else begin
state <= MMU_L1;
page_base <= l1_page_base;
end
end
end

MMU_L1 状态同样先检查 PTE 读取,再在 L1 叶子 PTE 命中时检查 l1_paddr。

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
mmu.sv
MMU_L1: begin
if (!pte_pmp_allow) begin
state <= MMU_FAULT;
fault_cause <= saved_access_fault_cause;
fault_tval <= saved_vaddr;
end else if (dresp.data_ok) begin
if (pte_fault) begin
state <= MMU_FAULT;
fault_cause <= saved_fault_cause;
fault_tval <= saved_vaddr;
end else if (pte_is_leaf) begin
if (!l1_pmp_allow) begin
state <= MMU_FAULT;
fault_cause <= l1_pmp_fault_cause;
fault_tval <= saved_vaddr;
end else begin
state <= MMU_ACCESS;
saved_req.addr <= l1_paddr;
end
end else begin
state <= MMU_L0;
page_base <= l0_page_base;
end
end
end

MMU_L0 状态检查 PTE 读取后,对最终 4KB 页物理地址 l0_paddr 做 PMP 检查。通过后进入 MMU_ACCESS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mmu.sv
MMU_L0: begin
if (!pte_pmp_allow) begin
state <= MMU_FAULT;
fault_cause <= saved_access_fault_cause;
fault_tval <= saved_vaddr;
end else if (dresp.data_ok) begin
if (pte_fault) begin
state <= MMU_FAULT;
fault_cause <= saved_fault_cause;
fault_tval <= saved_vaddr;
end else begin
if (!l0_pmp_allow) begin
state <= MMU_FAULT;
fault_cause <= l0_pmp_fault_cause;
fault_tval <= saved_vaddr;
end else begin
state <= MMU_ACCESS;
saved_req.addr <= l0_paddr;
end
end
end
end

三、输出与报错

经过测试,无法通过 make test-labplus-4,程序长时间无法退出。

经过排查源码,发现错误原因是这行代码:

1
80006048: 34402373   csrr t1,mip

但当时的特权级别是 U 模式,无权访问 mip 寄存器,应该直接报异常 解决办法也很直接,U 模式下的非法请求直接拦截报异常就行。整体上分成这几步:

1. 检查 U 模式实现

得益于 Lab 5 中的实现 S 模式 Bonus,直接复用已有的 xret_type 通路即可,没有代码改动

2. 检查当前 CSR 访问是否合法

分成三个部分:

  • 特权级别够不够
  • CSR 寄存器是否存在
  • CSR 寄存器是否允许写入 这些直接新开一个 csr_checker 模块来统一判断

3. 判断当前指令是否合法

除了 CSR 之外,ecall,mret 等system opcode指令需要额外检查特权级别是否足够。直接新开一个 sys_inst_checker 专门检查

四、进一步改进

1. U 模式

不用改

2. checkers

由于新增两个checker,算上刚刚完成的 pmp_checker,直接新增一个checkers文件夹单独存放这三个。

2.1 csr_checker

首先,新增 csr_checker 模块统一检查 CSR 访问是否合法,主要分成三类:CSR 是否存在、当前特权级是否足够、当前 CSR 指令是否会写入只读 CSR。这样 U 模式访问 M 态 CSR、访问未实现 CSR、写入只读 CSR 时,都能在译码阶段被归类为非法指令异常。

三种错误分别对应csr_exists,csr_priv_ok,csr_write_ok三种报错,代码如下:

  • csr_exists:直接根据 CSR 寄存器的名字来判断是否存在
  • csr_priv_ok:当前特权级别是否大于csr_addr[9:8]位置上要求的特权级别。这可以直接比较,因为mypkg中priv_t对各个模式的数学定义与 CSR 指令规定的一致
  • csr_write_ok:判断涉及到写入时,当前 CSR 寄存器是否可以写入。
  • csr_read_only:通过 CSR 指令的[11:10]判断是否是只读的
  • csr_write:判断是否是写入的RW类型;RS与RC需要根据zimm与rs1的值判断 代码如下:
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
csr_checker.sv
`ifndef __CSR_CHECKER_SV
`define __CSR_CHECKER_SV

`ifdef VERILATOR
`include "include/common.sv"
`include "include/csr.sv"
`include "src/utils/mypkg.sv"
`endif

module csr_checker
import common::*;
import csr_pkg::*;
import my_pkg::*;
(
input u1 is_csr,
input u12 csr_addr,
input csr_t csr_op,
input u1 csr_use_imm,
input u5 rs1_id,
input u64 csr_zimm,
input priv_t current_priv,

output u1 csr_exists,
output u1 csr_priv_ok,
output u1 csr_write_ok,
output u1 csr_access_illegal
);
u1 csr_write, csr_read_only;
u2 csr_required_priv;

assign csr_required_priv = csr_addr[9:8];
assign csr_priv_ok = current_priv >= csr_required_priv;
assign csr_read_only = (csr_addr[11:10] == 2'b11);
assign csr_write_ok = !csr_write || !csr_read_only;
assign csr_access_illegal = is_csr && (!csr_exists || !csr_priv_ok || !csr_write_ok);

always_comb begin
unique case (csr_addr)
CSR_MSTATUS,
CSR_MTVEC,
CSR_MIP,
CSR_MIE,
CSR_MSCRATCH,
CSR_MCAUSE,
CSR_MTVAL,
CSR_MEPC,
CSR_MCYCLE,
CSR_MHARTID,
CSR_SATP,
CSR_PMPCFG0,
CSR_PMPADDR0,
CSR_MEDELEG,
CSR_MIDELEG,
CSR_SSTATUS,
CSR_STVEC,
CSR_SSCRATCH,
CSR_SEPC,
CSR_SCAUSE,
CSR_STVAL,
CSR_SIE,
CSR_SIP: begin
csr_exists = 1'b1;
end
default: begin
csr_exists = 1'b0;
end
endcase
end

always_comb begin
unique case (csr_op)
CSR_RW: begin
csr_write = 1'b1;
end
CSR_RS,
CSR_RC: begin
csr_write = csr_use_imm ? (csr_zimm != 64'b0) : (rs1_id != 5'b0);
end
default: begin
csr_write = 1'b0;
end
endcase
end

endmodule

`endif

然后,在 core 中引入 csr_checker,把 decoder 解出的 CSR 信息和当前特权级传进去,并拿到合法性检查结果。

1
2
3
4
core.sv
`include "src/checkers/csr_checker.sv"

u1 csr_checker_csr_exists, csr_checker_csr_priv_ok, csr_checker_csr_write_ok, csr_checker_csr_access_illegal;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
core.sv
csr_checker u_csr_checker(
.is_csr (decoder_is_csr),
.csr_addr (decoder_csr_rid),
.csr_op (decoder_csr_op),
.csr_use_imm (decoder_csr_use_imm),
.rs1_id (decoder_rs1_id),
.csr_zimm (decoder_csr_zimm),
.current_priv (current_priv),

.csr_exists (csr_checker_csr_exists),
.csr_priv_ok (csr_checker_csr_priv_ok),
.csr_write_ok (csr_checker_csr_write_ok),
.csr_access_illegal(csr_checker_csr_access_illegal)
);

最后把 csr_access_illegal 接入 ID 阶段的非法指令异常生成逻辑。这样非法 CSR 访问同样属于非法指令,cause=2,可以与~decoder_is_valid_inst或在一起,并把原始指令写入 tval。

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 || csr_checker_csr_access_illegal || 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 || csr_checker_csr_access_illegal)) begin
id_exc_cause = 64'd2;
id_exc_tval = {32'b0, if_id_inst};
end else if (if_id_valid && decoder_is_ecall) begin
unique 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
2
core.sv
`UNUSED_OK({iresp, csr_mcycle, ex_inst_should_jump, csr_checker_csr_exists, csr_checker_csr_priv_ok, csr_checker_csr_write_ok});

2.2 sys_inst_checker

新增 sys_inst_checker 模块检查非 CSR 类 system 指令的特权级是否合法。这里主要检查 mret、sret、sfence.vma 这三类指令:mret 只能在 M 模式执行,sret 和 sfence.vma 至少需要 S 模式,U 模式执行时应该触发 illegal instruction。

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
sys_inst_checker.sv
`ifndef __SYS_INST_CHECKER_SV
`define __SYS_INST_CHECKER_SV

`ifdef VERILATOR
`include "include/common.sv"
`include "src/utils/mypkg.sv"
`endif

module sys_inst_checker
import common::*;
import my_pkg::*;
(
input u32 inst,
input xret_t xret_type,
input priv_t current_priv,

output u1 sys_inst_access_illegal
);
u1 is_sfence_vma;

assign is_sfence_vma = (inst[6:0] == 7'b1110011) &&
(inst[14:12] == 3'b000) &&
(inst[31:25] == 7'b0001001) &&
(inst[11:7] == 5'b00000);
assign sys_inst_access_illegal = ((xret_type == XRET_M) && (current_priv != PRIV_M)) ||
((xret_type == XRET_S) && (current_priv == PRIV_U)) ||
(is_sfence_vma && (current_priv == PRIV_U));

endmodule

`endif

在 core 中引入并例化 sys_inst_checker。它直接使用当前 IF/ID 阶段指令、decoder 已经解出的 xret_type 和当前特权级 current_priv。

1
2
3
4
core.sv
`include "src/checkers/sys_inst_checker.sv"

u1 sys_inst_checker_sys_inst_access_illegal;
1
2
3
4
5
6
7
8
core.sv
sys_inst_checker u_sys_inst_checker(
.inst (if_id_inst),
.xret_type (decoder_xret_type),
.current_priv (current_priv),

.sys_inst_access_illegal(sys_inst_checker_sys_inst_access_illegal)
);

最后把 sys_inst_access_illegal 合并到 ID 阶段的非法指令异常判断中。这样 system 指令权限不足时,会和非法 CSR 访问一样产生 cause 为 2 的 illegal instruction。

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 || csr_checker_csr_access_illegal || sys_inst_checker_sys_inst_access_illegal || 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 || csr_checker_csr_access_illegal || sys_inst_checker_sys_inst_access_illegal)) 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

五、依然报错

仍然陷入死循环。让 AI 帮忙看一下测试文件,发现除了 PMP 还需要实现更多内容。根据终端输出,它实际跑了 all-test-privfull.bin。根据文件名,需要实现全套特权架构。这超出了 PMP 这个 Bonus 的范畴。依据 Bonus 的 Wiki 文档,我在实现正确的 PMP 之后决定放弃通过 test-labplus-4 这个测试

六、参考资料

  1. 知乎-RISC-V PMP 物理内存保护机制详解
  2. RISC-V官方手册: The RISC-V Instruction Set Manual Volume II: Privileged Architecture

简单性能优化:分支预测与 I-Cache

一、思路:

一共有jal,jalr,branch三种跳转指令。由于jalr需要阅读寄存器内容,这涉及到forward转发。因此实现分支预测比较麻烦,准确性提升也不会很高。相对地,jal和branch都只涉及imm立即数的计算,实现方便且准确性更高。因此,先实现jal和branch的分支预测。 具体实施则是在 core 中根据当前 pc 和imm计算出预测 pc,交给 pc。ex 阶段检测是否算错,错了就 flush 掉 ex 之后的新指令。ctrl 的相关 flush 信号也需要同步改进。最后在 ex 阶段实现准确率计算。 冲刷流水线的优先级为异常/中断/csr/mret > 分支跳转

二、具体实施

1. 第一版

第一版实现了完整的静态预测、错误修正和准确率统计链路,但 ID 阶段预测信号额外依赖 ctrl_pc_enable 和 fetch_iresp.data_ok,导致很多 jal 在取指等待时无法及时发出预测,最后退化为 EX 阶段修正。

1.1 core.sv

在 core.sv 中新增分支预测相关信号。ID 阶段根据已有 decoder 输出产生预测方向和预测目标,EX 阶段再比较预测结果和真实跳转结果。

1
2
3
4
5
6
7
8
9
10
core.sv
u1 id_pred_taken, id_predict_redirect;
u64 id_pred_target;

u1 id_ex_pred_taken;
u64 id_ex_pred_target;

u1 ex_actual_taken, ex_pred_taken, ex_mispredict;
u64 ex_actual_target, ex_pred_target, ex_correct_pc;
u1 bp_stat_stall;

PC 的跳转来源按优先级选择。异常、xret 等提交级重定向优先级最高,其次是 EX 阶段发现预测错误后的修正,最后才是 ID 阶段的预测跳转。

1
2
3
4
5
6
7
core.sv
assign pc_should_jump = trap_unit_redirect_valid ? 1'b1 :
ex_should_jump_real ? 1'b1 :
id_predict_redirect;
assign pc_jump_addr = trap_unit_redirect_valid ? trap_unit_redirect_pc :
ex_should_jump_real ? ex_jump_addr_real :
id_pred_target;

在 decoder 实例化之后,根据当前 ID 阶段指令进行静态预测。目前只预测 jal 和 branch,预测目标为 if_id_pc + decoder_imm。

1
2
3
4
5
6
7
8
9
core.sv
assign id_pred_taken = if_id_valid &&
~id_exc_valid &&
(decoder_is_jal || decoder_is_branch);
assign id_pred_target = if_id_pc + decoder_imm;
assign id_predict_redirect = id_pred_taken &&
(id_pred_target[1:0] == 2'b00) &&
ctrl_pc_enable &&
fetch_iresp.data_ok;

这里的 ctrl_pc_enable 和 fetch_iresp.data_ok 限制是第一版的问题来源。取指总线等待下一条指令返回时,ID 阶段虽然已经拿到了当前跳转指令,却无法及时发出预测跳转,导致 jal 基本退化为 EX 阶段修正,准确率统计中 jump_success 异常偏低。

ctrl 接口拆分为 ID 阶段预测跳转和 EX 阶段修正跳转。预测跳转只需要清掉 IF/ID 中顺序取回的错误指令;EX 阶段修正才需要继续清掉 ID/EX。

1
2
3
4
5
6
core.sv
ctrl u_ctrl(
.predict_redirect(id_predict_redirect),
.ex_redirect (ex_should_jump_real),
.commit_flush (trap_unit_redirect_valid)
);

预测结果需要随流水线传入 EX 阶段,因此 core.sv 在实例化 id_ex 时新增预测方向和预测目标的输入输出连接。

1
2
3
4
5
6
7
8
core.sv
id_ex u_id_ex(
.pred_taken_in (id_predict_redirect),
.pred_target_in (id_pred_target),

.pred_taken_out (id_ex_pred_taken),
.pred_target_out (id_ex_pred_target)
);

1.2 id_ex.sv

id_ex.sv 新增 pred_taken 和 pred_target 端口,把 ID 阶段预测结果随流水线传到 EX 阶段。

1
2
3
4
5
6
id_ex.sv
input u1 pred_taken_in,
input u64 pred_target_in,

output u1 pred_taken_out,
output u64 pred_target_out,

无效指令或复位时清空预测控制信号,避免气泡在 EX 阶段被误判为真实预测。

1
2
3
4
5
6
7
8
9
10
11
12
13
id_ex.sv
if (reset) begin
pred_taken_out <= 1'b0;
pred_target_out <= 64'b0;
end else if (enable) begin
pred_target_out <= pred_target_in;
if (valid_in) begin
pred_taken_out <= pred_taken_in;
end else begin
pred_taken_out <= 1'b0;
pred_target_out <= 64'b0;
end
end

1.3 core.sv

EX 阶段使用已有的真实跳转结果 ex_should_jump_valid 和 ex_jump_addr,与 ID 阶段传下来的预测结果比较。如果方向或目标错误,就产生 ex_mispredict,并把 PC 修正为真实目标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
core.sv
assign ex_actual_taken = ex_should_jump_valid;
assign ex_actual_target = ex_actual_taken ? ex_jump_addr : (id_ex_pc + 64'd4);
assign ex_pred_taken = id_ex_pred_taken;
assign ex_pred_target = id_ex_pred_target;
assign ex_mispredict = id_ex_valid &&
~id_ex_exc_valid &&
~ex_invalid_addr &&
(id_ex_is_jal || id_ex_is_jalr || id_ex_is_branch) &&
((ex_pred_taken != ex_actual_taken) ||
(ex_actual_taken && (ex_pred_target != ex_jump_addr)));
assign ex_correct_pc = ex_actual_target;
assign ex_should_jump_real = (id_ex_valid && id_ex_is_csr) || ex_mispredict;
assign ex_jump_addr_real = (id_ex_valid && id_ex_is_csr) ? (id_ex_pc + 64'd4) : ex_correct_pc;

1.4 ctrl.sv

ctrl.sv 将原来的跳转冲刷拆成 predict_redirect 和 ex_redirect。ID 阶段预测跳转只冲刷 IF/ID;EX 阶段发现预测错误或提交级重定向时才冲刷 IF/ID 和 ID/EX。

1
2
3
4
5
6
7
ctrl.sv
input u1 ex_mem_read, predict_redirect, ex_redirect, commit_flush,

assign if_id_flush = (global_enable && (predict_redirect || ex_redirect || commit_flush) || pending_jump);

assign id_ex_flush = (global_enable && (ex_redirect || commit_flush) || pending_jump) |
(global_enable && load_use_stall);

1.5 bp_stat.sv

新增 bp_stat.sv 用于记录预测准确率。统计点放在 EX 阶段,因为此时同时拥有预测结果和真实结果。为避免流水线暂停时重复计数,模块额外接收 stall 信号。

1
2
3
4
5
6
7
8
9
10
11
12
bp_stat.sv
module bp_stat
import common::*;
(
input logic clk,
input u1 reset,

input u1 valid, stall,
input u1 is_branch, is_jal, is_jalr,
input u1 pred_taken, actual_taken,
input u64 pred_target, actual_target
);

模块内部统计条件分支和跳转指令的总数、成功数,以及跳转目标错误次数。方向正确且目标正确时,才算预测成功。

1
2
3
4
5
6
7
bp_stat.sv
assign sample_valid = valid && !stall;
assign branch_success = (pred_taken == actual_taken) &&
(!actual_taken || (pred_target == actual_target));
assign jump_success = (pred_taken == actual_taken) &&
(!actual_taken || (pred_target == actual_target));
assign jump_target_wrong = actual_taken && (pred_target != actual_target);
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
bp_stat.sv
always_ff @(posedge clk) begin
if (reset) begin
total_branch <= 64'b0;
succ_branch <= 64'b0;
total_jump <= 64'b0;
succ_jump <= 64'b0;
wrong_target_jump <= 64'b0;
print_cnt <= 32'b0;
end else begin
if (sample_valid && is_branch) begin
total_branch <= total_branch + 64'd1;
if (branch_success) begin
succ_branch <= succ_branch + 64'd1;
end
end

if (sample_valid && (is_jal || is_jalr)) begin
total_jump <= total_jump + 64'd1;
if (jump_success) begin
succ_jump <= succ_jump + 64'd1;
end
if (jump_target_wrong) begin
wrong_target_jump <= wrong_target_jump + 64'd1;
end
end
end
end

在 VERILATOR 环境下,统计结果会周期性写入 bp_accuracy.log,用于后续报告中展示预测准确率。

1
2
3
4
5
6
7
8
bp_stat.sv
`ifdef VERILATOR
integer bp_log_fd;

initial begin
bp_log_fd = $fopen("bp_accuracy.log", "w");
end
`endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bp_stat.sv
`ifdef VERILATOR
if (print_cnt[24]) begin
if (bp_log_fd != 0) begin
$fdisplay(bp_log_fd, "total_branch:%0d", total_branch);
$fdisplay(bp_log_fd, "branch_success:%0d/%0d", succ_branch, total_branch);
$fdisplay(bp_log_fd, "total_jump:%0d", total_jump);
$fdisplay(bp_log_fd, "jump_success:%0d/%0d", succ_jump, total_jump);
$fdisplay(bp_log_fd, "jump_wrong_target:%0d/%0d", wrong_target_jump, total_jump);
$fflush(bp_log_fd);
end
print_cnt <= 32'b0;
end else begin
print_cnt <= print_cnt + 32'd1;
end
`endif

1.6 core.sv

最后在 core.sv 中实例化该统计模块,将 EX 阶段的预测结果和真实结果接入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
core.sv
assign bp_stat_stall = ~ctrl_ex_mem_enable || ctrl_ex_mem_flush;

bp_stat u_bp_stat(
.clk (clk),
.reset (reset),
.valid (id_ex_valid && ~id_ex_exc_valid),
.stall (bp_stat_stall),
.is_branch (id_ex_is_branch),
.is_jal (id_ex_is_jal),
.is_jalr (id_ex_is_jalr),
.pred_taken (id_ex_pred_taken),
.actual_taken (ex_actual_taken),
.pred_target (id_ex_pred_target),
.actual_target(ex_actual_target)
);

2. 第二版

第二版修复第一版暴露出的两个问题:第一,ID 阶段预测信号不能依赖下一条取指是否已经返回;第二,pc.sv 中新的重定向必须可以覆盖旧的 pending_jump 目标。

2.1 core.sv

去掉 id_predict_redirect 中的 ctrl_pc_enable 和 fetch_iresp.data_ok 限制。这样 ID 阶段只要已经拿到当前跳转指令,就可以立即发出预测跳转;PC 如果暂时不能更新,会由 pc.sv 中的 pending_jump 机制锁存目标。 > 但即便如此,由于ibus卡住了 pc,pc 就算收到了跳转请求也无法马上跳转。这导致第二版并没有真正改进运行速度。

1
2
3
core.sv
assign id_predict_redirect = id_pred_taken &&
(id_pred_target[1:0] == 2'b00);

2.2 pc.sv

调整 pc.sv 中 next_pc 的优先级。新的 should_jump 优先于旧的 latched_jump,因此当 EX 阶段发现 ID 阶段旧预测错误时,可以用 EX 修正目标覆盖旧预测目标。

1
2
3
4
5
6
7
8
9
10
pc.sv
always_comb begin
if (should_jump) begin
next_pc = jump_addr;
end else if (latched_jump) begin
next_pc = latched_jump_addr;
end else begin
next_pc = cur_pc + 64'd4;
end
end

3. 第三版

第二版只把指令粗略分成 branch 和 jump,但不同跳转指令的预测难点并不一样。如果全部混在一起看,就无法判断下一步应该优先做 BTFNT、BTB、BHT 还是 RAS。

指令/场景分类 新增统计项 为什么需要单独看 对后续优化的意义
条件分支实际跳转 branch_taken_success 条件分支预测失败可能来自方向判断错误,taken和not taken混在一起会掩盖问题 判断当前策略是否偏向不跳转,决定是否需要 BHT
条件分支实际不跳转 branch_not_taken_success 前向条件分支常见于普通 if 路径,不跳转比例可能较高 和taken统计对比,判断静态策略是否失衡
后向条件分支 branch_backward_success 后向分支通常对应循环,BTFNT 会默认预测taken 判断 BTFNT 是否值得加入
前向条件分支 branch_forward_success 前向分支通常对应跳过某段代码,BTFNT 会默认预测not taken 判断前向分支是否应该保持顺序取指
直接跳转 jal jal_success、jal_wrong_target jal 目标由立即数决定,理论上最容易预测正确 如果该项低,说明预测时机或目标传递有问题;如果高,说明 BTB 可优先服务无条件跳转
间接跳转 jalr jalr_success、jalr_wrong_target jalr 目标来自寄存器,ID 阶段很难提前准确得到目标 判断是否需要 BTB 记录历史目标
函数调用 call_success call一般会产生固定跳转目标和返回地址 为后续 RAS 压栈提供依据
函数返回 ret_success ret目标来自返回地址寄存器,普通 BTB 可能不稳定 如果该项低,说明需要 RAS 预测返回地址

3.1 core.sv

在 core.sv 中新增统计分类信号。bp_branch_backward 使用立即数符号位区分前向/后向条件分支;bp_is_call 根据 rd=x1/x5 识别调用;bp_is_ret 根据 jalr x0, 0(x1/x5) 识别返回。

1
2
core.sv
u1 bp_stat_stall, bp_branch_backward, bp_is_call, bp_is_ret;
1
2
3
4
5
6
7
8
9
core.sv
assign bp_stat_stall = ~ctrl_ex_mem_enable || ctrl_ex_mem_flush;
assign bp_branch_backward = id_ex_is_branch && id_ex_imm[63];
assign bp_is_call = (id_ex_is_jal || id_ex_is_jalr) &&
((id_ex_inst[11:7] == 5'd1) || (id_ex_inst[11:7] == 5'd5));
assign bp_is_ret = id_ex_is_jalr &&
(id_ex_inst[11:7] == 5'd0) &&
(id_ex_imm == 64'b0) &&
((id_ex_inst[19:15] == 5'd1) || (id_ex_inst[19:15] == 5'd5));

实例化 bp_stat 时,把新增分类信号传入统计模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
core.sv
bp_stat u_bp_stat(
.stall (bp_stat_stall),
.is_branch (id_ex_is_branch),
.is_jal (id_ex_is_jal),
.is_jalr (id_ex_is_jalr),
.branch_backward(bp_branch_backward),
.is_call (bp_is_call),
.is_ret (bp_is_ret),
.pred_taken (id_ex_pred_taken),
.actual_taken (ex_actual_taken),
.pred_target (id_ex_pred_target),
.actual_target(ex_actual_target)
);

3.2 bp_stat.sv

bp_stat.sv 接口新增 branch_backward、is_call 和 is_ret,用于接收 core.sv 中拆出来的跳转分类。

1
2
3
4
5
6
bp_stat.sv
input u1 valid, stall,
input u1 is_branch, is_jal, is_jalr,
input u1 branch_backward, is_call, is_ret,
input u1 pred_taken, actual_taken,
input u64 pred_target, actual_target

新增不同分类的计数器。原来的 total_branch/succ_branch 和 total_jump/succ_jump 继续保留,保证总体准确率仍然可以直接查看。

1
2
3
4
5
6
7
bp_stat.sv
u64 total_branch, succ_branch, total_branch_taken, succ_branch_taken;
u64 total_branch_not_taken, succ_branch_not_taken;
u64 total_branch_backward, succ_branch_backward, total_branch_forward, succ_branch_forward;
u64 total_jump, succ_jump, wrong_target_jump;
u64 total_jal, succ_jal, wrong_target_jal, total_jalr, succ_jalr, wrong_target_jalr;
u64 total_call, succ_call, total_ret, succ_ret;

复位时清空所有新增统计项。

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
bp_stat.sv
always_ff @(posedge clk) begin
if (reset) begin
total_branch <= 64'b0;
succ_branch <= 64'b0;
total_branch_taken <= 64'b0;
succ_branch_taken <= 64'b0;
total_branch_not_taken <= 64'b0;
succ_branch_not_taken <= 64'b0;
total_branch_backward <= 64'b0;
succ_branch_backward <= 64'b0;
total_branch_forward <= 64'b0;
succ_branch_forward <= 64'b0;
total_jump <= 64'b0;
succ_jump <= 64'b0;
wrong_target_jump <= 64'b0;
total_jal <= 64'b0;
succ_jal <= 64'b0;
wrong_target_jal <= 64'b0;
total_jalr <= 64'b0;
succ_jalr <= 64'b0;
wrong_target_jalr <= 64'b0;
total_call <= 64'b0;
succ_call <= 64'b0;
total_ret <= 64'b0;
succ_ret <= 64'b0;
print_cnt <= 32'b0;
end
end

条件分支统计拆成实际跳转/实际不跳转,以及后向/前向两个维度。

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
bp_stat.sv
if (sample_valid && is_branch) begin
total_branch <= total_branch + 64'd1;
if (branch_success) begin
succ_branch <= succ_branch + 64'd1;
end

if (actual_taken) begin
total_branch_taken <= total_branch_taken + 64'd1;
if (branch_success) begin
succ_branch_taken <= succ_branch_taken + 64'd1;
end
end else begin
total_branch_not_taken <= total_branch_not_taken + 64'd1;
if (branch_success) begin
succ_branch_not_taken <= succ_branch_not_taken + 64'd1;
end
end

if (branch_backward) begin
total_branch_backward <= total_branch_backward + 64'd1;
if (branch_success) begin
succ_branch_backward <= succ_branch_backward + 64'd1;
end
end else begin
total_branch_forward <= total_branch_forward + 64'd1;
if (branch_success) begin
succ_branch_forward <= succ_branch_forward + 64'd1;
end
end
end

跳转指令统计拆成 jal、jalr、call和ret,方便判断目标错误主要来自哪一种跳转。

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
bp_stat.sv
if (sample_valid && is_jal) begin
total_jal <= total_jal + 64'd1;
if (jump_success) begin
succ_jal <= succ_jal + 64'd1;
end
if (jump_target_wrong) begin
wrong_target_jal <= wrong_target_jal + 64'd1;
end
end

if (sample_valid && is_jalr) begin
total_jalr <= total_jalr + 64'd1;
if (jump_success) begin
succ_jalr <= succ_jalr + 64'd1;
end
if (jump_target_wrong) begin
wrong_target_jalr <= wrong_target_jalr + 64'd1;
end
end

if (sample_valid && is_call) begin
total_call <= total_call + 64'd1;
if (jump_success) begin
succ_call <= succ_call + 64'd1;
end
end

if (sample_valid && is_ret) begin
total_ret <= total_ret + 64'd1;
if (jump_success) begin
succ_ret <= succ_ret + 64'd1;
end
end

最后把拆分后的统计项写入 bp_accuracy.log。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bp_stat.sv
if (print_cnt[24]) begin
if (bp_log_fd != 0) begin
$fdisplay(bp_log_fd, "branch_taken_success:%0d/%0d", succ_branch_taken, total_branch_taken);
$fdisplay(bp_log_fd, "branch_not_taken_success:%0d/%0d", succ_branch_not_taken, total_branch_not_taken);
$fdisplay(bp_log_fd, "branch_backward_success:%0d/%0d", succ_branch_backward, total_branch_backward);
$fdisplay(bp_log_fd, "branch_forward_success:%0d/%0d", succ_branch_forward, total_branch_forward);
$fdisplay(bp_log_fd, "jal_success:%0d/%0d", succ_jal, total_jal);
$fdisplay(bp_log_fd, "jal_wrong_target:%0d/%0d", wrong_target_jal, total_jal);
$fdisplay(bp_log_fd, "jalr_success:%0d/%0d", succ_jalr, total_jalr);
$fdisplay(bp_log_fd, "jalr_wrong_target:%0d/%0d", wrong_target_jalr, total_jalr);
$fdisplay(bp_log_fd, "call_success:%0d/%0d", succ_call, total_call);
$fdisplay(bp_log_fd, "ret_success:%0d/%0d", succ_ret, total_ret);
end
end

这些统计项仍然统一在 EX 阶段采样,因为 EX 阶段同时拥有预测结果、真实跳转方向和真实目标地址。这样既能保留总体准确率,也能判断预测失败主要来自方向错误、目标错误,还是某一类跳转指令本身不适合当前的 ID 阶段静态预测。

4. 第四版

第四版把第二版、第三版中的“无脑预测跳转”改成 BTFNT 静态策略。ID 阶段仍然复用已有的 decoder_imm 计算预测目标,但条件分支只有在立即数为负数时才预测跳转,也就是后向分支预测taken,前向分支预测not taken。jal 仍然是无条件跳转,所以继续预测taken。

1
2
3
4
5
6
7
8
core.sv
assign id_pred_target = if_id_pc + decoder_imm;
assign id_pred_taken = if_id_valid &&
~id_exc_valid &&
(decoder_is_jal ||
(decoder_is_branch && decoder_imm[63]));
assign id_predict_redirect = id_pred_taken &&
(id_pred_target[1:0] == 2'b00);

这里删除了旧版本中 decoder_is_jal || decoder_is_branch 的策略,避免所有前向条件分支都被预测为跳转。预测结果仍然通过已有的 id_predict_redirect 送给 PC;如果预测错误,EX 阶段原有的 ex_mispredict 和 ex_correct_pc 会继续负责修正 PC 并冲刷流水线。

5. 第五版

第五版实现了虚拟 PC 索引的8B direct-mapped I-Cache。由于当前下级 dbus_resp_t.data 本身是64 位,而 CPU 前端一次只消费32 位指令,因此 I-Cache 以8B作为 line 大小,一次 miss 优先取回两条指令;命中时直接向 IF 阶段返回32 位指令,避免每条指令都进入 MMU 和下级总线等待。

5.1 新增 icache.sv

icache 模块保持 CPU 侧 ibus_req_t/ibus_resp_t 接口不变,同时在 miss 侧使用 dbus_req_t/dbus_resp_t 接入仲裁器。默认 INDEX_BITS = 10,即1024行,每行8B,数据容量8KiB。

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
icache.sv
module icache
import common::*;
import my_pkg::*;
#(
parameter int INDEX_BITS = 10
)(
input logic clk,
input u1 reset,
input u1 flush,

input ibus_req_t cpu_req,
output ibus_resp_t cpu_resp,

output dbus_req_t miss_req,
input dbus_resp_t miss_resp,
input u1 miss_exc,

output u1 stat_hit,
output u1 stat_miss_start,
output u1 stat_fill_done,
output u1 stat_fill_exc,
output u1 stat_fallback_4b,
output u1 stat_stall
);

内部使用虚拟 PC 划分 tag/index/offset。为了兼容 PMP 边界,valid_table 每行使用2位half-valid,分别表示低 32 位和高32 位是否有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
icache.sv
localparam int LINE_NUM = 1 << INDEX_BITS;
localparam int TAG_BITS = 64 - INDEX_BITS - 3;

logic [1:0] valid_table [LINE_NUM-1:0];
logic [TAG_BITS-1:0] tag_table [LINE_NUM-1:0];
u64 data_table [LINE_NUM-1:0];

assign req_index = cpu_req.addr[INDEX_BITS+2:3];
assign req_tag = cpu_req.addr[63:INDEX_BITS+3];
assign req_offset = cpu_req.addr[2];
assign hit = cpu_req.valid &&
!flush &&
valid_table[req_index][req_offset] &&
(tag_table[req_index] == req_tag);
assign hit_inst = req_offset ? data_table[req_index][63:32] :
data_table[req_index][31:0];

命中时直接返回当前half对应的32 位指令。flush 同周期会屏蔽命中,避免 satp 或 PMP 配置变化时继续消费旧 line。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
icache.sv
always_comb begin
cpu_resp = '0;
miss_req = '0;

unique case (state)
IC_IDLE: begin
if (hit) begin
cpu_resp.addr_ok = 1'b1;
cpu_resp.data_ok = 1'b1;
cpu_resp.data = hit_inst;
end
end
endcase
end

miss 状态机优先发起8B取指。若8B fill成功,整行写入并把两个half都置为有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
icache.sv
IC_MISS8: begin
miss_req.valid = 1'b1;
miss_req.addr = {miss_pc[63:3], 3'b000};
miss_req.size = MSIZE8;
miss_req.strobe = 8'b0;
miss_req.data = 64'b0;

if (miss_resp.data_ok && !miss_exc && !miss_killed && !flush) begin
cpu_resp.addr_ok = 1'b1;
cpu_resp.data_ok = 1'b1;
cpu_resp.data = miss8_inst;
end
end

如果8B fill因为 PMP 边界等原因异常,不能直接向 CPU 报告异常,而是降级到4B精确取当前指令。4B成功时只写入当前half,另一个half保持无效,避免旧tag残留造成假命中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
icache.sv
IC_MISS4: begin
miss_req.valid = 1'b1;
miss_req.addr = {miss_pc[63:2], 2'b00};
miss_req.size = MSIZE4;
miss_req.strobe = 8'b0;
miss_req.data = 64'b0;

if (miss_resp.data_ok && !miss_killed && !flush) begin
cpu_resp.addr_ok = 1'b1;
cpu_resp.data_ok = 1'b1;
cpu_resp.data = miss_exc ? 32'b0 : miss4_inst;
end
end
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
icache.sv
IC_MISS8: begin
if (miss_resp.data_ok) begin
if (miss_killed || flush) begin
state <= IC_IDLE;
miss_killed <= 1'b0;
end else if (miss_exc) begin
state <= IC_MISS4;
end else begin
tag_table[miss_index] <= miss_tag;
data_table[miss_index] <= miss_resp.data;
valid_table[miss_index] <= 2'b11;
state <= IC_IDLE;
end
end
end

IC_MISS4: begin
if (miss_resp.data_ok) begin
if (!miss_killed && !flush && !miss_exc) begin
tag_table[miss_index] <= miss_tag;
if (miss_offset) begin
data_table[miss_index][63:32] <= miss4_inst;
valid_table[miss_index] <= 2'b10;
end else begin
data_table[miss_index][31:0] <= miss4_inst;
valid_table[miss_index] <= 2'b01;
end
end
state <= IC_IDLE;
miss_killed <= 1'b0;
end
end

如果 miss 期间发生 flush,则设置 miss_killed,禁止旧 miss 返回后写入cache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
icache.sv
if (flush) begin
for (int i = 0; i < LINE_NUM; i++) begin
valid_table[i] <= 2'b00;
end
end

IC_MISS8: begin
if (flush) begin
miss_killed <= 1'b1;
end
end

IC_MISS4: begin
if (flush) begin
miss_killed <= 1'b1;
end
end

5.2 改造 DBusArbiter

原来的取指侧接口是 ibus_req_t/ibus_resp_t,仲裁器内部固定把取指请求转成 MSIZE4,并只返回32 位。现在 I-Cache miss 侧已经生成了完整 dbus_req_t,所以仲裁器取指侧改为 dbus_req_t/dbus_resp_t,并把完整64 位响应返回给 I-Cache。

1
2
3
4
5
6
7
8
9
10
11
dbus_arbiter.sv
input dbus_req_t ireq,
input dbus_req_t dreq,
input dbus_resp_t oresp,
input u1 exc_valid,

output dbus_resp_t iresp,
output dbus_resp_t dresp,
output dbus_req_t oreq,
output mmu_access_t oreq_access_type,
output u1 pc_exc, mem_exc

仲裁优先级仍然是数据访存优先,I-Cache miss 其次;I-Cache miss 必须按 MMU_REQ_INST 进入 MMU,保证取指页错误和 PMP 执行权限检查仍然使用取指语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
dbus_arbiter.sv
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 = ireq;
selected_access_type = MMU_REQ_INST;
end
end
1
2
3
4
5
6
7
8
9
10
11
dbus_arbiter.sv
always_comb begin
dresp = '0;
iresp = '0;

if (busy && saved_select_is_dbus) begin
dresp = oresp;
end else if (busy) begin
iresp = oresp;
end
end

5.3 在 core.sv 中接入 I-Cache

顶层新增 csr_pkg 导入和 I-Cache 相关模块包含,用于检测 satp/PMP CSR 写入并实例化新模块。

1
2
3
4
5
6
7
8
9
core.sv
`include "include/csr.sv"
`include "src/utils/icache.sv"
`include "src/utils/icache_stat.sv"

module core
import common::*;
import csr_pkg::*;
import my_pkg::*;

取指路径拆成 CPU 侧 fetch_ireq/fetch_iresp 和 miss 侧 icache_miss_req/icache_miss_resp。

1
2
3
4
5
6
7
8
core.sv
ibus_req_t fetch_ireq;
ibus_resp_t fetch_iresp;
dbus_req_t icache_miss_req;
dbus_resp_t icache_miss_resp;
u1 icache_flush, icache_access_fire;
u1 icache_stat_hit, icache_stat_miss_start, icache_stat_fill_done;
u1 icache_stat_fill_exc, icache_stat_fallback_4b, icache_stat_stall;

PC 侧仍然只发普通 ibus_req_t,I-Cache 在命中时直接产生 fetch_iresp,miss 时通过 icache_miss_req 访问下级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
core.sv
assign fetch_ireq.valid = ~reset;
assign fetch_ireq.addr = pc_cur;

icache u_icache(
.clk,
.reset,
.flush (icache_flush),
.cpu_req (fetch_ireq),
.cpu_resp (fetch_iresp),
.miss_req (icache_miss_req),
.miss_resp (icache_miss_resp),
.miss_exc (dbus_arbiter_pc_exc),
.stat_hit (icache_stat_hit),
.stat_miss_start(icache_stat_miss_start),
.stat_fill_done (icache_stat_fill_done),
.stat_fill_exc (icache_stat_fill_exc),
.stat_fallback_4b(icache_stat_fallback_4b),
.stat_stall (icache_stat_stall)
);

I-Cache 命中率统计模块接收 I-Cache 事件信号,并以 CPU 真正消费响应的 icache_access_fire 作为访问计数口径。

1
2
3
4
5
6
7
8
9
10
11
12
13
core.sv
icache_stat u_icache_stat(
.clk,
.reset,
.access_fire(icache_access_fire),
.hit (icache_stat_hit),
.miss_start (icache_stat_miss_start),
.fill_done (icache_stat_fill_done),
.fill_exc (icache_stat_fill_exc),
.fallback_4b(icache_stat_fallback_4b),
.flush (icache_flush),
.stall (icache_stat_stall)
);

虚拟 PC I-Cache 在 satp 变化、PMP 配置变化、sfence.vma 提交、trap/xret重定向时清空。这样可以避免虚拟地址空间或执行权限改变后继续命中旧 line。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
core.sv
assign satp_write_commit = csr_file_write_enable && (csr_file_write_addr == CSR_SATP);
assign pmp_write_commit = csr_file_write_enable &&
((csr_file_write_addr == CSR_PMPCFG0) ||
(csr_file_write_addr == CSR_PMPADDR0));
assign sfence_vma_commit = commit_fire &&
(mem_wb_inst[6:0] == 7'b1110011) &&
(mem_wb_inst[14:12] == 3'b000) &&
(mem_wb_inst[31:25] == 7'b0001001) &&
(mem_wb_inst[11:7] == 5'b00000);
assign icache_flush = trap_unit_redirect_valid ||
trap_unit_xret_valid ||
satp_write_commit ||
pmp_write_commit ||
sfence_vma_commit;
assign icache_access_fire = fetch_ireq.valid &&
fetch_iresp.data_ok &&
ctrl_if_id_enable &&
~ctrl_if_id_flush &&
~interrupt_unit_interrupt_valid;

DBusArbiter 不再接 CPU 侧取指请求,而是接 I-Cache miss 侧请求;CPU 侧 fetch_iresp 完全由 I-Cache 产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
core.sv
DBusArbiter u_dbus_arbiter(
.clk, .reset,
.ireq (icache_miss_req),
.dreq (mem_dreq),
.oresp (mmu_core_resp),
.exc_valid (mmu_exc_valid),

.iresp (icache_miss_resp),
.dresp (mem_dresp),
.oreq (mmu_core_req),
.oreq_access_type(mmu_core_access_type),
.pc_exc (dbus_arbiter_pc_exc),
.mem_exc (dbus_arbiter_mem_exc)
);

5.4 新增 icache_stat.sv

icache_stat 仿照 bp_stat 周期性写出 icache_stat.log,用于判断 I-Cache 命中率、miss 次数、PMP fallback次数以及 miss 等待周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
icache_stat.sv
module icache_stat
import common::*;
(
input logic clk,
input u1 reset,

input u1 access_fire,
input u1 hit,
input u1 miss_start,
input u1 fill_done,
input u1 fill_exc,
input u1 fallback_4b,
input u1 flush,
input u1 stall
);
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
icache_stat.sv
always_ff @(posedge clk) begin
if (reset) begin
icache_access <= 64'b0;
icache_hit <= 64'b0;
icache_miss <= 64'b0;
icache_fill <= 64'b0;
icache_fill_exc <= 64'b0;
icache_fallback_4b <= 64'b0;
icache_flush_count <= 64'b0;
icache_stall_cycle <= 64'b0;
print_cnt <= 32'b0;
end else begin
if (access_fire) begin
icache_access <= icache_access + 64'd1;
if (hit) begin
icache_hit <= icache_hit + 64'd1;
end
end

if (miss_start) begin
icache_miss <= icache_miss + 64'd1;
end

if (fill_done) begin
icache_fill <= icache_fill + 64'd1;
end

if (fill_exc) begin
icache_fill_exc <= icache_fill_exc + 64'd1;
end

if (fallback_4b) begin
icache_fallback_4b <= icache_fallback_4b + 64'd1;
end

if (flush) begin
icache_flush_count <= icache_flush_count + 64'd1;
end

if (stall) begin
icache_stall_cycle <= icache_stall_cycle + 64'd1;
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
icache_stat.sv
if (print_cnt[24]) begin
if (icache_log_fd != 0) begin
$fdisplay(icache_log_fd, "icache_access:%0d", icache_access);
$fdisplay(icache_log_fd, "icache_hit:%0d/%0d", icache_hit, icache_access);
$fdisplay(icache_log_fd, "icache_miss:%0d/%0d", icache_miss, icache_access);
$fdisplay(icache_log_fd, "icache_fill:%0d", icache_fill);
$fdisplay(icache_log_fd, "icache_fill_exc:%0d", icache_fill_exc);
$fdisplay(icache_log_fd, "icache_fallback_4b:%0d", icache_fallback_4b);
$fdisplay(icache_log_fd, "icache_flush:%0d", icache_flush_count);
$fdisplay(icache_log_fd, "icache_stall_cycle:%0d", icache_stall_cycle);
$fdisplay(icache_log_fd, "icache_avg_miss_stall:%0d", icache_avg_miss_stall);
$fflush(icache_log_fd);
end
end

5.5 修正预测重定向与发射时序

接入 I-Cache 后,测试中发现 ID 阶段预测跳转不能在流水线冻结时提前改变 PC。否则会出现“目标地址指令被取回并提交,但跳转指令自己没有进入流水线提交”的错误。因此新增 id_predict_fire,只有当 ID/EX 允许接收当前 ID 指令时才真正发出预测重定向,并把该值写入 id_ex.pred_taken。

1
2
3
4
5
6
7
8
9
10
core.sv
u1 id_pred_taken, id_predict_redirect, id_predict_fire;

assign pc_should_jump = trap_unit_redirect_valid ? 1'b1 :
ex_should_jump_real ? 1'b1 :
id_predict_fire;

assign id_predict_fire = id_predict_redirect &&
ctrl_id_ex_enable &&
~ctrl_id_ex_flush;
1
2
3
4
5
6
7
8
core.sv
ctrl u_ctrl(
.predict_redirect(id_predict_fire)
);

id_ex u_id_ex(
.pred_taken_in (id_predict_fire)
);

三、测试结果

1. 第一版

只实现分支预测后的结果

测试项 测试内容 结果 最短时间/ms 单项分数
qsort Quick sort Passed 7 73057
queen Queen placement Passed 11 42790
bf Brainf**k interpreter Passed 62 38182
fib Fibonacci number Passed 181 15645
sieve Eratosthenes sieve Passed 112 35143
15pz A* 15-puzzle search Passed 16 28037
dinic Dinic’s maxflow algorithm Passed 28 38864
lzip Lzip compression Passed 18 42183
ssort Suffix sort Passed 8 56300
md5 MD5 digest Passed 88 19589
汇总项 数值
MicroBench Marks 38979
对比基准 100000 Marks(i7-7700K @ 4.20GHz)
Total time 624 ms
total guest instructions 1970040897
instrCnt 1970040897
cycleCnt 16228969451
IPC 0.121390
Guest cycle spent 16228969453
Host time spent 4583442 ms

准确率如下:

指标 原始计数 比例
branch_success 65936775/232585661 28.349458%
jump_success 0/53196949 0.000000%
jump_wrong_target 27869775/53196949 52.389800%

2. 第二版

经过改进,修正了jal预测正确率为0的问题

测试项 测试内容 结果 最短时间/ms 单项分数
qsort Quick sort Passed 7 73057
queen Queen placement Passed 11 42790
bf Brainf**k interpreter Passed 68 34813
fib Fibonacci number Passed 181 15645
sieve Eratosthenes sieve Passed 114 34527
15pz A* 15-puzzle search Passed 17 26388
dinic Dinic’s maxflow algorithm Passed 29 37524
lzip Lzip compression Passed 19 39963
ssort Suffix sort Passed 8 56300
md5 MD5 digest Passed 90 19154
汇总项 数值
MicroBench Marks 38016
对比基准 100000 Marks(i7-7700K @ 4.20GHz)
Total time 636 ms
total guest instructions 1970040897
instrCnt 1970040897
cycleCnt 16536774792
IPC 0.119131
Guest cycle spent 16536774794
Host time spent 4799442 ms

准确率如下:

指标 原始计数 比例
branch_success 166599630/232517557 71.650344%
jump_success 25314860/53184635 47.598070%
jump_wrong_target 27869775/53184635 52.401930%

从结果上看,预测错误的代价超过了预测正确的收益。

3. 第三版

重新设计准确率预测函数,得到如下:

指标 原始计数 比例
total_branch 232517557 -
branch_success 166599630/232517557 71.650344%
branch_taken_success 166599630/166599630 100.000000%
branch_not_taken_success 0/65917927 0.000000%
branch_backward_success 144241715/162640547 88.687426%
branch_forward_success 22357915/69877010 31.996096%
total_jump 53184635 -
jump_success 25314860/53184635 47.598070%
jump_wrong_target 27869775/53184635 52.401930%
jal_success 25314860/25314860 100.000000%
jal_wrong_target 0/25314860 0.000000%
jalr_success 0/27869775 0.000000%
jalr_wrong_target 27869775/27869775 100.000000%
call_success 12869974/12870713 99.994258%
ret_success 0/12870710 0.000000%

这一指标符合前两版的无脑跳转策略的预期。尤其是前向分支正确率只有31.99%;ret_success完全错误,这需要 BTB 或 RAS。下一版将会先尝试 BTFNT。

4. 第四版

加入BTNFT之后,分支预测终于带来了正收益。

整理后的测试指标如下:

测试项 测试内容 结果 最短时间/ms 单项分数
qsort Quick sort Passed 7 73057
queen Queen placement Passed 11 42790
bf Brainf**k interpreter Passed 64 36989
fib Fibonacci number Passed 182 15559
sieve Eratosthenes sieve Passed 113 34832
15pz A* 15-puzzle search Passed 16 28037
dinic Dinic’s maxflow algorithm Passed 28 38864
lzip Lzip compression Passed 18 42183
ssort Suffix sort Passed 7 64342
md5 MD5 digest Passed 89 19369
汇总项 数值
MicroBench Marks 39602
对比基准 100000 Marks(i7-7700K @ 4.20GHz)
Total time 627 ms
total guest instructions 1970040897
instrCnt 1970040897
cycleCnt 16314895787
IPC 0.120751
Guest cycle spent 16314895789
Host time spent 4750051 ms

最新 bp_accuracy.log 统计如下:

指标 原始计数 比例
total_branch 232561549 -
branch_success 191788391/232561549 82.467799%
branch_taken_success 144261350/166631459 86.575099%
branch_not_taken_success 47527041/65930090 72.087026%
branch_backward_success 144261350/162664399 88.686492%
branch_forward_success 47527041/69897150 67.995678%
total_jump 53192581 -
jump_success 25322806/53192581 47.605898%
jump_wrong_target 27869775/53192581 52.394102%
jal_success 25322806/25322806 100.000000%
jal_wrong_target 0/25322806 0.000000%
jalr_success 0/27869775 0.000000%
jalr_wrong_target 27869775/27869775 100.000000%
call_success 12869974/12870713 99.994258%
ret_success 0/12870710 0.000000%

可以看出,BTNFT牺牲了一部分 taken 分支正确率,但换来了大量 not-taken 分支正确率,净收益很明显。尤其前向分支从第三版提到的约 31.99%,提升到约 68.00%。

5. 第五版

进一步优化后,test-labplus-2 的 MicroBench 结果如下:

测试项 测试内容 结果 最短时间/ms 单项分数
qsort Quick sort Passed 2 255700
queen Queen placement Passed 3 156900
bf Brainf**k interpreter Passed 19 124594
fib Fibonacci number Passed 92 30780
sieve Eratosthenes sieve Passed 29 135727
15pz A* 15-puzzle search Passed 6 74766
dinic Dinic’s maxflow algorithm Passed 9 120911
lzip Lzip compression Passed 5 151860
ssort Suffix sort Passed 3 150133
md5 MD5 digest Passed 22 78359
汇总项 数值
MicroBench Marks 127973
对比基准 100000 Marks(i7-7700K @ 4.20GHz)
Total time 228 ms
total guest instructions 1970040961
instrCnt 1970040961
cycleCnt 5933598745
IPC 0.332015
Guest cycle spent 5933598747
Host time spent 1840308 ms

第五版对应的两个统计文件末尾指标如下。

bp_accuracy.log 最后一组完整统计如下:

指标 原始计数 比例
total_branch 232109725 -
branch_success 191505046/232109725 82.506257%
branch_taken_success 144059688/166304702 86.623942%
branch_not_taken_success 47445358/65805023 72.099903%
branch_backward_success 144059688/162419353 88.696135%
branch_forward_success 47445358/69690372 68.080219%
total_jump 53110905 -
jump_success 25241128/53110905 47.525321%
jump_wrong_target 27869777/53110905 52.474679%
jal_success 25241128/25241128 100.000000%
jal_wrong_target 0/25241128 0.000000%
jalr_success 0/27869777 0.000000%
jalr_wrong_target 27869777/27869777 100.000000%
call_success 12869974/12870714 99.994251%
ret_success 0/12870711 0.000000%

加入 I-Cache 后,分支预测统计仍然保持在第四版 BTFNT 附近:总条件分支正确率约82.51%,后向分支正确率约88.70%,前向分支正确率约68.08%。jal 预测仍然为100%,但 jalr 和 ret 仍然没有有效目标预测。

icache_stat.log 最后一组完整统计如下:

指标 原始计数 比例或含义
icache_access 2012663728 -
icache_hit 2012175201/2012663728 99.975727%
icache_miss 975820/2012663728 0.048484%
icache_fill 975820 -
icache_fill_exc 0 -
icache_fallback_4b 0 -
icache_flush 0 -
icache_stall_cycle 6391173 -
icache_avg_miss_stall 6 统计文件中的整数平均值
icache_avg_miss_stall_exact 6391173/975820 6.549541 cycles/miss

I-Cache 命中率约99.98%,说明MicroBench的指令局部性很强,8KiB直接映射 I-Cache 已经能够覆盖绝大多数热点取指。第五版分数显著提升,甚至超过了参考的i7-7700K @ 4.20GHz。主要原因是原先大量取指访问需要经过总线、MMU 和仲裁等待,而现在绝大多数取指可以直接在 I-Cache 中命中返回。

四、总结

本次 Bonus 优化可以分成两条主线:第一条是分支预测,第二条是取指缓存。前几版中,简单的静态预测虽然能够让部分跳转提前发生,但由于策略过于粗糙,预测错误带来的冲刷流水线代价会抵消甚至超过预测正确的收益。加入 BTFNT 后,条件分支预测开始体现出稳定正收益,总条件分支正确率稳定在约82%左右,其中后向分支接近89%,前向分支约68%,说明循环和普通前向分支的方向特征确实能被静态规则利用。不过当前 jalr 和 ret 仍然没有有效目标预测,后续如果继续优化,应优先加入 BTB 和 RAS。

第五版的主要提升来自8KiB直接映射 I-Cache。MicroBench具有很强的指令局部性,最终 I-Cache 命中率达到约99.98%,平均每次 miss 等待约6.55个周期。由于原本取指路径需要经过总线、MMU 和仲裁等待,I-Cache 命中后可以直接返回指令,前端等待大幅减少,因此 cycleCnt 从第四版的约16.31B降低到第五版的约5.93B,IPC从约0.121提升到约0.332,MicroBench分数也从39602提升到127973。这个分数超过参考i7并不表示仿真 CPU 真实性能超过i7,而是说明在该测试的 guest cycle计分口径下,取指瓶颈被显著缓解。

原子指令

一、实现目标

本部分实现了 RISC-V A 扩展中 Bonus 要求的 32 位原子指令,包括 amoswap.w、amoadd.w、amoxor.w、amoand.w、amoor.w、amomin.w、amomax.w、amominu.w、amomaxu.w,以及 lr.w、sc.w。由于当前 CPU 是单核,实现重点不是多核内存一致性,而是保证单条原子指令在流水线中不会被中断或异常拆开提交。

整体数据流为:decoder 识别原子指令并产生 is_amo/is_lr/is_sc/amo_op,通过 id_ex 和 ex_mem 传到 mem 阶段;mem 阶段用状态机完成 AMO 的读-改-写,用两个 entry 保存 LR/SC 的 reservation set;ctrl 在 AMO 中间阶段冻结流水线,防止原子操作半途被提交或打断。

二、指令类型定义

在 mypkg.sv 中增加 amo_t,用于区分不同 AMO 运算。LR.W 和 SC.W 不需要进入 amo_t,它们通过单独的 is_lr/is_sc 信号判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
mypkg.sv
typedef enum logic [3:0] {
AMO_NONE = 4'd0,
AMO_SWAP = 4'd1,
AMO_ADD = 4'd2,
AMO_XOR = 4'd3,
AMO_AND = 4'd4,
AMO_OR = 4'd5,
AMO_MIN = 4'd6,
AMO_MAX = 4'd7,
AMO_MINU = 4'd8,
AMO_MAXU = 4'd9
} amo_t;

三、译码阶段

decoder 新增原子指令输出端口,并取出 funct5 字段。A 扩展的 opcode 为 0101111,本实验只实现 .w,所以还要求 funct3 == 3'b010。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
decoder.sv
output div_t div_op,
output u1 is_amo, is_lr, is_sc,
output amo_t amo_op
);
u7 opcode;
u3 funct3;
u7 funct7;
u5 funct5;

u1 is_alu_i, is_alu_r, is_alu_iw, is_alu_rw, is_load, is_store, is_lui, is_auipc, is_system, is_csr_imm;
u1 is_atomic, is_atomic_opcode;

assign funct5=inst[31:27];
assign is_atomic_opcode = (opcode == 7'b0101111) && (funct3 == 3'b010);

根据 funct5 区分 LR.W、SC.W 和各类 AMO 指令。其中 lr.w 要求 rs2 字段为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
decoder.sv
assign is_lr = is_atomic_opcode && (funct5 == 5'b00010) && (inst[24:20] == 5'b00000);
assign is_sc = is_atomic_opcode && (funct5 == 5'b00011);
assign is_amo = is_atomic_opcode &&
((funct5 == 5'b00001) ||
(funct5 == 5'b00000) ||
(funct5 == 5'b00100) ||
(funct5 == 5'b01100) ||
(funct5 == 5'b01000) ||
(funct5 == 5'b10000) ||
(funct5 == 5'b10100) ||
(funct5 == 5'b11000) ||
(funct5 == 5'b11100));
assign is_atomic = is_lr || is_sc || is_amo;

在合法指令判断中加入原子指令。

1
2
3
4
decoder.sv
7'b0101111: begin // RV64A word atomics
is_valid_inst = is_atomic;
end

原子指令需要写回 rd,其中 lr.w 和 AMO 需要读内存,sc.w 和 AMO 需要写内存。AMO 的地址为 rs1 + 0,所以复用 ALU 加法通路,并将 imm 置 0。

1
2
3
4
5
decoder.sv
assign use_imm = is_alu_i | is_alu_iw | is_load | is_store | is_lui | is_auipc| is_jal | is_jalr | is_branch | is_atomic;
assign reg_write = is_alu_i | is_alu_r | is_alu_iw | is_alu_rw | is_load | is_lui | is_auipc| is_jal | is_jalr | is_csr | is_atomic;
assign mem_read = is_load | is_lr | is_amo;
assign mem_write = is_store | is_sc | is_amo;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
decoder.sv
always_comb begin
amo_op = AMO_NONE;
else if (is_atomic) begin
op = ALU_ADD;
imm = 64'b0;
wb_sel = WB_LOAD;
msize = MSIZE4;
case (funct5)
5'b00001: amo_op = AMO_SWAP;
5'b00000: amo_op = AMO_ADD;
5'b00100: amo_op = AMO_XOR;
5'b01100: amo_op = AMO_AND;
5'b01000: amo_op = AMO_OR;
5'b10000: amo_op = AMO_MIN;
5'b10100: amo_op = AMO_MAX;
5'b11000: amo_op = AMO_MINU;
5'b11100: amo_op = AMO_MAXU;
default: amo_op = AMO_NONE;
endcase
end
end

四、流水线寄存器传递

id_ex 增加原子指令控制信号,在 reset 和 bubble 时清零,防止无效指令误触发原子访存。

1
2
3
4
5
6
id_ex.sv
input u1 is_amo_in, is_lr_in, is_sc_in,
input amo_t amo_op_in,

output u1 is_amo_out, is_lr_out, is_sc_out,
output amo_t amo_op_out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id_ex.sv
always_ff @(posedge clk) begin
if (reset) begin
is_amo_out <= 1'b0;
is_lr_out <= 1'b0;
is_sc_out <= 1'b0;
amo_op_out <= AMO_NONE;
end else if (enable) begin
amo_op_out <= amo_op_in;
if (valid_in) begin
is_amo_out <= is_amo_in;
is_lr_out <= is_lr_in;
is_sc_out <= is_sc_in;
end else begin
is_amo_out <= 1'b0;
is_lr_out <= 1'b0;
is_sc_out <= 1'b0;
amo_op_out <= AMO_NONE;
end
end
end

ex_mem 同样增加原子指令控制信号,传给 mem 阶段实际执行。

1
2
3
4
5
6
ex_mem.sv
input u1 is_amo_in, is_lr_in, is_sc_in,
input amo_t amo_op_in,

output u1 is_amo_out, is_lr_out, is_sc_out,
output amo_t amo_op_out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ex_mem.sv
always_ff @(posedge clk) begin
if (reset) begin
is_amo_out <= 1'b0;
is_lr_out <= 1'b0;
is_sc_out <= 1'b0;
amo_op_out <= AMO_NONE;
end else if (enable) begin
amo_op_out <= amo_op_in;
if (valid_in) begin
is_amo_out <= is_amo_in;
is_lr_out <= is_lr_in;
is_sc_out <= is_sc_in;
end else begin
is_amo_out <= 1'b0;
is_lr_out <= 1'b0;
is_sc_out <= 1'b0;
amo_op_out <= AMO_NONE;
end
end
end

mem_wb 增加 sc_failed 信号,用于最后连接 difftest 的 scFailed 字段。

1
2
3
4
5
mem_wb.sv
input u1 sc_failed_in,

output xret_t xret_type_out,
output u1 sc_failed_out
1
2
3
4
5
6
7
8
9
10
11
12
mem_wb.sv
always_ff @(posedge clk) begin
if (reset) begin
sc_failed_out <= 1'b0;
end else if (enable) begin
if (valid_in) begin
sc_failed_out <= sc_failed_in && !mem_exc;
end else begin
sc_failed_out <= 1'b0;
end
end
end

五、EX 阶段异常分类

AMO 同时具有读和写语义,但 misaligned 时应按 store/AMO 类异常处理。因此 ex 增加 is_amo 输入,并在 load misaligned 判断中排除 AMO。

1
2
ex.sv
input u1 is_mul, is_div, mem_read, mem_write, is_amo,
1
2
3
ex.sv
assign invalid_load_addr = mem_read && !is_amo && data_addr_misaligned;
assign invalid_store_addr = mem_write && data_addr_misaligned;

六、MEM 阶段原子操作

mem 阶段新增时序状态机、原子控制输入、sc_failed 和 atomic_busy 输出。atomic_busy 用来冻结流水线,避免 AMO 在读旧值后、写新值前被提交。

1
2
3
4
5
6
7
8
9
10
11
mem.sv
input logic clk,
input u1 reset,
input u1 valid,
input u1 kill,
input u1 is_amo, is_lr, is_sc,
input amo_t amo_op,
input u1 mem_exc,

output u1 atomic_busy,
output u1 sc_failed,

AMO_READ 用来等待读旧值,AMO_WRITE 用来把计算后的新值写回原地址。Reservation Set 用两个 entry 保存地址,满足实验中“SC 前最多两个 LR”的假设。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mem.sv
typedef enum u2 {
AMO_IDLE = 2'd0,
AMO_READ = 2'd1,
AMO_WRITE = 2'd2
} amo_state_t;

amo_state_t amo_state;
u32 amo_old_word, amo_new_word;
u64 amo_addr;
u1 res_valid0, res_valid1;
logic [61:0] res_addr0, res_addr1;

u1 atomic_valid, normal_mem_valid, sc_success, amo_read_req, amo_write_req;
u1 lr_read_done, amo_read_done, sc_done, normal_write_done;
u32 mem_read_word, rs2_word, amo_calc_word;
u64 atomic_rd_data, normal_mem_data;

访存请求生成时,普通 load/store 保持原行为;lr.w 只读,sc.w 在 reservation 命中时才写,AMO 则先读后写。所有原子指令都以 MSIZE4 访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mem.sv
assign atomic_valid = valid && !kill && (is_amo || is_lr || is_sc);
assign normal_mem_valid = valid && !kill && (mem_read || mem_write) && !atomic_valid;
assign sc_success = is_sc && ((res_valid0 && (res_addr0 == alu_ans[63:2])) ||
(res_valid1 && (res_addr1 == alu_ans[63:2])));
assign sc_failed = atomic_valid && is_sc && !sc_success;
assign amo_read_req = atomic_valid && is_amo && ((amo_state == AMO_IDLE) || (amo_state == AMO_READ));
assign amo_write_req = atomic_valid && is_amo && (amo_state == AMO_WRITE);

assign dreq_valid = normal_mem_valid ||
(atomic_valid && is_lr) ||
(atomic_valid && is_sc && sc_success) ||
amo_read_req ||
amo_write_req;
assign dreq_addr = amo_write_req ? amo_addr : alu_ans;
assign dreq_size = (atomic_valid || (amo_state != AMO_IDLE)) ? MSIZE4 : msize;

写请求根据当前实际请求地址生成位移和 strobe。AMO 写回的是 amo_new_word,SC 成功时写入 rs2 低 32 位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mem.sv
assign shift_bits = {dreq_addr[2:0], 3'b000};

always_comb begin
case(dreq_size)
MSIZE1: base_strobe = 8'b0000_0001;
MSIZE2: base_strobe = 8'b0000_0011;
MSIZE4: base_strobe = 8'b0000_1111;
MSIZE8: base_strobe = 8'b1111_1111;
default:base_strobe = 8'b0000_0000;
endcase
end

assign dreq_strobe = ((normal_mem_valid && mem_write) ||
(atomic_valid && is_sc && sc_success) ||
amo_write_req) ? (base_strobe << dreq_addr[2:0]) : 8'b0;
assign dreq_data = amo_write_req ? ({32'b0, amo_new_word} << shift_bits) :
((atomic_valid && is_sc) ? ({32'b0, store_data[31:0]} << shift_bits) :
(store_data << shift_bits));

AMO 只处理 32 位数据。写回 rd 的旧值需要符号扩展,sc.w 写回 0 表示成功、1 表示失败。

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
mem.sv
assign normal_mem_data = final_mem_data;
assign mem_read_word = shifted_read_data[31:0];
assign rs2_word = store_data[31:0];

always_comb begin
amo_calc_word = 32'b0;
case (amo_op)
AMO_SWAP: amo_calc_word = rs2_word;
AMO_ADD: amo_calc_word = mem_read_word + rs2_word;
AMO_XOR: amo_calc_word = mem_read_word ^ rs2_word;
AMO_AND: amo_calc_word = mem_read_word & rs2_word;
AMO_OR: amo_calc_word = mem_read_word | rs2_word;
AMO_MIN: amo_calc_word = ($signed(mem_read_word) < $signed(rs2_word)) ? mem_read_word : rs2_word;
AMO_MAX: amo_calc_word = ($signed(mem_read_word) < $signed(rs2_word)) ? rs2_word : mem_read_word;
AMO_MINU: amo_calc_word = (mem_read_word < rs2_word) ? mem_read_word : rs2_word;
AMO_MAXU: amo_calc_word = (mem_read_word < rs2_word) ? rs2_word : mem_read_word;
default: amo_calc_word = mem_read_word;
endcase
end

assign atomic_rd_data = is_sc ? {63'b0, sc_failed} :
((amo_state == AMO_WRITE) ? {{32{amo_old_word[31]}}, amo_old_word} :
{{32{mem_read_word[31]}}, mem_read_word});
assign mem_data_out = (is_amo || is_lr || is_sc) ? atomic_rd_data : normal_mem_data;

AMO 读旧值返回后一拍进入 AMO_WRITE,此时 atomic_busy 拉高,让流水线继续冻结,确保不会出现“旧值已经写回 rd,但新值还没写回内存”的半提交状态。

1
2
3
4
5
6
7
8
9
mem.sv
assign lr_read_done = atomic_valid && is_lr && dresp_data_ok && !mem_exc;
assign amo_read_done = atomic_valid && is_amo &&
(((amo_state == AMO_IDLE) || (amo_state == AMO_READ)) && dresp_data_ok);
assign sc_done = atomic_valid && is_sc && (!sc_success || dresp_data_ok);
assign normal_write_done = normal_mem_valid && mem_write && dresp_data_ok && !mem_exc;
assign atomic_busy = amo_read_done && !mem_exc;

assign is_mmio_out = dreq_valid && (dreq_addr[31] == 1'b0);

状态机在 reset 或 kill 时回到空闲。读旧值成功后保存旧值、新值和地址,随后发起写请求,写完成后回到 AMO_IDLE。

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
mem.sv
always_ff @(posedge clk) begin
if (reset) begin
amo_state <= AMO_IDLE;
amo_old_word <= 32'b0;
amo_new_word <= 32'b0;
amo_addr <= 64'b0;
res_valid0 <= 1'b0;
res_valid1 <= 1'b0;
res_addr0 <= 62'b0;
res_addr1 <= 62'b0;
end else if (kill) begin
amo_state <= AMO_IDLE;
end else begin
case (amo_state)
AMO_IDLE: begin
if (amo_read_done && !mem_exc) begin
amo_state <= AMO_WRITE;
amo_old_word <= mem_read_word;
amo_new_word <= amo_calc_word;
amo_addr <= alu_ans;
end
end
AMO_READ: begin
if (amo_read_done && !mem_exc) begin
amo_state <= AMO_WRITE;
amo_old_word <= mem_read_word;
amo_new_word <= amo_calc_word;
amo_addr <= alu_ans;
end else if (amo_read_done) begin
amo_state <= AMO_IDLE;
end
end
AMO_WRITE: begin
if (dresp_data_ok) begin
amo_state <= AMO_IDLE;
end
end
default: begin
amo_state <= AMO_IDLE;
end
endcase

if (atomic_valid && is_amo && (amo_state == AMO_IDLE) && !dresp_data_ok) begin
amo_state <= AMO_READ;
end
end
end

lr.w 成功读取后设置 reservation;sc.w 无论成功或失败都会清空当前核的 reservation。普通 store 或 AMO 写入对应地址时,也会清除命中的 reservation。

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
mem.sv
always_ff @(posedge clk) begin
if (lr_read_done) begin
if (res_valid0 && (res_addr0 == alu_ans[63:2])) begin
res_valid0 <= 1'b1;
end else if (res_valid1 && (res_addr1 == alu_ans[63:2])) begin
res_valid1 <= 1'b1;
end else if (!res_valid0) begin
res_valid0 <= 1'b1;
res_addr0 <= alu_ans[63:2];
end else begin
res_valid1 <= 1'b1;
res_addr1 <= alu_ans[63:2];
end
end

if (sc_done) begin
res_valid0 <= 1'b0;
res_valid1 <= 1'b0;
end else if (normal_write_done || (amo_write_req && dresp_data_ok && !mem_exc)) begin
if (res_valid0 && (res_addr0 == dreq_addr[63:2])) begin
res_valid0 <= 1'b0;
end
if (res_valid1 && (res_addr1 == dreq_addr[63:2])) begin
res_valid1 <= 1'b0;
end
end
end

七、流水线控制

原子指令和 load 一样,写回结果到 MEM/WB 后才可靠,因此 load-use 冒险检测增加 ex_late_wb。另外 mem_atomic_busy 会压低 global_enable,让 AMO 读改写的中间周期冻结流水线。

1
2
3
4
ctrl.sv
input u1 ex_mem_read, ex_late_wb, predict_redirect, ex_redirect, commit_flush,
input u1 ex_is_mul, ex_mul_done, ex_is_div, ex_div_done,
input u1 mem_atomic_busy,
1
2
3
4
5
ctrl.sv
assign load_use_stall = (ex_mem_read || ex_late_wb) && (ex_rd_id != 5'd0) &&
((ex_rd_id == id_rs1_id) || (ex_rd_id == id_rs2_id));

assign global_enable = ~dbus_stall && ~mem_atomic_busy;

八、顶层连接

core 中增加原子指令信号,并按 IF/ID、ID/EX、EX/MEM、MEM/WB 的方向传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
core.sv
u1 decoder_is_amo, decoder_is_lr, decoder_is_sc;
amo_t decoder_amo_op;

u1 id_ex_is_amo, id_ex_is_lr, id_ex_is_sc;
amo_t id_ex_amo_op;

u1 ex_mem_is_amo, ex_mem_is_lr, ex_mem_is_sc;
amo_t ex_mem_amo_op;

u1 mem_atomic_busy;
u1 mem_sc_failed;

u1 mem_wb_sc_failed;

译码器实例接出原子指令信号。

1
2
3
4
5
6
7
8
core.sv
decoder u_decoder(
.div_op (decoder_div_op),
.is_amo (decoder_is_amo),
.is_lr (decoder_is_lr),
.is_sc (decoder_is_sc),
.amo_op (decoder_amo_op)
);

控制器实例接入 ex_late_wb 和 mem_atomic_busy。

1
2
3
4
5
6
core.sv
ctrl u_ctrl(
.ex_mem_read (id_ex_mem_read),
.ex_late_wb (id_ex_is_amo || id_ex_is_lr || id_ex_is_sc),
.mem_atomic_busy(mem_atomic_busy)
);

id_ex 实例把 decoder 的原子信号送入 EX 阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
core.sv
id_ex u_id_ex(
.div_op_in (decoder_div_op),
.is_amo_in (decoder_is_amo && ~id_exc_valid),
.is_lr_in (decoder_is_lr && ~id_exc_valid),
.is_sc_in (decoder_is_sc && ~id_exc_valid),
.amo_op_in (decoder_amo_op),

.div_op_out (id_ex_div_op),
.is_amo_out (id_ex_is_amo),
.is_lr_out (id_ex_is_lr),
.is_sc_out (id_ex_is_sc),
.amo_op_out (id_ex_amo_op)
);

ex 实例接入 is_amo,用于异常类型判断。

1
2
3
4
5
6
core.sv
ex u_ex(
.mem_read (id_ex_mem_read),
.mem_write (id_ex_mem_write),
.is_amo (id_ex_is_amo)
);

ex_mem 实例继续传递原子控制信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
core.sv
ex_mem u_ex_mem(
.xret_type_in (ex_exc_valid ? XRET_NONE : id_ex_xret_type),
.is_amo_in (id_ex_is_amo && ~ex_exc_valid),
.is_lr_in (id_ex_is_lr && ~ex_exc_valid),
.is_sc_in (id_ex_is_sc && ~ex_exc_valid),
.amo_op_in (id_ex_amo_op),

.xret_type_out (ex_mem_xret_type),
.is_amo_out (ex_mem_is_amo),
.is_lr_out (ex_mem_is_lr),
.is_sc_out (ex_mem_is_sc),
.amo_op_out (ex_mem_amo_op)
);

mem 实例执行原子访存,并输出 atomic_busy 和 sc_failed。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
core.sv
mem u_mem(
.clk (clk),
.reset (reset),
.valid (ex_mem_valid),
.kill (ex_mem_exc_valid || trap_kill_mem),
.is_amo (ex_mem_is_amo),
.is_lr (ex_mem_is_lr),
.is_sc (ex_mem_is_sc),
.amo_op (ex_mem_amo_op),
.mem_exc (dbus_arbiter_mem_exc),

.atomic_busy (mem_atomic_busy),
.sc_failed (mem_sc_failed)
);

mem_wb 实例保存 sc_failed,最后在 difftest 提交时使用。

1
2
3
4
5
6
7
8
core.sv
mem_wb u_mem_wb(
.xret_type_in (ex_mem_xret_type),
.sc_failed_in (mem_sc_failed),

.xret_type_out (mem_wb_xret_type),
.sc_failed_out (mem_wb_sc_failed)
);
1
2
3
4
core.sv
DifftestInstrCommit DifftestInstrCommit(
.scFailed (mem_wb_sc_failed)
);

通过截图

xv6

一、相较于完整 xv6,当前 CPU 还缺什么

1. 特权架构

这涉及异常/中断处理时的特权级别切换,相对来说已经比较完善。因此首先我补完了这个部分。

1)将更多异常和中断开放给 S 态委托

原先 medeleg 只开放了 U/S 态 ecall,mideleg 仍受 include 中的空 mask 限制。为了让 xv6 在 S 态处理缺页、非法指令、访问异常以及 S 级中断,我在 csr_file 中改成使用本地可写 mask。

1
2
3
csr_file.sv
localparam u64 MEDELEG_WRITABLE_MASK = 64'h000000000000B3FF;
localparam u64 MIDELEG_WRITABLE_MASK = 64'h0000000000000222;

写入委托寄存器时,medeleg 和 mideleg 都按新的本地 mask 保留合法位。

1
2
3
4
5
6
7
8
9
10
11
csr_file.sv
always_comb begin
if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_MEDELEG: medeleg = csr_write_data & MEDELEG_WRITABLE_MASK;
CSR_MIDELEG: mideleg = csr_write_data & MIDELEG_WRITABLE_MASK;
default: begin
end
endcase
end
end

2)根据 mideleg 把课程中断输入映射到 S 级 pending 位

课程顶层只提供 swint/trint/exint 三根输入,原本它们只进入 M 级 pending 位。这里在 mideleg 打开对应位时,把 M 级 pending 映射为 S 级 pending,并清掉对应的外部 M 级 pending。

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
csr_file.sv
localparam int unsigned SSIP_BIT = 1;
localparam int unsigned MSIP_BIT = 3;
localparam int unsigned STIP_BIT = 5;
localparam int unsigned MTIP_BIT = 7;
localparam int unsigned SEIP_BIT = 9;
localparam int unsigned MEIP_BIT = 11;

function automatic u64 map_external_mip(input u64 value, input u64 delegation);
begin
map_external_mip = value;
if (delegation[SSIP_BIT]) begin
map_external_mip[SSIP_BIT] = value[SSIP_BIT] | value[MSIP_BIT];
map_external_mip[MSIP_BIT] = 1'b0;
end
if (delegation[STIP_BIT]) begin
map_external_mip[STIP_BIT] = value[STIP_BIT] | value[MTIP_BIT];
map_external_mip[MTIP_BIT] = 1'b0;
end
if (delegation[SEIP_BIT]) begin
map_external_mip[SEIP_BIT] = value[SEIP_BIT] | value[MEIP_BIT];
map_external_mip[MEIP_BIT] = 1'b0;
end
end
endfunction

组合输出阶段先用寄存器中的 mideleg_reg 计算当前可见的 mip,在同一拍 CSR 写入修改 mideleg 后,再用新的 mideleg 重新计算一次,保证委托写入后 pending 映射能立刻反映到 CSR 输出。

1
2
3
4
5
6
7
8
9
10
csr_file.sv
always_comb begin
mip_next = mip_reg;
external_mip_mapped = map_external_mip(external_mip, mideleg_reg);
mip = mip_next | external_mip_mapped;

external_mip_mapped = map_external_mip(external_mip, mideleg);
mip = mip_next | external_mip_mapped;
sstatus = mstatus & SSTATUS_LOCAL_MASK;
end

3)interrupt_unit 同时支持 M/S 两组中断

interrupt_unit 新增 mideleg 输入,用来判断 S 级中断是否已经委托。

1
2
interrupt_unit.sv
input u64 mstatus, mip, mie, mideleg,

模块内部补齐 S 级中断位、pending、enable、delegated 和 can-take 信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
interrupt_unit.sv
localparam int unsigned SSIP_BIT = 1;
localparam int unsigned MSIP_BIT = 3;
localparam int unsigned STIP_BIT = 5;
localparam int unsigned MTIP_BIT = 7;
localparam int unsigned SEIP_BIT = 9;
localparam int unsigned MEIP_BIT = 11;

u1 m_interrupt_enable, s_interrupt_enable;
u1 ssip_pending, msip_pending, stip_pending, mtip_pending, seip_pending, meip_pending;
u1 ssip_enabled, msip_enabled, stip_enabled, mtip_enabled, seip_enabled, meip_enabled;
u1 ssip_delegated, stip_delegated, seip_delegated;
u1 ssip_can_take, msip_can_take, stip_can_take, mtip_can_take, seip_can_take, meip_can_take;

M 级中断沿用原本规则:当前不是 M 态时天然可被 M 态接管,当前是 M 态时需要 mstatus.MIE=1。S 级中断在 U 态天然可接管,在 S 态需要 sstatus.SIE=1。

1
2
3
4
5
interrupt_unit.sv
assign m_interrupt_enable = (current_priv != PRIV_M) ||
mstatus[MSTATUS_MIE_BIT];
assign s_interrupt_enable = (current_priv == PRIV_U) ||
((current_priv == PRIV_S) && mstatus[MSTATUS_SIE_BIT]);

各类 pending、enable 和 delegated 组合成 can-take 条件。S 级中断必须满足 mideleg 对应位有效。

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
interrupt_unit.sv
assign ssip_pending = mip[SSIP_BIT];
assign msip_pending = mip[MSIP_BIT];
assign stip_pending = mip[STIP_BIT];
assign mtip_pending = mip[MTIP_BIT];
assign seip_pending = mip[SEIP_BIT];
assign meip_pending = mip[MEIP_BIT];

assign ssip_enabled = mie[SSIP_BIT];
assign msip_enabled = mie[MSIP_BIT];
assign stip_enabled = mie[STIP_BIT];
assign mtip_enabled = mie[MTIP_BIT];
assign seip_enabled = mie[SEIP_BIT];
assign meip_enabled = mie[MEIP_BIT];

assign ssip_delegated = mideleg[SSIP_BIT];
assign stip_delegated = mideleg[STIP_BIT];
assign seip_delegated = mideleg[SEIP_BIT];

assign ssip_can_take = ssip_pending && ssip_enabled && ssip_delegated;
assign msip_can_take = msip_pending && msip_enabled;
assign stip_can_take = stip_pending && stip_enabled && stip_delegated;
assign mtip_can_take = mtip_pending && mtip_enabled;
assign seip_can_take = seip_pending && seip_enabled && seip_delegated;
assign meip_can_take = meip_pending && meip_enabled;

最终中断 cause 从原来的 M 级 3/7/11 扩展为同时支持 S 级 1/5/9。当前接管顺序采用 M 级优先,即 MEIP -> MSIP -> MTIP -> SEIP -> SSIP -> STIP。这样当 M/S 中断同时 pending 时,会先进入 M 态 trap,避免 S 级委托中断压住 M 级中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interrupt_unit.sv
always_comb begin
if (!reset && can_accept_interrupt) begin
if (m_interrupt_enable && meip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd11};
end else if (m_interrupt_enable && msip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd3};
end else if (m_interrupt_enable && mtip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd7};
end else if (s_interrupt_enable && (current_priv != PRIV_M) && seip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd9};
end else if (s_interrupt_enable && (current_priv != PRIV_M) && ssip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd1};
end else if (s_interrupt_enable && (current_priv != PRIV_M) && stip_can_take) begin
interrupt_valid = 1'b1;
interrupt_cause = {1'b1, 63'd5};
end
end
end

4)trap_unit 使用 mideleg 判断中断目标特权级

trap_unit 新增 csr_mideleg 输入。同步异常看 medeleg,中断看 mideleg,且只有当前不是 M 态时才允许委托到 S 态。

1
2
3
4
5
6
7
8
9
10
11
12
trap_unit.sv
input u64 csr_mideleg,

u1 trap_is_interrupt;
u1 trap_delegated;

assign trap_is_interrupt = mem_wb_exc_cause[63];
assign trap_delegated = (current_priv != PRIV_M) &&
(trap_is_interrupt ?
csr_mideleg[mem_wb_exc_cause[5:0]] :
csr_medeleg[mem_wb_exc_cause[5:0]]);
assign trap_target_priv = trap_delegated ? PRIV_S : PRIV_M;

重定向 PC 由目标特权级先选择 stvec/mtvec,再在后续 mtvec/stvec 小节中拆出对齐后的 base 和 mode,避免把 mode 位当作真实 PC 低位。

5)顶层接入 mideleg

core 将 CSR 中的 mideleg 接给 interrupt_unit,使中断单元能判断 S 级中断是否可接管。

1
2
3
4
core.sv
interrupt_unit u_interrupt_unit(
.mideleg (csr_mideleg)
);

同时将 mideleg 接给 trap_unit,使提交阶段可以根据中断委托位选择写入 sepc/scause/stval 还是 mepc/mcause/mtval。

1
2
3
4
core.sv
trap_unit u_trap_unit(
.csr_mideleg (csr_mideleg)
);

6)mtvec/stvec 保留 mode 位,trap 跳转时再计算入口

原先为了避免软件写入 vectored mode 后把 mtvec/stvec 的 mode bit 当作 PC 低位,我在写 CSR 时直接清掉低 2 位,只保存对齐后的 trap base。这个做法可以避免跳到非对齐地址,但会改变 CSR 本身的架构可见值:例如 Lab 4 测试中写入 mtvec=0xd 时,参考模型保留 mode=1,而 CPU 会读出 0xc,导致 Difftest 的 CSR 状态对不上。

因此这里把策略改成:CSR 内部保留合法的 mode=0/1,只清掉当前不支持的 bit1;真正发生 trap 时,再在 trap_unit 中拆出 {tvec[63:2], 2'b00} 作为对齐后的 base。这样既兼容 Lab 4 对 mtvec/stvec 可见值的检查,也能让 xv6 写入 vectored mode 时不会把 mode 位误用成跳转地址。

改动完成后, Lab 4 测试仍然可以通过

1
2
3
4
5
6
7
8
csr_file.sv
localparam u64 TVEC_MODE_MASK = ~64'h2;

function automatic u64 legalize_tvec(input u64 value);
begin
legalize_tvec = value & TVEC_MODE_MASK;
end
endfunction

mtvec/stvec 写入路径改为调用新的合法化函数。写入时不再清 bit0,因此 mode=1 可以作为 CSR 状态被读回;bit1 仍清零,避免保留编码进入后续 trap 入口计算。

1
2
3
4
5
6
7
8
9
10
11
csr_file.sv
always_comb begin
if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_MTVEC: mtvec = legalize_tvec(csr_write_data);
CSR_STVEC: stvec = legalize_tvec(csr_write_data);
default: begin
end
endcase
end
end

trap_unit 中新增中间信号,先根据目标特权级选择 csr_stvec 或 csr_mtvec,再拆出对齐后的 base。若当前是 interrupt 且 mode 为 2'b01,则按 vectored mode 计算 base + 4 * cause;同步异常仍跳 base。

1
2
3
4
5
6
7
8
9
trap_unit.sv
u64 trap_tvec, trap_tvec_base, trap_vector_offset, trap_redirect_pc;

assign trap_tvec = (trap_target_priv == PRIV_S) ? csr_stvec : csr_mtvec;
assign trap_tvec_base = {trap_tvec[63:2], 2'b00};
assign trap_vector_offset = (trap_is_interrupt && (trap_tvec[1:0] == 2'b01)) ?
{56'b0, mem_wb_exc_cause[5:0], 2'b00} :
64'b0;
assign trap_redirect_pc = trap_tvec_base + trap_vector_offset;

最后,redirect_pc 不再直接使用 CSR 原值,而是使用已经计算好的 trap_redirect_pc。这样 CSR 里的 mode 位可以保留给 Difftest 和软件读取,PC 跳转路径仍然始终使用对齐后的入口地址。

1
2
3
trap_unit.sv
assign redirect_pc = trap_valid ? trap_redirect_pc :
((mem_wb_xret_type == XRET_S) ? csr_sepc : csr_mepc);

2. MMU

MMU 涉及的改动较少,只局限于访存部分,也比较好做

这一步主要补上 xv6 运行用户态时必须依赖的 SUM/MXR/MPRV 语义。PTE A/D 暂时不在硬件中自动写回:为了兼容 Lab 5 官方镜像和未预置 A/D 的 xv6 页表,当前 CPU 侧选择忽略 A/D 位,不再因为 A=0 或 store 时 D=0 触发 page fault。

原本也可以修改 xv6,让 xv6 在创建页表项时就把 A/D 置 1,也就是由操作系统软件预置访问位和脏位。但这样会让 CPU 侧仍依赖软件配合;而 Lab 5 官方镜像中的 mappages() 只写入 PA2PTE(pa) | perm | PTE_V,没有预置 A/D,因此硬件侧继续严格检查 A/D 会导致合法页第一次访问时被误判成 page fault。

硬件自动设置 A/D 也不是单纯改一个判断条件。MMU 在页表遍历时一旦发现 A=0 或 store 时 D=0,就需要额外发起一次页表项写回,等待总线完成,再重新尝试原始访存。这会给当前已经有多级页表遍历、PMP 检查、I/D 总线仲裁和流水线冻结的控制路径引入新的 stall 和重试状态,容易让整条流水线的提交、异常和访存副作用控制变得复杂。因此当前先跳过 PTE A/D 自动写回,也不把 A/D 作为 page fault 条件,只保留 V/R/W/X/U/SUM/MXR 和巨页对齐等关键权限检查。

1)补充 SUM/MXR 位定义

MPRV 原先已经存在,这里继续补充 SUM 和 MXR 的位号,供 CSR 和 MMU 共用。

1
2
3
4
5
6
mypkg.sv
typedef enum logic [5:0] {
MSTATUS_MPRV_BIT = 6'd17,
MSTATUS_SUM_BIT = 6'd18,
MSTATUS_MXR_BIT = 6'd19
} mstatus_bit_t;

2)让 sstatus 暴露并可写 SUM/MXR

xv6 通常通过 sstatus 控制 SUM/MXR,所以不能只让 mstatus 写掩码包含这些位,还需要让 sstatus 的本地 mask 暴露它们。

1
2
3
4
5
6
7
csr_file.sv
localparam u64 SSTATUS_LOCAL_MASK = SSTATUS_MASK |
(64'b1 << MSTATUS_SIE_BIT) |
(64'b1 << MSTATUS_SPIE_BIT) |
(64'b1 << MSTATUS_SPP_BIT) |
(64'b1 << MSTATUS_SUM_BIT) |
(64'b1 << MSTATUS_MXR_BIT);

3)把 mstatus 接入 MMU

MMU 需要读取 MPRV/MPP/SUM/MXR,因此顶层把 CSR 文件输出的 csr_mstatus 接入 MMU。

1
2
3
4
core.sv
MMU u_mmu(
.mstatus (csr_mstatus)
);

MMU 模块接口对应增加 mstatus 输入。

1
2
mmu.sv
input u64 mstatus,

4)计算数据访存的有效特权级

取指始终使用当前特权级;数据访存则在 MPRV=1 时使用 MPP 作为有效访存特权级。为了避免非法 MPP 值污染权限判断,我先用 legalize_priv 做一次合法化。

1
2
3
4
5
6
7
8
9
10
11
12
13
mmu.sv
priv_t effective_priv, saved_priv;

function automatic priv_t legalize_priv(input u2 value);
begin
unique case (value)
PRIV_U: legalize_priv = PRIV_U;
PRIV_S: legalize_priv = PRIV_S;
PRIV_M: legalize_priv = PRIV_M;
default: legalize_priv = PRIV_M;
endcase
end
endfunction

PMP 检查和是否启用地址翻译都改用 effective_priv。这样 M 态设置 MPRV+MPP 后,数据访存会按目标特权级执行权限检查。

1
2
3
4
5
6
7
8
9
mmu.sv
pmp_checker u_req_pmp_checker(
.current_priv (effective_priv)
);

assign effective_priv = ((req_access_type != MMU_REQ_INST) && mstatus[MSTATUS_MPRV_BIT]) ?
legalize_priv(mstatus[MSTATUS_MPP_HIGH:MSTATUS_MPP_LOW]) :
current_priv;
assign translation_enabled = (effective_priv != PRIV_M) && (satp[63:60] == 4'd8);

5)锁存 SUM/MXR 并实现 S/U 页权限判断

页表遍历可能跨多个周期,因此请求进入 MMU 时需要锁存有效特权级以及当时的 SUM/MXR,保证同一次访问的权限判断稳定。

1
2
mmu.sv
u1 saved_sum, saved_mxr;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mmu.sv
always_ff @(posedge clk) begin
if (reset) begin
saved_priv <= PRIV_M;
saved_sum <= 1'b0;
saved_mxr <= 1'b0;
end else begin
case (state)
MMU_IDLE: begin
if (core_req.valid) begin
saved_priv <= effective_priv;
saved_sum <= mstatus[MSTATUS_SUM_BIT];
saved_mxr <= mstatus[MSTATUS_MXR_BIT];
end
end
default: begin
end
endcase
end
end

权限判断中,U 态仍只能访问 PTE_U=1 的页;S 态取指不能从用户页取指;S 态访问用户数据页则需要 SUM=1。

1
2
3
4
mmu.sv
assign pte_priv_fault = ((saved_priv == PRIV_U) && !pte_u) ||
((saved_priv == PRIV_S) &&
((saved_access_type == MMU_REQ_INST) ? pte_u : (pte_u && !saved_sum)));

6)实现 MXR

原先 load 只能访问 PTE_R=1 的页;加入 MXR 后,当 MXR=1 时,load 也允许读取 execute-only 页。

1
2
3
4
5
6
7
8
9
10
mmu.sv
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 || (saved_mxr && pte_x));
MMU_REQ_STORE: pte_access_fault = !pte_w;
default: pte_access_fault = 1'b0;
endcase
end

3. 中间调试

做到这里发现 Lab 4,5,6都过不了了,于是开始排错

1) Lab 4:mtvec/stvec 不能在 CSR 内部直接清低两位

Lab 4 挂掉的原因是 mtvec/stvec 的 CSR 可见值和参考模型不一致。原先为了避免 trap 时跳到带 mode 位的非对齐地址,我在写 CSR 时直接清掉低两位;但 mtvec/stvec 低两位本来就是架构可见的 mode 字段,测试写入 0xd 时参考模型会保留 mode=1,CPU 若读出 0xc 就会 Difftest 失败。

因此修正为:CSR 中保留合法的 mode=0/1,只清掉不支持的 bit1;真正跳转时再由 trap_unit 拆出对齐后的 base。

1
2
3
4
5
6
7
8
csr_file.sv
localparam u64 TVEC_MODE_MASK = ~64'h2;

function automatic u64 legalize_tvec(input u64 value);
begin
legalize_tvec = value & TVEC_MODE_MASK;
end
endfunction

mtvec/stvec 写入路径调用新的合法化函数,保留 bit0 作为 mode。

1
2
3
4
5
6
7
8
9
10
11
csr_file.sv
always_comb begin
if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_MTVEC: mtvec = legalize_tvec(csr_write_data);
CSR_STVEC: stvec = legalize_tvec(csr_write_data);
default: begin
end
endcase
end
end

trap 重定向时单独计算入口地址。同步异常跳 base;中断且 mode 为 vectored 时跳 base + 4 * cause。

1
2
3
4
5
6
7
8
9
trap_unit.sv
u64 trap_tvec, trap_tvec_base, trap_vector_offset, trap_redirect_pc;

assign trap_tvec = (trap_target_priv == PRIV_S) ? csr_stvec : csr_mtvec;
assign trap_tvec_base = {trap_tvec[63:2], 2'b00};
assign trap_vector_offset = (trap_is_interrupt && (trap_tvec[1:0] == 2'b01)) ?
{56'b0, mem_wb_exc_cause[5:0], 2'b00} :
64'b0;
assign trap_redirect_pc = trap_tvec_base + trap_vector_offset;
1
2
3
trap_unit.sv
assign redirect_pc = trap_valid ? trap_redirect_pc :
((mem_wb_xret_type == XRET_S) ? csr_sepc : csr_mepc);

2) Lab 5:PMP 未启用时不能把 S/U 访问全部拒绝

继续跑 xv6 时,第一次进入用户态后原先报 mcause=1,也就是 instruction access fault。排查后发现不是 Sv39 地址格式错误,而是 pmpcfg0.A=0 时没有任何 PMP entry 生效,原逻辑把 S/U 态未匹配 PMP entry 当作拒绝访问。课程的 PMP 测试会在进入用户态前显式配置 PMP,但 xv6 当前启动路径没有配置 PMP,所以空 PMP 状态不应该拦截所有 S/U 访问。

这里新增 pmp_active 表示 PMP entry 是否启用。

1
2
3
4
pmp_checker.sv
u1 pmp_r, pmp_w, pmp_x, pmp_l, pmp_active, access_overflow;

assign pmp_active = (pmp_a != 2'b00);

权限判断中,若 pmpcfg0.A=0,直接认为 PMP 未启用并放行;只有显式配置 PMP 后,才继续按 M 态特例、地址匹配和 R/W/X 权限判断。

1
2
3
4
5
6
7
8
9
10
11
12
pmp_checker.sv
always_comb begin
if (!pmp_active) begin
allow = 1'b1;
end else if ((current_priv == PRIV_M) && !pmp_l) begin
allow = 1'b1;
end else if (!addr_match) begin
allow = (current_priv == PRIV_M);
end else begin
allow = perm_allow;
end
end

3) Lab 5:CPU 侧暂时放行 PTE A/D

PMP 放行后,xv6 第一次进入用户态取指不再是 access fault,而变成 mcause=12 的 instruction page fault。原因是当前 CPU 保留了 Lab 5 缺页异常中对 PTE A/D 的检查:A=0 会 page fault,store 时 D=0 也会 page fault。

但 Lab 5 官方镜像和当前 xv6 的 mappages() 都是按基础 flags 建页表,新建 leaf PTE 不一定预置 A/D。如果 CPU 继续严格检查 A/D,合法映射也会在第一次访问时反复触发 page fault。硬件自动写回 A/D 又需要额外的 PTE 写回和重试状态机,改动范围比较大。因此这里选择更小的兼容方案:CPU 侧暂时忽略 A/D 位。

修改后,A=0、D=0 不再参与 page fault 判断;但 V/R/W/X/U/SUM/MXR、非叶子项、非法 W=1 && R=0、巨页低位不对齐等检查仍然保留。

1
2
mmu.sv
assign pte_ad_fault = 1'b0;

4) Lab 5:修正 flush 后旧取指 miss 的上下文

继续跑 xv6 时,A/D 放行后不再触发 page fault,但在用户态 ecall 附近出现 0x7ffff0010 的 RAM out of range。这个地址是用户态顺序预取产生的虚拟 PC,本来应该按 U 态和用户页表走 Sv39 翻译;但旧取指 miss 可能在 trap flush 后才被 MMU 接收,如果仍使用当时全局的 M 态 current_priv,MMU 会走 direct path,把虚拟地址直接送到 RAM。

这一步补上两个约束:第一,flush 后的 I-Cache miss 不再继续向下游发请求;第二,DBus 仲裁器锁存请求发起时的特权级,MMU 使用锁存后的 mmu_core_priv 判断是否需要翻译。

首先在 core 中增加 MMU 请求对应的特权级信号。

1
2
3
4
5
core.sv
dbus_req_t mmu_core_req;
dbus_resp_t mmu_core_resp;
mmu_access_t mmu_core_access_type;
priv_t mmu_core_priv;

重定向时,PC 不能继续等待旧取指返回,否则旧 PC 会在 flush 后重新发起同一个 miss。但普通 jalr/branch 不能绕过取指响应,否则会出现“PC 已跳到目标地址,但 IF/ID 收到的还是顺序地址指令”的错配。因此这里单独增加 pc_trap_pending_jump,只允许 trap/xret 这类会 flush 前端的重定向在旧取指未返回时推进 PC。

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
core.sv
word_t pc_cur;
u1 pc_pending_jump, pc_trap_pending_jump;

u1 pc_should_jump, pc_fetch_enable, pc_redirect_enable, pc_enable_real;
u64 pc_jump_addr;

assign pc_fetch_enable = ctrl_pc_enable && fetch_iresp.data_ok;
assign pc_redirect_enable = (trap_unit_redirect_valid || pc_trap_pending_jump) && ctrl_if_id_enable;
assign pc_enable_real = (pc_fetch_enable || pc_redirect_enable) && !interrupt_unit_interrupt_valid;

always_ff @(posedge clk) begin
if (reset) begin
pc_trap_pending_jump <= 1'b0;
end else if (pc_enable_real) begin
pc_trap_pending_jump <= 1'b0;
end else if (trap_unit_redirect_valid) begin
pc_trap_pending_jump <= 1'b1;
end
end

pc u_pc(
.clk, .reset,
.enable (pc_enable_real),
.should_jump(pc_should_jump),
.jump_addr (pc_jump_addr),
.cur_pc (pc_cur),
.pending_jump(pc_pending_jump)
);

DBusArbiter 接收 current_priv_comb,这样 mret/sret/trap 提交同拍的新取指请求会锁存即将生效的特权级;同时将锁存后的 oreq_priv 送给 MMU。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
core.sv
DBusArbiter u_dbus_arbiter(
.clk, .reset,
.ireq (icache_miss_req),
.dreq (mem_dreq),
.oresp (mmu_core_resp),
.exc_valid (mmu_exc_valid),
.flush_ireq (icache_flush),
.current_priv (current_priv_comb),

.iresp (icache_miss_resp),
.dresp (mem_dresp),
.oreq (mmu_core_req),
.oreq_priv (mmu_core_priv),
.oreq_access_type(mmu_core_access_type),
.pc_exc (dbus_arbiter_pc_exc),
.mem_exc (dbus_arbiter_mem_exc)
);

MMU 不再直接使用全局 current_priv,而是使用仲裁器随请求输出的 mmu_core_priv。

1
2
3
4
5
6
7
8
9
10
11
core.sv
MMU u_mmu(
.clk, .reset,
.current_priv (mmu_core_priv),
.mstatus (csr_mstatus),
.satp (csr_satp),
.pmpcfg0 (csr_pmpcfg0),
.pmpaddr0 (csr_pmpaddr0),
.core_req (mmu_core_req),
.req_access_type(mmu_core_access_type)
);

DBusArbiter 接口新增 flush_ireq、current_priv 和 oreq_priv。内部增加 saved_kill_iresp 用于丢弃已经被 flush 的旧取指响应,增加 selected_priv/saved_priv 用于锁存请求发起时的特权级。

1
2
3
4
5
6
7
8
9
10
11
12
13
dbus_arbiter.sv
input u1 flush_ireq,
input priv_t current_priv,

output dbus_req_t oreq,
output priv_t oreq_priv,
output mmu_access_t oreq_access_type,
output u1 pc_exc, mem_exc
);
u1 busy, saved_select_is_dbus, saved_kill_iresp;
dbus_req_t selected_req, saved_req;
mmu_access_t selected_access_type, saved_access_type;
priv_t selected_priv, saved_priv;

组合选择请求时,同步选出对应的 selected_priv。取指和访存都记录当前传入的请求上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dbus_arbiter.sv
always_comb begin
selected_req = '0;
selected_access_type = MMU_REQ_LOAD;
selected_priv = current_priv;

if (dreq.valid) begin
selected_req = dreq;
selected_access_type = (dreq.strobe == 8'b0) ? MMU_REQ_LOAD : MMU_REQ_STORE;
selected_priv = current_priv;
end else if (ireq.valid) begin
selected_req = ireq;
selected_access_type = MMU_REQ_INST;
selected_priv = current_priv;
end
end

仲裁器输出请求时,若处于 busy 状态就输出已经锁存的地址、访问类型和特权级。若旧取指已经被 flush 标记为 killed,则不再上报取指异常,也不把响应送回 I-Cache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dbus_arbiter.sv
assign oreq = busy ? saved_req : selected_req;
assign oreq_priv = busy ? saved_priv : selected_priv;
assign oreq_access_type = busy ? saved_access_type : selected_access_type;
assign pc_exc = busy && oresp.data_ok && exc_valid && !saved_select_is_dbus && !saved_kill_iresp;
assign mem_exc = busy && oresp.data_ok && exc_valid && saved_select_is_dbus;

always_comb begin
dresp = '0;
iresp = '0;

if (busy && saved_select_is_dbus) begin
dresp = oresp;
end else if (busy && !saved_kill_iresp) begin
iresp = oresp;
end
end

时序部分在接收请求时锁存 saved_priv。如果 busy 期间发生取指 flush,则只标记旧取指响应为 killed,等原请求完成后清除标记。

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
dbus_arbiter.sv
always_ff @(posedge clk) begin
if (reset) begin
busy <= 1'b0;
saved_select_is_dbus <= 1'b0;
saved_kill_iresp <= 1'b0;
saved_req <= '0;
saved_access_type <= MMU_REQ_LOAD;
saved_priv <= PRIV_M;
end else if (busy) begin
if (flush_ireq && !saved_select_is_dbus) begin
saved_kill_iresp <= 1'b1;
end
if (oresp.data_ok) begin
busy <= 1'b0;
saved_kill_iresp <= 1'b0;
end
end else if (dreq.valid || ireq.valid) begin
busy <= 1'b1;
saved_select_is_dbus <= dreq.valid;
saved_kill_iresp <= 1'b0;
saved_req <= selected_req;
saved_access_type <= selected_access_type;
saved_priv <= selected_priv;
end
end

最后修改 icache 的 miss 输出。IC_MISS8 和 IC_MISS4 只有在没有被 flush/kill 时才继续向下游发 miss 请求。

1
2
3
4
5
6
7
8
9
10
icache.sv
IC_MISS8: begin
if (!miss_killed && !flush) begin
miss_req.valid = 1'b1;
miss_req.addr = {miss_pc[63:3], 3'b000};
miss_req.size = MSIZE8;
miss_req.strobe = 8'b0;
miss_req.data = 64'b0;
end
end
1
2
3
4
5
6
7
8
9
10
icache.sv
IC_MISS4: begin
if (!miss_killed && !flush) begin
miss_req.valid = 1'b1;
miss_req.addr = {miss_pc[63:2], 2'b00};
miss_req.size = MSIZE4;
miss_req.strobe = 8'b0;
miss_req.data = 64'b0;
end
end

I-Cache 状态机在 miss 等待期间收到 flush 时直接回到 IC_IDLE,不再保留旧 miss 继续等待返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
icache.sv
IC_MISS8: begin
if (flush) begin
state <= IC_IDLE;
miss_killed <= 1'b0;
end else if (miss_resp.data_ok) begin
if (miss_killed || flush) begin
state <= IC_IDLE;
miss_killed <= 1'b0;
end else if (miss_exc) begin
state <= IC_MISS4;
end else begin
tag_table[miss_index] <= miss_tag;
data_table[miss_index] <= miss_resp.data;
valid_table[miss_index] <= 2'b11;
state <= IC_IDLE;
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
icache.sv
IC_MISS4: begin
if (flush) begin
state <= IC_IDLE;
miss_killed <= 1'b0;
end else if (miss_resp.data_ok) begin
if (!miss_killed && !flush && !miss_exc) begin
tag_table[miss_index] <= miss_tag;
if (miss_offset) begin
data_table[miss_index][63:32] <= miss4_inst;
valid_table[miss_index] <= 2'b10;
end else begin
data_table[miss_index][31:0] <= miss4_inst;
valid_table[miss_index] <= 2'b01;
end
end
state <= IC_IDLE;
miss_killed <= 1'b0;
end
end

5) Lab 6:把异步中断收敛到提交边界处理

m_trap_test 会在 csrsi mstatus,8 打开 MIE 后立刻执行 csrci mstatus,8 关闭中断,因此 timer interrupt 只有一个很短的提交边界窗口可以被接收。原先 IF 阶段也会注入中断,加入 ICache/MMU/flush 后,这个入口容易受到前端 PC 和旧取指响应影响。因此这里关闭旧 IF 中断入口,新增提交边界中断检测,让异步中断统一以 mem_wb_pc + 4 作为 epc 进入 trap。

1
2
3
4
core.sv
u64 csr_pre_trap_mstatus, csr_pre_trap_mip, csr_pre_trap_mie, csr_pre_trap_medeleg, csr_pre_trap_mideleg;
u1 commit_interrupt_valid;
u64 commit_interrupt_pc, commit_interrupt_cause, commit_interrupt_tval;

旧 IF 阶段中断入口保留实例但不再接收中断,避免同一个 pending interrupt 被前端路径抢先消费。

1
2
core.sv
assign interrupt_unit_can_accept = 1'b0;

新增提交边界中断检测。它只在当前提交指令本身没有同步异常、也不是 xret 时生效,并且使用 csr_file 给出的 pre-trap CSR 视图判断中断是否可接收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
core.sv
interrupt_unit u_commit_interrupt_unit(
.reset (reset),
.can_accept_interrupt(commit_fire && !mem_wb_exc_valid && (mem_wb_xret_type == XRET_NONE)),
.current_priv (current_priv),
.cur_pc (mem_wb_pc + 64'd4),
.mstatus (csr_pre_trap_mstatus),
.mip (csr_pre_trap_mip),
.mie (csr_pre_trap_mie),
.mideleg (csr_pre_trap_mideleg),
.interrupt_valid (commit_interrupt_valid),
.interrupt_pc (commit_interrupt_pc),
.interrupt_cause (commit_interrupt_cause),
.interrupt_tval (commit_interrupt_tval)
);

csr_file 增加 pre-trap CSR 输出。这里的 pre-trap 视图表示“当前 CSR 写入已经生效,但 trap 对 CSR 的修改还没有覆盖”的状态,用来支持 csrsi mstatus,8 同拍触发 timer interrupt。

1
2
3
4
csr_file.sv
output u64 pre_trap_mstatus, pre_trap_mip, pre_trap_mie, pre_trap_medeleg, pre_trap_mideleg,

u64 mip_next, external_mip_mapped, pre_trap_mip_next;

pre-trap 视图单独用一个组合块计算,只依赖 CSR 寄存器和当前提交的 CSR 写入,不依赖 trap_commit。这样可以避免 commit_interrupt_valid -> trap_unit_trap_valid -> csr_file -> commit_interrupt_valid 的组合环。

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
csr_file.sv
always_comb begin
pre_trap_mstatus = mstatus_reg;
pre_trap_mip_next = mip_reg;
pre_trap_mie = mie_reg;
pre_trap_medeleg = medeleg_reg;
pre_trap_mideleg = mideleg_reg;

if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_MSTATUS: pre_trap_mstatus = csr_write_data & MSTATUS_MASK;
CSR_SSTATUS: pre_trap_mstatus = (mstatus_reg & ~(SSTATUS_LOCAL_MASK & MSTATUS_MASK)) |
(csr_write_data & SSTATUS_LOCAL_MASK & MSTATUS_MASK);
CSR_MIP: pre_trap_mip_next = csr_write_data & MIP_MASK;
CSR_SIP: pre_trap_mip_next = (mip_reg & ~S_INTERRUPT_MASK) | (csr_write_data & S_INTERRUPT_MASK & MIP_MASK);
CSR_MIE: pre_trap_mie = csr_write_data;
CSR_SIE: pre_trap_mie = (mie_reg & ~S_INTERRUPT_MASK) | (csr_write_data & S_INTERRUPT_MASK);
CSR_MEDELEG: pre_trap_medeleg = csr_write_data & MEDELEG_WRITABLE_MASK;
CSR_MIDELEG: pre_trap_mideleg = csr_write_data & MIDELEG_WRITABLE_MASK;
default: begin
end
endcase
end

pre_trap_mip = pre_trap_mip_next | map_external_mip(external_mip, pre_trap_mideleg);
end

trap 保存 MPIE/SPIE 时也改用 pre-trap 的中断使能位。这样当 csrsi mstatus,8 同拍触发 M 态 timer interrupt 时,MPIE 保存的是已经打开后的 MIE=1。

1
2
3
4
5
6
7
8
csr_file.sv
if (trap_commit && (trap_target_priv == PRIV_M)) begin
mstatus[MSTATUS_MPIE_BIT] = pre_trap_mstatus[MSTATUS_MIE_BIT];
mstatus[MSTATUS_MIE_BIT] = 1'b0;
end else if (trap_commit && (trap_target_priv == PRIV_S)) begin
mstatus[MSTATUS_SPIE_BIT] = pre_trap_mstatus[MSTATUS_SIE_BIT];
mstatus[MSTATUS_SIE_BIT] = 1'b0;
end

core 将 pre-trap 视图接到 trap_unit。异常委托判断也使用 pre-trap 的 medeleg/mideleg,避免 trap 修改后的 CSR 输出反过来影响本次 trap 选择。

1
2
3
4
5
6
7
8
core.sv
csr_file u_csr_file(
.pre_trap_mstatus(csr_pre_trap_mstatus),
.pre_trap_mip (csr_pre_trap_mip),
.pre_trap_mie (csr_pre_trap_mie),
.pre_trap_medeleg(csr_pre_trap_medeleg),
.pre_trap_mideleg(csr_pre_trap_mideleg)
);
1
2
3
4
5
6
7
8
9
core.sv
trap_unit u_trap_unit(
.commit_interrupt_valid(commit_interrupt_valid),
.commit_interrupt_pc (commit_interrupt_pc),
.commit_interrupt_cause(commit_interrupt_cause),
.commit_interrupt_tval (commit_interrupt_tval),
.csr_medeleg (csr_pre_trap_medeleg),
.csr_mideleg (csr_pre_trap_mideleg)
);

trap_unit 新增提交边界中断输入,并把同步异常和提交边界中断统一成一组 trap_req_*。同步异常优先;没有同步异常时,才使用提交边界中断的 pc/cause/tval。

1
2
3
4
5
6
7
8
9
trap_unit.sv
input u1 commit_interrupt_valid,
input u64 commit_interrupt_pc,
input u64 commit_interrupt_cause,
input u64 commit_interrupt_tval,

u1 trap_req_valid, trap_is_interrupt;
u1 trap_delegated;
u64 trap_req_pc, trap_req_cause, trap_req_tval;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trap_unit.sv
assign trap_req_valid = mem_wb_exc_valid || commit_interrupt_valid;
assign trap_req_pc = mem_wb_exc_valid ? mem_wb_pc : commit_interrupt_pc;
assign trap_req_cause = mem_wb_exc_valid ? mem_wb_exc_cause : commit_interrupt_cause;
assign trap_req_tval = mem_wb_exc_valid ? mem_wb_exc_tval : commit_interrupt_tval;
assign trap_is_interrupt = trap_req_cause[63];
assign trap_delegated = (current_priv != PRIV_M) &&
(trap_is_interrupt ?
csr_mideleg[trap_req_cause[5:0]] :
csr_medeleg[trap_req_cause[5:0]]);
assign trap_vector_offset = (trap_is_interrupt && (trap_tvec[1:0] == 2'b01)) ?
{56'b0, trap_req_cause[5:0], 2'b00} :
64'b0;
assign trap_valid = commit_fire && trap_req_valid;
assign trap_epc = trap_req_pc;
assign trap_cause = trap_req_cause;
assign trap_tval = trap_req_tval;

二、尝试跑 xv6

中间经历了多次调整

1. 将 xv6 与 CPU 对齐

为了让 xv6 能在当前 CPU 上进入内核并打印启动信息,这一阶段主要做了两类事情:先把 xv6 镜像重新编译成不含压缩指令的 rv64ima 版本,并补齐内核启动早期会访问到的 UART、CSR、中断和取指刷新行为;随后再把 fence/fence.i 作为合法空操作处理,并增加 +xv6_debug 下的 trap/xret 调试输出。

本次改动内涉及文件如下:

  • xv6-riscv/Makefile:去掉 rv64gc,改用 rv64ima/lp64 生成无 RVC 的 xv6 内核。
  • ready-to-run/xv6/kernel.bin:更新为重新生成后的无压缩指令 xv6 镜像。
  • vsrc/src/basics/mem.sv:增加 xv6 16550 UART 的最小兼容逻辑,把字符输出转发到课程 UART 地址。
  • vsrc/src/checkers/csr_checker.sv、vsrc/src/utils/csr_file.sv:补齐 xv6 启动中会访问的 CSR,并提供 trap 前 CSR 视图。
  • vsrc/src/utils/trap_unit.sv、vsrc/src/core.sv:把中断移动到提交点处理,避免早期中断和流水线提交顺序冲突。
  • vsrc/src/utils/dbus_arbiter.sv、vsrc/src/utils/icache.sv:在跳转、trap 或 flush 后杀掉旧取指响应,并把 MMU 请求的特权级锁存在总线请求上。
  • vsrc/src/basics/decoder.sv:识别 fence/fence.i,当前按 no-op 合法指令执行。

2. 重新生成不含压缩指令的 xv6 镜像

原来的 xv6 使用 rv64gc,会生成当前 CPU 尚未实现的压缩指令。这里把内核编译选项改为 rv64ima/lp64,同时汇编文件规则也保持同样的 ISA 和 ABI。

1
2
3
4
5
6
xv6-riscv/Makefile
CFLAGS += -march=rv64ima
CFLAGS += -mabi=lp64

$K/%.o: $K/%.S
$(CC) -march=rv64ima -mabi=lp64 -g -c -o $@ $<

ready-to-run/xv6/kernel.bin 也同步替换为重新生成后的镜像,二进制大小从 30784 字节变为 38976 字节,后续仿真加载的就是这个无 RVC 版本。

3. 兼容 xv6 启动早期 UART 访问

xv6 默认访问的是 0x10000000 附近的 16550 UART,而课程环境里实际能打印字符的是 0x40600004。因此在访存阶段增加一层最小兼容:读 LSR 时直接返回 THR 空闲,写 THR 时转发到课程 UART,其余 16550 初始化寄存器写入不真正下发到总线。

1
2
3
4
5
6
7
8
9
10
11
12
13
mem.sv
output u1 is_mmio_out
);
u1 mem_req_valid, mem_req_write;
u1 xv6_uart_hit, xv6_uart_lsr_read, xv6_uart_thr_write, xv6_uart_ignore;
u64 atomic_rd_data, normal_mem_data, raw_dreq_addr;
u8 xv6_uart_lcr;

localparam u64 XV6_UART_BASE = 64'h0000_0000_1000_0000;
localparam u64 XV6_UART_THR = XV6_UART_BASE + 64'd0;
localparam u64 XV6_UART_LCR = XV6_UART_BASE + 64'd3;
localparam u64 XV6_UART_LSR = XV6_UART_BASE + 64'd5;
localparam u64 COURSE_UART_OUT = 64'h0000_0000_4060_0004;

访存请求先统一生成 mem_req_valid、mem_req_write 和原始地址 raw_dreq_addr。如果命中 xv6 UART 的 THR 写,就把地址改写到课程 UART;如果是 LSR 读或其它初始化寄存器访问,则不向外部总线发真实请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mem.sv
assign mem_req_valid = normal_mem_valid ||
(atomic_valid && is_lr) ||
(atomic_valid && is_sc && sc_success) ||
amo_read_req ||
amo_write_req;
assign mem_req_write = ((normal_mem_valid && mem_write) ||
(atomic_valid && is_sc && sc_success) ||
amo_write_req);
assign raw_dreq_addr = amo_write_req ? amo_addr : alu_ans;
assign xv6_uart_hit = (raw_dreq_addr[63:3] == XV6_UART_BASE[63:3]);
assign xv6_uart_lsr_read = normal_mem_valid && mem_read &&
(raw_dreq_addr == XV6_UART_LSR);
assign xv6_uart_thr_write = normal_mem_valid && mem_write &&
(raw_dreq_addr == XV6_UART_THR) &&
!xv6_uart_lcr[7];
assign xv6_uart_ignore = xv6_uart_hit && !xv6_uart_lsr_read && !xv6_uart_thr_write;

assign dreq_valid = mem_req_valid && !xv6_uart_lsr_read && !xv6_uart_ignore;
assign dreq_addr = xv6_uart_thr_write ? COURSE_UART_OUT : raw_dreq_addr;

因为 UART LSR 读不会真的访问总线,所以读数据在 MEM 内部直接构造为 0x20,表示发送保持寄存器为空,xv6 可以继续输出字符。

1
2
3
mem.sv
assign normal_mem_data = xv6_uart_lsr_read ? 64'h0000_0000_0000_0020 : final_mem_data;
assign mem_data_out = (is_amo || is_lr || is_sc) ? atomic_rd_data : normal_mem_data;

写掩码使用统一后的 mem_req_write,避免被 UART 兼容逻辑改写地址后仍然沿用旧的普通访存判断。

1
2
mem.sv
assign dreq_strobe = mem_req_write ? (base_strobe << dreq_addr[2:0]) : 8'b0;

MMIO 判定改为看原始地址,而不是看可能已经被 UART 转发后的 dreq_addr。

1
2
mem.sv
assign is_mmio_out = mem_req_valid && (raw_dreq_addr[31] == 1'b0);

xv6 初始化 UART 时会写 LCR 的 DLAB 位。这里保存 LCR,是为了区分 “向 THR 输出字符” 和 “DLAB 置位时写 divisor latch” 两种访问。

1
2
3
4
5
6
7
8
9
10
11
12
mem.sv
always_ff @(posedge clk) begin
if (reset) begin
xv6_uart_lcr <= 8'b0;
end else if (kill) begin
amo_state <= AMO_IDLE;
end else begin
if (normal_mem_valid && mem_write && (raw_dreq_addr == XV6_UART_LCR)) begin
xv6_uart_lcr <= store_data[7:0];
end
end
end

4. 补齐 xv6 会访问的 CSR

xv6 启动时会访问 mcounteren、menvcfg、time 和 stimecmp。检查器里先把这些 CSR 认定为存在,避免合法 CSR 被报成非法指令。

1
2
3
4
5
csr_checker.sv
localparam u12 CSR_MCOUNTEREN = 12'h306;
localparam u12 CSR_MENVCFG = 12'h30a;
localparam u12 CSR_TIME = 12'hc01;
localparam u12 CSR_STIMECMP = 12'h14d;
1
2
3
4
5
6
7
8
9
10
11
12
csr_checker.sv
always_comb begin
unique case (csr_addr)
CSR_MCOUNTEREN,
CSR_MENVCFG,
CSR_TIME,
CSR_STIMECMP: begin
csr_exists = 1'b1;
end
// 其它 CSR 分支保持原有处理
endcase
end

CSR 文件中增加这些寄存器,同时输出一组 pre_trap_* 信号,供同一条提交指令既写 CSR 又触发 trap/interrupt 时使用。

1
2
3
4
5
6
7
8
9
10
csr_file.sv
output u64 pre_trap_mstatus, pre_trap_mip, pre_trap_mie, pre_trap_medeleg, pre_trap_mideleg,

u64 mcounteren_reg, menvcfg_reg, stimecmp_reg;
u64 mip_next, external_mip_mapped, pre_trap_mip_next;

localparam u12 CSR_MCOUNTEREN = 12'h306;
localparam u12 CSR_MENVCFG = 12'h30a;
localparam u12 CSR_TIME = 12'hc01;
localparam u12 CSR_STIMECMP = 12'h14d;

pre_trap_* 先从当前 CSR 值出发,再叠加本周期 CSR 写入的效果。这样中断判断和 trap 委托使用的是提交点上更准确的 CSR 状态。

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
csr_file.sv
always_comb begin
pre_trap_mstatus = mstatus_reg;
pre_trap_mip_next = mip_reg;
pre_trap_mie = mie_reg;
pre_trap_medeleg = medeleg_reg;
pre_trap_mideleg = mideleg_reg;

if (csr_write_enable) begin
unique case (csr_write_addr)
CSR_MSTATUS: pre_trap_mstatus = csr_write_data & MSTATUS_MASK;
CSR_SSTATUS: pre_trap_mstatus = (mstatus_reg & ~(SSTATUS_LOCAL_MASK & MSTATUS_MASK)) |
(csr_write_data & SSTATUS_LOCAL_MASK & MSTATUS_MASK);
CSR_MIP: pre_trap_mip_next = csr_write_data & MIP_MASK;
CSR_SIP: pre_trap_mip_next = (mip_reg & ~S_INTERRUPT_MASK) | (csr_write_data & S_INTERRUPT_MASK & MIP_MASK);
CSR_MIE: pre_trap_mie = csr_write_data;
CSR_SIE: pre_trap_mie = (mie_reg & ~S_INTERRUPT_MASK) | (csr_write_data & S_INTERRUPT_MASK);
CSR_MEDELEG: pre_trap_medeleg = csr_write_data & MEDELEG_WRITABLE_MASK;
CSR_MIDELEG: pre_trap_mideleg = csr_write_data & MIDELEG_WRITABLE_MASK;
default: begin
end
endcase
end

pre_trap_mip = pre_trap_mip_next | map_external_mip(external_mip, pre_trap_mideleg);
end

进入 trap 时,MPIE/SPIE 使用 pre_trap_mstatus 里的中断使能位,而不是旧寄存器值。

1
2
3
4
5
6
csr_file.sv
if (trap_commit && (trap_target_priv == PRIV_M)) begin
mstatus[MSTATUS_MPIE_BIT] = pre_trap_mstatus[MSTATUS_MIE_BIT];
end else if (trap_commit && (trap_target_priv == PRIV_S)) begin
mstatus[MSTATUS_SPIE_BIT] = pre_trap_mstatus[MSTATUS_SIE_BIT];
end

CSR 读路径中,time 直接复用 mcycle,mcounteren/menvcfg/stimecmp 返回对应寄存器。

1
2
3
4
5
6
7
8
9
10
csr_file.sv
always_comb begin
unique case (csr_read_addr)
CSR_MCOUNTEREN: csr_read_data = mcounteren_reg;
CSR_MENVCFG: csr_read_data = menvcfg_reg;
CSR_TIME: csr_read_data = mcycle;
CSR_STIMECMP: csr_read_data = stimecmp_reg;
default: csr_read_data = 64'b0;
endcase
end

时序写回中给新增 CSR 增加复位和写入逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
csr_file.sv
always_ff @(posedge clk) begin
if (reset) begin
mcounteren_reg <= 64'b0;
menvcfg_reg <= 64'b0;
stimecmp_reg <= 64'b0;
end else begin
mcounteren_reg <= (csr_write_enable && (csr_write_addr == CSR_MCOUNTEREN)) ? csr_write_data : mcounteren_reg;
menvcfg_reg <= (csr_write_enable && (csr_write_addr == CSR_MENVCFG)) ? csr_write_data : menvcfg_reg;
stimecmp_reg <= (csr_write_enable && (csr_write_addr == CSR_STIMECMP)) ? csr_write_data : stimecmp_reg;
end
end

5. 把中断移动到提交点处理

之前中断判断在流水线较早位置,容易和正在提交的 CSR 写、异常、xret 顺序冲突。这里新增提交点中断信号,由 WB 提交时决定是否接收中断。

1
2
3
4
5
6
7
core.sv
u1 pc_pending_jump, pc_trap_pending_jump;
priv_t mmu_core_priv;
u64 csr_pre_trap_mstatus, csr_pre_trap_mip, csr_pre_trap_mie, csr_pre_trap_medeleg, csr_pre_trap_mideleg;
u1 commit_interrupt_valid;
u64 commit_interrupt_pc, commit_interrupt_cause, commit_interrupt_tval;
u1 pc_should_jump, pc_fetch_enable, pc_redirect_enable, pc_enable_real;

旧的早期中断单元不再接收中断,新的提交点中断单元只在当前提交没有异常、也不是 xret 时生效,并使用 pre_trap_* CSR 视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
core.sv
assign interrupt_unit_can_accept = 1'b0;

interrupt_unit u_commit_interrupt_unit(
.reset (reset),
.can_accept_interrupt(commit_fire && !mem_wb_exc_valid && (mem_wb_xret_type == XRET_NONE)),
.current_priv (current_priv),
.cur_pc (mem_wb_pc + 64'd4),
.mstatus (csr_pre_trap_mstatus),
.mip (csr_pre_trap_mip),
.mie (csr_pre_trap_mie),
.mideleg (csr_pre_trap_mideleg),
.interrupt_valid (commit_interrupt_valid),
.interrupt_pc (commit_interrupt_pc),
.interrupt_cause (commit_interrupt_cause),
.interrupt_tval (commit_interrupt_tval)
);

csr_file 实例把 trap 前 CSR 视图接出来,供提交点中断单元和 trap 单元共同使用。

1
2
3
4
5
6
7
8
core.sv
csr_file u_csr_file(
.pre_trap_mstatus(csr_pre_trap_mstatus),
.pre_trap_mip (csr_pre_trap_mip),
.pre_trap_mie (csr_pre_trap_mie),
.pre_trap_medeleg(csr_pre_trap_medeleg),
.pre_trap_mideleg(csr_pre_trap_mideleg)
);

trap_unit 同时接收异常和提交点中断。异常优先;没有异常时,提交点中断可以生成 trap 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trap_unit.sv
input u1 commit_interrupt_valid,
input u64 commit_interrupt_pc,
input u64 commit_interrupt_cause,
input u64 commit_interrupt_tval,

u1 trap_req_valid, trap_is_interrupt;
u64 trap_req_pc, trap_req_cause, trap_req_tval;

assign trap_req_valid = mem_wb_exc_valid || commit_interrupt_valid;
assign trap_req_pc = mem_wb_exc_valid ? mem_wb_pc : commit_interrupt_pc;
assign trap_req_cause = mem_wb_exc_valid ? mem_wb_exc_cause : commit_interrupt_cause;
assign trap_req_tval = mem_wb_exc_valid ? mem_wb_exc_tval : commit_interrupt_tval;
assign trap_is_interrupt = trap_req_cause[63];

委托判断、向量偏移和最终 trap 输出都统一改用 trap_req_*,从而让异常和中断共用同一条 trap 重定向路径。

1
2
3
4
5
6
7
8
9
10
11
12
trap_unit.sv
assign trap_delegated = (current_priv != PRIV_M) &&
(trap_is_interrupt ?
csr_mideleg[trap_req_cause[5:0]] :
csr_medeleg[trap_req_cause[5:0]]);
assign trap_vector_offset = (trap_is_interrupt && (trap_tvec[1:0] == 2'b01)) ?
{56'b0, trap_req_cause[5:0], 2'b00} :
64'b0;
assign trap_valid = commit_fire && trap_req_valid;
assign trap_epc = trap_req_pc;
assign trap_cause = trap_req_cause;
assign trap_tval = trap_req_tval;

core 中的 trap_unit 实例同步接入提交点中断,并把委托 CSR 换成 pre_trap_* 版本。

1
2
3
4
5
6
7
8
9
core.sv
trap_unit u_trap_unit(
.commit_interrupt_valid(commit_interrupt_valid),
.commit_interrupt_pc (commit_interrupt_pc),
.commit_interrupt_cause(commit_interrupt_cause),
.commit_interrupt_tval (commit_interrupt_tval),
.csr_medeleg (csr_pre_trap_medeleg),
.csr_mideleg (csr_pre_trap_mideleg)
);

6. 修正 trap 重定向和旧取指响应

trap/xret 产生重定向时,如果 PC 当周期不能真正更新,需要把这次 trap 跳转暂存下来,直到 IF/ID 可以接收新的 PC。否则 xv6 早期 trap 可能被流水线停顿吞掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
core.sv
assign pc_fetch_enable = ctrl_pc_enable && fetch_iresp.data_ok;
assign pc_redirect_enable = (trap_unit_redirect_valid || pc_trap_pending_jump) && ctrl_if_id_enable;
assign pc_enable_real = (pc_fetch_enable || pc_redirect_enable) && !interrupt_unit_interrupt_valid;

always_ff @(posedge clk) begin
if (reset) begin
pc_trap_pending_jump <= 1'b0;
end else if (pc_enable_real) begin
pc_trap_pending_jump <= 1'b0;
end else if (trap_unit_redirect_valid) begin
pc_trap_pending_jump <= 1'b1;
end
end

pc u_pc(
.enable(pc_enable_real)
);

总线仲裁器增加 flush_ireq 和 current_priv 输入,并把选中的请求特权级锁存到 saved_priv。这样 MMU 看到的是发起请求时的特权级,而不是响应回来时可能已经变化的特权级。

1
2
3
4
5
6
7
8
dbus_arbiter.sv
input u1 flush_ireq,
input priv_t current_priv,

output priv_t oreq_priv,

u1 busy, saved_select_is_dbus, saved_kill_iresp;
priv_t selected_priv, saved_priv;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dbus_arbiter.sv
always_comb begin
selected_req = '0;
selected_access_type = MMU_REQ_LOAD;
selected_priv = current_priv;

if (dreq.valid) begin
selected_req = dreq;
selected_access_type = (dreq.strobe == 8'b0) ? MMU_REQ_LOAD : MMU_REQ_STORE;
selected_priv = current_priv;
end else if (ireq.valid) begin
selected_req = ireq;
selected_access_type = MMU_REQ_INST;
selected_priv = current_priv;
end
end

assign oreq_priv = busy ? saved_priv : selected_priv;

当取指请求在等待响应时遇到 flush,仲裁器标记 saved_kill_iresp,后续即使旧响应回来也不再送给取指端,也不再产生对应的取指异常。

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
dbus_arbiter.sv
assign pc_exc = busy && oresp.data_ok && exc_valid && !saved_select_is_dbus && !saved_kill_iresp;

always_comb begin
dresp = '0;
iresp = '0;

if (busy && saved_select_is_dbus) begin
dresp = oresp;
end else if (busy && !saved_kill_iresp) begin
iresp = oresp;
end
end

always_ff @(posedge clk) begin
if (reset) begin
saved_kill_iresp <= 1'b0;
saved_priv <= PRIV_M;
end else if (busy) begin
if (flush_ireq && !saved_select_is_dbus) begin
saved_kill_iresp <= 1'b1;
end
if (oresp.data_ok) begin
busy <= 1'b0;
saved_kill_iresp <= 1'b0;
end
end else if (dreq.valid || ireq.valid) begin
saved_kill_iresp <= 1'b0;
saved_priv <= selected_priv;
end
end

core 里把取指 flush 和当前组合特权级接入仲裁器,再把仲裁器输出的请求特权级送给 MMU。

1
2
3
4
5
6
7
8
9
10
core.sv
DBusArbiter u_dbus_arbiter(
.flush_ireq (icache_flush),
.current_priv(current_priv_comb),
.oreq_priv (mmu_core_priv)
);

MMU u_mmu(
.current_priv(mmu_core_priv)
);

I-Cache 在 miss 状态下如果遇到 flush,就不再继续发旧 miss 请求,也不会把旧 miss 响应填入缓存或返回给 IF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
icache.sv
always_comb begin
unique case (state)
IC_MISS8: begin
if (!miss_killed && !flush) begin
miss_req.valid = 1'b1;
miss_req.addr = {miss_pc[63:3], 3'b000};
miss_req.size = MSIZE8;
miss_req.strobe = 8'b0;
miss_req.data = 64'b0;
end
end
IC_MISS4: begin
if (!miss_killed && !flush) begin
miss_req.valid = 1'b1;
miss_req.addr = {miss_pc[63:2], 2'b00};
miss_req.size = MSIZE4;
miss_req.strobe = 8'b0;
miss_req.data = 64'b0;
end
end
endcase
end

状态机也在 IC_MISS8/IC_MISS4 两个状态里优先响应 flush,直接回到 IC_IDLE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
icache.sv
always_ff @(posedge clk) begin
unique case (state)
IC_MISS8: begin
if (flush) begin
state <= IC_IDLE;
miss_killed <= 1'b0;
end else if (miss_resp.data_ok) begin
// 原有 miss_resp.data_ok 处理保持不变
end
end
IC_MISS4: begin
if (flush) begin
state <= IC_IDLE;
miss_killed <= 1'b0;
end else if (miss_resp.data_ok) begin
// 原有 miss_resp.data_ok 处理保持不变
end
end
endcase
end

7. 支持 fence/fence.i 并增加 xv6 调试输出

xv6 启动路径里会出现 fence 或 fence.i。当前 CPU 暂时没有乱序和真实 I/D cache 一致性问题,所以先把 0001111 opcode 识别为合法 no-op 指令。

1
2
3
4
decoder.sv
u1 is_alu_i, is_alu_r, is_alu_iw, is_alu_rw, is_load, is_store, is_lui, is_auipc, is_system, is_fence, is_csr_imm;

assign is_fence = (opcode == 7'b0001111);
1
2
3
4
5
6
7
8
9
decoder.sv
always_comb begin
case (opcode)
7'b0001111: begin
is_valid_inst = 1'b1;
end
// 其它 opcode 分支保持原有处理
endcase
end

最后,为了排查 xv6 卡住时到底是在 trap 还是 xret 附近循环,core 在 Verilator 下增加 +xv6_debug 调试输出。不开 plusarg 时不影响正常输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
core.sv
`ifdef VERILATOR
always_ff @(posedge clk) begin
if (!reset && $test$plusargs("xv6_debug")) begin
if (trap_unit_trap_valid) begin
$display("[xv6_debug] trap pc=%h cause=%h tval=%h from=%0d to=%0d redirect=%h",
trap_unit_trap_epc,
trap_unit_trap_cause,
trap_unit_trap_tval,
trap_unit_trap_from_priv,
trap_unit_trap_target_priv,
trap_unit_redirect_pc);
end
if (trap_unit_xret_valid) begin
$display("[xv6_debug] xret pc=%h type=%0d redirect=%h return_priv=%0d",
mem_wb_pc,
mem_wb_xret_type,
trap_unit_redirect_pc,
csr_xret_return_priv);
end
end
end
`endif

最终输出

最终终端可以打印“xv6 kernel is booting”

三、继续排查 xv6 启动卡住问题

本轮继续排查 xv6 只能打印 xv6 kernel is booting 的问题。修改范围分为三类:一是为课程仿真器补磁盘后端和仿真器初始化修复;二是对 xv6 启动路径做临时定位与启动加速;三是在 MMU fault 分支加入临时输出,定位开启分页后的异常来源。当前结论是:执行到 kvminithart() 开启分页后,下一次串口输出访问了 0x40600004,但 xv6 内核页表没有映射该课程 UART MMIO 地址,触发 page fault;由于此时 stvec 尚未设置,异常入口为 0,最终 PC 掉到 0x0。

1. 仿真器参数初始化修复

EmuArgs 原先没有初始化 enable_jtag。这会导致不传 --enable-jtag 时也可能随机启用 JTAG,尝试监听 23334 端口并在端口冲突时 abort。这里补默认值为 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
src/test/csrc/verilator/emu.h
EmuArgs() {
seed = 0;
max_cycles = -1;
max_instr = -1;
warmup_instr = -1;
stat_cycles = -1;
log_begin = 1;
log_end = -1;
snapshot_path = NULL;
image = NULL;
enable_waveform = false;
enable_snapshot = true;
force_dump_result = false;
enable_diff = true;
enable_jtag = false;
}

2. 在 difftest RAM 中加入简化磁盘 MMIO

ram.sv 新增 simple_disk_rw DPI 接口,并记录简化磁盘的 block、buffer 地址和状态。

1
2
3
4
5
ram.sv
import "DPI-C" function int simple_disk_rw(input longint blockno, input longint paddr, input int write);

u64 mtime, mtimecmp;
u64 simple_disk_block, simple_disk_addr, simple_disk_status;

在 NONE 状态的立即写路径中,捕获 0x10001000/08/10/18 四个寄存器;其中 0x10001010 写入 1 代表读,写入 2 代表写。同时屏蔽 PLIC 地址范围写入,避免 PLIC 初始化污染普通 RAM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ram.sv
unique case (oreq.addr)
64'h38000000: msip <= oreq.data[0];
64'h38004000: mtimecmp <= oreq.data;
64'h3800bff8: mtime <= oreq.data;
64'h10001000: simple_disk_block <= oreq.data;
64'h10001008: simple_disk_addr <= oreq.data;
64'h10001010: begin
if (oreq.data == 64'd1 || oreq.data == 64'd2) begin
simple_disk_status <= {32'b0, simple_disk_rw(simple_disk_block, simple_disk_addr, oreq.data == 64'd2)};
end
end
64'h10001018: simple_disk_status <= oreq.data;
default: begin
if (oreq.addr != 64'h4060000c &&
!((oreq.addr >= 64'h0c000000) && (oreq.addr < 64'h10000000))) begin
ram_write_helper(`IDX(oreq.addr), oreq.data, wmask, '1);
end
end
endcase

同样的写处理也补到 WRITE 状态,保证带等待周期的写请求也能访问简化磁盘寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ram.sv
unique case (addr)
64'h38000000: msip <= oreq.data[0];
64'h38004000: mtimecmp <= oreq.data;
64'h3800bff8: mtime <= oreq.data;
64'h10001000: simple_disk_block <= oreq.data;
64'h10001008: simple_disk_addr <= oreq.data;
64'h10001010: begin
if (oreq.data == 64'd1 || oreq.data == 64'd2) begin
simple_disk_status <= {32'b0, simple_disk_rw(simple_disk_block, simple_disk_addr, oreq.data == 64'd2)};
end
end
64'h10001018: simple_disk_status <= oreq.data;
default: begin
if (addr != 64'h4060000c &&
!((addr >= 64'h0c000000) && (addr < 64'h10000000))) begin
ram_write_helper(idx, oreq.data, wmask, '1);
end
end
endcase

复位时初始化磁盘寄存器,状态默认为完成。

1
2
3
4
5
ram.sv
if (reset) begin
{simple_disk_block, simple_disk_addr} <= '0;
simple_disk_status <= 64'd1;
end

读路径中,PLIC 范围读返回 0;简化磁盘寄存器可以被 CPU 轮询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ram.sv
always_comb begin
if (state == READ || (state == NONE && oreq.valid && count_down == 0 && ~oreq.is_write)) begin
oresp.ready = '1;
oresp.last = count_down == 0;
if ((oreq.addr >= 64'h0c000000) && (oreq.addr < 64'h10000000)) begin
oresp.data = '0;
end else begin
unique case (oreq.addr)
64'h40600008: oresp.data = '0;
64'h38000000: oresp.data = {63'b0, msip};
64'h38004000: oresp.data = mtimecmp;
64'h3800bff8: oresp.data = mtime;
64'h10001000: oresp.data = simple_disk_block;
64'h10001008: oresp.data = simple_disk_addr;
64'h10001018: oresp.data = simple_disk_status;
64'h20003000: oresp.data = ms_cnt;
64'h23333008: oresp.data = {'0, get_switch()};
default: oresp.data = ram_read_helper('1, `IDX(oreq.addr));
endcase
end
end
end

3. difftest C++ 侧实现 fs.img 读写

ram.cpp 增加 simple_disk_fp 和 open_simple_disk(),按固定顺序查找 xv6-riscv/fs.img、fs.img、ready-to-run/xv6/fs.img。

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
src/test/csrc/common/ram.cpp
static FILE *simple_disk_fp = NULL;

static FILE *open_simple_disk() {
if (simple_disk_fp != NULL) {
return simple_disk_fp;
}

const char *paths[] = {
"xv6-riscv/fs.img",
"fs.img",
"ready-to-run/xv6/fs.img",
NULL
};

for (int i = 0; paths[i] != NULL; i++) {
simple_disk_fp = fopen(paths[i], "r+b");
if (simple_disk_fp != NULL) {
printf("Using xv6 disk image %s\n", paths[i]);
return simple_disk_fp;
}
}

printf("ERROR: xv6 disk image not found. Build xv6-riscv/fs.img first.\n");
return NULL;
}

仿真结束时关闭磁盘镜像文件。

1
2
3
4
5
6
7
8
src/test/csrc/common/ram.cpp
void ram_finish() {
if (simple_disk_fp != NULL) {
fclose(simple_disk_fp);
simple_disk_fp = NULL;
}
munmap(ram, EMU_RAM_SIZE);
}

新增 simple_disk_rw():每个 xv6 block 按 1024 字节处理,把 guest 物理地址转换为 host RAM 偏移后读写 fs.img。

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
src/test/csrc/common/ram.cpp
extern "C" int simple_disk_rw(uint64_t blockno, uint64_t paddr, int write) {
const uint64_t block_size = 1024;
FILE *fp = open_simple_disk();
if (fp == NULL || ram == NULL) {
return 2;
}

if (paddr < 0x80000000 || paddr + block_size > 0x80000000 + EMU_RAM_SIZE) {
printf("ERROR: simple disk buffer paddr 0x%lx out of range\n", paddr);
return 2;
}

uint8_t *buf = (uint8_t *)ram + (paddr - 0x80000000);
uint64_t offset = blockno * block_size;

pthread_mutex_lock(&ram_mutex);
int seek_ok = fseek(fp, offset, SEEK_SET) == 0;
if (!seek_ok) {
pthread_mutex_unlock(&ram_mutex);
printf("ERROR: simple disk seek failed, block=%lu\n", blockno);
return 2;
}

size_t n;
if (write) {
n = fwrite(buf, 1, block_size, fp);
fflush(fp);
} else {
n = fread(buf, 1, block_size, fp);
if (n < block_size) {
memset(buf + n, 0, block_size - n);
}
}
pthread_mutex_unlock(&ram_mutex);

if (write && n != block_size) {
printf("ERROR: simple disk write failed, block=%lu bytes=%lu\n", blockno, (uint64_t)n);
return 2;
}

return 1;
}

ram.h 对外声明该 DPI 函数。

1
2
3
4
5
6
7
8
src/test/csrc/common/ram.h
#ifdef __cplusplus
extern "C" {
#endif
int simple_disk_rw(uint64_t blockno, uint64_t paddr, int write);
#ifdef __cplusplus
}
#endif

4. xv6 内核启动加速

memset() 从逐字节写改为先构造 64 位 pattern,再按 8 字节批量写。这样可以显著降低 kinit() 清理大量 4KB 页时的周期数。

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
xv6-riscv/kernel/string.c
void *
memset(void *dst, int c, uint n)
{
char *cdst = (char *)dst;
uchar uc = c;
uint64 pattern = uc;

pattern |= pattern << 8;
pattern |= pattern << 16;
pattern |= pattern << 32;

while (((uint64)cdst & 7) != 0 && n > 0) {
*cdst++ = uc;
n--;
}

uint64 *qdst = (uint64 *)cdst;
while (n >= 8) {
*qdst++ = pattern;
n -= 8;
}

cdst = (char *)qdst;
while (n > 0) {
*cdst++ = uc;
n--;
}

return dst;
}

kalloc.c 临时删除 kfree() 和 kalloc() 中用于调试悬空引用的整页填充,减少启动阶段大量无必要写内存。

1
2
3
4
5
6
7
xv6-riscv/kernel/kalloc.c
// 删除 kfree() 中的整页填充:
// memset(pa, 1, PGSIZE);

// 删除 kalloc() 中的整页填充:
// if (r)
// memset((char *)r, 5, PGSIZE);

5. xv6 启动路径临时定位输出

main() 对每个启动阶段前后增加 printk(),用来确认当前卡在 kvminithart()。

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
xv6-riscv/kernel/main.c
printk("main: kinit begin\n");
kinit();
printk("main: kinit done\n");
printk("main: kvminit begin\n");
kvminit();
printk("main: kvminit done\n");
printk("main: kvminithart begin\n");
kvminithart();
printk("main: kvminithart done\n");
printk("main: procinit begin\n");
procinit();
printk("main: procinit done\n");
printk("main: trapinit begin\n");
trapinit();
printk("main: trapinit done\n");
printk("main: trapinithart begin\n");
trapinithart();
printk("main: trapinithart done\n");
printk("main: plicinit begin\n");
plicinit();
printk("main: plicinit done\n");
printk("main: plicinithart begin\n");
plicinithart();
printk("main: plicinithart done\n");
printk("main: binit begin\n");
binit();
printk("main: binit done\n");
printk("main: iinit begin\n");
iinit();
printk("main: iinit done\n");
printk("main: fileinit begin\n");
fileinit();
printk("main: fileinit done\n");
printk("main: virtio_disk_init begin\n");
virtio_disk_init();
printk("main: virtio_disk_init done\n");
printk("main: userinit begin\n");
userinit();
printk("main: userinit done\n");

forkret() 中增加 fsinit() 和 kexec() 阶段输出,判断是否已经进入第一个进程的内核上下文。

1
2
3
4
5
6
7
8
xv6-riscv/kernel/proc.c
printk("forkret: fsinit begin\n");
fsinit(ROOTDEV);
printk("forkret: fsinit done\n");

printk("forkret: kexec begin\n");
p->trapframe->a0 = kexec("/init", (char *[]){"/init", 0});
printk("forkret: kexec ret %ld\n", p->trapframe->a0);

fsinit() 和日志恢复路径增加阶段输出,用来继续确认后续是否卡在文件系统初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xv6-riscv/kernel/fs.c
void
fsinit(int dev)
{
printk("fsinit: readsb begin\n");
readsb(dev, &sb);
printk("fsinit: readsb done magic=0x%x size=%d logstart=%d nlog=%d\n",
sb.magic, sb.size, sb.logstart, sb.nlog);
if (sb.magic != FSMAGIC)
panic("invalid file system");
printk("fsinit: initlog begin\n");
initlog(dev, &sb);
printk("fsinit: initlog done\n");
printk("fsinit: ireclaim begin\n");
ireclaim(dev);
printk("fsinit: ireclaim done\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xv6-riscv/kernel/log.c
static void
recover_from_log(void)
{
printk("recover_from_log: read_head begin\n");
read_head();
printk("recover_from_log: read_head done n=%d\n", log.lh.n);
printk("recover_from_log: install begin\n");
install_trans(1);
printk("recover_from_log: install done\n");
log.lh.n = 0;
printk("recover_from_log: write_head begin\n");
write_head();
printk("recover_from_log: write_head done\n");
}

kexec() 成功后输出加载的程序入口。

1
2
3
xv6-riscv/kernel/exec.c
printk("exec ok: %s entry=0x%lx\n", path, elf.entry);
return argc;

syscall() 输出前 32 次系统调用,避免成功进入用户态后仍无法判断运行位置。

1
2
3
4
5
6
7
8
9
10
11
xv6-riscv/kernel/syscall.c
static int trace_count;
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if (trace_count < 32) {
printk("syscall trace: pid=%d name=%s num=%d epc=0x%lx\n", p->pid,
p->name, num, p->trapframe->epc);
trace_count++;
}

virtio_disk_rw() 输出前 64 次磁盘请求,确认是否读到了 fs.img 以及访问了哪些块。

1
2
3
4
5
6
7
8
9
10
xv6-riscv/kernel/virtio_disk.c
static int trace_count;
uint64 status;

acquire(&vdisk_lock);
if (trace_count < 64) {
printk("disk %s block=%d data=0x%lx\n", write ? "write" : "read",
b->blockno, (uint64)b->data);
trace_count++;
}

6. MMU 临时 fault 输出

为了定位开启分页后的异常,MMU 在 bad virtual address、PMP fault、L2/L1/L0 页表项 fault、leaf PMP fault 等分支输出访问类型、特权级、虚拟地址、PTE 地址和 PTE 内容。

1
2
3
4
5
6
7
8
mmu.sv
if (req_bad_vaddr) begin
$display("MMU fault bad vaddr access=%0d priv=%0d vaddr=%h satp=%h",
req_access_type, effective_priv, core_req.addr, satp);
state <= MMU_FAULT;
fault_cause <= request_fault_cause;
fault_tval <= core_req.addr;
end
1
2
3
4
5
6
7
8
9
mmu.sv
end else if (!req_pmp_allow) begin
$display("MMU fault direct PMP access=%0d priv=%0d addr=%h cause=%h pmpcfg0=%h pmpaddr0=%h",
req_access_type, effective_priv, core_req.addr,
req_pmp_fault_cause, pmpcfg0, pmpaddr0);
state <= MMU_FAULT;
fault_cause <= req_pmp_fault_cause;
fault_tval <= core_req.addr;
end
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
mmu.sv
MMU_L2: begin
if (!pte_pmp_allow) begin
$display("MMU fault L2 PTE PMP access=%0d priv=%0d vaddr=%h pte_addr=%h cause=%h",
saved_access_type, saved_priv, saved_vaddr, pte_addr,
saved_access_fault_cause);
state <= MMU_FAULT;
fault_cause <= saved_access_fault_cause;
fault_tval <= saved_vaddr;
end else if (dresp.data_ok) begin
if (pte_fault) begin
$display("MMU fault L2 PTE access=%0d priv=%0d vaddr=%h pte_addr=%h pte=%h invalid=%0d leaf=%0d priv_fault=%0d access_fault=%0d super_mis=%0d",
saved_access_type, saved_priv, saved_vaddr, pte_addr,
dresp.data, pte_invalid, pte_is_leaf, pte_priv_fault,
pte_access_fault, pte_superpage_misaligned);
state <= MMU_FAULT;
fault_cause <= saved_fault_cause;
fault_tval <= saved_vaddr;
end else if (pte_is_leaf) begin
if (!l2_pmp_allow) begin
$display("MMU fault L2 leaf PMP access=%0d priv=%0d vaddr=%h paddr=%h cause=%h pte=%h",
saved_access_type, saved_priv, saved_vaddr, l2_paddr,
l2_pmp_fault_cause, dresp.data);
state <= MMU_FAULT;
fault_cause <= l2_pmp_fault_cause;
fault_tval <= saved_vaddr;
end
end
end
end
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
mmu.sv
MMU_L1: begin
if (!pte_pmp_allow) begin
$display("MMU fault L1 PTE PMP access=%0d priv=%0d vaddr=%h pte_addr=%h cause=%h",
saved_access_type, saved_priv, saved_vaddr, pte_addr,
saved_access_fault_cause);
state <= MMU_FAULT;
fault_cause <= saved_access_fault_cause;
fault_tval <= saved_vaddr;
end else if (dresp.data_ok) begin
if (pte_fault) begin
$display("MMU fault L1 PTE access=%0d priv=%0d vaddr=%h pte_addr=%h pte=%h invalid=%0d leaf=%0d priv_fault=%0d access_fault=%0d super_mis=%0d",
saved_access_type, saved_priv, saved_vaddr, pte_addr,
dresp.data, pte_invalid, pte_is_leaf, pte_priv_fault,
pte_access_fault, pte_superpage_misaligned);
state <= MMU_FAULT;
fault_cause <= saved_fault_cause;
fault_tval <= saved_vaddr;
end else if (pte_is_leaf) begin
if (!l1_pmp_allow) begin
$display("MMU fault L1 leaf PMP access=%0d priv=%0d vaddr=%h paddr=%h cause=%h pte=%h",
saved_access_type, saved_priv, saved_vaddr, l1_paddr,
l1_pmp_fault_cause, dresp.data);
state <= MMU_FAULT;
fault_cause <= l1_pmp_fault_cause;
fault_tval <= saved_vaddr;
end
end
end
end
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
mmu.sv
MMU_L0: begin
if (!pte_pmp_allow) begin
$display("MMU fault L0 PTE PMP access=%0d priv=%0d vaddr=%h pte_addr=%h cause=%h",
saved_access_type, saved_priv, saved_vaddr, pte_addr,
saved_access_fault_cause);
state <= MMU_FAULT;
fault_cause <= saved_access_fault_cause;
fault_tval <= saved_vaddr;
end else if (dresp.data_ok) begin
if (pte_fault) begin
$display("MMU fault L0 PTE access=%0d priv=%0d vaddr=%h pte_addr=%h pte=%h invalid=%0d leaf=%0d priv_fault=%0d access_fault=%0d not_leaf=%0d",
saved_access_type, saved_priv, saved_vaddr, pte_addr,
dresp.data, pte_invalid, pte_is_leaf, pte_priv_fault,
pte_access_fault, pte_not_leaf_at_l0);
state <= MMU_FAULT;
fault_cause <= saved_fault_cause;
fault_tval <= saved_vaddr;
end else begin
if (!l0_pmp_allow) begin
$display("MMU fault L0 leaf PMP access=%0d priv=%0d vaddr=%h paddr=%h cause=%h pte=%h",
saved_access_type, saved_priv, saved_vaddr, l0_paddr,
l0_pmp_fault_cause, dresp.data);
state <= MMU_FAULT;
fault_cause <= l0_pmp_fault_cause;
fault_tval <= saved_vaddr;
end
end
end
end

7. 当前仿真现象与下一步

重新生成 ready-to-run/xv6/kernel.bin 后,有限周期仿真可以打印到:

1
2
3
4
5
6
xv6 kernel is booting
main: kinit begin
main: kinit done
main: kvminit begin
main: kvminit done
main: kvminithart begin

随后 MMU 输出第一条关键 fault:

1
MMU fault L2 PTE access=2 priv=1 vaddr=0000000040600004 pte_addr=0000000087fff008 pte=0000000000000000 invalid=1 leaf=0 priv_fault=0 access_fault=1 super_mis=0

这说明开启分页后,S 态 store 访问课程 UART 地址 0x40600004,但 xv6 的 kernel page table 没有映射该 MMIO 页。后续反复出现 vaddr=0 的取指 fault,是因为 trap vector 尚未设置,异常重定向到了 0。下一步应在 xv6-riscv/kernel/vm.c 或 memlayout.h 中补充课程 UART MMIO 映射,并在定位结束后删除本节标注的临时 printk() 和 MMU $display() 调试代码。

四、最终修正:让 xv6 跑到 shell

前面定位出的 0x40600004 fault 最终不是通过修改 xv6 页表解决,而是把 UART 地址转换移到仿真 RAM 外设层处理:CPU 访存阶段继续把 xv6 的物理地址 0x10000000 交给 MMU 翻译,避免在开启页表后访问 xv6 没有映射的课程 UART 地址。

1
2
3
mem.sv
assign dreq_valid = mem_req_valid && !xv6_uart_lsr_read && !xv6_uart_ignore;
assign dreq_addr = raw_dreq_addr;

xv6 的调度器会执行 wfi,因此在译码阶段把 0x10500073 作为合法 system 指令处理。

1
2
3
4
5
decoder.sv
3'b000: is_valid_inst = is_ecall ||
(inst == 32'h10500073) ||
((inst[31:25] == 7'b0001001) && (inst[11:7] == 5'b00000)) ||
(xret_type != XRET_NONE); // ecall/wfi/sfence.vma/mret/sret

仿真 RAM 新增 xv6 UART 物理地址输出路径,保留原课程测试使用的 0x40600004 输出路径。

1
2
3
4
5
6
7
8
9
10
ram.sv
unique case (oreq.addr)
64'h10000000: if (oreq.strobe[0]) begin
$fwrite(32'h8000_0001, "%c", oreq.data[7:0]); // xv6 uart stdout
$fflush(32'h8000_0001);
end
64'h40600004: if (oreq.strobe[4]) begin
$fwrite(32'h8000_0001, "%c", oreq.data[39:32]); // stdout
$fflush(32'h8000_0001);
end

仿真 RAM 新增简单磁盘 MMIO 寄存器,用 0x10001000 写块号、0x10001008 写缓冲区物理地址、0x10001010 写命令并同步调用 DPI 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ram.sv
import "DPI-C" function int simple_disk_rw(input longint blockno, input longint paddr, input int write);

u64 simple_disk_block, simple_disk_addr, simple_disk_status;

unique case (oreq.addr)
64'h10001000: simple_disk_block <= oreq.data;
64'h10001008: simple_disk_addr <= oreq.data;
64'h10001010: begin
if (oreq.data == 64'd1 || oreq.data == 64'd2) begin
simple_disk_status <= {32'b0, simple_disk_rw(simple_disk_block, simple_disk_addr, oreq.data == 64'd2)};
end
end
64'h10001018: simple_disk_status <= oreq.data;

仿真 RAM 对 xv6 访问的 PLIC 区间返回 0,避免没有实现完整 PLIC 时出现无意义内存读写。

1
2
3
4
5
6
7
8
ram.sv
if ((oreq.addr >= 64'h0c000000) && (oreq.addr < 64'h10000000)) begin
oresp.data = '0;
end else begin
unique case (oreq.addr)
64'h10001000: oresp.data = simple_disk_block;
64'h10001008: oresp.data = simple_disk_addr;
64'h10001018: oresp.data = simple_disk_status;

C++ RAM 后端打开 xv6 文件系统镜像,并把 guest 物理地址 0x80000000 之后的地址映射到仿真 RAM 指针中读写 1024 字节块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ram.cpp
static FILE *open_simple_disk() {
const char *paths[] = {
"xv6-riscv/fs.img",
"fs.img",
"ready-to-run/xv6/fs.img",
NULL
};

for (int i = 0; paths[i] != NULL; i++) {
simple_disk_fp = fopen(paths[i], "r+b");
if (simple_disk_fp != NULL) {
return simple_disk_fp;
}
}
return NULL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ram.cpp
extern "C" int simple_disk_rw(uint64_t blockno, uint64_t paddr, int write) {
const uint64_t block_size = 1024;
uint8_t *buf = (uint8_t *)ram + (paddr - 0x80000000);
uint64_t offset = blockno * block_size;

fseek(fp, offset, SEEK_SET);
if (write) {
n = fwrite(buf, 1, block_size, fp);
fflush(fp);
} else {
n = fread(buf, 1, block_size, fp);
}

return 1;
}

为了避免 JTAG 开关未初始化导致仿真器意外监听端口,EmuArgs 构造函数里显式关闭 JTAG。

1
2
3
emu.h
enable_diff = true;
enable_jtag = false;

定位完成后删除了 xv6 内核中的阶段性 printk()、系统调用 trace、磁盘 trace,以及 MMU 中的临时 $display() fault 输出;这些只是调试信息,不属于最终实现路径。

五、总结

1. 对 xv6 源码的假设与对应修改

假设课程 CPU 当前只支持 rv64ima,因此 xv6 使用 rv64ima 编译,避免生成压缩指令和当前工具链不支持的 zicsr/zifencei 写法。

1
2
3
4
5
Makefile
CFLAGS += -march=rv64ima

kernel/entry.o: kernel/entry.S
$(CC) -march=rv64ima -mabi=lp64 -g -c -o $@ $<

假设仿真器不实现完整 virtio-mmio block device,因此 xv6 磁盘驱动改成同步简单 MMIO 协议。

1
2
3
4
5
6
7
8
9
10
11
xv6-riscv/kernel/virtio_disk.c
#define SDISK_BLOCK 0x00
#define SDISK_ADDR 0x08
#define SDISK_CMD 0x10
#define SDISK_STATUS 0x18

*R64(SDISK_BLOCK) = b->blockno;
*R64(SDISK_ADDR) = (uint64)b->data;
*R64(SDISK_CMD) = write ? SDISK_CMD_WRITE : SDISK_CMD_READ;
while ((status = *R64(SDISK_STATUS)) == 0)
;

假设仿真 UART 没有真实 TX 中断,因此 uartwrite() 改为轮询 LSR 后直接写 THR,避免控制台输出睡眠等待中断。

1
2
3
4
5
6
xv6-riscv/kernel/uart.c
for (int i = 0; i < n; i++) {
while ((ReadReg(LSR) & LSR_TX_IDLE) == 0)
;
WriteReg(THR, buf[i]);
}

假设启动性能比内存填充调试更重要,因此 memset() 用 64 位批量写提升清零速度。

1
2
3
4
5
6
xv6-riscv/kernel/string.c
uint64 *qdst = (uint64 *)cdst;
while (n >= 8) {
*qdst++ = pattern;
n -= 8;
}

假设本实验不依赖 xv6 用 junk pattern 检查释放页和新分配页,因此去掉 kfree()/kalloc() 中的大块填充以减少启动周期。

1
2
3
4
5
xv6-riscv/kernel/kalloc.c
if (r)
kmem.freelist = r->next;

return (void *)r;

2. 如何从头编译 xv6 并用本 CPU 运行

先在 xv6-riscv 目录生成文件系统镜像和内核 ELF。

1
2
cd /home/winchester/projects/arch-rebuild/26-Arch/xv6-riscv
make fs.img kernel/kernel

如果看到 /bin/sh: 1: qemu-system-riscv64: not found,但后面 riscv64-unknown-elf-ld 成功生成了 kernel/kernel,这条 qemu 探测提示可以忽略,因为这里不使用 qemu 运行 xv6。

把 xv6 内核 ELF 转成 CPU 仿真器加载的裸二进制镜像。

1
riscv64-unknown-elf-objcopy -O binary kernel/kernel ../ready-to-run/xv6/kernel.bin

回到项目根目录重新编译 CPU 仿真器。

1
2
cd /home/winchester/projects/arch-rebuild/26-Arch
make sim

用有限周期运行 xv6,必须带 --max-cycles,防止 shell 等输入时无限跑。

1
TEST=sys ./build/emu --no-diff -i ./ready-to-run/xv6/kernel.bin --max-cycles 100000000

成功现象是看到 xv6 启动并进入 shell 提示符。

1
2
3
4
xv6 kernel is booting

init: starting sh
$

如果之后出现 EXCEEDING CYCLE/INSTR LIMIT,并且 $ 已经打印出来,表示 xv6 已进入 shell 后等待输入,有限周期退出是预期行为。

六、上板

为了复用 Lab 3 的上板流程,先把 xv6 的裸二进制内核转换成 Vivado BRAM IP 可以加载的 64 位宽 .coe 文件。该文件由 ready-to-run/xv6/kernel.bin 生成,当前大小约 81KB,对应的 kernel.bin 约 38KB,可以放入现有深度为 21000、宽度为 64 位的 BRAM。

1
2
3
4
5
6
ready-to-run/xv6/kernel.coe
memory_initialization_radix = 16;
memory_initialization_vector =
7901011300009117
f14025f300001537
02b5053300158593

修改 Vivado 的 bram_0 IP 配置,让初始化文件从 Lab 5 镜像切换到 xv6 镜像。

1
2
3
vivado/test-cpu/src/ip/bram_0/bram_0.xci
- <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Coe_File">../../../../../ready-to-run/lab5/kernel.coe</spirit:configurableElementValue>
+ <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Coe_File">../../../../../ready-to-run/xv6/kernel.coe</spirit:configurableElementValue>

板上原有 UART 外设只识别课程测试地址 0x40600004/0x40600008,因此新增 xv6 原生 UART 地址常量,分别对应 THR 和 LSR。

1
2
3
device.svh
parameter logic [63:0] XV6_UART_THR = 64'h10000000;
parameter logic [63:0] XV6_UART_LSR = 64'h10000005;

为了让 xv6 的 uartwrite() 轮询可以继续执行,板上 device 在读取 XV6_UART_LSR 时返回 0x20,表示 TX idle。

1
2
3
4
5
6
device.sv
unique case(addr)
XV6_UART_LSR: begin
rdata = 64'h20;
end
endcase

发送条件同时兼容原课程 UART 地址和 xv6 UART THR 地址;如果 UART 发送状态机忙,则阻止重复投递字符。

1
2
3
4
5
6
7
8
9
device.sv
always_ff @(posedge clk) begin
if (reset) putchar <= '1;
else if (~valid) putchar <= '1;
else if ((addr == TX_DATA || addr == XV6_UART_THR) && valid && wvalid && txState != RDY) putchar <= '0;
end

assign send = (idx != 0 && finish) ||
((addr == TX_DATA || addr == XV6_UART_THR) && valid && wvalid);

原课程 UART 写字符位于 wdata[39:32],xv6 UART 写 THR 时字符位于 wdata[7:0],因此根据地址选择不同字节作为发送数据。

1
2
device.sv
assign char_data = finish ? str[idx] : (addr == XV6_UART_THR ? wdata[7:0] : wdata[39:32]);

Vivado 仿真输出分支也同步兼容 xv6 UART 地址,便于在 behavioral simulation 中直接观察 xv6 输出。

1
2
3
4
5
6
7
8
9
10
device.sv
always_ff @(posedge clk) begin
if (~reset && valid && wvalid) begin
if (addr == TX_DATA || addr == XV6_UART_THR) begin
$write("%c", char_data);
end else if (addr == FINISH_ADDR) begin
$write("Hello World!\n");
end
end
end

Vivado 中也可以通过图形界面修改同一个 BRAM 初始化位置:在 IP Sources 中右键 bram_0,选择 Re-customize IP,在初始化文件选项里选择 ready-to-run/xv6/kernel.coe,之后重新生成 output products、综合、实现和 bitstream。直接修改 .xci 的好处是配置可以随仓库保存,后续重新打开工程时路径可追踪。

第一次上板尝试

synthesis 通过,implementation 报错

从 Vivado 报错看,implementation 失败的主因不是语法错误或 .coe 文件格式错误,而是目标 FPGA 资源不足:当前设计需要 16595 个 F7 Mux,但 Basys3 目标器件只有 16300 个可用 F7 Mux,因此 DRC 阶段直接失败,placer 没有继续执行。

1
2
3
[DRC UTLZ-1] Resource utilization: F7 Muxes over-utilized in Top Level Design
This design requires 16595 of such cell types but only 16300 compatible sites are available in the target device.
[Vivado_Tcl 4-23] Error(s) found during DRC. Placer not run.

同时还有一个次要约束问题:Basys-3-Master.xdc 第 132 行约束了 RsRx,但当前 basys3_top 只导出了 RsTx,没有 RsRx 输入端口,因此 Vivado 找不到该端口对象;这不是本次 implementation 失败的主因,但会产生 critical warning,后续可以注释该约束或补一个未使用的 RsRx 端口。

1
2
3
Basys-3-Master.xdc
set_property -dict {PACKAGE_PIN B18 IOSTANDARD LVCMOS33} [get_ports RsRx]
set_property -dict {PACKAGE_PIN A18 IOSTANDARD LVCMOS33} [get_ports RsTx]

如果需要小幅减少资源占用,优先选择不影响“打印 xv6 启动信息”的功能裁剪:第一优先级是去掉板上暂时不需要的完整磁盘路径,因为当前上板阶段只追 xv6 kernel is booting,不追进入 shell;第二优先级是裁剪 AMO/原子指令路径,因为早期内核打印阶段通常不依赖完整原子指令;第三优先级是裁剪复杂 PMP/权限检查或调试输出相关逻辑;最后才考虑裁剪 MMU、CSR、trap 或 mret/sret,因为这些是 xv6 正常进入 S 态和后续用户态的核心路径。

1
2
3
4
5
6
建议裁剪优先级:
1. 板上不用的磁盘/MMIO 复杂路径
2. 暂不上板验证的 AMO/原子指令路径
3. 非必要 PMP/调试/统计逻辑
4. 宽 mux 较多的旁路或异常辅助信号
5. 不建议优先裁剪 MMU、CSR、trap、mret/sret

本次实际先选择裁剪 PMP 检查器,而不是裁剪 AMO 或 MMU:PMP 逻辑包含 64 位范围比较、NAPOT 地址范围计算、权限选择等组合逻辑,容易消耗 F7 Mux;同时 xv6 早期启动通常只需要 PMP 放行 S 态访问内存,因此板上路径可以先简化为 always allow,而 Verilator 路径仍保留完整 PMP 逻辑,尽量不影响已有测试。

1
2
3
4
5
6
7
8
9
pmp_checker.sv
`ifndef VERILATOR
`UNUSED_OK({paddr, access_size, access_type, current_priv, pmpcfg0, pmpaddr0})

assign allow = 1'b1;
assign fault_cause = 64'b0;
`else
// Verilator 中保留原完整 PMP 检查逻辑
`endif

完整 PMP 检查代码被包进 VERILATOR 分支中,因此软件仿真、difftest 和之前的实验测试仍使用原来的 PMP 行为;Vivado 上板综合时只综合简化分支,以减少资源占用。

1
2
3
4
5
6
7
8
9
10
11
12
pmp_checker.sv
`else
u8 cfg0;
u7 napot_ones, napot_shift;
u64 access_bytes, access_last, access_fault_cause;
u64 range_start, range_end, napot_mask, napot_bytes;
u1 pmp_r, pmp_w, pmp_x, pmp_l, pmp_active, access_overflow;
u1 range_valid, addr_match, perm_allow;
u2 pmp_a;
...
assign fault_cause = allow ? 64'b0 : access_fault_cause;
`endif

第二次上板尝试

第二次尝试中,Vivado 已经使用简化后的 PMP 检查器重新 synthesis,但 implementation 仍然在 DRC 阶段失败,并且不再只是 F7 Mux 略微超出,而是 LUT、寄存器和多级 mux 等多类资源均明显超过 Basys3 目标器件容量。

1
2
3
4
5
6
7
8
9
10
F7 Mux:                 16659 / 16300
F8 Mux: 8212 / 8150
FDRE: 127169 / 65518
LUT as Logic: 58079 / 20800
LUT6: 46875 / 32600
MUXF7: 16659 / 16300
MUXF8: 8212 / 8150
Register as Flip Flop: 127196 / 41600
Slice LUTs: 58079 / 20800
Slice Registers: 127196 / 41600

从这些数据可以看出,当前完整 labplus CPU 设计已经远超 Basys3 板卡资源:Slice LUT 约为可用资源的 2.8 倍,Slice Register 约为可用资源的 3.1 倍。PMP 简化属于低风险的小幅裁剪,无法改变整体资源规模。因此结论是:当前目标板 FPGA 资源不足,无法在保持现有完整功能的前提下完成 xv6 上板。


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