很多开发者在接触 STM32 MPU(Memory Protection Unit)时,往往只学会了“怎么开”,却卡在了“怎么用”。

你配置了 MPU 保护 0x24000000 这一块内存,但你怎么确信你关心的关键变量(比如 SystemConfig)真的落在了这块地盘上?

本文将从 MPU 原理讲起,手把手教你打通 链接脚本/分散加载文件 -> 变量属性 -> MPU 配置 的全链路,涵盖 STM32CubeIDE (GCC)Keil MDK 两种环境。


一、 什么是 MPU?为什么要用它?

MPU(内存保护单元) 是 Cortex-M 内核中的一个硬件组件,它像一个**“内存海关”**。

在没有 MPU 的裸机或 RTOS 系统中,所有代码都可以随意读写 4GB 空间。这带来了极大的风险:

  1. 野指针踩踏:一个缓冲区的溢出(Buffer Overflow)可能悄悄改写了系统控制标志位。
  2. 代码区被改:程序跑飞后,错误地向 Flash 或 RAM 中的代码段写入数据,导致指令被篡改。
  3. 栈溢出:任务堆栈耗尽,直接覆盖了邻居任务的 TCB(任务控制块)。

MPU 的作用:一旦 CPU 试图访问未授权的地址(比如写“只读区”,或在“不可执行区”运行代码),MPU 会立即拦截并触发 MemManage_Handler 异常,让系统在造成更大破坏前停下来。


二、 核心原理:MPU 保护的是“地盘”

MPU 保护的是物理地址,不是变量名。要实现精准保护,必须分三步走:

  1. 圈地:在链接脚本(GCC)或分散加载文件(Keil)中划分专属区域。
  2. 落户:在 C 代码中强制将变量指定到该区域。
  3. 设防:配置 MPU 保护该物理区域。

三、 第一步:划分内存区域 (IDE 分类教学)

我们需要把 0x24000000 开始的 32KB 内存单独划出来,作为“安全区”。

方案 A:如果你用 STM32CubeIDE (GCC)

找到工程目录下的 .ld 文件(如 STM32H7xx_FLASH.ld),修改 MEMORYSECTIONS

/* 1. 在 MEMORY 中挖出一块 RAM_SAFE */
MEMORY
{
  FLASH    (rx)    : ORIGIN = 0x08000000, LENGTH = 2048K
  DTCMRAM  (xrw)   : ORIGIN = 0x20000000, LENGTH = 128K
  
  /* 新增:定义 RAM_SAFE 区域,起始 0x24000000,大小 32K */
  RAM_SAFE (xrw)   : ORIGIN = 0x24000000, LENGTH = 32K 
  
  /* 剩下的区域改个名字或缩小,避免重叠 */
  RAM_D1   (xrw)   : ORIGIN = 0x24008000, LENGTH = 480K 
}

/* 2. 在 SECTIONS 中定义输出段 */
SECTIONS
{
  /* ... 其他段 ... */

  /* 定义一个段名为 .safe_zone_data,放到 RAM_SAFE 区域 */
  .safe_zone_data :
  {
    . = ALIGN(4);
    KEEP(*(.safe_zone_data)) /* 匹配 C 代码中的 section */
    . = ALIGN(4);
  } >RAM_SAFE
}

方案 B:如果你用 Keil MDK (ARMCC/ARMCLANG)

Keil 不使用 .ld,而是使用 分散加载文件 (.sct)

  1. 点击魔术棒 (Options for Target) -> Linker。
  2. 勾选 Use Memory Layout from Target Dialog 取消掉
  3. 点击 Edit... 编辑分散加载文件。

修改代码如下:

; 这是一个典型的 Keil Scatter File 示例
LR_IROM1 0x08000000 0x00200000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00200000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  
  RW_IRAM1 0x20000000 0x00020000  {  ; 普通 RAM 区域
   .ANY (+RW +ZI)
  }

  ; === 新增部分:定义 MPU 安全区 ===
  ; 名字随意,地址必须是 0x24000000,大小 0x8000 (32KB)
  RW_RAM_SAFE 0x24000000 0x00008000  {
    ; 将所有标记为 .safe_zone_data 的变量放这里
    *(.safe_zone_data)
  }
}

