第三章 内存保护单元(MPU):给ADAS系统装上“内存警察”

各位同学,咱们今天聊MPU。说实话,我在做ADAS开发的头两年,对MPU的理解也就停留在“能保护内存”这个层面。直到有一次,一个非安全任务的内存越界,直接把安全关键任务的刹车控制数据给覆盖了——车在测试场里差点冲出去。从那以后,我对MPU的态度就变成了:这不是可选项,这是必选项

3.1 MPU工作原理:说白了就是个“门禁系统”

MPU的全称是Memory Protection Unit,内存保护单元。你想想看,一个ADAS系统里,有处理摄像头数据的、有做路径规划的、有控制刹车的——这些任务如果都能随便访问任何内存地址,那不乱套了?

MPU的工作原理其实很简单:它监控每一次内存访问,检查是否符合预设的规则。如果符合,放行;如果不符合,触发异常。

具体来说,MPU会检查三个东西:

  • 地址范围:你要访问的地址在不在允许的区域内?
  • 访问类型:你是读、写还是执行?权限够不够?
  • 特权级别:当前代码运行在特权模式还是用户模式?

我在项目中遇到过一种情况:某个传感器驱动在中断服务程序里写了一个不该写的全局变量。如果没有MPU,这种bug可能要等到整车测试才能发现。有了MPU,当场就给你一个MemManage Fault,调试起来方便太多了。

核心要点:MPU不是用来提高性能的,它是用来保证安全的。在ADAS领域,安全就是一切。

3.2 区域配置与权限管理:怎么给任务“划地盘”

Cortex-R系列处理器支持8个或16个内存区域(具体看型号)。每个区域都可以独立配置:

配置项 说明 我的建议
基地址 区域的起始地址 建议按4KB对齐,省得麻烦
大小 区域的大小(2的幂次) 别贪大,够用就行
访问权限 读、写、执行、不可访问 安全任务尽量只给最小权限
子区域 可以把一个区域分成8个子区域 我一般不用,除非内存特别紧张

权限管理这块,我个人习惯用“最小权限原则”。说白了就是:一个任务能干活就行,多一个权限都不要给

举个例子,摄像头数据采集任务:

  • 它需要读传感器寄存器 → 给读权限
  • 它需要写DMA描述符 → 给写权限
  • 它不需要执行任何代码 → 不给执行权限

小技巧:Cortex-R的MPU支持“背景区域”的概念。如果你不配置任何区域,默认所有地址都是不可访问的。这其实是个好事——从零开始,只开放你需要的东西。

3.3 在ADAS中隔离安全关键任务与非安全任务

这是MPU在ADAS中最核心的应用。咱们把任务分成两类:

安全关键任务

  • 刹车控制
  • 转向控制
  • 安全气囊触发
  • ASIL-D级别的任何功能

非安全任务

  • 信息娱乐系统
  • 日志记录
  • OTA升级(非关键部分)
  • 调试接口

隔离的思路是这样的:

  1. 给安全关键任务分配专用的内存区域,设置只允许特权模式访问
  2. 非安全任务运行在用户模式,只能访问自己的“小地盘”
  3. 安全任务之间如果需要共享数据,通过MPU配置一个“共享区域”,双方都有读写权限

我曾经踩过一个坑:两个安全任务共享一个缓冲区,我忘了给其中一个任务配置写权限。结果那个任务写数据时直接触发异常,整个系统复位了。嗯,调试了整整一个下午才发现问题。所以共享区域的权限配置一定要反复检查

警告:千万不要让非安全任务有任何途径修改安全任务的代码或数据。哪怕是一个字节的修改,都可能导致灾难性的后果。这不是危言耸听,ISO 26262里明确要求了这种隔离。

3.4 实战配置示例:手把手教你配MPU

好了,理论说完了,咱们来点实际的。下面是一个Cortex-R5的MPU配置示例,我假设咱们有一个ADAS系统,包含三个任务:

  • Task1:刹车控制(安全关键,特权模式)
  • Task2:摄像头采集(安全关键,特权模式)
  • Task3:日志记录(非安全,用户模式)
/* MPU区域配置结构体 */
typedef struct {
    uint32_t base_addr;   /* 基地址 */
    uint32_t size;        /* 区域大小 */
    uint32_t access_perm; /* 访问权限 */
    uint32_t subregion;   /* 子区域掩码 */
} MPU_RegionConfig;

/* 配置三个区域 */
MPU_RegionConfig regions[3] = {
    /* 区域0:刹车控制任务 - 只有特权模式可读写 */
    {
        .base_addr = 0x20000000,
        .size = 0x10000,        /* 64KB */
        .access_perm = 0x03,    /* 特权读写,用户不可访问 */
        .subregion = 0xFF       /* 所有子区域使能 */
    },
    /* 区域1:摄像头数据缓冲区 - 特权读写,用户不可访问 */
    {
        .base_addr = 0x20010000,
        .size = 0x20000,        /* 128KB */
        .access_perm = 0x03,
        .subregion = 0xFF
    },
    /* 区域2:日志缓冲区 - 用户模式可读写 */
    {
        .base_addr = 0x20030000,
        .size = 0x10000,        /* 64KB */
        .access_perm = 0x07,    /* 特权+用户都可读写 */
        .subregion = 0xFF
    }
};

/* 初始化MPU */
void MPU_Init(void) {
    int i;
    
    /* 先禁用MPU */
    __set_MPU_CTRL(0);
    
    /* 配置各个区域 */
    for(i = 0; i < 3; i++) {
        __set_MPU_RNR(i);                    /* 选择区域编号 */
        __set_MPU_RBAR(regions[i].base_addr); /* 设置基地址 */
        __set_MPU_RASR(
            (regions[i].access_perm << 24) |  /* 访问权限 */
            (regions[i].subregion << 8)  |   /* 子区域 */
            (regions[i].size << 1) |          /* 区域大小 */
            1                                  /* 使能该区域 */
        );
    }
    
    /* 使能MPU,启用特权模式下的默认映射 */
    __set_MPU_CTRL(0x01);
    
    /* 数据同步屏障,确保配置生效 */
    __DSB();
    __ISB();
}

这段代码配置了三个内存区域。注意看区域2的权限设置——我给了用户模式读写权限,因为日志记录任务运行在用户模式。而区域0和区域1只有特权模式才能访问,这就实现了安全隔离。

关键点:配置完MPU之后,一定要加__DSB()__ISB()。我见过有人忘了加,结果MPU配置没生效,排查了半天。这两个指令的作用是确保所有配置都写入硬件并生效。

最后说一句:MPU配置不是一劳永逸的。随着系统迭代,任务的内存需求会变,权限策略也会调整。我个人习惯在每次版本发布前,专门花一天时间审查MPU配置,看看有没有遗漏或者过度授权的区域。这个习惯救过我好几次。

好了,这一章就到这里。下一章咱们聊聊Cache和TCM,这两个东西在ADAS实时性优化里可是大杀器。