CUBEMX+Keil5+STM32H7+SRAM:系列如何使用MPU功能,合理分配和使用SRAM存放各种类型的数据
目录
2.4 什么是Cache(高速缓存)? 它和MPU是什么关系?
2.6 链接文件的作用是什么(Keil5中为.sct 文件; CubeIDE 为.ld文件)?
3.1.1 CubeMX: 基础配置(时钟树、烧写口、时钟源等配置)
3.1.2 CubeMX: 测试外设配置:串口1+DMA发送+指示灯
3.1.3 CubeMX: keil5: 链接文件的编写与保存
3.3 实验二:在实验一的基础上增加MPU相关配置,实现分区保护(重要)
一、概述
在嵌入式系统开发中,尤其是使用STM32H7这类高性能MCU时,合理的内存管理是提升系统性能和稳定性的关键。本文将详细介绍如何在STM32H743VGT6上通过CubeMX和Keil5实现SRAM的合理分区,并配置MPU(内存保护单元)来优化内存使用。我们将从基础概念入手,逐步完成两个实验:简单的内存分区和完整的MPU保护配置。
二、前提知识
2.1 STM32H7系列芯片的特点
2.1.1 STM32H743VGT6芯片详细介绍
STM32H743VGT6是STMicroelectronics推出的高性能Arm® Cortex®-M7内核MCU,属于STM32H7系列中的高性能型号。该芯片在性能和资源方面都具有突出表现:
核心特性:
-
CPU内核:Arm® Cortex®-M7,最高主频480MHz
-
工艺技术:40nm工艺,高性能与低功耗的完美平衡
-
存储资源:
-
Flash:1MB,支持ECC错误校正
-
SRAM:总计约1MB,分布在多个物理区域
-
-
数学加速:支持单精度和双精度FPU(浮点单元)
-
DSP扩展:完整的DSP指令集,适合数字信号处理
丰富的外设接口:
-
通信接口:4个USART、4个UART、6个SPI、4个I2C、3个I2S
-
高级连接:2个USB OTG(1个HS,1个FS)、2个CAN-FD、以太网MAC
-
模拟外设:3个12位ADC(16位模式)、2个12位DAC
-
定时器:多达22个定时器,包括高级电机控制定时器
-
图形接口:LCD-TFT控制器,Chrom-ART加速器
-
存储器接口:FMC(Flexible Memory Controller)支持SDRAM、NOR Flash等
安全特性:
-
存储器保护:MPU(内存保护单元)
-
加密硬件:AES、HASH、RNG(真随机数发生器)
-
安全启动:支持安全固件安装(SFI)
2.1.2 STM32H43VGT6的SRAM分配详解
以STM32H743VGT6为例,其采用了复杂的多总线矩阵架构,将1MB的SRAM物理上分布在多个区域,每个区域挂载在不同的总线上,这就能让程序更加精细化的管理(各种分区如果都被充分利用到,那么加大幅度提高运行效率和稳定性),并且支持并行访问。通俗来讲,就是可以通过配置来实现对不同功能的缓冲区进行存储和调用。
目前芯片的SRAM具体有ITCM-RAM、DTCM-RAM、AXI SRAM、SRAM1/2/3、SRAM4、Backup SRAM这几个分区。我们都知道,程序一般都是存放在Flash里面的,一般在程序运行的过程中,如果不手动进行和分配内存,那么默认只会使用到DTCM-RAM和极其少量的ITCM-RAM,DTCM-RAM是默认存放程序的栈、堆、全局变量、静态变量,所以被重用,而ITCM-RAM是存放一些代码的,如果不特殊说明,程序都是默认存放在Flash中的,所以一般使用不到,但不代表它没有被设置,正常运行代码的时候我们是能使用这个区域的,工程默认给我们分配好了,具体原因请看3.1.3修改链接文件时Keil5的默认设置(默认的链接文件已经包含了DTCM-RAM和ITCM-RAM的配置)。
这是经过查阅数据手册芯片内存分布:

上面的图可能看着不通俗易懂,我结合AI给大家生成了一个更通俗的表格:

