news 2026/10/7 14:41:56

RISC-V内存属性实战:PMA与Svpbmt PBMT非缓存访问解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RISC-V内存属性实战:PMA与Svpbmt PBMT非缓存访问解析

1. 从一个真实场景说起:为什么需要关心内存属性

几年前我第一次在 RISC-V 平台上调试一块以太网控制器,DMA 描述符写进去之后,网卡死活收不到包。用调试器读内存,数据明明写对了,但设备侧看到的却是旧值。折腾了大半天才反应过来——这段内存被标记成了可缓存(Cacheable),CPU 的写入还躺在缓存里没落到物理内存,DMA 引擎自然读不到。把这段区域改成非缓存属性之后,问题立刻消失。

这个坑在 RISC-V 开发里非常典型。x86 和 ARM 上我们习惯了用 MTRR、PAT 或者页表属性来管理内存类型,到了 RISC-V,这套机制换了个名字,叫PMA(Physical Memory Attributes,物理内存属性)和PBMT(Page-Based Memory Types,基于页的内存类型)。前者是平台层面的物理地址属性,后者是 RISC-V 特权架构里通过 Svpbmt 扩展提供的页级控制手段。两者配合,才能把"哪段内存能缓存、哪段必须直写、哪段可以合并写"这件事说清楚。

这篇内容适合谁看?如果你正在做 RISC-V 的裸机开发、BSP 移植、驱动编写,或者单纯想搞明白 Linux 内核里那些dma_alloc_coherent、ioremap背后到底发生了什么,那这篇东西应该能帮你少走不少弯路。我会从 PMA 的基础概念讲起,把 PBMT 的编码格式拆开揉碎,再落到非缓存访问路径的实际实现上,中间穿插我自己踩过的坑和实测有效的排查手法。

需要提前说明的是,Svpbmt 是 RISC-V 特权规范里的一个可选扩展,不是所有核都实现了。你在动手之前,第一件事应该是翻你手上那颗芯片的文档,确认它到底支不支持。这个后面会详细讲怎么查。

2. PMA 基础:物理内存属性的底层逻辑

2.1 PMA 到底是什么,为什么它不是页表能管的事

要理解 PMA,得先建立一个认知:内存属性这件事,在 RISC-V 里被拆成了两个层次。一个是物理地址层面的,跟地址本身绑定,不管你怎么映射页表,这段物理地址的属性是固定的;另一个是虚拟地址层面的,通过页表项来指定,可以动态变化。

PMA 属于前者。它描述的是"物理地址空间里某一段区域,硬件允许你用什么方式访问"。这个属性由平台(SoC 设计者)决定,通常写死在硬件里,或者通过一些平台特定的配置寄存器来设定。CPU 发出一个物理地址访问请求时,总线或者内存控制器会根据这个地址落在哪个 PMA 区域,来决定这次访问能不能缓存、能不能乱序、能不能合并。

为什么不让页表全权负责?因为有些访问根本不经过页表。比如机器模式(M-mode)下的代码,默认是不开 MMU 的,它发出的就是物理地址。再比如 DMA 引擎,它看到的是物理地址,压根不知道页表的存在。所以必须有一个不依赖页表的机制来约束物理地址的访问行为,这就是 PMA 存在的意义。

我习惯用一个类比来解释:PMA 像是小区大门的门禁规则,规定了这个小区里哪些楼栋允许访客进入、哪些必须登记、哪些禁止通行;而页表更像是每户人家自己的门锁,管的是具体哪个虚拟地址映射到哪个物理地址。门禁规则是物业(平台)定的,你住户改不了;门锁是你自己可以换的。两者各管一层,缺一不可。

2.2 PMA 的典型属性有哪些

不同平台的 PMA 实现细节不一样,但核心属性大同小异。下面这张表是我根据常见 RISC-V 平台整理出来的,实际芯片可能还有额外属性:

属性含义典型用途
Cacheable允许缓存,读写可经过 Cache普通 DRAM 主存
Non-cacheable禁止缓存,每次访问直达设备外设寄存器、DMA 缓冲区
Read-only只允许读ROM、Boot ROM
Write-only只允许写某些 FIFO 端口
Idempotent重复读返回相同值,无副作用判断能否做投机访问
Coherent与其它主设备保持缓存一致多核共享区域
AMO 支持是否允许原子内存操作同步原语所在区域
Execute是否允许取指代码段

