一、引子:为什么我们不能“一招鲜吃遍天”?

很多 RISC-V 初学者在配置 PMP (Physical Memory Protection) 时,会听到一条金科玉律:

“能不用 TOR 就不用 TOR,尽量用 NAPOT,因为它省 Entry。”

这句话本身没错。NAPOT (Naturally Aligned Power-of-Two) 就像一块高效的大积木,一块就能盖住一大片区域。但在真实的嵌入式工程中,内存布局往往不是童话里的完美方块,而是像被狗啃过的饼干——参差不齐

当你的代码段起始于 0x20010003,或者你需要在一片 Flash 中间切出一块不可执行的只读数据区时,只用 NAPOT 的结果往往是灾难性的

  • 要么 PMP Entry 瞬间爆炸(切成无数个 4KB 小块);
  • 要么根本无法配置(因为起始地址没对齐,硬件直接拒绝)。

👉 结论先行:

NAPOT 负责“宏观覆盖”——快、省;TOR 负责“微观修补”——准、狠。


二、深度剖析:NAPOT 与 TOR 的硬件哲学

要写好 PMP 配置,必须先理解硬件是怎么看待这两个模式的。

1. NAPOT:不仅仅是 2^N,而是“自掩码”的智慧

NAPOT 的本质是 “地址高位匹配 + 低位任意”
它的匹配规则基于一个核心公式:
RegionSize=2N+3 bytesRegionSize = 2^{N+3} \text{ bytes}RegionSize=2N+3 bytes

  • 硬件行为: 配置寄存器 pmpaddr 时,值形如 ...yyyy0111(看最低位)。
  • 对齐要求(强制): Start Address % Size == 0
    • 通俗理解:你要画一个 256KB 的方块,这个方块的起点必须是 256KB 的整数倍。
[ NAPOT 示意图:完美的俄罗斯方块 ]

 0x2000_0000 (对齐边界)
     ↓
┌──────────────────────────────┐
│                              │
│      256KB NAPOT 区域         │  ← 一块积木直接搞定
│                              │
└──────────────────────────────┘
     ↑
 0x2004_0000 (下一个对齐边界)
* 要求:起始地址必须能被 256KB 整除。

2. TOR:两堵墙之间的“任意形状”

TOR (Top of Range) 的匹配逻辑更像是在内存上插篱笆

pmpaddr[i-1]≤Address<pmpaddr[i] \text{pmpaddr[i-1]} \leq \text{Address} < \text{pmpaddr[i]} pmpaddr[i-1]Address<pmpaddr[i]

  • 硬件行为: 使用两个 Entry 构成一个区间。
  • 灵活性: 不需要对齐。你可以精确到 4 字节边界。
[ TOR 示意图:任意位置的区间 ]

Prev_Entry_Addr                Current_Entry_Addr
      ↓ (墙 A)                       ↓ (墙 B)
      ├──────────────────────────────┤
      |    任意大小、任意起点的区间      |
      └──────────────────────────────┘
      ↑ 这里可以是不对齐的地址 0x1003

注意,特别地,对于 pmpaddr[0]0≤pmpaddr[0]<pmpaddr[1] \text{0} \leq \text{pmpaddr[0]} < \text{pmpaddr[1]} 0pmpaddr[0]<pmpaddr[1]


三、战场复盘:什么情况必须请出 TOR 这把“手术刀”?

当你的代码尝试只使用 pmpcfg.A = NAPOT 配置以下区域时,硬件会直接报错或产生非预期结果。

场景 1:无法整除的“尴尬起点”

问题: 起始地址 0x1003,大小 64KB
NAPOT 校验: 0x1003 % (64*1024) != 0 ❌ 非法。
解决策略: 切一刀。

[ 解剖图 ]
0x1000 (合法对齐点)        0x1003 (真实起点)                0x11003
      |                         |                               |
      ├───────── TOR ───────────┼─────────── NAPOT ───────────────┤
      |     (补丁区域)           |         (主体 64KB)              |
      └─────────────────────────┴─────────────────────────────────┘

场景 2:权限的“三八线”

问题: 同一块物理连续内存 0x20000000 - 0x20010000,前半段要执行代码 (X),后半段存只读数据 ®。
NAPOT 困境: NAPOT 块内权限统一。如果用一个大块覆盖,要么代码不能跑,要么数据能被当指令执行(安全隐患)。
解决策略: 权限隔离必用 TOR 切分边界。

场景 3:碎片的“收容所”

问题: 还剩 3KB 的区域没用。
NAPOT 表示: 最小颗粒是 8 字节 (N=0),但 3KB 不是 2 的幂,NAPOT 只能拆成 2KB + 1KB,甚至更碎。
工程实践: 最后那点不规则的小尾巴,直接用一个 TOR Entry 收尾,干净利落。


四、组合拳法:四大实战模型 (含动效图解)

核心思想:用 NAPOT 跑车拉货(运大块),用 TOR 叉车理货(摆正边角)。

模型 1:左切 —— 头部不对齐

场景: 区域从 0x10030x20000

地址轴:
0x1000       0x1003              0x2000                              0x20000
  |             |                   |                                    |
  |<-- TOR #0 -->|<----------------- NAPOT (起始对齐于 0x2000) ---------->|
  |   (修补头部)  |                     (8KB/16KB/... 大块)                |
  ^             ^
 对齐边界      实际起点

配置逻辑:
1. TOR [0x1000, 0x1003) : 过渡区 (或无权限)
2. NAPOT [0x2000, ...)  : 加载主体逻辑