2.2 分配内存区的意义是什么?
以上表格所展示的存储区,除了之前说的DTCM-RAM区域被系统占用,其他的区域如果不设置,都是空白的!只有128KB的默认数据存储区来存储数据,其他都没有被充分利用的,这样容易造成资源的浪费,并且对于一般的小程序来说,不设置倒是没影响,但是对于大型程序或者是加了类似Freertos操作系统之后的程序就显得力不从心,甚至可能一个freertos所需要的最大站容量就得100K(我自己做一个较大的程序,栈的内存就是100*1024B),所以通俗一点来讲,我们需要根据我们的程序,来灵活的应用对应存储区来存放我们的缓冲区。
提升性能:将频繁访问的数据放在高速内存。
避免冲突:多主设备(CPU、DMA、外设)并行访问不同内存区域。
优化Cache:为不同用途的内存配置合适的Cache策略。
增强稳定性:防止任务间内存访问冲突。
分配原则如下:
- ITCM-RAM可以用来存放一些中断服务例程、时间关键的算法(不推荐这样做,因为Flash的2MB空间已经够大了)。
- AXI SRAM 或 SRAM1/2/3可以用来存放大型缓冲区(如图像、音频、DMA数组)。
- SRAM4用来存放一些需要保护的重要数据。
2.3 什么是MPU?
STM32H743作为STM32系列中的高性能代表,其内存保护单元(MPU)是确保系统稳定、安全和可靠运行的关键组件。深入研究MPU对于开发复杂的、高要求的嵌入式应用(如RTOS应用、功能安全认证项目)至关重要。MPU是内存保护单元,它允许您为不同的内存区域(如Flash, RAM, Peripherals)定义访问权限。
STM32H7系列可以管理的内存空间一共有4GB,包含Flash,RAM,各个系统寄存器等,它们共享同一地址空间。我们上文提到的RAM和Flash只占到了2MB左右。通俗意义上来讲,MPU的任务就是给这4GB的空间加上保护,具体功能如下:
-
特权与用户模式分离:将代码执行环境分为特权模式(通常为操作系统内核、关键驱动)和用户模式(通常为应用程序任务)。用户模式下的代码访问受限制的资源会触发故障。
-
内存区域隔离:防止任务意外访问或修改其他任务或内核的内存空间,提升系统的鲁棒性。
-
访问权限控制:对内存区域设置只读、读/写、禁止执行等权限,防止数据被篡改或执行非代码内存(如数据区),这有助于防范某些软件攻击。
-
关键特性(STM32H7):
-
最多16个区域:可以同时定义多达16个内存保护区域。
-
可配置区域大小:大小可以是 32B 到 4GB 的 2的幂次方。
-
重叠区域:区域可以重叠,优先级高的区域设置会覆盖优先级低的区域。
-
丰富的属性:
-
访问权限:无访问、只读、读/写。
-
执行权限:允许执行(eXecute Never, XN=0)或禁止执行(XN=1)。
-
内存类型:
-
Normal:用于RAM,支持缓存,访问可能会被处理器优化(重排序)。
-
Device:用于外设,访问是严格顺序的,不会被缓存。
-
Strongly-ordered:最严格的顺序,用于共享关键外设,所有访问都按程序顺序完成。
-
-
共享性:可配置为共享或非共享,对于多核(H7是单核,但此属性仍有意义)和DMA操作有影响。
-
-
子区域禁用:对于大于等于256字节的区域,可以禁用其中一半(8个等分中的任意几个)的子区域,提供更精细的控制。
-
经过上述参数的介绍,我们可以知道,其实MPU可以管理的范围时4GB,我们可以只选择其中还一个或者几个区域来管理,每个区域又可以分为八个小区域,进行精细化的保护,这些区域又有优先级,当多个区域出现交集的 时候,该区域将以高优先级的设置为主。并且,这些子区域的对齐方式有严格的要求(但是目前本人没有具体深究,所以后续如果研究出来了会继续更新这篇文字,因为本次的实验的对齐方式非常严格)。当然,我们今天所提到的SRAM区域,就是在这4GB空间之中。
2.4 什么是Cache(高速缓存)? 它和MPU是什么关系?
2.4.1 CPU的数据传输过程

CPU和外部存储的访问模式总体分为两大类,三种方式,第一种(device memory)是CPU与SRAM直接访问;第二种(strongely ordered memory)是CPU经过一个缓冲区再和SRAM访问,第三种(normal memory)就是经过Cache进行访问。
2.4.2 Cache是什么?
当前芯片厂商出的M7内核芯片基本都做了一级Cache支持,Cache又分数据缓存D-Cache和指令 缓冲I-Cache,STM32H7的数据缓存和指令缓存大小都是16KB。对于指令缓冲,用户不用管,这里主 要说的是数据缓存D-Cache。以STM32H7为例,主频是400MHz,除了TCM和Cache以400MHz 工作,其它AXI SRAM,SRAM1,SRAM2等都是以200MHz工作。数据缓存D-Cache就是解决CPU 加速访问SRAM。
Cache的本质:在CPU和主内存之间设置的一个小型但极快的存储器,用于存放最常访问的数据(D-Cache)和指令(I-Cache)。上述三种方法的速度依次变大,有人可能疑问,中间加上一个Cache为什么还能这么快。通俗的讲,CPU的处理速度非常快,有400MB,而SRAM只有200MB,那么就会出现CPU等待SRAM的情况,这样会阻碍他们的访问效率,而Cache和CPU之间的访问速度可以达到400MB,相当于加上一个中介,使得他们的速度变得很快,有人会问,那么Cache和SRAM之间的传输速度是多少?答案还是200MB,但是Cache内置两个32KB的缓冲区,他会将SRAM事先存入它自己的缓冲区,当CPU想要读取的时候,之间从Cache中读取,但是由于Cache的缓冲区有限,会不会出现读不到的情况,有的兄弟,有的。这就涉及到了CPU和Cache之间存在读取不到的情况。如果对这边的速度没有理解,没关系,下面会有图介绍为什么是400MB和200MB的,根据他们挂载的总线可以得到他们的速度。

