构建五级流水线——lab+-PMP

本文最后更新于 2026年6月19日 08:17:00

一、PMP的背景原理

1. 组成

PMP分成pmpcfg和pmpaddr。根据csrfile的内容可知,本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}\)个字节 NA4和NAPOT表示的字节范围

二、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
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
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
assign addr_match = range_valid &&
!access_overflow &&
(paddr >= range_start) &&
(access_last <= range_end);

2. csr_file

CSR 文件把 pmpcfg0pmpaddr0 作为输出暴露给顶层,并实现 L 位锁定语义。锁定后对 pmpcfg0pmpaddr0 的后续写入都会被忽略,直到 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
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_fileMMU

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,程序长时间无法退出。经过排查源码,发现需要实现U模式,但之前的lab没有这一要求(我只实现了S模式) alt text

四、参考资料

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

构建五级流水线——lab+-PMP
https://travellingsheep.github.io/riscv/lab+-PMP/
作者
trs62
发布于
2026年6月18日
许可协议