数码之家

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 38|回复: 0

《STM32H7R7开发指南》第二十四章 内存保护(MPU)实验

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
第二十四章 内存保护(MPU)实验


STM32的Cortex M4(STM32F3/F4系列)和Cortex M7(STM32F7/H7系列)系列的产品,都带有内存保护单元(memory protection unit),简称:MPU。使用MPU可以设置不同存储区域的存储器访问特性(如只支持特权访问或全访问)和存储器属性(如可缓存、可共享),从而提高嵌入式系统的健壮性,使系统更加安全。接下来,我们将以STM32H7R7为例,给大家介绍STM32H7内存保护单元(MPU)的使用。
本章分为如下几个小节:
24.1 MPU简介
24.2 硬件设计
24.3 程序设计
24.4 下载验证


24.1 MPU简介
MPU,即内存保护单元,可以设置不同存储区域的存储器访问特性(如只支持特权访问或全访问)和存储器属性(如可缓存、可缓冲、可共享),对存储器(主要是内存和外设)提供保护,从而提高系统可靠性:
1,阻止用户应用程序破坏操作系统使用的数据。
2,阻止一个任务访问其他任务的数据区,从而隔离任务。
3,可以把关键数据区域设置为只读,从根本上解决被破坏的可能。
4,检测意外的存储访问,如堆栈溢出,数组越界等。
5,将SRAM或者RAM空间定义为不可执行(用不执行,XN),防止代码注入攻击。
注意,MPU不仅可以保护内存区域(SRAM区),还可以保护外设区(比如FMC)。我们可以通过MPU设置存储器的访问权限,当存储器访问和MPU定义的访问权限相冲突的时候,则访问会被阻止,并且触发一次错误异常(一般是MemManage异常)。然后,在异常处理的时候,就可以确定系统是否应该复位或者执行其他操作。
STM32H7R7MPU提供多达16个可编程保护区域(region),每个区域(region)都有自己的可编程起始地址、大小及设置。MPU功能必须开启才会有效,默认条件下,MPU是关闭的,所以,我们要向使用MPU,必须先打开MPU才行。
16个可编程保护区域(region),一般来说是足够使用的了,如果觉得不够,每个区域(region)还可以被进一步划分为更小的子区域(sub region),另外,还允许启用一个背景区域(即没有MPU设置的其他所有地址空间),背景区域只允许特权访问。在启用MPU后,就不得再访问定义之外的地址区间,也不得访问未经授权的区域(region),否则,将以“访问违例”处理,触发MemManage异常。
此外,MPU定义的区域(region)还可以相互交迭。如果某块内存落在多个区域(region)中,则访问属性和权限将由编号最大的region来决定。比如,若2region5region交迭,则交迭的部分受5region控制。
MPU设置是由CTRL、RNR、RBAR和RASR等寄存器控制的,接下来,我们分别介绍一下这几个寄存器。
首先是MPU控制寄存器(CTRL),该寄存器只有最低三位有效,其描述如表24.1.1所示:
表24.1.1 MPU_CTRL寄存器各位描述
PRIVDEFENA位用于设置是否开启背景区域(region),通过设置该位为1,可以在没有建立任何region就使能MPU的情况下,依然允许特权级程序访问所有地址,而只有用户级程序被卡死。但是,如果设置了其它的region(最多16region)并使能MPU,则背景region与这些region重合的部分,就要受各region的限制。HFNMIENA位用于控制是否在NMI和硬件fault中断服务例程中禁止MPU,一般设置为0即可。ENABLE位,则用于控制是否使能MPU,我们一般在MPU配置完以后,才对其进行使能,从而开启MPU
接下来,介绍MPU区域编号寄存器(RNR),该寄存器只有低8位有效,其描述如表24.1.2所示:
表24.1.2 MPU_RNR寄存器各位描述
在配置任何一个区域(region)之前,必须先在MPU内选中这个区域,我们可以通过将区域编号写入MPU_RNR寄存器来完成这个操作。该寄存器只有低8位有效,不过由于STM32H7R7最多只支持16个区域,所以,实际上只有最低4位有效(0~15)。在配置完区域编号以后,我们就可以对区域属性进行设置了。
接下来,介绍MPU基地址寄存器(RBAR),该寄存器各位描述如表24.1.3所示:
表24.1.3 MPU_RBAR寄存器各位描述
注意,表中ADDR字段所设置的基址必须对齐到区域(region)容量的边界。例如,我们定义某个region的容量是64KB(通过RASR寄存器设置),那么它的基址(ADDR)就必须能被64KB整除,比如0X0001 00000X0002 00000X0003 0000等(低16位全为0)。
VALID用于控制REGION段(bit[3:0])的数据是否有效,如果VALID=1,则REGION段的区域编号将覆盖MPU_RNR寄存器所设置的区域编号,否则将使用MPU_RNR所设置的区域编号。我们一般设置VALID0,这样MPU_RBAR寄存器的低5位就没有用到。
特别注意,表24.1.3中的N值最少也是5,所以,基址必须是32的倍数,从而可以知道我们设置region的容量,必须是32字节的倍数。
最后,我们介绍MPU区域属性和容量寄存器(RASR),该寄存器各位描述如表24.1.4所示:
表24.1.4 MPU_RASR寄存器各位描述
XN位,用于控制是否允许从此区域取指,如果XN=1,说明禁止从区域取指,如果强行取指,将产生一个MemManage异常。如果设置XN=0,则允许取指。
AP位,由3个位(bit[26:24])组成,用于控制数据的访问权限(访问许可),控制关系如表24.1.5所示:
表24.1.5 不同AP设置及其访问权限
TEXSCB等位,对应着存储系统中比较高级的概念,可以通过对这些位段的编程,来支持多样的内存管理模型,这些位组合的详细功能如表24.1.6所示:
表24.1.6 TEXSCB对存储器类型的定义
有些情况下,内部和外部内存可能需要不同的缓存策略,次数需要设置TEX的第二位为1,这样TEX[1:0]的定义就会变为外部策略表(表24.1.6种表示为BB),而CB位则会变为内部策略表(表24.1.6种表示为AA)。缓存策略的定义(AABB)如表24.1.7所示:
表24.1.7 TEX最高位为1时内外缓存策略编码
S位用于控制存储器的共享特性,设置S=1,则二级存储器不可以缓存(Cache),如果设置S=0,则可以缓存(Cache),一般我们设置该位为0即可。
C位用于控制存储器的缓存特性,也就是是否可以CacheSTM32H7R7自带Cache,如果我们想要某个存储器可以被Cache,则必须设置C=1。此位需要根据具体的需要设置。
B位用于控制存储器的缓冲特性,设置B=1,则二级存储器可以缓冲,即写回模式,设置B=0,则二级存储器不可以缓冲,即写通模式。此位需根据具体的需要进行设置。
SRD[15:8],这8个位用于控制子区域(sub region)使能。前面提到,STM32H7R7MPU最多支持8region,有时候可能不够用,通过子区域的概念,可以将每个region的内部进一步划分成更小的块,这就是sub region,每个sub region可以独立地使能或除能(相当于可以部分地使能一个region)。
sub region的使用必须满足:
1,每个region必须8等分,每份是一个sub region,其属性与主region完全相同。
2,可以被分为8sub regionregion,其大小必须大于等于256字节。
SRD中的8个位,每个位控制一个sub region是否被除能。如SRD.4=0,则4sub region被除能。如果某个sub region被除能,且其对应的地址范围又没有落在其它region中,则对该区的访问将引发fault
REGIONSIZE[5:1],这5个位用于控制region的容量(大小),计算关系如下:
Rsize = 2^(REGIONSIZE+1)
rsizeregion的容量,必须大于等于32字节,即REGIONSIZE必须大于等于4region的容量范围为:32B~4GB,根据实际需要进行设置。
SZENABLE位,用于设置region的使能。该位一般最后设置,设置为1,则启用此region,使能MPU保护。
至此,关于MPU的简介就介绍完了,关于MPU更详细的说明,请参考:《STM32H7编程手册.pdf 、《STM32 MPU说明》和《Cortex M3权威指南(中文)》第14章。
24.2 硬件设计1. 例程功能
本实验使用STM32H7R7自带的MPU功能,对一个特定的内存空间(数组,地址:0x20000000)进行写访问保护。开机时,串口调试助手显示:MPU unprotected,表示默认是没有写保护的。按KEY0可以往数组里面写数据,按KEY1,可以读取数组里面的数据。按KEY_UP则开启MPU保护,此时,如果再按KEY0往数组写数据,就会引起MemManage错误,进入MemManage_Handler中断服务函数,此时LED1点亮,同时打印错误信息,最后软件复位,系统重启。LED0用于提示程序正在运行,所有信息都是通过串口1输出,需要用串口调试助手查看。
2. 硬件资源
1LED
      LED0 PD14