2.4.3 Cache和MPU是什么关系?
通俗一样上来讲,MPU是为了使用Cache而生的,用户通过配置MPU上面的参数,可以实现不同的传输功能,我作为初学者,我也就没有深究这里面的具体设置,如果只是单纯从应用和的角度,初学者可知先跟着本片文章做。
至于他们之间的关系,大家可以形象的理解为:一个正在图书馆写作的学者,CPU是学者的大脑,主内存(SRAM)就是书架上的书籍,而Cache就是学者的书桌。当学者想要拿去书籍时,如果书籍再书桌上就有一份,那么学者可以快速的拿到书本,如果书籍在书架上面,那么学者可能需要直接区书架上面拿,或者先将所需要的书本一起放在书桌上,那么这个过程就稍微就慢一些了。
以下是MPU和Cache的区别与联系:
-
MPU是"交通警察" - 控制谁可以访问哪里,以什么方式访问
-
Cache是"高速公路" - 加速获准通行的车辆
| 特性 | MPU (内存保护单元) | Cache (高速缓存) |
|---|---|---|
| 主要功能 | 内存访问控制和保护 | 内存访问加速 |
| 工作层级 | 在CPU和内存总线之间 | 在CPU和MPU之间 |
| 配置方式 | 通过MPU寄存器配置区域属性 | 通过MPU配置Cache策略 |
| 影响范围 | 影响内存访问权限和属性 | 影响内存访问速度 |
以下是在程序中正常的数据读取流程:
CPU发出内存访问请求
↓
L1 Cache检查是否命中 (首先在这里查找)
↓
Cache未命中 → 访问请求继续向下传递
↓
MPU检查访问权限和属性
↓
MPU允许访问 → 访问主内存(SRAM)
↓
MPU拒绝访问 → 触发MemManage Fault
这里涉及到初始》化流程问题,一般流程都是:
void system_init(void) {
// 阶段1: 基础初始化
HAL_Init();
// 阶段2: 配置MPU - 必须在Cache之前!
MPU_Config();
// 阶段3: 配置时钟和外设
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
// 阶段4: 启用Cache - 必须在MPU之后!
SCB_EnableICache();
SCB_EnableDCache();
// 现在系统以最优性能运行
}
2.4.4 Cache如何配置 (配置策略)?

具体参数介绍参考编程手册如下,表格中TEX、C、B、S等参数都可以通过MPU进行设置。其中的TEX是用来设置Cache策略的,C是Cache,B是缓冲用来配合Cache设置的,而S是共享, 用来解决多总线或者多核访问时的同步问题。MPU配置的时候,最主要的也是配置这几个参数。总体来说,如果配置了Cache,由于它是一个中间的高速缓冲,在CPU和外部设备读取的时候,会不可避免的出现如下问题:
数据一致性问题:当CPU或者外设想进行读操作时,由于数据可能还在Cache中或者没有更新到对应区域,会出现读取的数据时旧的数据或者是错误的,但是HAL为我们集成了解决对应问题的API接口函数,更多函数还得等大家去探索,我只接触到过清理Cache相关的函数。