四、 第二步:在 C 代码中“对号入座”

无论你用 Keil 还是 GCC,只要按上述配置好了链接文件,C 代码的写法是通用的(Keil AC5/AC6 均支持标准 attribute 写法)。

typedef struct {
    uint32_t DeviceID;
    uint32_t SecretKey[4];
    float    CalibrationData;
} SystemParam_t;

/* * 使用 __attribute__((section("段名"))) 
 * 强制将变量放到刚才定义的 .safe_zone_data 段中 
 */
SystemParam_t g_SysParam __attribute__((section(".safe_zone_data"))) = {
    .DeviceID = 0x12345678,
    .CalibrationData = 1.05f
};

/* 验证函数:打印地址确认是否成功 */
void Check_Address(void) {
    // 应该输出 0x24000000
    printf("SysParam Addr: %p\r\n", &g_SysParam);
}

五、 第三步:配置 MPU 设防

变量到位后,使用 HAL 库配置 MPU 规则。此代码通用。

我们将该区域配置为 “特权级只读 (Privileged RO),用户级禁止访问”

void MPU_Config_SafeRegion(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    /* 1. 禁止 MPU */
    HAL_MPU_Disable();

    /* 2. 配置 Region 0 */
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    
    // 必须和链接文件/分散加载文件中的地址一致
    MPU_InitStruct.BaseAddress = 0x24000000; 
    
    // 大小必须覆盖变量,且符合 Cortex-M 对齐规则 (2^N)
    MPU_InitStruct.Size = MPU_REGION_SIZE_32KB; 
    
    MPU_InitStruct.SubRegionDisable = 0x00;
    
    // 权限核心:特权级只读,用户级不可访问
    // 效果:代码只能读,不能直接写。写操作会触发 HardFault/MemManage
    MPU_InitStruct.AccessPermission = MPU_REGION_PRIV_RO_USER_INVALID;
    
    // 禁止执行 (XN)
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    
    // 内存属性 (STM32H7 建议不用 Cache 防止一致性麻烦,F4 可根据需要开启)
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    /* 3. 使能 MPU */
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

六、 如何安全地更新数据?

因为设为了“只读”,直接赋值 g_SysParam.CalibrationData = 2.0; 会导致死机。你需要一个“后门”函数:

void Update_SysParam(SystemParam_t *newVal)
{
    // 1. 临时关闭 MPU 保护
    HAL_MPU_Disable();
    
    // 2. 只有在此期间才能写入数据
    memcpy(&g_SysParam, newVal, sizeof(SystemParam_t));
    
    // 3. 立即恢复保护
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

七、 避坑指南

  1. Keil 的名为 at 的黑魔法: 在 Keil 中你可能见过 int var __attribute__((at(0x24000000)));

    • 优点:不用改分散加载文件,简单。
    • 缺点:它只占用了那个变量大小的空间,而 MPU Region 最小也是 32B 且要是 2 的幂。如果变量很小,MPU 保护范围会比变量大,容易误伤旁边的无辜变量。
    • 建议正规军请使用“分散加载文件 + section”的方法,这样 Linker 会自动把无关变量挤出这 32KB 的安全区。
  2. 地址对齐:MPU Region 的 BaseAddress 必须是 Size 的整数倍。如果你定义 Size 为 32KB,地址只能是 0x240000000x24008000 等,不能是 0x24000010

  3. 调试:如果触发了异常,查看 SCB->CFSR 寄存器:

    • MMFSR (Memory Manage Fault Status Register) 会指示是否发生了数据访问违规 (DACCVIOL)。

总结

  • Keil 用户:改 .sct 分散加载文件。
  • GCC 用户:改 .ld 链接脚本。
  • C 代码:用 __attribute__((section("..."))) 定位。
  • MPU:按物理地址设防。

四者合一,你的 STM32 系统就有了真正的“防弹衣”。