这里有个容易混淆的点:Non-cacheable 不等于 Strongly Ordered。很多新手以为只要标成非缓存就万事大吉,其实非缓存还分好几种强弱程度。比如 ARM 里有 Device-nGnRnE、Device-nGnRE 这些细分,RISC-V 的 PMA 虽然没有完全对应的命名,但通过 IO 属性、顺序性约束等组合,也能表达类似语义。这个区别在写驱动时很关键,后面讲非缓存访问路径时会展开。

2.3 怎么查你手上芯片的 PMA 配置

这是实操层面最要紧的一步。PMA 不像页表那样有统一的遍历方式,它高度依赖具体实现。我一般按这个顺序去找:

  1. 先翻芯片的数据手册(Datasheet)和参考手册(Reference Manual),找 "Memory Map" 章节。正规的芯片手册会有一张物理地址映射表,标注每段区域的属性。如果手册里只给了地址范围没给属性,那就得往下走。

  2. 看设备树(Device Tree)。Linux 下很多平台的 PMA 信息会体现在设备树的ranges、reg属性以及一些平台特定的节点里。比如有些 SoC 会用memory-region或者自定义的 compatible 来描述保留区域和它的属性。

  3. 查核的微架构手册。PMA 的检查逻辑通常在 CPU 核或者总线互连(Interconnect)里实现。如果你用的是 SiFive、Andes、平头哥这类厂商的核,它们的核手册里会有 PMA checker 的描述。

  4. 实在找不到就实测。写一段代码往目标地址写值,然后从另一个主设备(比如 DMA)读回来,看能不能读到最新值。读不到说明有缓存;能读到说明至少对这条路径是非缓存的。这个方法土,但管用。

注意:不要想当然地认为"外设地址段一定是非缓存的"。我见过一些平台,外设区域默认是 cacheable 的,需要软件显式配置某些寄存器才能改成非缓存。如果你直接按经验办事,很可能踩坑。

3. Svpbmt 与 PBMT 编码:页表级的属性控制

3.1 Svpbmt 扩展解决了什么问题

前面说 PMA 是物理地址层面的,改不了。那问题来了:如果我想让同一段物理内存,在某些场景下走缓存、在另一些场景下不走缓存,怎么办?PMA 做不到,因为它跟物理地址绑定死了。

这时候就需要Svpbmt。它是 RISC-V 特权规范里的一个扩展,全称是 "Sv39/Sv48/Sv57 Page-Based Memory Types",允许在页表项(PTE)里编码内存类型。这样一来,同一段物理内存,通过不同的虚拟地址映射,可以有不同的缓存行为。这跟 ARM 的页表属性、x86 的 PAT 是同一个思路。

Svpbmt 的核心价值在于灵活性。举几个实际场景:

  • 驱动里用ioremap映射一段外设寄存器,希望它是非缓存的;同时内核的直接映射(linear mapping)里这段物理地址可能是缓存的。有了 Svpbmt,两种映射可以共存,各走各的属性。
  • 用户态做高性能计算,想把某块缓冲区标成 write-combining,减少写内存的开销。
  • 调试时临时把某段内存改成非缓存,方便观察 DMA 行为。

没有 Svpbmt 的话,这些需求要么做不到,要么得靠平台特定的 hack。

3.2 PBMT 的编码格式详解

Svpbmt 在 PTE 里占用了两个位,通常记作PBMT[1:0]。这两个位的编码含义如下:

PBMT 值名称含义
00PMA使用物理内存属性,即回到 PMA 决定
01NCNon-cacheable,非缓存
10IOI/O 类型,通常带更强的顺序性约束
11保留当前规范保留,遇到应视为错误

这个编码设计很巧妙。00是默认值,意味着"我不在页表层面做特殊指定,交给 PMA 去管"。这样保证了向后兼容——不支持 Svpbmt 的软件把这两位当保留位写 0,行为跟以前一致。01和10分别对应两种不同强度的非缓存语义,11留给未来扩展。