1、配置Non-cacheable:这个最好理解,就是正常的读写操作,无Cache。
TEX = 000 C=0 B=0 S=忽略此位,强制为共享
TEX = 000 C=0 B=1 S=忽略此位,强制为共享
TEX = 001 C=0 B=0 S=0
TEX = 001 C=0 B=0 S=1
2、配置Write through,read allocate,no write allocate
TEX = 000 C=1 B=0 S=1
TEX = 000 C=1 B=0 S=0
注意,M7内核只要开启了Cache,read allocate就是开启的。
使能了此配置的SRAM缓冲区写操作:
如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域,那么会同时写到Cache里面和 SRAM里面;如果没有,就用到配置no write allocate了,意思就是CPU会直接往SRAM里面写数据, 而不再需要在Cache里面开辟空间了。 在写Cache命中的情况下,这个方式的优点是Cache和SRAM的数据同步更新了,没有多总线访问 造成的数据一致性问题。缺点也明显,Cache在写操作上无法有效发挥性能。
使能了此配置的SRAM缓冲区读操作:
如果CPU要读取的SRAM区数据在Cache中已经加载好,就可以直接从Cache里面读取。如果没 有,就用到配置read allocate了,意思就是在Cache里面开辟区域,将SRAM区数据加载进来,后续 的操作,CPU可以直接从Cache里面读取,从而时间加速。
安全隐患,如果Cache命中的情况下,DMA写操作也更新了SRAM区的数据,CPU直接从Cache 里面读取的数据就是错误的。
3、配置Write back,read allocate,no write allocate
TEX = 000 C=1 B=1 S=1
TEX = 000 C=1 B=1 S=0
使能了此配置的SRAM缓冲区写操作:
如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域,那么会写到Cache里面,而不 会立即更新SRAM;如果没有,就用到配置no write allocate了,意思就是CPU会直接往SRAM里面 写数据,而不再需要在Cache里面开辟空间了。 安全隐患,如果Cache命中的情况下,此时仅Cache更新了,而SRAM没有更新,那么DMA直接 从SRAM里面读出来的就是错误的。
使能了此配置的SRAM缓冲区读操作:
如果CPU要读取的SRAM区数据在Cache中已经加载好,就可以直接从Cache里面读取。如果没有, 就用到配置read allocate了,意思就是在Cache里面开辟区域,将SRAM区数据加载进来,后续的操 作,CPU可以直接从Cache里面读取,从而时间加速。
安全隐患,如果Cache命中的情况下,DMA写操作也更新了SRAM区的数据,CPU直接从Cache 里面读取的数据就是错误的。
4、配置Write back,read allocate,write allocate
TEX = 001 C=1 B=1 S=1
TEX = 001 C=1 B=1 S=0
使能了此配置的SRAM缓冲区写操作:
如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域,那么会写到Cache里面,而不 会立即更新SRAM;如果没有,就用到配置write allocate了,意思就是CPU写到往SRAM里面的数据, 会同步在Cache里面开辟一个空间将SRAM中写入的数据加载进来,如果此时立即读此SRAM区,那么 就会有很大的速度优势。
安全隐患,如果Cache命中的情况下,此时仅Cache更新了,而SRAM没有更新,那么DMA直接 从SRAM里面读出来的就是错误的。
使能了此配置的SRAM缓冲区读操作:
如果CPU要读取的SRAM区数据在Cache中已经加载好,就可以直接从Cache里面读取。如果没 有,就用到配置read allocate了,意思就是在Cache里面开辟区域,将SRAM区数据加载进来,后续 的操作,CPU可以直接从Cache里面读取,从而时间加速。
安全隐患,如果Cache命中的情况下,DMA写操作也更新了SRAM区的数据,CPU直接从Cache 里面读取的数据就是错误的。 这个配置被誉为可以最大程度发挥Cache性能,不过具体应用仍需具体分析。
2.4.5 Cache的命中与未命中
如果每次CPU要读写SRAM区的数据,都能够在Cache里面进行,自然是最好的,实现了200MHz 到400MHz的飞跃,实际是做不到的,因为数据Cache只有16KB大小,总有用完的时候。
Cache的读操作:对于使能了Cache的SRAM区,要分读写两种情况考虑。如果CPU要读取的SRAM区数据在Cache中已经加载好,这就叫读命中(Cache hit),如果Cache 里面没有怎么办,这就是所谓的读Cache Miss。
读命中:即Cache中有CPU想要读的数据。
CPU直接从Cache中读取
读未命中:Cache中没有CPU想要读的数据
情况1(直接读:read through):CPU直接从SRAM中读取(速度慢)
情况2(间接读:read allocate):Cache先从SRAM中读取,然后CPU再从Cache中读取(较慢)
Cache的写操作:如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域(专业词汇叫Cache Line,以 32 字节为单位),这就叫写命中(Cache hit),如果Cache里面没有开辟对应的区域怎么办,这就是所谓 的写Cache Miss。
写命中:即Cache中有CPU想要写的数据。
情况1(直接写:write through):CPU直接把数据分别同时写到Cache和SRAM中,注意,这两个区域是同时改变。
情况2(稍后写:write back):CPU先向Cache写入新的数据,Cache在接收到新的数据后,把旧数据发送给SRAM后自己替换新数据,这个过程导致了CPU发送命令之后到SRAM中会有滞后的过程。
写未命中:即Cache中没有CPU想要写的数据。
情况1(直接写:no write allocate):CPU直接把数据写到SRAM中。
情况2(间接写:write allocate):CPU先向Cache写入新的数据,Cache在接收到新的数据后把数据发送给SRAM。
初学者如果不知道或者不理解,了解即可,本次实验只需要做出一些配置,就可以实现上述功能,但是上述功能会涉及到数据一致性的问题,所以后面还会再讲。
2.4.6 Cache 的相关函数(API)
CMSIS软件包的core_cm7.h文件为Cache的配置提供了11个函数:
◆ SCB_EnableICache
◆ SCB_DisableICache
◆ SCB_InvalidateICache
◆ SCB_EnableDCache
◆ SCB_DisableDCache
◆ SCB_InvalidateDCache
◆ SCB_CleanDCache
◆ SCB_CleanInvalidateDCache
◆ SCB_InvalidateDCache_by_Addr
◆ SCB_CleanDCache_by_Addr
◆ SCB_CleanInvalidateDCache_by_Addr
2.5 如果分区不配置MPU有什么潜在风险?
2.5.1 单纯做分区处理,不配置MPU和Cache
只利用链接文件进行简单分区,并未开启MPU和Cache功能,在这种情况使用DMA功能,如发送串口,读取ADC,那么这种情况是没有风险的,但是缺点是速度有点慢,可以从下面的图看出,速度只有200MB,但是说实话,能触及到分区已经是很大的进步,一般的项目开发不需要如此高的速度要求,对于初学者来说,能搞懂并且运用分区建立缓冲区就是很大的进步了。
| Cache状态 | MPU配置 | DMA操作 | 风险等级 | 需要维护操作 |
|---|---|---|---|---|
| 关闭 | 无配置 | 任何DMA | 🟢 安全 | 不需要 |
| 关闭 | 有配置 | 任何DMA | 🟢 安全 | 不需要 |
| 开启 | 无配置 | DMA传输 | 🔴 危险 | 必须手动维护 |
| 开启 | 正确配置 | DMA传输 | 🟡 中等 | 需要维护 |
| 开启 | 错误配置 | DMA传输 | 🔴 高危 | 可能维护无效 |
2.5.2 分区处理后,不配置MPU和Cache
根据上面的表格可以看出,在STM32H7这类高性能MCU中,虽然通过链接脚本可以实现内存分区,但如果不配置MPU,会面临严重的稳定性、性能和安全风险,这些风险在实际项目中往往是致命性的。下面两个实验我会分别展示加了MPU和不加MPU的版本,下面的问题如果目前看不懂的话,跟着做完实验再回看也可以。至于带来的风险有很多,我以数据一致性问题(最严重且隐蔽)为例,并AI得到简单的维护方案,其中的清理和无效相关函数,你可以简单的把他理解为把数据传到SRAM中,并清理自身。
由下面的代码我们可以看到,我们在".RAM_D2"区中建立了DMA传输的数组,经过函数的初始化赋值后,立马开启DMA传输,DMA是从SRAM中提取数据的,那么此时CPU才刚刚把数据写入D-Cache中,D-Cache还没有来及把数据存入SRAM中,这就导致了DMA有可能读到的值是旧的或者是乱码。反过来一个道理。

