第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之间总得通信吧?那就需要共享内存。但共享内存是个双刃剑——用好了是桥梁,用不好就是后门。

我个人的做法是:

  1. 划出一块专门的共享内存区域,大小固定
  2. 这块区域对非AUTOSAR代码只读,对SWC可读写
  3. 或者反过来,看谁生产数据谁消费数据
  4. 用信号量或者事件机制做同步,别直接轮询
/* 共享内存区域配置 */
#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资源。