目录

一、概述

二、前提知识

2.1 STM32H7系列芯片的特点

2.1.1 STM32H743VGT6芯片详细介绍

2.1.2 STM32H43VGT6的SRAM分配详解

2.2 分配内存区的意义是什么?

2.3  什么是MPU?

2.4 什么是Cache(高速缓存)? 它和MPU是什么关系?

2.4.1 CPU的数据传输过程

2.4.2 Cache是什么?

2.4.3 Cache和MPU是什么关系?

2.4.4 Cache如何配置 (配置策略)?

2.4.5 Cache的命中与未命中

2.4.6 Cache 的相关函数(API)

2.5 如果分区不配置MPU有什么潜在风险?

2.5.1 单纯做分区处理,不配置MPU和Cache

2.5.2 分区处理后,不配置MPU和Cache      

2.6 链接文件的作用是什么(Keil5中为.sct 文件; CubeIDE 为.ld文件)?

2.7 推荐的学习链接以及相关手册

2.7.1 相关数据手册

2.7.2 相关视频讲解

2.7.3 相关优秀博客

三、功能实现及测试(实现分区+配置MPU)

3.1 实验一:实现简单的分区以及数据存储

3.1.1 CubeMX: 基础配置(时钟树、烧写口、时钟源等配置)

3.1.2 CubeMX: 测试外设配置:串口1+DMA发送+指示灯

3.1.3 CubeMX: keil5: 链接文件的编写与保存

3.1.4 keil5: 链接文件的添加

3.1.5 keil5: main.c中的实现

3.2 数据存储位置的测试以及验证

3.2.1 keil5的窗口中

3.2.2 map文件中

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的区别与联系:

    1. MPU是"交通警察" - 控制谁可以访问哪里,以什么方式访问

    2. 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写,目的是为了更通俗易懂一些,以上代码我都经过测试,希望大家给个三连!!!

    Logo

    DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

    更多推荐