
本篇定位:RISC-V 专题第 6 篇。你做 M-mode bare-metal,管的是物理内存(PMP);MMU 是 S/U 的事但要知道边界;RVWMO 内存模型和 DMA/cache 一致性是你做 DMA 和多核必踩的坑。本篇讲清四件事:① PMP 怎么配;② MMU/Sv39 知道到什么程度;③ RVWMO 什么时候要 fence;④ DMA 一致性怎么处理。先理清 M-mode 和内存的关系M-mode全权访问所有物理内存,不受 PMP 限制、不经 MMU(默认)。你写裸机,看到的就是物理地址,写的也是物理地址。PMP 是你(M-mode)给 S/U 划的权限围栏;MMU 是 S/U 用来做虚拟地址的。所以本篇对你是:PMP 是你用的,MMU 是你要懂边界的,DMA 一致性是你必踩的。目录一、PMP(Physical Memory Protection)1.1 PMP 的作用1.2 PMP 寄存器1.3 地址匹配模式1.4 配置示例1.5 PMP 对 M-mode 的特殊性二、MMU 与 Sv39/Sv48(知道边界)2.1 什么时候用 MMU2.2 Sv39 页表2.3 三级页表翻译2.4 satp 寄存器(S-mode 专属)2.5 你需要懂到什么程度三、RVWMO:弱内存模型(多核/DMA 必懂)3.1 为什么有 RVWMO3.2 什么时候需要 fence(1) 多核同步(2) DMA 相关(3) 自修改代码3.3 fence 指令详解四、DMA 与 cache 一致性(重点)4.1 问题根源4.2 两种解法(1) 软件维护一致性(常见于 MCU)(2) 硬件维护一致性(高性能芯片)4.3 coherent vs non-coherent buffer五、内存模型小结:你该用什么六、原子指令(A 扩展)简述七、本篇小结速查表一、PMP(Physical Memory Protection)PMP 是 M-mode 给低特权模式(S/U)设的物理内存权限保护。M-mode 自己不受 PMP 限制(这是特权)。1.1 PMP 的作用16 个区(pmpcfg0-15 pmpaddr0-15)每区配:地址范围 权限(R/W/X) 模式主要用途:保护 S/U 模式不能访问的内存区(如 M-mode 代码/数据、外设寄存器)1.2 PMP 寄存器pmpcfg0-15(每寄存器配 4 个区,每区 8 位): bit0: R 可读 bit1: W 可写 bit2: X 可执行 bit3-4: L/ 锁定模式 bit5-7: A 地址匹配模式(00禁用,01TOR,10NA4,11NAPOT) pmpaddr0-15: 地址(右移2位,所以粒度至少4字节)1.3 地址匹配模式模式含义OFF(00)该区禁用TOR(01)Top of Range:pmpaddr 是该区上界,下界是上一区上界NA4(10)Naturally Aligned 4 字节:精确匹配 4 字节NAPOT(11)Naturally Aligned Power of Two:2 的幂对齐区域TOR 最常用:你配一组 TOR 区,从低地址到高地址划分权限。1.4 配置示例保护 M-mode 代码区(0x0-0x10000)不被 U 模式访问:// 区0:M-mode 代码区,U/S 不可访问pmpaddr00x100002;// 上界(右移2)pmpcfg0(00)|(01)|(02)|(13)|(14);// L1,TOR// 实际:pmpcfg0 的第0区 L|ATOR,RWX0 → U/S 完全不能访问 0x0-0x10000// 区1:U 模式可读写的 RAM 区(0x10000-0x20000)pmpaddr10x200002;pmpcfg0|(18)|(19)|(010)|(111)|(112);// 第1区 L|ATOR, R1,W1,X0 → U/S 可读写 0x10000-0x200001.5 PMP 对 M-mode 的特殊性M-mode默认不受 PMP 限制(全权)但如果某区设了L(Lock)位,M-mode 也要遵守该区权限用途:M-mode 想自我约束(如安全启动后锁定 M 代码区不可写)嵌入式视角:PMP 是 MCU 上轻量 MMUMCU 裸机通常不开 MMU(没 S/U),但 PMP 仍有用:① 保护关键内存区防止跑飞写入;② 多任务时隔离任务栈;③ 调试时锁定代码区防意外覆盖。功能安全(ISO 26262),PMP 是隔离安全区和非安全区的硬件基础。二、MMU 与 Sv39/Sv48(知道边界)MMU 是 S/U 模式用的虚拟内存机制,M-mode 默认不用。2.1 什么时候用 MMU跑 Linux(必须有 MMU)跑需要虚拟内存的 RTOS(少数)多进程隔离(每个进程独立地址空间)MCU bare-metal/FreeRTOS 不用 MMU——你直接操作物理地址。2.2 Sv39 页表Sv39 是 RISC-V 64 位常用的页表模式:虚拟地址(39位): [38:30] VPN[2](9位,512个PGD项) [29:21] VPN[1](9位,512个PUD项) [20:12] VPN[0](9位,512个PTE项) [11:0] offset(12位,4KB页) 物理地址(56位): [55:12] PPN(44位) [11:0] offset 页表项(PTE)64位: bit0: V(valid) bit1: R(readable) bit2: W(writable) bit3: X(executable) bit4: U(user accessible) bit5: G(global) bit6: A(accessed) bit7: D(dirty) bit8-9: RSW(reserved for software) bit10-53: PPN2.3 三级页表翻译satp 寄存器:存页表根地址 模式(Sv39) → 读 VPN[2] 索引 PGD → PUD 地址 → 读 VPN[1] 索引 PUD → PTE 地址 → 读 VPN[0] 索引 PTE → 物理页地址 → offset → 物理地址2.4 satp 寄存器(S-mode 专属)satp[63:60]: MODE(0Bare, 8Sv39, 9Sv48, 10Sv57) satp[59:44]: ASID(地址空间ID,TLB 切换用) satp[43:0]: PPN(页表根物理地址)M-mode 不能直接配 satp(它是 S 级 CSR),M 级 bootloader 切 S 级前配好切换 satp 切换地址空间(如进程切换)2.5 你需要懂到什么程度概念:知道 Sv39 是三级页表、4KB 页、satp 管根地址边界:M-mode 不用 MMU,S-mode 用;M 级 bootloader 切 S 前配 satp调试:跑 Linux 时页表错了会页 fault(mcause 12-15),知道是 MMU 问题不用懂:页表项具体位段、TLB 刷新细节(Linux 内核专栏再深度分析)MCU 工程师的 MMU 心态深度:能看懂、能改配置,不要求能从零写页表。三、RVWMO:弱内存模型(多核/DMA 必懂)RISC-V 采用RVWMO(RISC-V Weak Memory Ordering)——弱内存模型。这意味着内存读写不保证按程序顺序对其他观察者可见。3.1 为什么有 RVWMO顺序一致性(SC)对硬件限制大,性能差弱内存模型允许硬件重排/乱序/缓存延迟,性能好代价:程序员在需要顺序的地方要显式加 fence3.2 什么时候需要 fence单核单线程普通代码:不用 fence(对你自己观察,程序顺序执行)。这些场景必须 fence:(1) 多核同步// 核0 写数据 标志data42;fence rw,rw;// 保证 data 写入先于 flagflag1;// 核1 等标志 读数据while(flag!1);fence r,r;// 保证先读 flag 再读 dataxdata;// 一定能读到 42没 fence,核0 可能先写 flag 再写 data(硬件重排),核1 看到 flag1 时 data 还没写入。(2) DMA 相关// CPU 准备 DMA 发送 buffertx_buf[0]...;tx_buf[1]...;fence w,o;// fence.i 或 fence w,o:保证 buffer 写入对 DMA 可见dma_start(tx_buf);// 启动 DMADMA 不经 CPU cache,如果你写 buffer 时数据还在 cache 没下刷,DMA 读到的是旧数据。(3) 自修改代码// 写新代码到内存memcpy(func_addr,new_code,size);fence.i;// 指令缓存同步!必须func_addr();// 执行新代码fence.i同步 icache 和 dcache——你改了内存里的指令,但 icache 还是旧的,不加 fence.i 会执行旧指令。3.3 fence 指令详解fence predecessor, successor predecessor/successor 可组合: i: instruction r: read w: write o: output(设备写) i: input(设备读) 常用: fence rw, rw 全内存屏障(最强) fence r, r 读读屏障 fence w, w 写写屏障 fence.i 指令屏障(同步 icache) fence o, i 输出-输入(设备IO)fence predecessor, successor语义:predecessor 之前的内存操作,对 successor 之后的内存操作可见。四、DMA 与 cache 一致性(重点)这是你做 DMA 收发 SPI/UART 数据必踩的坑,RISC-V 和 ARM 都有。4.1 问题根源CPU 写数据 → 进 CPU cache → 还没下刷到内存 DMA 从内存读 → 读到旧数据(缓存命中不到 DMA)或反过来:DMA 写数据到内存 → CPU cache 还是旧值 CPU 读 → 命中 cache → 读到旧数据4.2 两种解法(1) 软件维护一致性(常见于 MCU)DMA 读前:CPU 写过的 buffer 要flush(下刷 cache 到内存)DMA 写后:CPU 要读的 buffer 要invalidate(丢弃 cache,强制从内存读)// DMA 发送prepare_tx_buffer(tx_buf,size);cache_flush(tx_buf,size);// 让 DMA 看到 CPU 写的dma_start(tx_buf,size);while(!dma_done);// DMA 接收dma_start(rx_buf,size);while(!dma_done);cache_invalidate(rx_buf,size);// 让 CPU 看到 DMA 写的process(rx_buf,size);(2) 硬件维护一致性(高性能芯片)DMA 经 cache controller(总线监听)CPU/DMA 共享一致性,软件不用管4.3 coherent vs non-coherent buffercoherent buffer:DMA 和 CPU 自动一致,软件不管(硬件支持)non-coherent buffer:软件管 flush/invalidate(常见 MCU)你分配 DMA buffer 时要知道是哪种,决定要不要手动维护。五、内存模型小结:你该用什么场景要不要 fence怎么做单核普通代码不要—多核共享数据要fence rw,rw 或用原子指令DMA 发送前要cache flush fenceDMA 接收后要cache invalidate自修改代码要fence.i设备寄存器 IO看实现MMIO 通常强序,但 volatile 必须用中断返回不要mret 隐含顺序保证六、原子指令(A 扩展)简述A 扩展提供原子操作,多核同步用,不需要 fence:lr.w rd, (rs1) # Load-Reserved:加载并标记 sc.w rd, rs2, (rs1) # Store-Conditional:有条件存储,成功返回0 amoadd.w rd, rs2, (rs1) # 原子加 amoswap.w rd, rs2, (rs1) # 原子交换LR/SC 实现自旋锁:voidspin_lock(int*lock){while(1){// 等待 lock 释放while(lr.w(lock)!0);// 尝试获取if(sc.w(lock,1)0)return;// 成功// 失败重试}}A 扩展的原子指令隐含内存屏障语义,不用额外 fence。多核 RTOS/驱动要用。七、本篇小结PMP 是 M-mode 给 S/U 设的物理内存权限,M-mode 默认不受限(L 位锁定除外)MMU(Sv39/Sv48)是 S-mode 虚拟内存,M-mode 不用,跑 Linux 才配 satpRVWMO 弱内存模型:多核/DMA/自修改代码要显式 fencefence 指令:predecessor/successor 组合,fence.i 同步 icacheDMA 一致性:non-coherent buffer 要软件 flush/invalidate原子指令(A 扩展)隐含屏障,多核同步用 LR/SC速查表场景怎么做保护 M 代码区不被 U 访问配 PMP(TOR 模式,RWX0)M-mode 自我锁定PMP L 位置 1DMA 发送前cache flush(buffer) fence w,oDMA 接收后cache invalidate(buffer)改代码后执行fence.i多核共享数据fence rw,rw 或用 A 扩展原子自旋锁lr.w sc.w(A 扩展)跑 Linux 配页表satp(S-mode CSR)技术之路漫漫分享是为了更好地交流。如果本文的内容对你有启发希望能得到你的点赞 和收藏 ⭐。如果你在调试过程中遇到了其他问题欢迎在评论区 留言我们一起探讨。也欢迎关注 我一起交流底层开发的那些事儿。