具体这两个位在 PTE 里的位置,跟 Sv39/Sv48/Sv57 的 PTE 布局有关。以 Sv39 为例,PTE 是 64 位,其中 bit 0-9 是标志位,bit 10-53 是物理页号,bit 54-60 保留,bit 61-63 是 PBMT 和 N 位。准确地说,PBMT 占的是 bit 62-61,bit 63 是 N(NAPOT)位。这个位置在不同版本的规范里可能有微调,写代码时一定要对着你手上那份规范确认。

提示:如果你在写内核代码,Linux 里对应的宏是_PAGE_PBMT,定义在arch/riscv/include/asm/pgtable-bits.h里。不同内核版本这个宏的位定义可能不一样,别直接抄网上的代码。

3.3 NC 和 IO 到底差在哪

这是很多人搞不清的地方。既然都是非缓存,为什么还要分两种?区别在于顺序性和副作用的约束强度。

NC(Non-cacheable):不经过缓存,但硬件仍然可以对访问做一定程度的优化,比如合并多个写操作、预取读操作。它假设这段内存是"正常的",重复读返回相同值,写操作没有奇怪的副作用。适合 DMA 缓冲区这种场景——你希望它别被缓存,但也不介意硬件做点优化。

IO:约束更强。通常要求访问严格按程序顺序执行,不允许合并、不允许预取、不允许投机。它假设这段内存背后是设备寄存器,读一次可能有副作用(比如读清中断标志),写一次可能触发动作。适合 MMIO 寄存器映射。

用生活化的例子:NC 像是"快递放门口就行,你可以一次拿好几个";IO 像是"每份文件必须当面签收,不能代签、不能攒着一起签"。约束强度不同,代价也不同——IO 更安全但性能更差。

实际选型时,我的经验是:DMA 描述符和缓冲区用 NC,设备寄存器用 IO。如果拿不准,先用 IO,确认没有副作用再降级到 NC。反过来做容易出玄学问题。

4. 非缓存访问路径:从 CPU 到设备的完整链路

4.1 一次非缓存写操作经历了什么

理解非缓存访问路径,最好的办法是跟着一次写操作走一遍。假设 CPU 要往一段标记为 NC 的地址写一个 32 位数据,大致流程是这样的:

  1. CPU 执行 store 指令,产生虚拟地址。
  2. MMU 查页表,把虚拟地址翻译成物理地址,同时读出 PTE 里的 PBMT 位。如果 PBMT 是01或10,这次访问就被标记为非缓存。
  3. 请求进入 L1 Cache。因为是非缓存访问,L1 直接 bypass,不分配 cache line,也不查 tag。
  4. 请求进入 L2/L3 Cache。同样 bypass。有些实现里,非缓存访问会走一条独立的旁路通道,避免污染缓存。
  5. 请求到达总线互连(Interconnect)。这里可能会做地址解码,判断目标是从设备还是主存。
  6. 请求到达目标设备或内存控制器,完成实际读写。

这条路径上,任何一环出问题都会导致非缓存访问失效。我遇到过最隐蔽的一次,是总线互连里有个配置寄存器,默认把某段地址的访问重新路由到了缓存路径,导致明明 PTE 标了 NC,实际还是走了缓存。这种问题只能靠抓总线波形或者用性能计数器来定位。

4.2 内存屏障:非缓存访问的好搭档

非缓存访问不等于自动有序。这一点必须强调。即使你把内存标成 IO 类型,CPU 和编译器仍然可能重排指令。要保证顺序,必须显式加屏障。

RISC-V 里的屏障指令主要是fence。常用的几种:

# 保证之前的读写都完成后,才执行之后的读写 fence rw, rw # 只保证写顺序 fence w, w # 保证设备 I/O 顺序,配合 IO 属性使用 fence iorw, iorw

在 C 代码里,对应的封装是mb()、wmb()、rmb()这些宏。写驱动时,典型的模式是:

writel(value, reg_addr); wmb(); /* 确保写操作真正到达设备 */ writel(next_value, next_reg_addr);