LED1 PC0
2)独立按键
     KEY0  - PE9
     KEY1  - PE8
     WK_UP PC13
2) 串口1(PB14/PB15连接在板载USB转串口芯片CH340上面)
3) MPU
3. 原理图
MPU属于STM32H7R7的内部资源,只需要软件设置好即可正常工作。我们借助按键和LED灯验证MPU工作是否正常,然后通过电脑串口上位机软件观察打印出来的信息。
24.3 程序设计 24.3.1 MPUHAL库驱动
MPU在HAL库中的驱动代码在stm32h7rsxx_hal_cortex.c文件(及其头文件)中。
1. HAL_MPU_ConfigRegion函数
MPU的初始化函数,其声明如下:
void HAL_MPU_ConfigRegion(MPU_Region_InitTypeDef *MPU_Init);
l 函数描述:
用于配置MPU。
l 函数形参:
形参1是MPU_Region_InitTypeDef结构体类型指针变量,其定义如下:
typedef struct
{
  uint8_t                Enable;              /* 区域使能/禁止 */
  uint8_t                Number;              /* 区域编号 */         
  uint32_t               BaseAddress;        /* 配置区域基地址 */      
  uint8_t                Size;                 /* 区域容量 */         
  uint8_t                SubRegionDisable;  /* region除能位段设置 */      
  uint8_t                TypeExtField;       /* 类型扩展级别 */
  uint8_t                AccessPermission;  /* 设置访问权限 */           
  uint8_t                DisableExec;        /* 允许/禁止取指 */               
  uint8_t                IsShareable;        /* 禁止/允许共享 */              
  uint8_t                IsCacheable;        /* 禁止/允许缓存 */               
  uint8_t                IsBufferable;       /* 禁止/允许缓冲 */         
}MPU_Region_InitTypeDef;
该结构体成员变量很多,每个成员变量的含义我们都在上面定义中有注释。这里大家注意,除了BaseAddressNumber两个成员变量是分别用来配置MPU->RBAR和MPU->RNR寄存器之外,其他成员变量都是用来配置MPU->RASR寄存器相关位,大家如果对这些配置项不理解的话,可以直接对照我们前面讲解的寄存器MPU->RASR各个位含义来理解。
l 函数返回值:
2. HAL_MPU_Enable函数
HAL_MPU_Enable函数是MPU使能函数。其声明如下:
void HAL_MPU_Enable(uint32_t MPU_Control);
l 函数描述:
该函数用于使能MPU,形参一般使用MPU_PRIVILEGED_DEFAULT。
l 函数形参:
形参1是uint32_t类型变量,共有四个选择,对应设置如表24.1.1MPU_CTRL寄存器,四个选择参数设置情况如下:
#define  MPU_HFNMI_PRIVDEF_NONE      ((uint32_t)0x00000000U)
该参数设置MPUCTL控制寄存器的PRIVDEFENA位为0,即禁止了背景区,访问任何未使能 MPU的区域均会造成内存异常MemFault
此参数设置MPUCTL控制寄存器的HFNMIENA位为0,即NMI不可屏蔽中断服务程序和硬件异常中断服务程序执行期间会强制关闭MPU
#define  MPU_HARDFAULT_NMI           ((uint32_t)0x00000002U)
该参数设置MPUCTL控制寄存器的PRIVDEFENA位为0,即禁止了背景区,访问任何未使能 MPU 的区域均会造成内存异常MemFault
此参数设置MPUCTL控制寄存器的HFNMIENA位为1,即NMI不可屏蔽中断服务程序和硬件异常中断服务程序执行期间会继续开启MPU
#define  MPU_PRIVILEGED_DEFAULT      ((uint32_t)0x00000004U)
该参数设置MPUCTL控制寄存器的PRIVDEFENA位为1,即使能了背景区,特权级下可以正常访问任何未使能MPU的区域。
此参数设置MPUCTL控制寄存器的HFNMIENA位为0,即NMI不可屏蔽中断服务程序和硬件异常中断服务程序执行期间会强制关闭MPU
#define  MPU_HFNMI_PRIVDEF           ((uint32_t)0x00000006U)
该参数设置MPUCTL控制寄存器的PRIVDEFENA位为1,即使能了背景区,特权级下可以正常访问任何未使能MPU的区域。
此参数设置MPUCTL控制寄存器的HFNMIENA位为1,即NMI不可屏蔽中断服务程序和硬件异常中断服务程序执行期间会继续开启MPU
l 函数返回值:
3. HAL_MPU_Disable函数
HAL_MPU_Disable函数是MPU失能函数。其声明如下:
void HAL_MPU_Disable (void);
l 函数描述:
该函数用于禁止MPU,我们配置MPU前需要先调用该函数禁止MPU,才能进行配置。
l 函数形参:
l 函数返回值:
MPU配置步骤
1)禁止MPU以及MemManage中断
调用HAL_MPU_Disable函数禁止MPU以及MemManage中断。
2)配置某个区域的MPU保护参数
在进行MPU配置之前我们必须先通过MPU_RNR区域编号寄存器用来选择下一个要配置的区域,然后通过配置MPU_RBAR基地址寄存器来配置基地址,最后通过区域属性和容量寄存器RASR来配置区域相关属性和参数。这些过程都可以通过调用HAL_MPU_ConfigRegion函数来完成。除了BaseAddress和Number两个成员变量是分别用来配置MPU->RBAR和MPU->RNR寄存器之外,其他成员变量都是用来配置MPU->RASR寄存器相关位,大家如果对这些配置项不理解的话,可以直接对照我们前面讲解的寄存器MPU->RASR各个位含义来理解。
3)使能MPU以及MemManage中断
调用HAL_MPU_Enable函数禁止MPU以及MemManage中断。
24.3.2 程序解析1. MPU驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MPU驱动源码包括两个文件:mpu.c和mpu.h。
mpu.h头文件只有函数声明,就不看了,所以我们直接看mpu.c文件,先看设置某个区域的MPU保护函数:
/**
* @brief   设置MPU区域保护
* @param   baseaddr: 区域基地址
* @param   size: 区域大小
* @param   rnum: MPU区域编号
* @param   de: 指令访问状态
* @param   ap: 访问权限
* @param   sen: 可共享状态
* @param   cen: Cache状态
* @param   ben: 可缓冲状态
* @retval  
*/
void mpu_set_protection(uint32_t baseaddr, uint32_t size, uint32_t rnum, uint8_t de, uint8_t ap, uint8_t sen, uint8_t cen, uint8_t ben)
{
    MPU_Region_InitTypeDef mpu_region_init_struct = {0};
   
    mpu_region_init_struct.Enable = MPU_REGION_ENABLE;   /* 使能该保护区域 */
    mpu_region_init_struct.Number = rnum;                /* 设置保护区域 */
    mpu_region_init_struct.BaseAddress = baseaddr;       /* 设置基址 */
    mpu_region_init_struct.Size = size;                  /* 设置保护区域大小 */
    mpu_region_init_struct.SubRegionDisable = 0x00;      /* 禁止子区域 */
    mpu_region_init_struct.TypeExtField = MPU_TEX_LEVEL1;/* 设置扩展域为level1 */
    mpu_region_init_struct.AccessPermission = ap;        /* 设置访问权限 */
    mpu_region_init_struct.DisableExec = de;             /* 是否允许指令访问 */
    mpu_region_init_struct.IsShareable = sen;            /* 是否允许共用 */
    mpu_region_init_struct.IsCacheable = cen;            /* 是否允许cache */
mpu_region_init_struct.IsBufferable = ben;           /* 是否允许缓冲 */
HAL_MPU_Disable();
    HAL_MPU_ConfigRegion(&mpu_region_init_struct);       /* 配置MPU */
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);              /* 开启MPU */
}
该函数使用HAL_MPU_ConfigRegion函数对MPU_Region_InitTypeDef结构体变量进行配置。注意:配置之前我们要调用HAL_MPU_Disable函数禁止MPU才能进行配置,最后再调用HAL_MPU_Enable函数开启MPU。
最后,MemManage_Handler函数,用于处理产生MemManage错误的中断服务函数,在该函数里面点亮了LED1,并输出一些串口信息,对系统进行软复位,以便观察本例程的实验结果。函数定义如下:
/**
* @brief   MemManage错误处理中断
* @param   
* @retval  
*/
void MemManage_Handler(void)
{
    LED1(0);                            /* 点亮DS1 */
    printf("Mem Access Error!!\r\n");   /* 输出错误信息 */
    printf("Soft Reseting...\r\n");     /* 提示软件重启 */
delay_ms(1000);
printf("Restarted!\r\n");           /* 输出错误信息 */
    NVIC_SystemReset();                 /* 软复位 */
}
mpu.c的内容介绍就到此,下面开始main.c文件的介绍。
2. main.c代码
main.c文件代码如下:
/* 定义一个起始地址为0x20002000的数组 */
Static char buffer[128] __attribute__((section(".bss.ARM.__at_0X20002000")));
int main(void)
{
    uint8_t t = 0;
    uint8_t key;
   
    sys_mpu_config();                   /* 配置MPU */
    sys_cache_enable();                 /* 使能Cache */
    HAL_Init();                         /* 初始化HAL*/
    sys_stm32_clock_init(300, 6, 2);    /* 配置时钟,600MHz */
    delay_init(600);                    /* 初始化延时 */
    usart_init(115200);                 /* 初始化串口 */
    led_init();                         /* 初始化LED */
    key_init();                         /* 初始化按键 */
   
    printf("\r\nMPU unprotected!\r\n");
    while (1)
    {
        key = key_scan(0);
        if (key == WKUP_PRES)
        {
            /* 配置MPU保护测试数组区域为只读 */
            mpu_set_protection(0x20002000,        MPU_REGION_SIZE_128B,MPU_REGION_NUMBER15,
MPU_INSTRUCTION_ACCESS_ENABLE,MPU_REGION_PRIV_RO_URO,
MPU_ACCESS_SHAREABLE, MPU_ACCESS_CACHEABLE,
MPU_ACCESS_BUFFERABLE);
            printf("\r\nMPU protected!\r\n");
        }
        else if (key == KEY0_PRES)
        {
            /* 往测试数据写入数据 */
            printf("Start writing data...\r\n");
            sprintf(buffer, "MPU protection test %d.", t);
            printf("Data write finshed!\r\n");
        }
        else if (key == KEY1_PRES)
        {
            /* 从测试数据读取数据 */
            printf("Arrar data is: %s\r\n", buffer);
        }
        
        if (++t == 20)
        {
            t = 0;
            LED0_TOGGLE();
        }
        
        delay_ms(10);
    }
}
main函数前面,我们定义了一个128字节大小的数组:buffer,其首地址为0X20002000,默认情况下,MPU保护关闭,可以对该数组进行读写访问。当我们按下KEY_UP按键的时候,通过mpu_set_protection函数,对其0X20002000为起始地址,大小为128字节的内存空间进行保护,仅支持特权读访问,此时如果再按KEY0,对数组进行写入操作,则会引起MemManage访问异常,进入MemManage_Handler中断服务函数,执行相关操作。
其他的代码比较简单,这里就不多做说明了,在整个代码编译通过之后,我们就可以开始下载验证了。
24.4 下载验证
下载代码后,LED0不停的闪烁,提示程序已经在运行了。然后,打开串口调试助手(XCOM V2.7),设置串口为开发板的USB转串口(CH340虚拟串口,得根据你自己的电脑选择,我的电脑是COM14,另外,请注意:波特率是115200),可以看到如图24.4.1所示信息(如果没有提示信息,请先按复位):
图24.4.1 复位后串口调试助手收到的信息
从图24.4.1可以看出,此时串口助手提示:MPU unprotected,即MPU保护是关闭的,我们可以按KEY0往数组里面写入数据,按KEY1,可以读取刚刚写入的数据,按KEY_UP,则开启MPU保护,提示:MPU protected!,此时,如果再按KEY0,往数组里面写数据的话,则会引起MemManage访问异常,进入MemManage_Handler中断服务函数,点亮LED1,并提示:Mem Access Error!!,并在1秒钟以后,重启系统(软复位),如图24.4.2所示:
图24.4.2 串口调试助手显示运行结果
整个过程,验证了我们代码的正确性,通过MPU实现了对特定内存的写保护功能。通过MPU,我们可以提高系统的可靠性,使代码更加安全的运行。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

APP|手机版|小黑屋|关于我们|联系我们|法律条款|数码之家-技术知识分享平台

闽公网安备35020502000485号

闽ICP备2021002735号-2

GMT+8, 2026-8-25 12:01 , Processed in 0.109200 second(s), 11 queries , Gzip On, Redis On.

Powered by Discuz!

© MyDigit.Net Since 2006

快速回复 返回顶部 返回列表