## 前言
MPU(Memory Protection Unit)即内存保护单元。其主要作用是保护指定的内存区域不受意外的访问、执行或者改写。MPU其实从STM32F1开始就有,但是一直鲜有使用,甚至冷门到CubeMX中都没有对应的配置选项。主要的原因还是在CM3和CM4内核中没有cache,MPU的缓存策略功能没有什么用途,并且大多数情况下我们并不需要手动去管理内存区域的读写保护。所以直到F7/H7产品线引入了cache,MPU的缓存读写策略管理功能有了用武之地后,其重要性才开始显现出来。
本文将对HAL库中MPU的相关配置进行详解。
## 详解
### 示例代码
我们先来看一段STM32F7/H7中典型的MPU配置代码:
```
MPU_Region_InitTypeDef MPU_InitStruct;
/* 禁止 MPU */
HAL_MPU_Disable();
/* 配置AXI SRAM的MPU属性为Write back, Read allocate,Write allocate */
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x24000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitSruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/*使能 MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
```
### 全局配置
第一行的`MPU_Region_InitTypeDef`是配置结构体,用来存储MPU配置参数。不多做解释。
在配置MPU之前需要调用`HAL_MPU_Disable`函数来临时禁用MPU,配置结束后则需要调用`HAL_MPU_Enable`函数重新使能。
使能MPU的函数 `HAL_MPU_Enable` 需要一个参数,该参数对应的是MPU的控制策略,共有如下可选项:
* `MPU_HFNMI_PRIVDEF_NONE` 此选项下,默认区域(default region)不允许访问,并且在硬故障,NMI和FAULTMASK处理程序期间**禁用**MPU。
* `MPU_HARDFAULT_NMI` 默认区域(default region)不允许访问,并且在硬故障,NMI和FAULTMASK处理程序期间**启用**MPU。
* `MPU_PRIVILEGED_DEFAULT` 默认区域(default region)仅允许特权访问,并且在硬故障,NMI和FAULTMASK处理程序期间**禁用**MPU。
* `MPU_HFNMI_PRIVDEF` 默认区域(default region)仅允许特权访问,并且在硬故障,NMI和FAULTMASK处理程序期间**启用**MPU。
默认区域(default region)的概念将在下文解释
MPU针对区域的具体配置的参数共有11个。以下是对这些参数进行详细的解析。
### Number参数
**代码:**
```
MPU_InitStruct.Number = MPU_REGION_NUMBER0
```
**作用:设定当前配置的保护区域编号。此处配置的是0号区域。**
MPU总共有16个可以独立配置的内存保护区域(在CM4内核的MCU中是8个),范围从0~15。对应的参数就是`MPU_REGION_NUMBER0`到`MPU_REGION_NUMBER15`。注意,这些区域编号顺序和其保护的内存区域的顺序无关。也就是说,完全可以配置1号区域为`0x24000000~0x24080000`,而2号区域用来保护`0x00000000~0x00010000`。
MPU保护的区域可以重叠。在发生重叠的时候,优先应用高序号的MPU配置。也就是,序号更大的MPU区域优先级更高。
除了0~15这16个保护区域外,还有一个编号为-1的内存保护区域,其称之为`默认区域(default region)`或`背景区域(background region)`。其优先级为最低。`默认区域`的访问策略由启用MPU的函数`HAL_MPU_Enable`的参数指定。
### Enable参数
**代码:**
```
MPU_InitStruct.Enable = MPU_REGION_ENABLE
```
**作用:此参数用来决定此区域的MPU配置是否启用。**
可选项为`MPU_REGION_ENABLE`(启用)和`MPU_REGION_DISABLE`(禁用),
### BaseAddress参数、Size参数
**代码:**
```
MPU_InitStruct.BaseAddress = 0x24000000
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB
```
**作用:此参数用来确定内存保护区域的具体范围**
这两个参数在一起确定了内存区域的范围。其中BaseAddress是保护区域的起始地址。Size则是区域的大小。内存区域的起始地址必须对齐到设定的大小。比如将`Size`设定为1M,那么`BaseAddress`就必去对齐到1M。即 `BaseAddress % Size == 0` 。此外,Size是不能随意设置的,只能从 `MPU_REGION_SIZE_xxx`的宏中进行选取
### AccessPermission参数
**代码:** `MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS` **作用:配置此区域的内存访问(读写)权限**
内存访问权限共有以下几个
| 权限 | 说明 |
| --- | --- |
| MPU_REGION_NO_ACCESS | 禁止所有情况下的访问(读/写) |
| MPU_REGION_PRIV_RW | 仅允许在特权模式下读写 |
| MPU_REGION_PRIV_RW_URO | 特权模式下可读写,用户模式下只读 |
| MPU_REGION_FULL_ACCESS | 完全访问,特权和用户模式下都可以读写 |
| MPU_REGION_PRIV_RO | 仅允许在特权模式下读取 |
| MPU_REGION_PRIV_RO_URO | 特权和用户模式下都只读 |
### IsBufferable、IsCacheable、IsShareable、TypeExtField参数
**代码:**
```
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
```
**这几个参数组合起来共同决定了所配置的内存区域的缓存和共享策略,是相当重要的参数。**
其中参数的具体定义为:
* `IsShareable`:设定配置的内存区域是否可以共享,可设置为1:`MPU_ACCESS_BUFFERABLE`和0:`MPU_ACCESS_NOT_BUFFERABLE`。这个参数在多用在总线和多核访问时的访问控制,一般情况下,除非使用STM32H745/H747,否则基本不用在乎此参数。
* `IsCacheable`:设定cache策略,可设置为1:`MPU_ACCESS_CACHEABLE`和0:`MPU_ACCESS_NOT_CACHEABLE`
* `IsBufferable`:设定Buffer策略,可设置为1:`MPU_ACCESS_BUFFERABLE`和0:`MPU_ACCESS_NOT_BUFFERABLE`
* `TypeExtField`:设定MPU的TEX Level,可设置为:`MPU_TEX_LEVEL0`、`MPU_TEX_LEVEL1`和`MPU_TEX_LEVEL2`。其中,`MPU_TEX_LEVEL2`实际应用不到,所以可以认为只有前两个值可以设定
这几个参数除了Shareable的作用比较清晰明了,其他的作用都不容易说明。实际上,这四个参数是需要组合使用的,单独拿出来分析的意义不大。这四个参数的不同组合对应了MPU可配置的三种内存类型(memory type),参照下表(x表示此位无效),此处的解释参考了安富莱的相关文章,括号中的则是笔者的理解。
| TypeExtField | Cacheable | Bufferable | Shareable | 内存类型和说明 |
| ------------ | --------- | ---------- | --------- | ------- |
| MPU_TEX_LEVEL0 | 0 | 0 | x | **Strongly ordered**
强顺序类型:程序完全按照代码顺序执行,CPU 需要等待当前的加载/存储指令执行完毕后才执行下一条指令。这样会导致性能下降。(笔者的理解是禁用了CPU流水线,所有指令都排队执行,一个结束后才执行下一个) | | MPU_TEX_LEVEL0 | 0 | 1 | x | **Device**
设备类型:此内存区域中的数据加载和存储严格按照次序进行,确保寄存器按照正确顺序设置。(与Strongly ordered的区别是,此处只需要保证数据按照正确的次序读取和存储,没说指令不可以并行执行) | | MPU_TEX_LEVEL0 | 1 | x | x | **Normal**
常规类型:CPU 以最高效的方式加载和存储字节、半字和字,对于这种内存区,CPU 的加载或存储不一定要按照程序列出的顺序执行。(流水线、分支预测和乱序执行) | | MPU_TEX_LEVEL1 | 0 | 0 | x | **Normal**
同上解释 | | MPU_TEX_LEVEL1 | 0 | 1 | x | **Reserved encoding**
保留 | | MPU_TEX_LEVEL1 | 1 | 0 | x | **Implementation defined attributes.**
*未查询到资料,实际编程时也用不到* | | MPU_TEX_LEVEL1 | 1 | 1 | x | **Normal**
同上解释 | 其中Normal的内存类型下,我们可以配置Cache策略。可用的cache策略共有四种,分别如下: | TypeExtField | Cacheable | Bufferable | Shareable | cache策略 | | ------------ | --------- | ---------- | --------- | ------- | | MPU_TEX_LEVEL0 | 1 | 0 | x | `Write through, no write allocate` | | MPU_TEX_LEVEL0 | 1 | 1 | x | `Write back, no write allocate` | | MPU_TEX_LEVEL1 | 0 | 0 | x | `Non-cacheable` | | MPU_TEX_LEVEL1 | 1 | 1 | x | `Write back, write and read allocate` | 这四种策略的详细解读: | 策略 | 描述 | | --- | --- | | `Non-cacheable` | 不使用Cache | | `Write back, write and read allocate` | **写入策略**:如果cache命中,则只写入cache,不再写入到对应的内存区域;如果未命中,则开辟对应的cache,并同时向cache和对应的内存区域写入。
**读取策略**:如果cache命中,则直接从cache中读取,不再读取对应的内存区域,如果cache未命中,则开辟对应的cache区域,并从对应的内存区域中将数据同步至cache,并读取cache。 | | `Write through, no write allocate` | **写入策略**:如果cache命中,则同时向cache和对应的内存区域中写入;如果未命中,则只写入对应的内存区域,不开辟新的cache。
**读取策略**:如果cache命中,则直接从cache中读取,不再读取对应的内存区域,如果cache未命中,则开辟对应的cache区域,并从对应的内存区域中将数据同步至cache,并读取cache。 | | `Write back, no write allocate` | **写入策略**:如果cache命中,则只写入cache,不再写入到对应的内存区域;如果未命中,则只写入对应的内存区域,不开辟新的cache。
**读取策略**:如果cache命中,则直接从cache中读取,不再读取对应的内存区域,如果cache未命中,则开辟对应的cache区域,并从对应的内存区域中将数据同步至cache,并读取cache。 | (关于cache命中:CPU读取或写入某个内存区域时,在D-cache中如果可以找到该区域的数据,则为命中,注意,cache中的数据和对应的实际内存区域中的数据可能不相同,但只要地址区域是一致的,即为命中。) 可以看到,除非关闭cache,否则我们无法控制CPU读取对应内存区域时的缓存策略。 ### SubRegionDisable参数 **代码:** ``` MPU_InitStruct.SubRegionDisable = 0x00; ``` 配置是否禁用对应的`Sub Region`子区域。每个MPU配置的内存区域都被分为了8个子区域。这8个子区域的配置都继承自当前MPU的配置,不可独立设置,只能通过此参数设置对应子区域的禁用或者启用。换句话说,STM32F7/H7支持16个独立可配置的内存区域,然后每个区域又分为8个子区域,也就是共有`16x8 = 128`个内存保护区域。考虑到MPU的区域可以重叠,所以可以多个区域重叠后使用子区域配置来实现更为精细的内存保护策略。 这个参数的数值从0x00~0xFF,其中每一个bit分别对应着一个子区域的禁用或者启用。最高位(MSB)控制最后一个子区域,最低位(LSB)则控制第一个子区域。对应的bit位设置为`0`表示**启用**该子区域,设置为`1`则表示**禁用**该子区域。注意,当区域大于128字节时可以启用。当前区域小于或等于128Bytes时此参数无效并且必须设置为`0x00`,否则MPU的保护策略将不可预知。 注意,如果没有其他可用的区域与禁用的子区域重叠,并且访问的是无特权的或被禁用的默认区域,那么MCU将抛出fault。 ### DisableExec参数 **代码:** ``` MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; ``` **这个参数用来控制当前区域中的指令(代码)是否可以执行。** 这个很容易理解就不多做解释了。可用的选项为`MPU_INSTRUCTION_ACCESS_ENABLE`和`MPU_INSTRUCTION_ACCESS_DISABLE` ## 扩展探讨:MPU不配行不行? 在大多数的情况下,如果不访问外部存储空间(如LCD,Flash等),不开启cache的情况下,不配MPU不会有什么问题。但是: 第一:H7关闭cache后,性能会有大幅度的下降。 第二:由于H7的定位,我们也基本不可能不使用外部存储器。而访问外部存储的内存空间时,如果不配置MPU,则会出现很多奇怪的BUG。 所以,在超过90%的H7应用场景下,MPU是应该首先被配置的。 # 参考 * 《STM32F7和STM32H7编程手册——英文第五版》 * 《安富莱_STM32-V7开发板_用户手册》
强顺序类型:程序完全按照代码顺序执行,CPU 需要等待当前的加载/存储指令执行完毕后才执行下一条指令。这样会导致性能下降。(笔者的理解是禁用了CPU流水线,所有指令都排队执行,一个结束后才执行下一个) | | MPU_TEX_LEVEL0 | 0 | 1 | x | **Device**
设备类型:此内存区域中的数据加载和存储严格按照次序进行,确保寄存器按照正确顺序设置。(与Strongly ordered的区别是,此处只需要保证数据按照正确的次序读取和存储,没说指令不可以并行执行) | | MPU_TEX_LEVEL0 | 1 | x | x | **Normal**
常规类型:CPU 以最高效的方式加载和存储字节、半字和字,对于这种内存区,CPU 的加载或存储不一定要按照程序列出的顺序执行。(流水线、分支预测和乱序执行) | | MPU_TEX_LEVEL1 | 0 | 0 | x | **Normal**
同上解释 | | MPU_TEX_LEVEL1 | 0 | 1 | x | **Reserved encoding**
保留 | | MPU_TEX_LEVEL1 | 1 | 0 | x | **Implementation defined attributes.**
*未查询到资料,实际编程时也用不到* | | MPU_TEX_LEVEL1 | 1 | 1 | x | **Normal**
同上解释 | 其中Normal的内存类型下,我们可以配置Cache策略。可用的cache策略共有四种,分别如下: | TypeExtField | Cacheable | Bufferable | Shareable | cache策略 | | ------------ | --------- | ---------- | --------- | ------- | | MPU_TEX_LEVEL0 | 1 | 0 | x | `Write through, no write allocate` | | MPU_TEX_LEVEL0 | 1 | 1 | x | `Write back, no write allocate` | | MPU_TEX_LEVEL1 | 0 | 0 | x | `Non-cacheable` | | MPU_TEX_LEVEL1 | 1 | 1 | x | `Write back, write and read allocate` | 这四种策略的详细解读: | 策略 | 描述 | | --- | --- | | `Non-cacheable` | 不使用Cache | | `Write back, write and read allocate` | **写入策略**:如果cache命中,则只写入cache,不再写入到对应的内存区域;如果未命中,则开辟对应的cache,并同时向cache和对应的内存区域写入。
**读取策略**:如果cache命中,则直接从cache中读取,不再读取对应的内存区域,如果cache未命中,则开辟对应的cache区域,并从对应的内存区域中将数据同步至cache,并读取cache。 | | `Write through, no write allocate` | **写入策略**:如果cache命中,则同时向cache和对应的内存区域中写入;如果未命中,则只写入对应的内存区域,不开辟新的cache。
**读取策略**:如果cache命中,则直接从cache中读取,不再读取对应的内存区域,如果cache未命中,则开辟对应的cache区域,并从对应的内存区域中将数据同步至cache,并读取cache。 | | `Write back, no write allocate` | **写入策略**:如果cache命中,则只写入cache,不再写入到对应的内存区域;如果未命中,则只写入对应的内存区域,不开辟新的cache。
**读取策略**:如果cache命中,则直接从cache中读取,不再读取对应的内存区域,如果cache未命中,则开辟对应的cache区域,并从对应的内存区域中将数据同步至cache,并读取cache。 | (关于cache命中:CPU读取或写入某个内存区域时,在D-cache中如果可以找到该区域的数据,则为命中,注意,cache中的数据和对应的实际内存区域中的数据可能不相同,但只要地址区域是一致的,即为命中。) 可以看到,除非关闭cache,否则我们无法控制CPU读取对应内存区域时的缓存策略。 ### SubRegionDisable参数 **代码:** ``` MPU_InitStruct.SubRegionDisable = 0x00; ``` 配置是否禁用对应的`Sub Region`子区域。每个MPU配置的内存区域都被分为了8个子区域。这8个子区域的配置都继承自当前MPU的配置,不可独立设置,只能通过此参数设置对应子区域的禁用或者启用。换句话说,STM32F7/H7支持16个独立可配置的内存区域,然后每个区域又分为8个子区域,也就是共有`16x8 = 128`个内存保护区域。考虑到MPU的区域可以重叠,所以可以多个区域重叠后使用子区域配置来实现更为精细的内存保护策略。 这个参数的数值从0x00~0xFF,其中每一个bit分别对应着一个子区域的禁用或者启用。最高位(MSB)控制最后一个子区域,最低位(LSB)则控制第一个子区域。对应的bit位设置为`0`表示**启用**该子区域,设置为`1`则表示**禁用**该子区域。注意,当区域大于128字节时可以启用。当前区域小于或等于128Bytes时此参数无效并且必须设置为`0x00`,否则MPU的保护策略将不可预知。 注意,如果没有其他可用的区域与禁用的子区域重叠,并且访问的是无特权的或被禁用的默认区域,那么MCU将抛出fault。 ### DisableExec参数 **代码:** ``` MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; ``` **这个参数用来控制当前区域中的指令(代码)是否可以执行。** 这个很容易理解就不多做解释了。可用的选项为`MPU_INSTRUCTION_ACCESS_ENABLE`和`MPU_INSTRUCTION_ACCESS_DISABLE` ## 扩展探讨:MPU不配行不行? 在大多数的情况下,如果不访问外部存储空间(如LCD,Flash等),不开启cache的情况下,不配MPU不会有什么问题。但是: 第一:H7关闭cache后,性能会有大幅度的下降。 第二:由于H7的定位,我们也基本不可能不使用外部存储器。而访问外部存储的内存空间时,如果不配置MPU,则会出现很多奇怪的BUG。 所以,在超过90%的H7应用场景下,MPU是应该首先被配置的。 # 参考 * 《STM32F7和STM32H7编程手册——英文第五版》 * 《安富莱_STM32-V7开发板_用户手册》