一块 RISC-V MCU 是怎么启动的: 内存保护、中断与从 Flash 直接执行

一块 RISC-V MCU 是怎么启动的: 内存保护、中断与从 Flash 直接执行

MCU (Microcontroller Unit, 微控制器) 内部有几种存储: 对外访问的 Flash (存固件)、片上 SRAM (跑数据)、可选的 DDR (大内存)。比存储分配更隐蔽的是, 不同架构对它们的访问方式和保护机制完全不同: ARM 的 Cortex-M 和新兴的 RISC-V 内核, 在内存保护与中断入口上就是两套机制。

本文以RISC-V 内核 MCU 为例, 讲四件事: 内存保护有几种、中断入口在 RISC-V 上什么样、为什么复位入口不在 SRAM、片上 SRAM 为什么拆两块。每部分先讲通用原理, 再落到该芯片的实测数据 (map 文件与 ELF 头)。结论先行:

  • 内存保护三件套: ARM 用 MPU, RISC-V 用 PMP, 都能查"内存能不能访问", 但机制和寄存器不同
  • 现代 RISC-V MCU 没有硬件向量表, 中断统一跳一个入口, 软件判断原因再分发
  • CPU 复位后第一段代码通常从 Flash 直接执行 (XIP), 不先拷到 RAM
  • 片上 SRAM 拆成"代码区 + 数据区", 是为代码只读、数据可写, 权限分开更安全

一、内存保护: MPU, PMP

1.1 为什么要内存保护

程序跑飞 (指针取错、数组越界) 可能把别的内存写坏, 甚至改掉自己的代码。内存保护就是在硬件上加一道闸: 每次访问检查"这块地址这个 CPU 能不能访问、能不能写、能不能执行", 违反就下 trap, 不让它继续错。这道闸靠硬件控制器, 软件绕不过。

1.2 ARM 的 MPU (对照)

Cortex-M3/M4/M7/M33 基本都内置 MPU (Memory Protection Unit, 内存保护单元)。硬件条件:

  • 8 到 16 个 region (区域) 寄存器, 每个描述一段地址
  • region 大小是 2 的幂, 32B 到 4GB; 每 region 又能切成 8 小块单独允/禁
  • 权限三档: 只读 RO、读写 RW、不可访问 NO ACCESS
  • 有特权 (Privileged) / 非特权 (Unprivileged) 双态, 用户程序跑非特权级

MPU 只判"能不能访问", 不做地址翻译, 把内存切成几块各自设权限。注意老型号 (Cortex-M0, 部分 M0+) 出厂没有 MPU, 靠 ARM 那套保护拿不到。

1.3 RISC-V 的 PMP

RISC-V 的对等物是 PMP (Physical Memory Protection, 物理内存保护), 特权架构标准机制:

  • 一组 CSR 描述 region, 常见 8 到 16 个
  • region 用 NAPOT 编码: 自然对齐的 2 的幂区间, 一条 CSR 同时给基址和大小
  • 直接判物理地址, 不做翻译、不是页表
  • 权限是 R (读) / W (写) / X (执行) 组合, 可锁定 L 防止被特权代码改

一次访问的判定:

flowchart LR A[CPU 内存访问] --> B{PMP 地址比对} B -->|命中 region| C{按 R/W/X 检查} B -->|都不中| D[按默认策略] C -->|合法| E[放行] C -->|越权| F[触发 trap]
维度 MPU (ARM) PMP (RISC-V)
region 描述 基址+大小两个寄存器 NAPOT 一条带出两者
权限 RO / RW / NO ACCESS R / W / X / L 组合
地址映射
特权态 CONTROL 双态 异常/中断切特权级

实测 (e906) : 对固件 ELF 跑 readelf -h 可确认架构:

arduino 复制代码
系统架构: RISC-V   标志: 0x5, RVC, double-float ABI

再查 rtconfig.hARCH_RISCV32ARCH_RISCV_FPU_D, 与 BSP 目录 risc-v/t-head/e906 互相印证。但实测该固件 rtconfig.h没有 RT_USING_MEM_PROTECTION, 也没有 pmpaddr/pmpcfg 初始化------出厂是"裸奔"态, 全跑在特权级, 没有硬件隔离。这是很多 MCU 固件的出厂常态; 要启用得自己适配 RISC-V 的 PMP, 不是套 ARM 的宏。所以"RTOS 有没有内存保护", ARM 和 RISC-V 的答案底层硬件不是一回事。

二、中断入口: 一张表, 还是一个门

2.1 ARM: 一张硬件表

Cortex-M 在启动时把一张"函数指针数组"放 Flash, 叫向量表: 栈顶、复位入口、NMI、各外设中断各占一项。哪个中断来了硬件直接查表跳转, 表址固定或由 SCB->VTOR 指定。