// 风险示例:DMA传输数据不一致
__attribute__((section(".RAM_D2"))) uint8_t dma_buffer[1024];
void dangerous_dma_operation(void) {
// CPU准备数据(数据可能只存在于Cache中)
for(int i = 0; i < 100; i++) {
dma_buffer[i] = calculate_data(i); // ✗ 写入D-Cache,未刷回内存
}
// 立即启动DMA传输
HAL_UART_Transmit_DMA(&huart1, dma_buffer, 100);
// ❌ 风险:DMA从物理内存读取,可能得到旧数据/全零数据
// ❌ 现象:随机出现数据错误,极难调试
}
// 相反的同样危险的情况
void dangerous_dma_receive(void) {
// DMA接收数据到内存
HAL_UART_Receive_DMA(&huart1, dma_buffer, 100);
// 等待DMA完成...
// CPU立即读取处理
process_data(dma_buffer); // ✗ 可能从Cache读取旧数据
// ❌ 风险:CPU看不到DMA写入的新数据
}
简单的维护方案如下:
// DMA传输的完整Cache维护模板
void safe_dma_template(void) {
__attribute__((section(".RAM_D2"))) uint8_t dma_buffer[1024];
// 场景1: CPU → DMA (发送数据)
printf("=== CPU → DMA 数据传输 ===\r\n");
// Step 1: CPU准备数据
for(int i = 0; i < 100; i++) {
dma_buffer[i] = i;
}
// Step 2: 清理Cache (确保数据写入内存)
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, 100);
printf("Cache cleaned: CPU data flushed to memory\r\n");
// Step 3: 启动DMA传输
HAL_UART_Transmit_DMA(&huart1, dma_buffer, 100);
printf("DMA transmit started\r\n");
// 场景2: DMA → CPU (接收数据)
printf("\r\n=== DMA → CPU 数据传输 ===\r\n");
// Step 1: 启动DMA接收
HAL_UART_Receive_DMA(&huart1, dma_buffer, 100);
// Step 2: 等待DMA完成
while(HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY) {
// 等待DMA完成
}
// Step 3: 无效化Cache (确保读取最新数据)
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, 100);
printf("Cache invalidated: CPU will read fresh data from memory\r\n");
// Step 4: CPU处理数据
process_received_data(dma_buffer);
}
2.6 链接文件的作用是什么(Keil5中为.sct 文件; CubeIDE 为.ld文件)?
链接文件是嵌入式系统开发中至关重要的配置文件,定义了整个程序在芯片内存中的"地图",它告诉链接器如何将编译后的代码和数据安排到物理内存中。Keil5和CubeIDE的链接文件稍微有点不同,但是功能一样。
以Keil5中的.sct文件为例,默认的.sct文件内容如下,默认系统只规定了LR_IROM1(Flash的加载区域)和默认的变量存储区域DTCM区域,在.sct文件中,采用“先到先得”的原则,在中括号先出现的选择器为最高优先级,例如.ANY代表其他文件的意思。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; ========== 默认区域:DTCM ==========
RW_IRAM1 0x20000000 0x00020000 { ; RW data
.ANY (+RW +ZI) ; 所有未指定段的变量放在这里
}
上面的代码具体含义如下:
| 段类型 | 内容 | 通常存放位置 |
|---|---|---|
| RESET | 中断向量表 | Flash开头 |
| +RO | 代码和常量 | Flash |
| +RW | 已初始化全局变量 | RAM |
| +ZI | 未初始化全局变量 | RAM |
| Stack | 栈空间 | RAM |
| Heap | 堆空间 | RAM |
我们可以根据数据手册,实现对上面提及到的SRAM其他区域进行分配和重命名,示例代码如下(这只是示例代码,如果是实操,请跟着实际的代码走),DTCM1作为普通变量的存放区域,AXI_SRAM作为图像缓冲区和DMA缓冲区,一个区域可以由多个命名。
; 复杂的多区域内存管理
LR_IROM1 0x08000000 0x00200000 {
ER_IROM1 0x08000000 0x00200000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
; 分区1: DTCM - 堆栈和关键数据
RW_IRAM1 0x20000000 0x00020000 {
.ANY (+RW +ZI) ; 普通变量
}
; 分区2: AXI SRAM - 大型缓冲区
RW_IRAM2 0x24000000 0x00080000 {
*.o(.AXI_RAM) ; 图像缓冲区
*.o(.DMA_BUFFER) ; DMA数据区
}
; 分区3: SRAM1 - 通信缓冲区
RW_IRAM3 0x30000000 0x00020000 {
*.o(.SRAM1_DATA) ; 网络数据包
*.o(.UART_BUFFER) ; 串口缓冲区
}
; 分区4: SRAM2 - 算法工作区
RW_IRAM4 0x30020000 0x00020000 {
*.o(.DSP_BUFFER) ; DSP处理数据
*.o(.FFT_WORKSPACE) ; FFT工作区
}
}
2.7 推荐的学习链接以及相关手册
2.7.1 相关数据手册
本篇内容可以参考的手册如下:
《安富莱_STM32-V7开发板_用户手册,含BSP驱动包设计(V3.5)》
《STM32H743数据手册》
《STM32H743参考手册》
《PM0253_STM32F7和 STM32H7单片机Cortex®-M7内核编程手册》
2.7.2 相关视频讲解
安富莱官网香瓜讲解
2.7.3 相关优秀博客
https://blog.csdn.net/wallace89/article/details/117001405?spm=1001.2014.3001.5506
https://blog.csdn.net/weixin_66689383/article/details/131993384?spm=1001.2014.3001.5506
三、功能实现及测试(实现分区+配置MPU)
3.1 实验一:实现简单的分区以及数据存储
3.1.1 CubeMX: 基础配置(时钟树、烧写口、时钟源等配置)
选择STM32H743VGT6,新版本的CUBEMX在选择芯片的搜索栏需要输入大写,不然会出现双次按键的情况。

