之前在一段时间内接触过MPU,当时由于要完成任务,所以对MPU没有做过多的研究,并且在网上搜索关于MPU的资料,能把他介绍的很全面的很少,下面是我个人结合ARM的官方文档以及自己整理的一些资料,以Cortex-M0+架构为基础讲解MPU,希望能对大家有所帮助。
MPU意思是Memory Protect Unit,即为存储保护单元,它是位于存储器内部的一个可编程的区域,定义了存储器的属性和存储器的访问权限。MPU不会提升嵌入式应用的性能,而是用于系统中问题的检测(比如试图访问非法或者不允许的存储器位置所导致的应用错误)。如果检测到有错误,则会触发HardFault异常。实际上,许多微控制器用不到MPU,但MPU可以提高嵌入式系统的健壮性,在如下的情况中使得系统更加安全:
还可以利用MPU定义其他存储器属性,例如可被输出到系统级的缓存单元或存储器 控制器的可缓存性。MPU默认是禁止的,此时对于存储器来说,其使用的是默认的存储器属性。
其实对于简单的应用,比如IO控制,不太可能会用到MPU,除非使用的微处理器中存在系统级的缓存且需要MPU对其进行定义。
物联网,如果应用是和网络相关的东西,或者应用面临着无法信任的通信接口,则MPU有助于提高安全性。例如:将用于通信缓冲的存储器区域定义为不可执行的地址区域后,就可以防止代码注入攻击。
工业控制领域,如果应用需要很高的可靠性,则MPU可为多任务系统中栈加以限制,以检测一些意想不到的错误。
汽车应用,MPU常用于汽车部件中。软件部件间不能互相有接口,所以需要MPU处理存储器区域。
我们可以将MPU的应用分为以下几类:
Cortex-M0+处理器中的MPU最多支持8个可编程的存储器空间以及1个可选的背景区域。每个可编程的区域都有自己的起始地址、大小以及属性设置。对于ARM-v6M和ARM-v7M架构,MPU的区域可以重叠,如果某存储区域位于两个已编程的MPU区域中,则其存储属性和权限会基于编号更大的那个区域
,处理器在执行不可屏蔽中断(NMI)或HardFault处理时,MPU访问权限会被忽略。例如,可以将SRAM栈底的一小块SRAM空间定义为不可执行,将MPU用作栈溢出的检测机制。当栈到达边界的时候,HardFault可以忽略MPU限制并在错误的处理中使用预留的SRAM空间。
下面介绍MPU相关寄存器。
1.MPU类型寄存器
其可用于确定MPU是否存在,若DREGION区域读出为0,则表明MPU不存在。
(MPU->TYPE, 0xE000ED90)
位 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
23:16 | IREGION | R | 0 | 本MPU支持的指令区域数,由于ARM-v6M架构使用统一的MPU,其总数为0 |
15:8 | DREGION | R | 0或8 | MPU支持区域数 |
0 | SEPARATE | R | 0 | 由于MPU为统一的,其总为0 |
2.MPU控制寄存器
其有3个控制位,复位后,该寄存器数值为0,这样会禁止MPU。若要使能MPU,软件需要首先设置MPU区域,然后设置控制寄存器中的ENABLE位。
(MPU->CTRL, 0xE000ED94)
位 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
2 | PRIVDEFENA | R/W | 0 | 特权等级默认的存储器映射使能,设置为1且MPU使能时,同背景区域一样,特权访问会使用默认的存储器映射,若未设置该位,则背景区域禁止且任何不在使能区域范围内的访问都将引发错误 |
1 | HFNMIENA | R/W | 0 | 若设置为1,则MPU在硬件错误处理和不可屏蔽中断(NMI)处理中也是使能的,否则在硬件错误以及NMI中不使能 |
0 | ENABLE | R/W | 0 | 若设置为1则使能MPU |
说明:MPU控制寄存器中的PRIVDEFENA位用于使能背景区
(区域“-1”),若未设置其他区域,那么通过PRIVDEFENA,特权程序可以访问所有的存储器位置,只有非特权程序才会被阻止。如果设置并使能其他的MPU区域,则背景区域可能会被覆盖。
HFNMIENA用于定义NMI、HardFault异常执行期间或FAULTMASK置位时MPU的行为。MPU在这些情况下默认被禁止,即便是MPU设置的不正确,它也可以使HardFault和NMI异常处理正常执行。
3.MPU区域编号寄存器
在设置每个区域前,写入该区域以选择编程区域。
(MPU->RNR, 0xE000ED98)
位 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
7:0 | REGION | R/W | – | 选择待编程的区域 |
4.MPU区域基地址寄存器
每个区域的起始地址在MPU区域基地址寄存器中,利用VALID和REGION域,可以跳过设置MPU区域编号寄存器这一步,可以降低程序的代码复杂度。
(MPU->RBAR, 0xE000ED9C)
位 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
31:N | ADDR | R/W | – | 区域基地址,N取决于区域的大小,例如64KB大小区域的基地址为[31:16] |
4 | VALID | R/W | – | 若为1,则bit[3:0]定义的REGION会用在编程阶段,否则会使用MPU区域编号寄存器选择的区域 |
3:0 | REGION | R/W | – | 若VALID为1,该域会覆盖MPU区域编号寄存器,否则会被忽略。由于Cortex-M3和Cortex-M4的MPU支持8个区域,当REGION域大于7时,会忽略掉区域编号覆盖。 |
5.MPU区域基本属性和大小寄存器
此外需要配置每个区域的属性。
(MPU->RASR, 0xE000EDA0)
位 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
31:29 | 保留 | – | – | |
28 | XN | R/W | 0 | 指令访问禁止(1=禁止从本区域取指令,强行访问会引起存储器管理错误) |
27 | 保留 | – | – | |
26:24 | AP | R/W | 000 | 数据访问允许域 |
23:22 | 保留 | – | – | |
21:19 | TEX | R/W | 000 | 类型展开域,在此架构中始终为0 |
18 | S | R/W | – | 可共用 |
17 | C | R/W | – | 可缓存 |
16 | B | R/W | – | 可缓冲 |
15:8 | SRD | R/W | 0x00 | 子区域禁止 |
7:6 | 保留 | – | – | |
5:1 | REGIO大小 | R/W | – | MPU保护区域大小 |
0 | ENABLE | R/W | 0 | 区域使能 |
其中REGION SIZE域决定了区域的大小,例如:
REGION大小 | 大小 |
---|---|
b00111 | 256字节 |
b01000 | 512字节 |
b01010 | 2KB |
… | … |
b11100 | 512MB |
… | … |
b11111 | 4GB |
子区域禁止
用于将一个区域分为8个相等的子区域并定义每个部分为使能或禁止。若一个子区域被禁止且和另一区域重叠,则另一区域的访问规则会起作用。若子区域禁止但并未和其他区域重叠,则对该存储器区域访问会导致HardFault异常。
数据访问权限(AP)域(bit[26:24])定义了区域的AP,如下所示:
AP数值 | 特权访问 | 用户访问 | 描述 |
---|---|---|---|
000 | 无访问 | 无访问 | 无访问 |
001 | R/W | 无访问 | 只支持特权访问 |
010 | R/W | R | 用户程序的写操作会导致异常 |
011 | R/W | R/W | 全访问 |
100 | 无法预测 | 无法预测 | 无法预测 |
101 | R | 无访问 | 只支持特权读 |
110 | R | R | R |
111 | R | R | R |
XN(永不执行)(bit[28])决定是否允许从该区域取指。
TEX(类型扩展)、S(可共享)、B(可缓冲)、C(可缓存)域(bit[21:16]),这些属性在每次寄存器和数据访问的时候都会被输出到总线系统,并且该信息可被写缓冲或缓存单元等总线系统使用。
而对于微控制器来说,只有B(可缓冲)属性会影响到处理器中的写缓冲。
若使用的微控制器支持设备缓存,则多数情况下可被配置为如下情况:
类型 | 存储器类型 | 常用存储器操作 |
---|---|---|
ROM,Flash(可编程存储器) | 普通存储器 | 不可共用,写通,C=1,B=0,TEX=0,S=0 |
内部SRAM | 普通寄存器 | 可共用,写通,C=1,B=0,TEX=0,S=0 |
外部RAM | 普通寄存器 | 可共用,写回,C=1,B=1,TEX=0,S=1 |
外设 | 设备 | 可共用,设备,C=0,B=1,TEX=0,S=1 |
其中,可共享属性
对于具有缓存的多处理器系统非常重要,若传输标志为可用的,则缓存系统需要额外一些工作以确保不同的处理期间的缓存数据的一致性。单处理系统一般用不到可共享属性。
在我们使用MPU之前,需要确定程序或应用程序访问的存储器区域,其中包括:
只支持特权访问
。只支持特权访问
。全访问
。全访问
。只支持特权访问
。N*0x100
//使能MPU时可以带有输入配置项
void mpu_enable(uint32_t options)
{
MPU->CTRL = MPU_CTRL_ENABLE_MASK | options; //禁止MPU
__DSB(); //确保MPU设置生效
__ISB(); //利用更新后的设置
return;
}
//禁止MPU
void mpu_disable(void)
{
__DMB(); //确保之前传输全部完成
MPU->CTRL = 0; //禁止MPU
return;
}
//禁止区域函数(0到7)
void mpu_region_disable(uint32_t region_num)
{
MPU->RNR = region_num;
MPU->RBAR = 0;
MPU->RASR = 0;
return;
}
//使能区域函数
void mpu_region_config(uint32_t region_num, uint32_t addr, uint32_t size, uint32_t attributes)
{
MPU->RNR = region_num;
MPU->RBAR = addr;
MPU->RASR = size | attributes;
return;
}
在上述的代码示例中,我们使用到了存储器屏障指令:
DMB
(数据存储器屏障),在禁止MPU前使用,确保数据的传输不会重新排序,并且如果有未完成的传输,会等到传输完成之后在写入MPU相应寄存器。DSB
(数据同步屏障),在使能MPU后使用,确保接下来的ISB指令只会在写入MPU控制寄存器结束后才执行,可以确保后续的数据传输使用新的MPU设置。ISB
(指令同步屏障),用于DSB之后,确保处理器流水线被清空且接下来指令利用更新后的MPU设置被重新读出。后续如果还有时间,可能会更新关于MPU子区域禁止的一些使用,但个人感觉这篇文章可以作为新手的入门了,希望能对你有所帮助,大家一起学习,共同进步!