模型 2:右切 —— 尾部残缺

场景: 大块 Flash (0x08000000 ~ 0x0804B000)。

地址轴:
0x08000000                         0x08040000          0x0804B000
     |                                  |                   |
     |<------- NAPOT (256KB 大块) ------->|<--- TOR #1 ---->|
     |                                    |    (修补 44KB)   |
     ^                                    ^
  完美对齐                              此处开始不对齐

配置逻辑:
1. NAPOT 覆盖 256KB 的倍数部分。
2. 剩余 44KB 使用 TOR 定义精确上界。

模型 3:中分 —— 权限隔离 (最关键)

场景: code (X) 紧挨着 flash_data (NX)

物理内存视图:
┌─────────────────┬─────────────────┐
│                 │                 │
│   CODE (X)      │   DATA (NX)     │
│                 │                 │
└────────┬────────┴────────┬────────┘
         │                 │
         └─── 边界线 ──────┘
        这里必须用 TOR 对齐精确地址,否则权限会泄漏!

错误做法:[     一个大 NAPOT (X or NX?)     ] ❌ 灾难

正确做法:
[ NAPOT (X) ] [ TOR 边界 ] [ NAPOT (NX) ]

模型 4:真实战场推演方案

资源需求: code = 504KB, flash = 7688KB

TMUC

方案一:单 NAPOT + 优先级 + TOR 补充策略

Step 1:Code 区域优化处理

  • 原始 Code 大小:504KB(非 2^N 对齐)
  • 优化方案:向上对齐至 512KB
  • 优势:
    • 仅需 1 个 NAPOT entry
    • 保留执行权限(X)
    • 避免多 entry 拆分

Step 2:Flash 区域处理

  • 原始 Flash 大小:7688KB

情况一:地址优先级递增(低→高)

  • 策略:
    • Flash 调整为 7680KB(保留8KB缓冲)
    • Code 扩展至512KB
  • 内存布局:
    | code (512KB, X) | flash (7680KB, RW) |
    
  • 特点:
    • 无地址冲突
    • 权限清晰
    • 存在少量空间浪费

情况二:地址优先级递减(高→低)

  • 策略:
    • Flash 扩展至8MB(8192KB
    • Code 保持512KB
  • 内存布局:
    [Flash 8MB RW](基础配置)
    ↓ 权限覆盖
    [Code 512KB X](优先配置)
    
  • 机制:
    • 利用PMP后配置优先特性
    • 实现权限自动修正
方案对比
方案 Code Flash Entry数 特点
拆分NAPOT 504KB 多块 通用但效率低
单NAPOT(递增) 512KB 7680KB 安全稳妥
单NAPOT(递减) 512KB 8MB 最少 ⭐ 最优方案
TOR补充策略应用场景
  1. 边界不对齐情况

    • 解决代码与Flash间的间隙问题
    • 使用TOR精确覆盖间隙区域
  2. 严格保持Code尺寸

    • 当必须保持504KB时
    • 采用NAPOT拆分+TOR补充方案
  3. 细粒度权限控制

    • 混合使用NAPOT和TOR
    • 实现代码、只读数据、Flash的差异化权限
方案演进

初级方案:

  • 强制拆分504KB
  • 导致entry数量多
  • 实现不够优雅

工程级方案:

  • 允许合理扩展
  • 善用覆盖优先级
  • 核心思路:从"精确匹配"转向"最优覆盖"
实施条件

单NAPOT最优方案需满足:

  1. 允许区域适当扩展(2^N对齐)
  2. 合理利用PMP优先级机制
  3. 确保权限配置无冲突

五、避坑指南:PMP 优先级与 Entry 编排艺术

RISC-V PMP 的匹配逻辑是静态优先级

pmp7cfg > pmp6cfg > … > pmp0cfg

这会导致一种常见的覆盖效应

实用心法:

“底层的铺地板,上层的贴瓷砖。”

错误示范:

  • Entry 0: TOR 保护 0x0 - 0x1000 (RW)
  • Entry 1: NAPOT 保护 0x0 - 0x2000 ®
  • 结果: 访问 0x500 时,Entry 0 先命中,拿到 RW 权限;Entry 1 永远没机会匹配 0x0-0x1000 区间。

正确编排顺序(自底向上配置):

  1. Entry 0 (低优先级): 放最大的背景权限(例如整个 Flash 设为只读)。
  2. Entry N (高优先级): 放特殊的、需要提升权限的小块(例如某个 ROM 函数需要执行权限)。
┌──────────────────────────────────────┐
│  Entry 7: [ 特殊小块 X ]             │ ← 优先级最高,盖在下面
├──────────────────────────────────────┤
│  Entry 6: [ 中等区域 RW ]            │
├──────────────────────────────────────┤
│  Entry 0: [ 巨大背景区域 R ]         │ ← 优先级最低,铺底
└──────────────────────────────────────┘

六、总结:工程师的 PMP 配置口诀

在实际写代码配置 PMP 时,请默念以下四句口诀:

  1. 先断权限后画图 (按 X/W/R 把内存切段)。
  2. 能对齐者用 NAPOT (效率优先,省 Entry)。
  3. 遇门不对找 TOR (起点不对齐、权限切换点、碎尾巴)。
  4. 小块在上大在下 (优先级倒置,特例覆盖通例)。

掌握了 TOR 与 NAPOT 的组合拳,你不仅能省下宝贵的 PMP Entry,更能写出健壮、无漏洞的 RISC-V 底层安全代码。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