第4章 内存保护与隔离:MPU配置策略
内存保护,说白了就是给每个任务画个圈。
非AUTOSAR代码就像个野孩子,你不给它圈好地盘,它随时可能跑到OS或者SWC的地盘上撒野。我在项目里见过最惨的一次——一个非AUTOSAR的驱动直接写穿了OS的堆栈,整个系统瞬间崩溃,连日志都没来得及打印。
4.1 为什么需要MPU?
Classic AUTOSAR平台下,OS和SWC运行在特权模式。非AUTOSAR代码如果也跑在特权模式,那它想干嘛就干嘛。你想想看,一个指针越界,可能就把关键数据给覆盖了。
MPU(Memory Protection Unit)就是干这个的。它把内存分成若干个区域,每个区域有独立的访问权限。我习惯把非AUTOSAR代码限制在用户模式,只给它分配特定的内存区域。
核心原则:非AUTOSAR代码只能访问它自己的数据段和栈,不能碰OS的TCB、SWC的RTE缓冲区、以及任何受保护的外设寄存器。
4.2 MPU的硬件基础
ARM Cortex-R系列和Cortex-M系列都支持MPU。以Cortex-R4为例,它支持8个或12个内存区域。每个区域可以配置:
- 起始地址(必须对齐到区域大小)
- 区域大小(从32字节到4GB)
- 访问权限(读/写/执行)
- 子区域(可以进一步细分)
| 区域编号 | 用途 | 权限 | 典型大小 |
|---|---|---|---|
| Region 0 | OS代码段 | 只读+执行 | 64KB |
| Region 1 | OS数据段 | 读写 | 32KB |
| Region 2 | SWC代码段 | 只读+执行 | 128KB |
| Region 3 | SWC数据段 | 读写 | 64KB |
| Region 4 | 非AUTOSAR代码段 | 只读+执行 | 32KB |
| Region 5 | 非AUTOSAR数据段 | 读写 | 16KB |
| Region 6 | 共享内存 | 读写(需互斥) | 4KB |
| Region 7 | 外设寄存器 | 读写(特权模式) | 1KB |
小技巧:我习惯把非AUTOSAR代码的栈放在Region 5的末尾,这样一旦栈溢出,MPU会立刻触发异常。比等它慢慢踩到OS的地盘再崩溃要友好得多。
4.3 MPU配置实战
配置MPU其实不复杂,关键是要想清楚每个区域怎么划分。我一般分三步走:
4.3.1 定义内存区域
先确定非AUTOSAR代码需要多大空间。别给多了,给多了它容易乱跑。也别给少了,否则频繁触发MPU异常也麻烦。
/* 非AUTOSAR代码的内存区域定义 */
#define NON_AUTOSAR_CODE_START 0x08020000
#define NON_AUTOSAR_CODE_SIZE 0x00008000 /* 32KB */
#define NON_AUTOSAR_DATA_START 0x20008000
#define NON_AUTOSAR_DATA_SIZE 0x00004000 /* 16KB */
#define NON_AUTOSAR_STACK_START (NON_AUTOSAR_DATA_START + NON_AUTOSAR_DATA_SIZE - 0x400)
#define NON_AUTOSAR_STACK_SIZE 0x00000400 /* 1KB */
4.3.2 配置MPU寄存器
嗯,这里要注意。配置MPU必须在特权模式下进行,而且最好在系统启动阶段就配好。我见过有人运行时动态改MPU配置,结果把自己锁在外面了。
void MPU_ConfigureNonAutosarRegion(void)
{
/* 关闭MPU,防止配置过程中产生异常 */
MPU->CTRL = 0;
/* 配置Region 4:非AUTOSAR代码段 */
MPU->RBAR = NON_AUTOSAR_CODE_START | (4 << 4); /* Region编号4 */
MPU->RASR = (0x3 << 24) | /* 全访问权限(用户模式只读+执行) */
(0x1 << 19) | /* 可执行 */
(0x1 << 18) | /* 可读 */
(0x0 << 17) | /* 不可写 */
(0x1 << 16) | /* 使能 */
(0x0E << 1); /* 32KB大小 */
/* 配置Region 5:非AUTOSAR数据段 */
MPU->RBAR = NON_AUTOSAR_DATA_START | (5 << 4);
MPU->RASR = (0x3 << 24) | /* 全访问权限(用户模式读写) */
(0x0 << 19) | /* 不可执行 */
(0x1 << 18) | /* 可读 */
(0x1 << 17) | /* 可写 */
(0x1 << 16) | /* 使能 */
(0x0D << 1); /* 16KB大小 */
/* 重新使能MPU */
MPU->CTRL = 0x01;
}
4.3.3 设置任务特权等级
光配MPU还不够,你得让非AUTOSAR任务跑在用户模式。在AUTOSAR OS里,每个任务都有个特权属性。
/* AUTOSAR OS任务配置 */
TASK(NonAutosarTask)
{
/* 切换到用户模式 */
__asm volatile("MRS R0, CONTROL");
__asm volatile("ORR R0, R0, #0x01");
__asm volatile("MSR CONTROL, R0");
/* 调用非AUTOSAR代码 */
NonAutosar_Entry();
/* 返回时自动切回特权模式 */
}
警告:千万不要在非AUTOSAR代码里尝试切回特权模式。我曾经见过一个团队,他们在非AUTOSAR代码里调用了系统服务,结果系统服务里有个bug,把MPU给关了。那之后整个系统就像脱缰的野马,什么都能干。
4.4 共享内存的MPU策略
非AUTOSAR代码和SWC之间总得通信吧?那就需要共享内存。但共享内存是个双刃剑——用好了是桥梁,用不好就是后门。
我个人的做法是:
- 划出一块专门的共享内存区域,大小固定
- 这块区域对非AUTOSAR代码只读,对SWC可读写
- 或者反过来,看谁生产数据谁消费数据
- 用信号量或者事件机制做同步,别直接轮询
/* 共享内存区域配置 */
#define SHARED_MEM_START 0x2000C000
#define SHARED_MEM_SIZE 0x00001000 /* 4KB */
/* 非AUTOSAR代码只能读共享内存 */
MPU->RBAR = SHARED_MEM_START | (6 << 4);
MPU->RASR = (0x3 << 24) | /* 全访问权限 */
(0x0 << 19) | /* 不可执行 */
(0x1 << 18) | /* 可读 */
(0x0 << 17) | /* 不可写 */
(0x1 << 16) | /* 使能 */
(0x0C << 1); /* 4KB大小 */
4.5 异常处理与恢复
MPU触发异常了怎么办?别慌,这是好事。说明你的保护机制起作用了。
我一般这样处理:
- 记录异常发生时的PC指针和访问地址
- 判断是哪个任务触发的
- 如果是非AUTOSAR任务,直接终止它,然后重启
- 如果是OS或SWC任务,那说明MPU配置有问题,得查代码
void MPU_ExceptionHandler(void)
{
uint32_t faultAddr = __get_M FAR(); /* 获取故障地址 */
uint32_t pc = __get_PC(); /* 获取程序计数器 */
/* 记录日志 */
Log_Write("MPU fault at 0x%X, access 0x%X", pc, faultAddr);
/* 判断故障来源 */
if (IsInNonAutosarRegion(pc))
{
/* 非AUTOSAR代码越界,终止任务 */
TerminateTask(NonAutosarTask);
ActivateTask(NonAutosarTask); /* 重新启动 */
}
else
{
/* OS或SWC故障,系统级错误处理 */
ShutdownOS();
}
}
避坑指南:我曾经在MPU异常处理里加了太多日志打印,结果日志缓冲区又触发了另一个MPU异常,死循环了。后来我改成用GPIO翻转来指示异常,简单可靠。
4.6 性能考量
加了MPU会不会影响性能?会,但影响不大。MPU的检查是硬件完成的,每个内存访问只增加几个时钟周期。比起系统崩溃后重启的时间,这点开销完全可以接受。
不过有一点要注意:频繁的上下文切换会导致MPU配置的重新加载。我建议把非AUTOSAR代码的任务优先级设低一点,减少切换次数。
4.7 小结
MPU配置说白了就是给非AUTOSAR代码画个圈。圈画好了,它再怎么折腾也翻不了天。圈画小了,它老触发异常,你得频繁处理。圈画大了,保护效果就打折扣。
我个人建议:先给个保守的大小,运行一段时间后根据实际使用量调整。别一上来就给太大,否则你都不知道它到底用了多少内存。
下一章我们聊聊任务调度策略——怎么让非AUTOSAR代码和SWC和谐共处,不抢CPU资源。