2.2 RISC-V: 一个门, 软件分发

RISC-V 反过来: 不给每个中断单独表项, 只留一个入口 。CPU 响应中断/异常时一律跳到 mtvec 寄存器指向的同一处, 把"为什么被打断"写进 mcause, 剩下交给软件:

flowchart LR T[中断/异常] --> M[mcause 记原因] M --> G[跳 mtvec 唯一入口] G --> Q{软件读 mcause} Q -->|外设中断| H[查 handlers 数组] Q -->|异常| F[异常处理]

三点: 所有中断/异常共用一入口、没有表; 表址存 CSR (mtvec), 运行期由系统初始化写入; 分发用的"软向量表" (中断号到处理函数数组) 是软件维护的。移植时别把 ARM 的 NVIC 写法直接搬来。

实测 (e906): 从 map 文件搜符号看入口排布:

arduino 复制代码
.text  0x0800d6c4  Reset_Handler       ; CPU 复位入口
.text  0x0800dae8  PendSV_Handler      ; 上下文切换
.text  0x0800dc5e  entry               ; RTOS 首个线程 main

trap_entryrisc-v/t-head/e906/, 把入口写进 mtvec 的是 rt_hw_interrupt_init。调试器可读 mtvec (CSR 335/0x14F) 看它指向哪。

三、为什么复位入口在 Flash, 不在 SRAM

3.1 XIP: 直接从 Flash 执行

很多 MCU 复位入口落在 Flash 区, 这是 XIP (execute in place, 原地执行): CPU 取指直接读 Flash, 不先拷进 RAM。常见做法是让 CPU 透过 cache 控制器读挂在 SPI 总线上的外部 Flash, 于是"Flash 代码区"是一块连续地址, 内容住在 Flash, 断电不丢。

3.2 为什么 SRAM 当不了入口

把代码放 SRAM 跑更快, 为什么不一开始就在 SRAM? 因为 SRAM 上电内容未知 , 不能保证是可执行指令; 要它能用, 得先有人把 Flash 拷进来, 可这个"搬运工"自己也是程序、也得被取指执行------鸡生蛋问题。所以复位后必须有一段能在 Flash 直接执行的 Reset_Handler, 由它初始化 SRAM、拷贝、再跳主程序。入口一定在能 XIP 的 Flash 区, 不在 SRAM。

3.3 复位后的顺序

sequenceDiagram participant ROM as BootROM participant MAP as cache 控制器 participant DEV as 外部 SPI Flash participant RAM as 片上 SRAM / DDR ROM->>MAP: 映射 Flash 起点为代码区 ROM->>DEV: PC 跳 Reset_Handler (XIP) DEV-->>ROM: 返回指令执行 ROM->>RAM: 初始化 SRAM, 拷贝数据段 ROM->>MAP: 跳入 RTOS 主流程

实测 (e906) : map 里 .text 起始是 Flash 页边界对齐, 利于 cache 预取:

ini 复制代码
.text  0x0800d6c0   __stext = .     ; 代码区起始
       0x0800d6c4   Reset_Handler   ; 段内 +4, CPU 入口

这块在链接脚本叫 CACHE, 但不是"一组 CPU 缓存", 是芯片经 cache 控制器做 XIP 访问外部 SPI Flash 的窗口------整个应用加 RTOS 内核都装得下, 这是 10KB SRAM 做不到的。

四、片上 SRAM 为什么拆两块

链接脚本里, 代码放在一块只读 SRAM (标 XR, 可执行+只读), 数据放另一块可写 SRAM (标 XRW)。既然都是片上 RAM, 拆开为何?

物理层面: 芯片里本来就是两块独立 SRAM, 地址不连续, 链接脚本只能分两个段, 硬件决定的。

权限层面: 拆开可给不同区设不同权限。代码区只读, 指针跑飞也写不进代码段, 防自改、防恶意数据注入; 数据区可执行+可写, 放堆、栈、全局变量。配合 MPU/PMP 能把 region 设得更细更安全。两块 SRAM 是硬件约束, 权限分开是主动取舍。

实测 (e906):

SRAM_TEXT SRAM_DATA
地址 0x04000000 0xa0005000
大小 10KB 120KB
内容 函数指令 (.text) 数据/栈 (.data, .bss)
属性 xr (可执行+只读) xrw (可读写)

两块地址不连续、容量差 12 倍, 各对应一块物理 SRAM。高频数据、DMA 缓冲用 SRAM_DATA; 海量数据 (视频、文件系统) 才落片外 DDR。

链接器输出 map 文件, 记录"每个地址放了什么", 是排查"代码放哪、Flash/RAM 用多少、符号是否重复"的第一手凭证。以真实固件的 map (rt-thread-YYYY-MM-DD-HH-MM-SS.map) 为例拆解。

5.1 三大部分

