简介:针对瑞萨RH850/F1K车规级32位MCU的MPU自检例程包,面向使用该芯片进行功能安全软件开发的工程师,以及希望深入理解单片机内存保护机制的学习者。RH850/F1K支持ASIL B功能安全等级,MPU(内存保护单元)用于管控指定存储空间的读、写、执行权限;压缩包围绕MPU访问权限配置与自检流程展开,既能演示如何配置保护区间,也能验证异常访问触发保护的过程,帮助读者快速掌握安全机制在工程中的落地方法。压缩包共79个文件,压缩后约795KB;内容以GHS工程为主,包括c/h源码、gpj工程文件,以及o/d/lst/dbo等编译输出、hex/map/ld等链接与烧录相关文件,另附有docx格式的说明文档,方便从源码、编译到运行结果全程对照学习。目录结构清晰,主控逻辑集中在独立MPU源文件中,便于定位关键代码。目前已有467人浏览学习,适合需要结合完整工程实例来理解RH850/F1K的MPU访问权限设计与自检流程的开发者。
1. 一个 7z 包为什么会以 MPU 访问权限命名
把F1K_GHS_14_R7F701587_MPU_AccessPrivilege91H.7z这个文件名拆开读一遍:F1K 是平台代号,GHS_14 表示 Green Hills MULTI 2014.1 左右的工具链版本,R7F701587 是瑞萨 RH850/P1M-C 系列的一颗 MCU,剩下的MPU_AccessPrivilege91H才是内容核心——一份针对这颗芯片的 MPU region 配置与访问权限实现。这种压缩包在量产 ECU 项目里常见于 boot/app 隔离、功能安全监控或者防盗刷改造,交付物拆开看无非三样:GHS 的链接指令文件、启动阶段初始化 MPU 的代码、以及一张记录各区域读写执行属性的权限描述。
MPU 配置好之前,任何一段代码都可能因为踩到错误区域而在 main 之前触发非法访问;配错之后,故障又往往表现为复位后现象不定、难以定位。下面先从 RH850 的 MPU 判定规则讲起,经过 GHS 链接脚本和最小驱动,最后落到一个能在 MULTI 里一次定位违规地址的验证手段。适合手里已经有 RH850 开发板、正要开始做 boot/app 隔离或者功能安全防护的工程师。
2. R7F701587 的 MPU 与 Access Privilege 判定规则
2.1 RH850/P1M-C 的 MPU 位于 CPU 与总线之间
先明确一颗 SoC 上 MPU 的位置。R7F701587 属于 RH850/P1M-C 系列,内核是瑞萨 G3M,MPU 挂在 CPU 内核侧,位于 CPU 与总线互连之间。它同时对指令取指和数据访问做检查,检查依据是当前 CPU 工作模式。G3M 内核支持特权模式和用户模式两种状态,默认复位后运行在特权模式;只有显式切换到用户模式并且 MPU 开启时,访问权限才开始体现差别。MPU 本身提供若干显式 region(常见实现为 16 个,具体以手册为准),每个 region 有起始地址、结束地址和属性字段,另有一个 background region 作为兜底。MPU 禁能时所有访问不被拦截,等价于全权限。
这种设计带来的后果是:MPU 不是一个可以开了就不管的外设。链接脚本里段的摆放、启动代码里 region 的使能时机、异常处理里对违规地址的读取,三者配合才决定隔离是否真的生效。实际项目里最常见的返工原因是只填了地址范围没填属性位,或者把 background region 设成全特权,结果是用户态代码可以把整个 Flash 当 RAM 读,权限隔离形同虚设。
2.2 Access Privilege 属性组合与判定顺序
Access Privilege 在 RH850 手册里通常由读、写、执行加上特权/用户两组位共同描述。判定一个访问是否合法时,MPU 先按地址匹配显式 region,命中就按该 region 的属性做裁决;没有命中任何显式 region 的访问才落到 background region。值得注意,region 之间如果发生地址重叠,不同子系列的处理方式不同,有些是低编号优先,有些直接报配置错误。配置前先查手册里 Priority 相关段落,比靠猜可靠得多。
| 属性组合(示意) | 特权态 | 用户态 | 典型用途 |
|---|---|---|---|
| 读+执行,禁止写 | 允许 | 允许读/执行,禁止写 | 代码 Flash 区 |
| 读+写,禁止执行 | 允许 | 只读 | 标定数据区 |
| 读+写+执行 | 允许 | 允许 | 调试期临时开放 |
| 禁止一切访问 | 可配置 | 禁止 | 保留区域/DFlash 窗口 |
这张表只给出组合语义,具体 bit 位在不同型号里不一致。P1M-C 手册里属性寄存器还带有空间 ID、特权限定等附加字段,iodefine 头文件里同一组寄存器出现过MPBnAT配MPBnSA/MPBnEA和MPBn[no].at两种写法,同系列不同版本头文件也有差异。移植代码时先编译一个空工程,去头文件里确认宏名,比对着老代码复制省时间。
2.3 文件名里的 91H 对应什么
回到标题里的AccessPrivilege91H。91H 更像是厂内变更号或缺陷单号,这种“平台_工具链_版本_芯片_模块_单号.7z”的命名在供应商交付包里很常见,表示这个包修复或实现了单号 91H 对应的权限配置。也可以换一个角度读它:如果 MPU 错误状态寄存器里抓到的违规信息是0x91,二进制展开后第 0、4、7 位为 1,这些位在手册里通常对应读、执行和特权限定中的某几个组合。包里如果带了 release notes,第一件事就是查 91H 对应的测试用例,多数情况下它会对应向只读区写入或者用户态访问特权区这类回归项。
3. 在 GHS 链接脚本里规划受保护的内存区域
3.1 GHS 的 .ld 不是 GNU ld 脚本
接手这种包,第一个容易踩的坑是把 .ld 当成 GNU ld 脚本去读。Green Hills MULTI 的链接指令文件用的是自家语法,文件里常见的是-place at、-section这类指令行,而不是 GNU 的SECTIONS { }块。两者混用不会得到任何有效提示,链接器会直接报行号错误。理解这一点后再打开 .ld,目标就很明确:它负责把编译器生成的.text/.rodata/.data/.bss等段放到指定地址,并且段与段之间不能和别的 region 地址空间重叠。
对 MPU 而言,链接脚本是权限配置的上游。因为 region 有对齐和最小粒度要求,段如果排得不紧凑,MPU 区域会被迫扩大,把相邻的外设寄存器窗口或别的 RAM 池圈进同一个 region。轻则误拦截,重则偶发故障,非常难查。建议每个受保护区域单独建段,不要把所有东西塞进默认的大 RAM 段。
3.2 用链接指令把三类段分开放
# GHS linker directives 示意(具体语法以所用 GHS 版本的模板为准) -place at 0xFF000000 { .text .rtext } -place at 0xFF200000 { .rodata .ldata } -place at 0xFEED0000 { .data .sdata .bss .sbss }这段指令的含义是三行三件事:第一行把代码段放到 Flash 起始地址;第二行把只读常量紧跟在代码之后,便于给 MPU 只读 region 画一条连续边界;第三行把可读写数据放到内部 RAM。0xFF000000和0xFEED0000是占位地址,R7F701587 的实际 Memory Map 以数据手册为准,不同封装和子型号会有差异。链接完成后,map 文件里.text的起始地址应该正好等于准备配置的 region 起始地址。
按这个布局可以规划出三个 MPU region:第一个覆盖代码区,属性是读+执行、禁止写;第二个覆盖只读常量区,属性只读,和代码区可以合并也可以分开,分开的好处是错误报告能精确到是访问代码还是访问常量;第三个覆盖 RAM 区,属性读+写、禁止执行。外设区域不设显式 region,让 background region 兜底,前提是启动流程里 background 被配置为无权访问,否则兜底策略不成立。
3.3 GHS 关键构建选项与 map 文件核对
构建时与 MPU 相关的选项集中在三处:内核选择、字节序、map 输出。
| GHS 选项 | 作用 | 与 MPU 的关系 |
|---|---|---|
-cpu=rh850g3m | 指定内核 | 决定指令集和特权指令可用性 |
-endian=little | 指定字节序 | MPU 属性寄存器按 32 位写,位域定义随字节序变化 |
-map=app.map | 输出链接映射文件 | 用于核对段范围与 region 边界对应关系 |
-Ospace/-Otime | 优化取向 | 影响段大小,可能让 region 上边界贴近,建议留余量 |
具体选项写法以 MULTI 里 Processor 下拉框生成值为准。构建完成后用文本工具核对段放置:
# 在链接 map 文件里查代码段和只读段的实际地址范围 grep -nE "\.text|\.rodata|\.data" app.map | head -40这一步一定要做。链接脚本写的地址和最终段地址不一致的情况,在换了 GHS 版本或者改过启动文件后经常出现。map 文件里每段的起始地址和结束地址,就是要填进 region 起始/结束寄存器的数值来源。
4. 最小 MPU 驱动:Region 初始化与违规用例
4.1 寄存器级 region 配置函数
/* iodefine 头文件由瑞萨提供, R7F701587 对应型号可用, GHS 下直接编译 */ #include "r7f701587.h" #define MPU_REGION_MAX 16u #define ATTR_PRIV_RW (0x11u) /* 示意值: 特权读+写, 具体位以手册为准 */ void mpu_region_set(uint32_t no, uint32_t start, uint32_t end, uint32_t attr) { if ((no >= MPU_REGION_MAX) || (start >= end)) { return; /* 参数越界直接拒绝, 避免写坏寄存器 */ } start &= ~0x1Fu; /* 32 字节对齐, 防止圈进相邻外设 */ end &= ~0x1Fu; MPBnSA(no) = start; /* region 起始地址 */ MPBnEA(no) = end; /* region 结束地址 */ MPBnAT(no) = attr; /* 属性: 读写执行 + 特权限定 */ }逻辑说明:参数检查把 region 编号超限和地址关系颠倒的情况挡在外面;对齐掩码把低 5 位清零。RH850 的 region 起始与结束地址按 32 字节对齐是安全做法,比手册要求更保守,能把地址越界圈到邻居的概率降到零。这里ATTR_PRIV_RW只是示意值,实际属性要按 R7F701587 手册和头文件定义替换,直接抄其它系列的数值会得到完全相反的权限。
4.2 启动阶段初始化顺序
void mpu_init(void) { MPDE = 0u; /* 1. 先禁能 MPU */ mpu_region_set(0u, 0xFF000000u, 0xFF1FFFFFu, ATTR_CODE); /* Flash: 读+执行 */ mpu_region_set(1u, 0xFF200000u, 0xFF3FFFFFu, ATTR_RODATA); /* 只读数据 */ mpu_region_set(2u, 0xFEED0000u, 0xFEED7FFFu, ATTR_PRIV_RW); /* RAM: 特权读写 */ MPBG = ATTR_NO_ACCESS; /* 2. 配置 background region */ MPDE = 1u; /* 3. 最后统一使能 */ }顺序上有一个容易被忽略的点:MPU 禁能状态下修改 region 寄存器,写操作不受访问权限影响;一旦使能,任何一条访问都要过检查。所以代码区、数据区、背景区必须全部配置完,最后再打开 MPDE。若把使能放在前面,后面每一行寄存器写入都会先经过 MPU 自己,初始化内部就会触发违规,程序停在启动阶段。背景区配成无访问权限,意味着未列出的地址一旦被访问就进异常,这是生产模式期望的行为。
提示:调试期先别把 background region 配成全部拒绝,否则任何遗漏地址都会立刻进异常,而异常处理又可能没就绪,只会无限复位。先放开 background,把确认过的 region 逐个加回来,最后再收紧。
4.3 一个故意触发违规的回归用例
void mpu_test(void) { volatile uint32_t v; v = *(volatile uint32_t *)0xFF000000u; /* 读代码区地址, 合法 */ (void)v; *(volatile uint32_t *)0xFF000000u = 0x5Au; /* 对只读代码区写入, 应触发 MPU 违规 */ }这个用例对应 91H 这类单号常见的回归项:向只读区写入。第一行读代码区是合法的,用于确认代码区属性没把读也禁掉;第二行写入同一地址,如果配置正确,程序执行到这里会立即进入 MPU 错误处理,而不是等到复位。注意volatile不能去掉,否则写入可能被当作死代码优化掉。测试通过后把测试函数从发布版本里摘除,或者用编译宏包起来,避免把故意写错的逻辑留到产线版本。
5. 在 MULTI 里把 MPU 违规地址一次定位到行的验证技巧
5.1 先把违规信息存进 RAM
异常处理例程里第一时间记录违规地址和属性,再进入死循环等待调试器。复位后变量还在,配合 MULTI 的 Watch 窗口,就能知道是哪一次访问踩了线。
struct fault_log { volatile uint32_t addr; volatile uint32_t info; }; struct fault_log g_fault; void mpu_error_handler(void) { g_fault.addr = MPER_SA; /* 违规访问地址, 宏名以 iodefine 为准 */ g_fault.info = MPER_AT; /* 违规属性/访问类型 */ for (;;) { /* 停在这里, 不自动复位 */ } }g_fault 建议放进不被启动代码清零的 RAM 段,这样复位后仍可读。for(;;)死循环的目的是给调试器留出挂载时间,避免异常处理一退出去又复位,证据被冲掉。
5.2 异常断点与地址反查
在 MULTI 里把断点下在异常向量或mpu_error_handler入口,触发后打开 Memory 窗口直接看 g_fault 所在地址,第一眼是违规地址,第二眼是属性值。对照第 2 章的属性表,判断出是用户态访问特权区还是对只读区写入,再查 map 文件把地址换算成符号,通常几步就回到具体的 C 行。启动早期配置错误建议开复位抓启动过程,运行期偶发违规则关闭复位抓现场,复位在这个场景里只会毁掉证据。
本文还有配套的精品资源,点击获取