本帖最后由 DKENNY 于 2024-8-15 16:43 编辑
#申请原创# @21小跑堂
最近学习了有关MPU的内容,本帖将对MPU的功能,相关寄存器的配置,以及在实际编程中如何运用MPU这三个方面作一个简单的分享。
## MPU 的功能 内存保护单元(MPU)能够将内存划分为多个区域,并为每个区域设定不同的访问权限,从而提升系统的可靠性。它的主要用途包括: - 区分特权用户和普通用户的访问区域,这样可以提高操作系统的稳定性。 - 设置只读区域,以防止重要数据被误修改。 - 监测堆和栈的溢出情况,以确保系统安全。
## MPU相关寄存器(基地址:0xE000ED90) MPU配备了一系列寄存器。接下来,我们将结合这些寄存器详细阐述MPU的功能。寄存器组如下表所示。
下面详细介绍各个寄存器。
### MPU 类型寄存器(MPU_TYPE,地址:0xE000ED90) 名称
| | |
| Bits[31:24] | | | | MPU 支持的指令 region 数量。因为 ARMv7‐M 只使用单个统一的 MPU,此位段一直为0 | | | MPU支持的区域数量。如果该字段的读数为0,则处理器不实现MPU |
| Bits[7:1] | | | | |
MPU寄存器是只读的,我们主要关注DREGION位。通过检查这个位,我们可以确定是否启用了MPU。如果这个字段的读取值为零,那么就意味着MPU没有被使用。如果MPU被启用,那么通常情况下,它会返回MPU支持的区域数量,Cortex_M4最多可以支持8个区域。
可使用jlink读取该寄存器地址,以查看该芯片内核是否支持MPU,如下是APM32F407的读取情况:
可以看到读取的数据是0x800,对应的DREGION值为8,说明MPU支持的region最大数量为8。
### MPU控制寄存器(MPU_CTRL,地址:0xE000ED94) 名称 | | |
| Bits[31:3] | | | | 设置特权访问模式下MPU的策略 1=启用默认内存映射作为特权访问的背景区域。 0=禁用默认内存映射,超出制定区域的任何数据或指令访问都将引起错误。 | | | 设置在不可屏蔽中断(NMI)和硬 件中断中MPU策略。 1= 在不可屏蔽中断和硬件中断服务函数 中MPU依然有效 0=在不可屏蔽中断和硬件中断服务 函数中MPU依然失效 | | | |
1. 寄存器概述: - 在MPU_CTRL寄存器中,ENABLE和HFNMIENA位的作用已有明确说明,因此不再详细讨论。
2. 重点讨论PRIVDEFEN位: - Cortex内核具有两种执行模式:特权模式和用户模式。 - 特权模式:具有更高的访问权限。 - 用户模式:权限较低,限制较多。
3. MPU的配置: - PRIVDEFENA位用于配置特权模式下的MPU访问策略。 - 当MPU启用时,内存空间可以被划分为1到8个区域(region0~region7)。 - 一些内存区域可能未被划分,这些未划分的区域称为背景区域(region-1)。
4. PRIVDEFENA位的作用: - 当PRIVDEFENA位设为1时: - 允许特权模式访问背景区域(region-1)。 - 当PRIVDEFENA位设为0时: - 禁止特权模式访问背景区域(region-1)。
### MPU区域号寄存器(MPU_RNR,地址:0xE000ED98)
1. MPU的区域配置: - MPU支持配置1到8个区域(region)。 - 每个区域的设置是独立的,可以根据需要进行个别配置。
2. 配置步骤: - 在进行区域配置之前,必须先通过MPU_RNR寄存器选择要设置的区域编号。 - 选择好区域编号后,再对MPU_RBAR和MPU_RASR寄存器进行配置。
### MPU区域基地址寄存器(MPU_RBAR,地址:0xE000ED9C) 名称 | | | | | | | | 决定REGION字段设置的区域编 号是否覆盖MPU_RNR寄存器设置的区域编号 | | | |
1. ADDR位段: - 位段:bits[31:5]。 - 功能描述:存储区域的基地址。
2. VALID位段: - 位段:bit[4]。 - 功能描述:决定REGION字段设置的区域编号是否覆盖MPU_RNR寄存器设置的区域编号。
3. REGION位段: - 位段:bits[3:0]。 - 功能描述:用于复写MPU区域编号。
4. 寄存器的便利性: - 通过设置REGION为区域编号,VALID为1,提供了一种更为便捷的方式来修改区域编号。 - 这种方式可以更快地设置区域编号。
### MPU区域属性和大小寄存器(MPU_RASR, 地址:0xE000EDA0) 名称 | | |
| bits[31:29] | | | | |
| bits[27] | | | | |
| bits[23:22] | | | | | | | | | | (Cachable)可缓存,1= 可缓存,0=不可缓存。 | | | (Bufferable)可传冲 1=可缓冲,0=不可缓冲 | | | 子区域使能位,对于256字节或更大的区域,该字段的每个位控制是否启用八个相等的子区域 |
| Bits[7:6] | | | | | | | |
1. MPU_RASR寄存器简介 - 功能:MPU_RASR寄存器主要负责配置MPU区域的属性。 - 重要性:这是一个关键的过程,涉及许多需要深入理解的概念。
2. SRD位的作用 - 子区域配置:SRD位用于配置子区域。 - 区域划分: - 所有超过128字节的区域被划分为8个等大小的子区域。 - 每个子区域的大小相同。
3. SRD的位解释 - 位段: - SRD寄存器的8位:从第8位到第15位。 - 控制功能: - 如果设置为1:启用相应的子区域(region)。 - 如果设置为0:禁用相应的子区域(region)。
区域属性B、C、S、TEX的设置,详细解释如下表。 TEX | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 外部内存或内部内存,Non-cachable型内存 | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 带缓存的内存,AA,针对内部内存,BB针对外部内存, | |
1. 内存特性: - 写通型内存(Write-Through): - 当Cache被激活,且内存A被设定为写通型时,CPU向内存A写入数据。如果Cache中已经为内存A的数据分配了对应空间,那么数据会同时写入Cache和内存。
- 写回型内存(Write-Back): - 当Cache被激活,且内存A被设定为写回型时,CPU向内存A写入数据。如果Cache中已经为内存A的数据分配了对应空间,那么数据会被写入Cache,但不会立刻写入内存。
- 非缓存(Non-cachable): - 这种类型的内存在CPU进行读写操作时不会经过Cache。
2. Cache策略: - 读分配(Read Allocate): - Cache被激活时会启用。意味着当CPU需要读取内存B的数据时,如果在Cache中找不到,则会直接从内存中读取,并在Cache中备份。
- 写分配(Write Allocate): - 功能与读分配类似,即在Cache被激活时,如果进行写操作且Cache中没有对应数据,则会从内存中读取数据到Cache后再进行写入。
3. TEX配置: - 当TEX的最高位设为1时,可以分别配置外部内存(片外内存)和内部内存(片内内存)的属性。 - 此时,TEX的后两位(BB)用于设定外部内存的属性,而C和B位(AA)用于设定内部内存的属性。
无论是AA还是BB,它们的每个数值都有相同的含义,如下表所示。
访问权限AP,字段AP用于设置访问权限,同样我们用一张表来说明该位,如下表所示。
MPU的设置的主要任务就是配置MPU_RASR寄存器,MPU_RASR寄存器寄存器主要分为区域属性设置和访问权限设置。
## MPU的使用 MPU的配置流程可以参考如下的流程图:
如下是一个简单的MPU配置例程代码,配套使用的开发板是APM32F407IG。
Main函数实现:
void MPU_Config(void)
{
ARM_MPU_Disable();
MPU->RBAR = ARM_MPU_RBAR(0,0x08004000);
MPU->RASR = ARM_MPU_RASR(0,ARM_MPU_AP_PRO,0,0,1,1,0,ARM_MPU_REGION_SIZE_1KB);
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
NVIC_EnableIRQ(MemoryManagement_IRQn);
}
void USARTInit(void)
{
/* USART Initialization */
USART_Config_T usartConfigStruct;
/* USART configuration */
USART_ConfigStructInit(&usartConfigStruct);
usartConfigStruct.baudRate = 115200;
usartConfigStruct.mode = USART_MODE_TX_RX;
usartConfigStruct.parity = USART_PARITY_NONE;
usartConfigStruct.stopBits = USART_STOP_BIT_1;
usartConfigStruct.wordLength = USART_WORD_LEN_8B;
usartConfigStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
/* COM1 init*/
APM_COMInit(COM1, &usartConfigStruct);
}
/*!
* [url=home.php?mod=space&uid=247401]@brief[/url] Main program
*
* @param None
*
* @retval None
*/
int main(void)
{
USARTInit();
MPU_Config();
*(uint32_t *)0x08004000 = 0x12;
while (1)
{
}
}
其中ARM相关的MPU操作函数在其相关的内核文件中,可以自行去了解,这里就不介绍了。
中断服务函数实现: /*!
* [url=home.php?mod=space&uid=247401]@brief[/url] This function handles Memory Manage exception
*
* @param None
*
* @retval None
*
*/
void MemManage_Handler(void)
{
/* Go to infinite loop when Memory Manage exception occurs */
while (1)
{
}
}
其中涉及到宏定义参数介绍如下: #define ARM_MPU_RBAR(Region, BaseAddress)
- Region,MPU区域编号,最多可以设置8个Region ,取值范围为(0到7) - BaseAddress,区域的起始地址。
#define ARM_MPU_RASR(DisableExec, AccessPermission, TypeExtField, IsShareable, IsCacheable, IsBufferable, SubRegionDisable, Size)
宏ARM_MPU_RASR与宏ARM_MPU_RBAR分别用于设置MPU_RBAR寄存器和MPU_RASR寄存器。我们知道宏定义中每个参数的含义即可,其他工作有宏定义自动完成。宏ARM_MPU_RASR的参数介绍如下: - DisableExec,禁用区域(region),1=禁用指定的区域,0=启用指定的区域。 - AccessPermission,设置数据访问权限。
#define ARM_MPU_AP_NONE 0u
#define ARM_MPU_AP_PRIV 1u
#define ARM_MPU_AP_URO 2u
#define ARM_MPU_AP_FULL 3u
#define ARM_MPU_AP_PRO 5u
#define ARM_MPU_AP_RO 6u
通过宏定义的名字我们可以知道设置的访问权限。 - TypeExtField、IsShareable、IsCacheable、IsBufferable,依次设置MPU_RASR寄存器的TEX、S、C、B字段。 - SubRegionDisable,禁用次区域。1=禁用,0=启用次区域。 - Size,区域大小。
在配置MPU(Memory Protection Unit)区域时,有两个关键点需要注意:
1. 区域重叠:如果两个或多个MPU区域有重叠部分,那么区域号较大的设置将会覆盖区域号较小的设置。这是因为在处理重叠区域时,MPU会优先考虑区域号较大的设置。例如,如果区域0和区域1有重叠,那么在重叠部分,区域1的设置将会被使用。
2. 区域号的最大值:在配置MPU区域时,区域号的值必须在有效范围内,通常是0到7(包含)。如果超过这个范围,可能会导致不可预测的错误。这是因为MPU硬件只设计了固定数量的区域(在这个例子中是8个),超出这个范围的区域号是无效的。
总的来说,这两点都是为了确保MPU配置的正确性和系统的稳定性。
上面的例程代码主要实现了以下功能:
- MPU_Config(void):这个函数用于配置内存保护单元(MPU)。在函数内部,首先禁用了MPU,然后设置了一个具有特定属性的内存区域,这个区域的基地址是0x08004000,大小是1KB,访问权限被设置为仅允许特权级别的访问。最后,启用了MPU和默认的内存管理错误中断。
- USARTInit(void):这个函数用于初始化USART(通用同步异步收发器)。在函数内部,首先定义了一个USART的配置结构体,然后初始化了这个结构体,并设置了一些参数,如波特率、模式、奇偶校验、停止位、字长和硬件流控制。最后,使用这个配置结构体初始化了COM1。
- main(void): 这是程序的主函数。在函数内部,首先调用了USARTInit()和MPU_Config()函数,然后试图向地址0x08004000写入一个值。由于这个地址被MPU设置为只允许特权级别的访问,所以这个写操作会引发内存管理错误中断,然后程序将会进入MemManage_Handler()函数。
- MemManage_Handler(void):这是一个中断处理函数,用于处理内存管理错误中断。在这个函数内部,程序进入了一个无限循环,也就是说,一旦发生了内存管理错误中断,程序就会停止在这个函数中。
实验现象 由于在main()函数中尝试向受保护的内存地址写入数据,这将触发内存管理错误中断,然后程序将进入MemManage_Handler()函数,并在该函数中无限循环。因此,程序的实际效果将是一旦开始运行,就会“冻结”在MemManage_Handler()函数中。
如上图的实验现象正好符合我们设置的需求。
## 问题
1、如何区分特权用户和普通用户的访问区域?
具体来说,我们可以通过MPU给特权用户和普通用户设定不同的内存区域,分配不同的访问权限。比如,特权用户可以访问一个既能读也能写的内存区域,而普通用户只能访问一个只能读或只能写的区域。这样,如果普通用户试图进入特权用户的内存区域,系统就会直接拒绝这个请求,保护特权用户的数据不被非法访问。
在实际编程时,我们需要写一些代码来设置MPU,来区分不同用户可以访问的区域。这通常涉及到对MPU内部设置的读写操作,以定义哪些区域允许访问,哪些区域被限制。
2、如何使用MPU监测堆和栈的溢出情况以确保系统安全?
- 1.配置MPU区域
- 划分内存区域: 将内存划分为多个区域,分别用于极其重要的任务(如操作系统、关键应用程序、堆、栈等)。
- 设置属性: 为每个区域设置访问权限。例如,可以将堆和栈区域设为可读写,但不可执行。其他重要区域,例如程序代码区域,应该为只读,并且不可写。
- 2.监测堆和栈的使用
- 栈溢出监测: 在栈的底部设置一个保护区域,通常是一个无效的内存地址。通过监测这些地址的访问情况,可以检测到栈的溢出。如果系统访问了保护区域,可以及时报警并采取措施(如重启系统)。
- 堆溢出监测: 在堆内存分配中,可以在每次分配之前和之后插入控制代码,以检查当前的堆指针和堆边界。如果堆指针越界并触及保护区域,则表示发生了堆溢出,可以记录相关信息并进行处理。
- 3.硬件异常处理
- 异常处理: 通过MPU,如果系统接收到访存错误的异常(例如访问被保护的内存区域),可以触发相应的中断服务程序(ISR)。在ISR中,可以实现错误日志记录、系统重启或进入安全模式等功能。
## 总结
这篇帖子主要介绍了内存保护单元(MPU)的核心功能及其相关寄存器的详细信息。MPU 是一种硬件组件,主要用于系统内存管理,以提高系统的安全性和稳定性。MPU通过为不同的内存区域设置访问权限,限制应用程序对系统内存的访问,从而防止错误或恶意代码的运行对系统造成影响。
同时,内存保护单元(MPU)是现代计算系统中不可或缺的组成部分,它通过提供灵活的内存访问控制和保护机制,增强了系统的安全性和稳定性。合理配置MPU对于构建高可靠、高安全性的嵌入式和实时系统至关重要。
以上就是本次分享的全部内容,有关MPU的相关内容,欢迎各位讨论交流。
附件:
MPU_Example.zip
(786.79 KB)
|
一篇详解MPU的功能和相关寄存器理论知识,并通过简单点案例讲解编程时的应用方法。