这里有个坑:很多人以为非缓存访问就不需要屏障了。错。非缓存只保证"这次访问不经过缓存",不保证"这次访问相对于其它访问的顺序"。CPU 的乱序执行引擎、写缓冲(Write Buffer)、总线上的重排序,都可能打乱顺序。屏障是必须的。

注意:fence指令本身也有性能开销。不要无脑到处加,只在真正需要顺序保证的地方加。加多了会让性能掉得很难看。

4.3 实测:用 DMA 验证非缓存路径是否生效

光看代码不够,得实测。我常用的验证方法是这样的:

准备两块内存,一块标成 NC,一块保持默认 cacheable。然后:

  1. CPU 往两块内存各写一个已知值。
  2. 立刻用 DMA 引擎从这两块内存读数据到另一个地方。
  3. 比较 DMA 读到的值和 CPU 写的值。

如果 NC 那块 DMA 读到了最新值,cacheable 那块读到旧值(或者不确定),说明非缓存路径生效了。如果两块都读到最新值,可能是 cacheable 那块刚好被刷出去了,多试几次或者加大数据量。如果两块都读到旧值,那说明 NC 没生效,得回去查 PTE 或者 PMA 配置。

这个测试我做过很多次,成功率很高。唯一要注意的是 DMA 引擎本身的缓存一致性能力——有些高端 DMA 是 IO-coherent 的,它自己能 snoop CPU 缓存,这种情况下测试就不准了。得先确认你的 DMA 是不是 coherent 的。

5. 常见问题与排查技巧实录

5.1 问题速查表

下面这张表是我这些年攒下来的,覆盖了大部分非缓存访问相关的疑难杂症:

现象可能原因排查方向
DMA 读到旧数据内存被缓存查 PTE 的 PBMT 位、查 PMA 配置
写寄存器没反应访问被合并或重排加 fence,确认属性是 IO 而非 NC
性能异常低属性过于严格确认是否误用了 IO,尝试降级到 NC
系统随机崩溃缓存一致性问题检查多核间的共享内存属性
PTE 写入后不生效TLB 未刷新执行 sfence.vma
非缓存访问仍然很慢总线路由问题抓总线波形,查互连配置

5.2 几个我踩过的坑

坑一:忘了刷 TLB。改完 PTE 的 PBMT 位之后,如果不执行sfence.vma,TLB 里可能还缓存着旧的翻译结果,新属性不生效。这个坑我踩过不止一次,尤其是在动态修改内存属性的场景下。记住:改页表必刷 TLB。

坑二:PMA 和 PBMT 冲突。如果 PMA 说这段地址是 cacheable,但 PTE 里 PBMT 标了 NC,最终以谁为准?规范里的说法是 PBMT 优先,但实际实现可能有差异。我遇到过一颗核,它的行为是"取两者中更严格的",也就是只要有一个说非缓存就非缓存。这种实现差异只能靠实测确认。

坑三:编译器优化掉了访问。写驱动时,如果连续写同一个寄存器,编译器可能觉得"反正结果一样"就优化掉一次。解决办法是把寄存器指针声明成volatile,或者用writel这类封装。这个坑在开-O2时特别容易遇到。

坑四:多核下的缓存一致性。非缓存访问绕过了缓存,但如果另一个核通过缓存访问了同一段物理内存,两边看到的数据可能不一致。这种场景下要么全用非缓存,要么用硬件一致性协议,不能混着来。

5.3 独家排查手法:二分法定位属性问题

当你不确定问题出在 PMA 还是 PBMT 还是别的地方时,我推荐用二分法:

第一步,把 PTE 的 PBMT 设成00,完全交给 PMA。如果问题消失,说明是 PBMT 配置错了;如果问题还在,往下走。

第二步,检查 PMA 配置。如果平台允许改 PMA(有些可以通过平台寄存器改),把目标区域改成非缓存试试。如果问题消失,说明是 PMA 的问题;如果还在,往下走。

第三步,怀疑总线或设备。这时候就得上示波器、逻辑分析仪或者总线追踪工具了。这一步成本高,但前面两步能排除掉大部分软件问题。

这个方法帮我省了很多时间。大部分情况下,问题在前两步就能定位。

6. 工具选型与调试环境搭建

6.1 调试工具怎么选