选择内存优化:Yes

烧写方式:单线烧写

外部高速晶振:25MHZ,如果由外部低速也可以选择,在这里就不选择了

时钟树配置如下:

3.1.2 CubeMX: 测试外设配置:串口1+DMA发送+指示灯
GPIO配置:选择PC13作为LED指示灯

串口配置:采用串口1(PA9,PA10)作为调试串口

DMA配置:添加串口1_TX的DMA发送即可。

串口接收中断也可以不开,这边顺手就给开上来了。

3.1.3 CubeMX: keil5: 链接文件的编写与保存
原来Keil5链接文件的查看:选择options -> Linker -> 取消勾选 Use Memory ...... ->点击Edit就可以查看原来的链接文件内容

原本的代码如下,可以看出,原本的链接文件只是默认的指明了Flash中一些中断和其他功能的地址范围(即ITCM-RAM),还有存放变量的默认地址(即DTCM-RAM),未定义其他变量。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00020000 { ; RW data
.ANY (+RW +ZI)
}
RW_IRAM2 0x24000000 0x00080000 {
.ANY (+RW +ZI)
}
}
LR_IROM2 0x08100000 0x00080000 {
ER_IROM2 0x08100000 0x00080000 { ; load address = execution address
.ANY (+RO)
}
}
3.1.4 keil5: 链接文件的添加
我们现在要做的就是增加我们自己的链接文件,首先利用Keil5自带的编辑器,创建空白页,然后保存为.sct文件,只要存放在工程文件夹以内都可以。