由前到后三块, 各回答一个问题:

  1. Archive member included to satisfy reference by file (symbol) --- "用到的函数从哪个静态库拉来的"
  2. Memory Configuration --- "芯片有哪些地址区间, 各多大"
  3. Linker script and memory map --- "每个段/符号落在哪个地址"

5.2 归档成员引入表

开头形如:

bash 复制代码
/opt/.../libc.a (lib_a-fopen.o)
                       /home/.../dm_whd_tool.o (fopen)

含义: dm_whd_tool.o 调了 fopen, 链接器从 libc.a 拉进 lib_a-fopen.o。静态库按需拉取, 只有被引用的函数才进镜像。能解释"没写调用怎么镜像里也有这个库函数"。

5.3 Memory Configuration (内存布局)

这是内存分区总纲:

objectivec 复制代码
Name          Origin       Length       Attributes
CACHE         0x0800d6c0   0x007f0000   xr
SRAM_TEXT     0x04000000   0x00002800   xr
SRAM_DATA     0xa0005000   0x0001d800   xrw
DDR_TEXT      0x20000000   0x00100000   xr
DDR_DATA      0x8d400000   0x00080000   rw
DM_MEM        0x8d480000   0x00008000   rw
RTE_MEM       0x8d488000   0x00038000   rw
VIDEO_MEM     0x8d4c0000   0x00004000   rw
DDR_HEAP      0x8d500000   0x02a00000   rw

列含义: Name 段名 (链接脚本 LD 定义); Origin 起始地址; Length 长度; Attributes 允许操作 (R 读 / W 写 / X 执行 / C 除非截断否则禁写)。观察得出:

  • CACHE: 名字像缓存, 实是 XIP 窗口 (见第三章), 整个固件代码装这
  • SRAM_TEXT / SRAM_DATA: 两块片上 SRAM, 一可执行只读、一可读写 (见第四章)
  • DM_MEM / RTE_MEM / VIDEO_MEM: 给不同模块划的独立段, 防互相越界
  • DDR_HEAP (约 42MB, rw): 最大可写区, 运行时堆, malloc 从这拿

5.4 Linker memory map (具体到符号)

xml 复制代码
.text  0x0800d6c0   . = ALIGN (0x4)
       0x0800d6c0   __stext = .
       0x0800d6c4   Reset_Handler    <startup_gcc.o>
       0x0800dae8   PendSV_Handler   <interrupt_gcc.o>
       0x0800dc5e   entry            <components.o>

格式 符号名 地址 <来自哪个 .o>。可见 Reset_Handler 在 0x0800d6c4、来自启动汇编, 后面紧跟 RTOS entry; 地址越靠后越大, 看代码区从低到高排布。查符号直接搜名字; 查段内存看地址增量。

5.5 常用三件事

  • Flash 占用 : CACHE/DDR_TEXT.text+.rodata 末尾地址减起始
  • RAM 占用 : SRAM_DATA/DDR_DATA/DDR_HEAP.data+.bss 末尾减起始
  • 重复符号 : 第三部分搜同一符号出现两次, 说明两个 .o 重复定义

六、常见误区小结

  1. "RTOS 有内存保护" ≠ 每块芯片都有: 依赖 MPU/PMP, 老 ARM 型号和部分廉价 RISC-V 型号没有这硬件, 软件绕不过
  2. ARM 和 RISC-V 向量表不是一回事: ARM 是硬件表, RISC-V 单入口+软件分发, 移植别直接搬
  3. 代码区不在 SRAM 旁: XIP 下复位代码住外部 Flash, 透过 cache 窗口被当可执行地址访问
  4. SRAM 拆两块既是物理事实也是安全设计: 两块独立 SRAM + "代码只读" 设权限
相关推荐
长栎1 小时前
JDBC 用了 30 年的 Bridge 模式,多数人以为它只是策略模式换了层皮
后端
Ai拆代码的曹操1 小时前
排查 3 小时,问题竟在 Dubbo 路由规则:一个 force 的坑
后端
无责任此方_修行中1 小时前
换个版本号就能升级?可没那简单:pnpm 11 升级踩坑记
javascript·后端·npm
SelectDB1 小时前
洋钱罐基于 SelectDB 实现 Hive 数据湖透明加速:查询 P95 从 300 秒降至 20 秒的完整实践
后端
Chen_LSN2 小时前
C语言——深度理解指针(4)
后端
用户638982245892 小时前
自定义注解@SyncNeo4j实现增删改导入时自动同步数据节点和关系到Neo4j
后端
AlunYegeer2 小时前
Spring Boot 多模块服务间 API Token 鉴权排查记录
java·spring boot·后端
沙湖遇雨2 小时前
3.客户端向服务端建立连接
后端
沙湖遇雨2 小时前
1.netty源码阅读-管理端Server启动
后端