搞 RISC-V 底层开发,工具链的选择很关键。我目前的主力配置是这样的:

  • 编译器:官方 GNU 工具链或者 LLVM。两者对 Svpbmt 的支持都还行,但 LLVM 在某些扩展上更新更快。如果你要用到比较新的特性,建议用 LLVM。
  • 调试器:OpenOCD + GDB 是标配。如果预算允许,J-Link 对 RISC-V 的支持也不错,速度快、稳定性好。
  • 仿真器:QEMU 是首选。它支持 Svpbmt,可以在没有硬件的情况下验证代码逻辑。Spike 也可以,但生态没 QEMU 丰富。
  • 总线追踪:如果要做深度调试,需要硬件支持。有些 FPGA 平台可以挂 ILA(Integrated Logic Analyzer),能抓到总线上的实际访问。

6.2 QEMU 下验证 Svpbmt 的配置

QEMU 是个很好的验证环境。配置起来大概是这样:

qemu-system-riscv64 \ -machine virt \ -cpu rv64,svpbmt=true \ -m 2G \ -kernel your_kernel.elf \ -nographic \ -s -S

关键是-cpu rv64,svpbmt=true这一项,显式打开 Svpbmt 扩展。不加的话,QEMU 默认可能不启用,你的 PBMT 位写了也没用。

启动之后,可以用 GDB 连上去,在页表相关的代码处下断点,检查 PTE 的实际值。我一般会写个小脚本,把关键 PTE dump 出来,确认 PBMT 位是不是按预期设置的。

6.3 硬件平台上的注意事项

真机上调试比 QEMU 麻烦得多。几个经验:

第一,先确认核支持 Svpbmt。查misa寄存器或者设备树里的riscv,isa属性。如果不支持,别浪费时间。

第二,注意 PMA 的默认配置。有些平台出厂时 PMA 配置得很保守,所有地址都是非缓存的,性能很差。需要软件初始化时改过来。

第三,保留区域的属性要特别小心。比如 Boot ROM、安全区域,这些地方的属性通常是锁死的,改不了。别在这些区域上折腾。

第四,多准备几块板子。底层调试有时候会把板子搞挂,有备用的能省很多事。我就有过把一块开发板的 PMA 配置改错,导致板子起不来的经历,最后只能靠 JTAG 救回来。

7. 性能优化:非缓存访问的代价与取舍

7.1 非缓存访问到底慢多少

这个问题没有标准答案,取决于具体平台。但可以给个量级参考。在我测过的一颗 1.5GHz 的 RISC-V 核上:

  • 缓存命中的读:约 1-2 个周期
  • 非缓存读(到 DRAM):约 80-120 个周期
  • 非缓存读(到外设):约 150-300 个周期
  • 非缓存写(带 fence):约 100-200 个周期

也就是说,非缓存访问比缓存命中慢两个数量级。这个代价在频繁访问的场景下是致命的。所以原则是:能用缓存就用缓存,非缓存只用在必须的地方。

7.2 减少非缓存访问开销的几种手法

如果确实需要非缓存访问,有几个优化方向:

批量访问。与其一次读一个字节,不如一次读一整块。很多平台支持非缓存的突发传输(Burst Transfer),能显著提高吞吐。写代码时尽量用memcpy这类批量操作,而不是逐个字节循环。

减少屏障。屏障很贵,能省则省。只在真正需要顺序保证的地方加。比如一段连续的寄存器写,如果它们之间没有依赖关系,可以只在最后加一个屏障。

用 write-combining。如果平台支持,把只写不读的缓冲区标成 write-combining,硬件会攒一批写操作再一起发出去,效率高很多。RISC-V 的 PBMT 编码里没有直接的 WC 类型,但有些平台通过 PMA 或者自定义扩展支持。

考虑 DMA。如果数据量大,与其让 CPU 一个个非缓存访问,不如交给 DMA。CPU 只负责配置 DMA 描述符,实际搬运让 DMA 干。这样 CPU 的开销小,DMA 也能跑满带宽。

7.3 一个实际的优化案例