下一步就是编辑我们自己链接文件,首先要保证之前默认的文件链接方法保持不变,在原有的基础上增加我们的地址,代码如下:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; ========== 默认区域:DTCM ==========
RW_IRAM1 0x20000000 0x00020000 { ; RW data
.ANY (+RW +ZI) ; 所有未指定段的变量放在这里
}
; ========== D1区域:AXI SRAM ==========
RW_IRAM2 0x24000000 0x00080000 {
*.o(.RAM_D1) ; 只包含显式标记为.RAM_D1的变量
}
; ========== D2区域:SRAM1 ==========
RW_IRAM3 0x30000000 0x00020000 {
*.o(.RAM_D2) ; 只包含显式标记为.RAM_D2的变量
}
; ========== D3区域:SRAM2 ==========
RW_IRAM4 0x30020000 0x00020000 {
*.o(.RAM_D3) ; 只包含显式标记为.RAM_D3的变量
}
}
LR_IROM2 0x08100000 0x00080000 {
ER_IROM2 0x08100000 0x00080000 { ; load address = execution address
.ANY (+RO)
}
}
3.1.5 keil5: main.c中的实现
实现了以上步骤,我们就已经完成了在写代码测试前的准备工作,接下来我们需要在main.c文件中增加自己的测试代码。
// 分区变量定义
__attribute__((section(".RAM_D1"))) uint8_t image_buffer[256 * 1024]; // AXI SRAM
__attribute__((section(".RAM_D2"))) uint32_t dma_buffer[16 * 1024]; // SRAM1
__attribute__((section(".RAM_D3"))) float processing_buffer[8 * 1024]; // SRAM2
// DTCM默认区域变量
uint32_t system_counter;
char status_message[128];
// printf重定向
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
void memory_access_test(void) {
// 测试各分区数据访问
for(int i = 0; i < 1000; i++) {
image_buffer[i] = i; // AXI SRAM
dma_buffer[i] = i; // SRAM1
processing_buffer[i] = i * 0.1f; // SRAM2
system_counter = i; // DTCM
}
}
//main函数中
printf("====== START =======\r\n");
memory_access_test();
while (1)
{
HAL_GPIO_TogglePin(LED_B_GPIO_Port,LED_B_Pin);
HAL_Delay(500);
printf("====== runing! =======\r\n");
}
3.2 数据存储位置的测试以及验证
完成上述代码之后,恭喜你,你已经完成了简单的内存分区应用的操作,但是目前你的速度中只能达到200MB,由于没有配置MPU,使用Cache。接下来我们来检验一下我们的代码是否正确存入我们设置的SRAM中去。
3.2.1 keil5的窗口中
编译好工程后,链接核心板后,点击调试,找出memory串口,可以查看到数据,可以看到D1和D2区域已经由我们写进去的0-1000的数据。
3.2.2 map文件中
也可以在工程中,找到.map文件查看数据的位置,地址为:工程所在文件夹\MDK-ARM\CSDN_BLOG_1,找到打开后搜索你定义的变量,就可以看到他们的地址被正确的分配到对应的SRAM中,到此为止,你就可以安心的使用这些变量,虽然速度只有200MB,但是完全够用了,起码对于我来说是够用了,接下来我会在此基础上配置MPU,使用Cache高速缓存,来提升访问速度。这一部分如果我讲的不好,可以去看看安富莱的文档,他们讲的真的特别好!!
3.3 实验二:在实验一的基础上增加MPU相关配置,实现分区保护(重要)
在做实验一之前,我们系统的讲解了什么MPU和Cache以及他们之间的关系是什么样的。由于此实验只需要在CUBEMX上面配置即可,所以我再次会详细讲解每个参数是什么,如何配置的。
实验二就是在实验一的基础上加上MPU权限保护,具体配置如下,同时我也会根据我下面截图的配置讲解。
在CubeMX中配置MPU需要进入System Core > CORTEX_M7界面,开启I和DCache,这样就开启了高速缓存。刚开始进入到这个界面的时候当使能Speculation default mode时,其默认只有一个区域,并且这个区域访问权限时FALL,可共享,我们把它删去即可,然后按照如上配置各个region的参数。
下面讲解一些参数的含义和作用(以region0为例):
CPU Control Mode:启用背景区域,特权模式可访问所有未配置内存,在这些高优先级异常中禁用MPU,这个就是对未配置区域的一个设置,在特权模式的时候,CP可以对其他地址进行访问,如片外RAM等。
Base Address (基地址):MPU保护区域的起始地址,必须对齐到区域大小(如512KB区域必须512KB对齐)。
Size (大小):保护区域的大小,必须是2的幂,范围从32B 到 4GB。
Access Permission(访问权限):
| 选项 | 特权模式 | 用户模式 | 适用场景 |
|---|---|---|---|
| No Access | 无访问 | 无访问 | 保护区域,任何访问触发Fault |
| Privileged Read Write | 读/写 | 无访问 | 内核数据、外设寄存器 |
| Unprivileged Read Write | 读/写 | 读/写 | 共享数据区 |
| Privileged Read Only | 只读 | 无访问 | 只读配置数据 |
| Read Only | 只读 | 只读 | 常量数据、共享只读 |
内存类型和Cache策略 (TEX/C/B/S):这个表是对上面编程手册上的解释。
| TEX | C | B | 内存类型 | Cache策略 | 适用场景 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | Strongly-ordered | Non-cacheable | 外设寄存器 |
| 0 | 0 | 1 | Device | Non-cacheable | 外设寄存器 |
| 0 | 1 | 0 | Normal | Write-Through | DMA缓冲区 |
| 0 | 1 | 1 | Normal | Write-Back | 内部计算数据 |
| 1 | 0 | 0 | Normal | Non-cacheable | 共享内存 |
| 1 | 0 | 1 | - | 保留 | - |
| 1 | 1 | 0 | Normal | Write-Through | - |
| 1 | 1 | 1 | Normal | Write-Back | 性能最优数据 |
Shareable (可共享):DMA缓冲区设为Shareable,CPU专用数据设为Not Shareable
-
Enabled:该区域可被多个总线主设备共享
-
Disabled:主要被单个主设备访问
Instruction Access(执行权限):
-
Enable:允许从该区域取指执行代码
-
Disable:禁止执行代码,任何执行尝试触发MemManage Fault
SubRegion Disable(子区域禁用):将大区域划分为8个子区域,可单独禁用保护,进行精细化的配置。例如0x03(二进制00000011)禁用前两个子区域。
讲解了以上的各种参数,现在给我整理出的配置清单
| Region | Base Address | Size | AP | TEX | S | C | B | SubRegion |
|---|---|---|---|---|---|---|---|---|
| 0 | 0x20000000 | 128KB | Full | 0 | 0 | 1 | 0 | 0x00 |
| 1 | 0x24000000 | 512KB | Full | 1 | 0 | 1 | 1 | 0x00 |
| 2 | 0x30000000 | 128KB | Full | 0 | 1 | 1 | 0 | 0x00 |
| 3 | 0x30020000 | 128KB | Full | 1 | 0 | 1 | 1 | 0x00 |
根据我在前提知识中讲解的,我们可以轻易的看出他们配置了什么,其中:
Region 0: DTCM RAM - Write-Through, 共享
Region 1: AXI SRAM - Write-Back, 不共享
Region 2: SRAM1 - Write-Through, 共享(DMA缓冲区)
Region 3: SRAM2 - Write-Back, 不共享
代码如下:
//main.c中:
// 分区变量定义
__attribute__((section(".RAM_D1"))) uint8_t image_buffer[256 * 1024]; // AXI SRAM
__attribute__((section(".RAM_D2"))) uint32_t dma_buffer[16 * 1024]; // SRAM1
__attribute__((section(".RAM_D3"))) float processing_buffer[8 * 1024]; // SRAM2
// DTCM默认区域变量
uint32_t system_counter;
char status_message[128];
// printf重定向
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
void memory_access_test(void) {
// 测试各分区数据访问
for(int i = 0; i < 1000; i++) {
image_buffer[i] = i; // AXI SRAM
dma_buffer[i] = i; // SRAM1
processing_buffer[i] = i * 0.1f; // SRAM2
system_counter = i; // DTCM
}
}
void verify_mpu_configuration(void)
{
printf("=== MPU配置验证 ===\r\n");
// 验证各区域访问权限
volatile uint32_t *test_addr;
// 测试DTCM访问
test_addr = (volatile uint32_t*)0x20000000;
*test_addr = 0x12345678;
printf("DTCM: 0x%08lX\r\n", *test_addr);
// 测试AXI SRAM访问
test_addr = (volatile uint32_t*)0x24000000;
*test_addr = 0x87654321;
printf("AXI SRAM: 0x%08lX\r\n", *test_addr);
// 测试执行权限(应该触发fault)
//void (*func_ptr)(void) = (void(*)(void))0x20000000;
//func_ptr(); // 取消注释测试,应该触发MemManage Fault
printf("MPU基础功能验证完成\r\n");
}
//main函数中
printf("====== START =======\r\n");
memory_access_test();
verify_mpu_configuration();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED_B_GPIO_Port,LED_B_Pin);
HAL_Delay(500);
printf("====== runing! =======\r\n");
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
当运行如上代码时,再根据实验一的操作,会发现初始化后,各个RAM区域出现了对应的缓冲数组,其中当我们把测试的代码注释删去后,运行烧写,调试的时候会发现它进入了MemManage Faul函数,这是正常的,因为我们对Region区域设置时,禁止运行代码,所有他会进入这个函数,同时,以后在配置中出现问题,可以在这个回调函数里面增加测试代码,这样就饿可以查明是哪个环节出了问题。
同时值得注意的是,我们的Region2设置了可以共享,并且是Write-Through,这就使得DMA和CPU在同时读写访问的时候会出现一致性问题,例如:
__attribute__((section(".RAM_D2"))) uint8_t shared_buffer[1024];
void cache_coherency_issue(void) {
// CPU准备数据(数据进入D-Cache)
for(int i = 0; i < 100; i++) {
shared_buffer[i] = i; // ✗ 可能只在Cache中,未写入内存
}
// DMA从物理内存读取
HAL_UART_Transmit_DMA(&huart1, shared_buffer, 100);
// ❌ DMA可能读取到旧数据!
// 或者相反的情况:
// DMA写入数据到内存
HAL_UART_Receive_DMA(&huart1, shared_buffer, 100);
// CPU立即读取
uint8_t data = shared_buffer[0]; // ✗ 可能从Cache读取旧数据
// ❌ CPU看不到DMA写入的新数据!
}
这时候就需要我们做维护,具体维护API如下,清理和无效化可能看起来比较抽象,大家可以通俗的理解,就是,在我读取之前,把Cache中的数据刷到物理内存中,在进行读取,这就解决了一致性问题!
// 1. 清理Cache (Clean) - CPU → DMA
SCB_CleanDCache(void); // 清理整个D-Cache
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize); // 清理指定区域
// 2. 无效化Cache (Invalidate) - DMA → CPU
SCB_InvalidateDCache(void); // 无效化整个D-Cache
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize); // 无效化指定区域
// 3. 清理并无效化 (Clean+Invalidate)
SCB_CleanInvalidateDCache(void);
SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
具体使用场景:
//CPU → DMA 数据传输:CPU准备数据,DMA发送
__attribute__((section(".RAM_D2"))) uint8_t tx_buffer[1024];
void cpu_to_dma_transfer(void) {
printf("=== CPU → DMA 传输 ===\r\n");
// 步骤1: CPU准备数据
for(int i = 0; i < 256; i++) {
tx_buffer[i] = (i * 3) & 0xFF; // 数据进入D-Cache
}
printf("数据准备完成\r\n");
// 步骤2: 清理Cache,确保数据写入物理内存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, 256);
printf("Cache清理完成,数据已刷入内存\r\n");
// 步骤3: 启动DMA传输
HAL_UART_Transmit_DMA(&huart1, tx_buffer, 256);
printf("DMA传输启动\r\n");
// 验证数据一致性
verify_data_consistency("CPU→DMA", tx_buffer, 256);
}
//DMA → CPU 数据传输:DMA接收数据,CPU处理
__attribute__((section(".RAM_D2"))) uint8_t rx_buffer[1024];
void dma_to_cpu_transfer(void) {
printf("=== DMA → CPU 传输 ===\r\n");
// 步骤1: 启动DMA接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, 256);
printf("DMA接收启动\r\n");
// 步骤2: 等待DMA完成
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_RX) {
// 等待DMA完成
}
printf("DMA接收完成\r\n");
// 步骤3: 无效化Cache,确保读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, 256);
printf("Cache无效化完成,将读取内存最新数据\r\n");
// 步骤4: CPU处理数据
process_received_data(rx_buffer, 256);
printf("数据处理完成\r\n");
}
//双向数据共享:CPU和DMA频繁读写同一缓冲区
__attribute__((section(".RAM_D2"))) uint8_t bidirectional_buffer[1024];
void bidirectional_data_share(void) {
printf("=== 双向数据共享 ===\r\n");
// 情况A: CPU修改后DMA读取
for(int i = 0; i < 100; i++) {
bidirectional_buffer[i] = calculate_value(i);
}
SCB_CleanDCache_by_Addr((uint32_t*)bidirectional_buffer, 100);
HAL_UART_Transmit_DMA(&huart1, bidirectional_buffer, 100);
// 情况B: DMA写入后CPU读取
HAL_UART_Receive_DMA(&huart1, &bidirectional_buffer[100], 100);
// 等待DMA完成...
SCB_InvalidateDCache_by_Addr((uint32_t*)&bidirectional_buffer[100], 100);
process_data(&bidirectional_buffer[100], 100);
printf("双向数据共享完成\r\n");
}
五、实验总结
这个实验较长,MPU听说是H7的一个非常重要的点,所有我写下这篇博客,一是给自己看看,也是给大家分享一下,上面的内容有部分是来自别人的博客,有一部分是来自于官方数据手册和安富莱的相关数据手册,同时因为我的时间有限,部分代码是AI写,目的是为了更通俗易懂一些,以上代码我都经过测试,希望大家给个三连!!!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)