我之前优化过一个网络驱动的收包路径。原始实现是 CPU 逐个读描述符,每个描述符都要非缓存访问,CPU 占用率很高。改成批量读之后,一次读 16 个描述符,CPU 占用率降了大概 40%。

具体做法是把描述符环设计成连续的,然后用一次memcpy把 16 个描述符拷到本地缓存里,再在缓存里处理。这样只有一次非缓存访问,后续都在缓存里操作。代价是需要保证描述符环的对齐和大小合适,不然memcpy本身也会退化成多次非缓存访问。

这个优化的关键是理解访问模式。如果你的访问是随机的、零散的,批量优化效果有限;如果是顺序的、密集的,批量优化收益很大。动手之前先分析访问模式,别盲目优化。

8. 写在最后的一点个人体会

RISC-V 的内存属性管理这套机制,刚接触时确实容易懵。PMA、PBMT、Svpbmt 这几个概念绕来绕去,文档又分散在不同地方。但一旦理清楚了,会发现它的设计其实很清晰:PMA 管物理层,PBMT 管页表层,两者配合覆盖了所有场景。

我个人在实际操作中的体会是,遇到非缓存相关的问题,先别急着改代码,先确认三件事:核支不支持 Svpbmt、PMA 是怎么配的、PTE 里的位写对没有。这三件事确认完,大部分问题都能定位。剩下的就是总线、设备层面的问题,那得上工具了。

还有一点,别迷信文档。文档说的和硬件实际行为可能有出入,尤其是早期芯片。我遇到过手册说支持 Svpbmt 但实际不支持的,也遇到过手册没提但实际支持的。最终还是要靠实测。手边常备一个能跑的最小测试程序,遇到疑问就跑一遍,比翻半天文档管用。

最后分享一个小技巧:如果你在调一个复杂的驱动,怀疑是内存属性问题,可以先把所有相关内存都标成非缓存,确认功能正常之后,再一块一块改回缓存,看哪块改回去会出问题。这样能快速定位到具体是哪段内存的属性配错了。这个方法笨,但有效,我在好几个项目里都用过。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 14:41:17

滤波器阶数如何影响谐波抑制?从一阶到四阶的工程权衡

1. 一开始,我们为什么会被“阶数”卡住做过信号处理的人,十有八九都经历过这个阶段:拿到一个带毛刺的传感器信号,脑子里第一个念头就是“低通滤波”。于是随手拉了个一阶RC低通,截止频率设成100Hz,示波器一…

作者头像 李华
网站建设 2026/10/7 14:41:07

Temu 跨境浏览器该怎么配置?搭建多店铺独立隔离运行环境

Temu 的招商是按区域铺开的:北美、欧洲、中东、日韩、东南亚各有各的站点节奏,全托管与半托管两套模式并行。商品可以在店铺之间复用,账号体系、后台入口与风控规则却彼此独立;同一个主体开几个店、再按类目分店,都是很…

作者头像 李华
网站建设 2026/10/7 14:39:31

嵌入式入门路线:从STM32到FreeRTOS与Linux实战

1. 嵌入式入门到底难在哪,先搞清楚这件事很多人一提到嵌入式,脑子里第一反应就是“难”,第二反应是“我该从哪开始”。网上搜一圈,有人说先学51单片机,有人说直接上STM32,还有人说要先啃C语言和数据结构&am…

作者头像 李华
网站建设 2026/10/7 14:39:11

用MECE法全面核对MapReduce框架Exclusive特性的支持情况

有个活儿看起来不大,做起来却特别磨人:给一套基于 MapReduce 模型实现的 M/R 计算框架做全量核对,看它对 exclusive 特性的支持到底到了哪一步。这里说的 exclusive,是指"排他性"——队列独占、资源独占、任务独占执行这…

作者头像 李华
网站建设 2026/10/7 14:39:06

大疆嵌入式招聘信号:BSP与Linux驱动开发核心能力全解析

1. 从一场新春派对聊起:大疆研发岗到底在找什么样的人 年前那阵子,朋友圈里刷到一条挺有意思的消息,大疆上海研发中心搞了个“新春派对”式的招聘专场,放出来的岗位集中在嵌入式、Linux、BSP、驱动开发、C/C这几个方向。乍一看像是…

作者头像 李华