STM32H747异构双核实战:启动顺序、Cache一致性与核间通信
2026/9/3 3:56:06 网站建设 项目流程

最近两年,很多从 STM32F103/F407 入门的老开发者,第一次认真研究 STM32H747 时,心态都是“这东西不就是主频高一点的 MCU 吗”。等真正拿到板子打开 CubeMX,看到界面里同时出现 Cortex-M7 和 Cortex-M4 两颗核,才发现事情没那么简单:代码该放哪颗核、flash 地址怎么分、M4 为什么不动、共享变量为什么读了半天是旧值,个个都是以前单核项目没遇到过的坑。

这篇文章不打算复述芯片手册。我只会讲实际做双核项目时绕不过去的几个技术点:双核架构与电源域、启动顺序、存储/Cache 一致性、核间通信、双核调试。读完你会知道一个 H747 硬核项目在动手写业务代码之前,应该先想清楚哪些事情。

1. STM32H747 到底强在哪:不是单纯“主频高”

很多人会把 STM32H747 理解成一颗“Cortex-M7 主频更高的 H743”,这是最大的误解。H747 的真正卖点是异构双核:一颗 Cortex-M7 负责高性能计算,一颗 Cortex-M4 负责实时控制或低功耗采集,两颗核封装在同一颗芯片里,共享 Flash、共享 SRAM、共享大部分外设,但又分别拥有独立的 NVIC 中断控制器和独立的调试接口。

从项目角度看,这颗芯片解决了单核 MCU 在高复杂度系统里的几个实际痛点:

  • 单核既要跑 1 kHz 实时控制环,又要处理通信协议栈、显示刷新、日志存储,中断抖动和任务切换开销很难控制;
  • 用外置 MPU 加 RTOS 虽然能扛住负载,但成本和硬件复杂度明显上升;
  • 需要“实时控制”和“复杂应用”分离时,单核只能靠任务优先级硬切,出了偶发问题极难复现。

双核方案把这两类负载物理隔开,实时性、稳定性、可维护性都更容易保证。

这颗芯片的典型硬件配置如下,具体数值以你手上的数据手册为准:

项目典型参数说明
Cortex-M7最高约 480 MHz带 I-Cache / D-Cache,适合高性能算法与主控逻辑
Cortex-M4最高约 240 MHz负责实时采集、控制、通信等确定性任务
Flash通常 2 MB双核工程需要按地址拆分
SRAM约 1 MB 级分布在多个总线域,不是一整块连续内存
电源域D1 / D2 / D3不同域的外设与内存归属不同

读到这里你应当有一个基本判断:H747 的难点不在某一颗核的性能,而在两颗核如何正确共享一颗芯片的资源。这也是标题里“硬核实战”四个字的含义。

2. 从单核思维切换到异构双核思维

嵌入式开发里经常听到 AMP、SMP 这两个词。STM32H747 属于典型的 AMP,也就是非对称多处理,两颗核跑的系统可以完全不同:M7 上跑 RTOS,M4 上跑裸机或者另一个 RTOS,各自独立调度。而 SMP 是两个核共享同一个操作系统,由内核统一调度,这在 MCU 领域不是主流。

用 AMP 思维去设计 H747 项目,第一件事就是“分活”。

2.1 什么任务适合放 M7

Cortex-M7 的优势是高频加缓存,适合算力需求明确的场景:

  • 音频编解码、FFT、FIR 滤波等 DSP 类算法;
  • 图形界面渲染、字符串处理、文件系统、网络协议栈;
  • 需要大缓存配合的批量数据搬运;
  • 机器学习推理、传感器融合等需要“跑得动”的算法。

放 M7 不是因为 M7 一定比 M4 强多少,而是这些任务通常会访问大块内存,需要缓存来提升效率,同时不介意偶尔被 OS 调度打断。

2.2 什么任务适合放 M4

Cortex-M4 的主频虽然低于 M7,但在“确定性”这件事上反而更友好:

  • 1 kHz 甚至 10 kHz 的电流环、速度环控制;
  • 电机换相、PWM 脉冲产生;
  • 低速但必须稳定的传感器采集;
  • 独立的看门狗、硬件安全逻辑;
  • 低功耗模式下仍需运行的后台任务。

这类任务的特点是周期固定、不允许被高优先级任务长时间抢占、代码逻辑相对简单,不需要动不动访问几 MB 数据。

2.3 一个典型任务分配实例

假设你做一个双轴伺服驱动器加本地 HMI 的项目,可以这样分配:

功能模块建议放置核原因
电流环 / 速度环M4周期固定,要求低抖动
位置规划与插补M4与伺服周期强相关
EtherCAT / Modbus 协议栈M7协议解析需要缓存和较多内存
HMI 显示与触摸M7图形刷新任务重,被抢占影响小
本地数据记录M7文件系统 + Flash 写入复杂
故障保护逻辑双核同时保留安全相关需要冗余

分完任务之后再谈内存和外设分配,才有依据。

3. 双核项目的启动顺序:CM7 先走,CM4 等“释放”

用 H747 做双核项目,新手最容易遇到的第一个现象是:M4 工程的代码明明烧进去了,上电后却不执行。

这不是代码写错了,而是 H747 的默认启动行为决定的。芯片复位后,Cortex-M7 作为主导核先从 Flash 起始地址启动,Cortex-M4 默认处于保持状态,需要 M7 侧程序主动把 M4“放出来”,M4 才会开始取指执行。

整体启动流程大致如下:

  1. 芯片上电复位,M7 从 0x08000000 取出向量表,设置栈指针和复位向量;
  2. M7 执行系统时钟初始化、MPU 配置、Cache 使能;
  3. M7 使能 M4 所在 D2 域必要的时钟和 SRAM;
  4. M7 设置 M4 的启动地址相关的 SYSCFG 控制位;
  5. M7 释放 M4 的保持位,M4 从自己的启动地址开始运行;
  6. M4 侧代码执行 SystemInit、时钟配置、应用初始化,完成双核握手。

从工程角度更要留意“两个核的代码放在同一颗 Flash”这个事实。最常见的分区方式是前半段 1 MB 放 M7,后半段 1 MB 放 M4。也就是说,M7 代码链接到 0x08000000,M4 代码链接到 0x08100000,M4 的向量表偏移也需要设置到对应地址。

在 STM32CubeMX/STM32CubeIDE 里操作双核工程时,通常要为两个核分别生成工程。M7 工程链接地址使用 0x08000000 起始,M4 工程使用 0x08100000 起始。M4 工程里还需要在系统初始化代码中设置SCB->VTOR,让 M4 知道自己的中断向量表在哪里。

M7 侧释放 M4 的示意代码如下。不同 HAL 版本的寄存器封装名称可能不同,真正重要的是理解“先配置再释放”的顺序。

// CM7 工程中释放 Cortex-M4 void CM4_Release(void) { // 1. 使能 M4 内核区域使用的 SRAM 时钟 // STM32CubeMX 会在 SystemClock_Config 中自动处理大部分时钟 // 2. 等待 D2 域 SRAM 可访问 while (READ_BIT(RCC->CFGR, RCC_CFGR_xxx) == 0) { } // 3. 配置 M4 启动地址相关控制位(名称以实际芯片头文件为准) // 4. 清除 CM4 保持位,让 M4 从复位状态释放 // 典型操作是修改 SYSCFG->CBR 中对应位 MODIFY_REG(SYSCFG->CBR, SYSCFG_CBR_M4_HOLD, 0); // 5. 等待 M4 侧完成初始化后置位握手标志 // 例如等待共享内存里 magic 字段变成预期值 }

这段代码最核心的动作是第 4 步“清除保持位”。如果这里没有执行,M4 永远停在复位状态,你在调试器里附加 M4 时会发现 PC 不变化。

还要提醒一点:如果 M4 工程是在 M7 已经运行之后才通过烧录器单独下载,往往需要先让 M7 跑起来并释放 M4,调试过程才完整。想要纯靠调试器从零开始同时调试两颗核,需要专门的 dual-core 调试配置,这会在后面单独讲。

4. 内存划分、Cache 与 DMA 一致性:最容易翻车的区域

STM32H747 的内存并不是一整块大 RAM。它的内存分布在多个总线域上,分别服务不同核心和不同外设。以常见型号为例,主要地址段大致如下:

地址范围内存大小典型值主要归属
0x00000000ITCM约 64 KBM7 指令紧耦合内存
0x20000000DTCM约 128 KBM7 数据紧耦合内存
0x24000000AXI SRAM约 512 KBD1 域,主频高,容量大
0x30000000SRAM1约 128 KBD2 域,M4 可直接访问
0x30020000SRAM2约 128 KBD2 域
0x30040000SRAM3约 32 KBD2 域
0x38800000SRAM4约 64 KBD3 域,低功耗场景常用
0x08000000Flash约 2 MB双核代码分区存放

这里要特别解释两个概念:ITCM/DTCM 是 M7 私有的紧耦合内存,访问延迟最低,但不是“共享内存”的合理选择。M4 与 M7 做核间通信时,通常优先选择 D2 域 SRAM1/SRAM2,或者选择 D3 域 SRAM4,因为这些内存从总线结构上能被双方都访问。

但内存能访问,不代表数据就是一致的。这就是 H7 项目中最经典的 Cache 一致性问题。

4.1 什么是 Cache 一致性问题

Cortex-M7 带有 D-Cache。当 M7 读写普通 SRAM 时,数据可能先停留在 Cache 里,而不是立刻写回物理内存。这带来一个后果:

  • M7 往共享缓冲区写完数据,没有执行 Cache clean 操作;
  • M4 去读内存,读到的可能是旧值,因为 M7 的新数据还“在路上”或者还在 Cache 里;
  • M4 往共享缓冲区写数据,如果 M7 的 Cache 里缓存了这个地址的旧数据,M7 去读时可能直接命中 Cache,永远看不到 M4 写入的新值。

这种 Bug 是最难查的一类:单步调试时数据是对的,全速运行时数据是错的;用调试器读内存是新的,程序读到的却是旧的。

解决思路通常有两种。第一种是把共享内存区域配置成 non-cacheable,也就是不走 Cache,直接用 MPU 把这个区域标记为不可缓存。第二种是保留 Cache,但在每次访问共享内存前后手动执行 clean/invalidate 操作。实际工程里,核间高频通信的数据区强烈建议直接做成非缓存区域,省心且可控。

以下是一个典型的 MPU 配置代码,把 D2 域某段共享 SRAM 配置为不可缓存、不可缓冲、可共享:

// 文件路径:CM7_Project/Core/Src/main.c 或 mpu.c #include "main.h" #define SHARED_MEM_BASE 0x30000000UL // D2 SRAM1 起始地址 #define SHARED_MEM_SIZE MPU_REGION_SIZE_128KB static void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = SHARED_MEM_BASE; MPU_InitStruct.Size = SHARED_MEM_SIZE; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }

如果在部分场景下无法关闭 Cache,那么至少要在共享数据交换点做显式 Cache 维护:

// 写共享缓冲区之后,把脏数据刷回物理内存 SCB_CleanDCache_by_Addr((uint32_t *)shared_buf, sizeof(shared_buf)); // 读共享缓冲区之前,让 Cache 里的旧数据失效 SCB_InvalidateDCache_by_Addr((uint32_t *)shared_buf, sizeof(shared_buf));

需要牢记:clean 是把 Cache 写回内存,invalidate 是丢弃 Cache 重新从内存加载。写数据后使用 clean,读数据前使用 invalidate。顺序反了,数据就会出错。

5. 核间通信:共享内存 + 硬件信号量的正确姿势

双核系统最难设计的不是单核内部的任务通信,而是两个核之间的通信。H747 没有复杂的操作系统级 IPC,工程上最稳定、最常用的组合是:

  • 共享内存:承载实际数据;
  • 硬件信号量 HSEM:保护临界区和多核互斥;
  • 软件标志位或中断:通知对方数据已就绪。

5.1 为什么不用全局变量直接通信

单核系统里,全局变量加中断屏蔽就能做线程间通信。双核系统里,两颗核是真正并行执行的,中断屏蔽只能屏蔽本地核,无法阻止另一颗核同时访问同一段内存。如果两个核同时读写同一个结构体,轻则读到半新半旧的数据,重则破坏数据一致性。

因此,双核共享变量必须满足三个条件:内存对齐、访问原子、共享区缓存语义一致。最简单的可靠方式是使用一个无锁环形缓冲区,配合共享区非缓存属性来实现。

下面是一个双核共用的环形缓冲区实现。它的特点是设计为单生产者单消费者模型,M7 只写,M4 只读,这样就不需要复杂的锁,只要保证读写指针的访问顺序正确。

// 文件路径:shared_ring.h(两个工程共用同一份定义) #ifndef SHARED_RING_H #define SHARED_RING_H #include <stdint.h> #define RING_LEN 512 #define RING_MASK (RING_LEN - 1) typedef struct { volatile uint32_t wr; volatile uint32_t rd; uint32_t data[RING_LEN]; } RingBuf_t; void Ring_Write(RingBuf_t *ring, uint32_t value); int Ring_Read(RingBuf_t *ring, uint32_t *value); #endif
// 文件路径:shared_ring.c #include "shared_ring.h" void Ring_Write(RingBuf_t *ring, uint32_t value) { uint32_t next = (ring->wr + 1) & RING_MASK; while (next == ring->rd) { // 缓冲区满,等待消费者读走数据 } ring->data[ring->wr] = value; __DSB(); // 确保数据写入完成 ring->wr = next; } int Ring_Read(RingBuf_t *ring, uint32_t *value) { if (ring->wr == ring->rd) { return 0; // 缓冲区空 } *value = ring->data[ring->rd]; __DSB(); // 确保读取完成 ring->rd = (ring->rd + 1) & RING_MASK; return 1; }

这段代码的关键在于分配角色:M7 作为生产者只调用Ring_Write,M4 作为消费者只调用Ring_Read。由于读写位置只有一个生产者一个消费者在更新,单生产单消费模型天然无锁安全。

这里还需要强调__DSB()的作用。它是一条数据同步屏障指令,作用是确保前面的内存访问真正完成之后再执行后面的指令。在双核共享内存通信里,这一条指令能防止编译器或 CPU 乱序访问导致“先更新了 wr 指针,再写入 data”这样的错误顺序。

5.2 什么时候用 HSEM

如果两个核需要对同一个资源做“读改写”,光靠环形缓冲区不够,比如共享配置结构体、共享统计计数、双核访问同一个外设寄存器,就需要硬件信号量 HSEM。

STM32CubeH7 固件库提供HAL_HSEM_FastTakeHAL_HSEM_Release,用法如下:

#define SHARED_HSEM_ID 0 void SharedSection_Enter(void) { while (HAL_HSEM_FastTake(HSEM, SHARED_HSEM_ID) != HAL_OK) { // 获取失败则等待 } } void SharedSection_Exit(void) { HAL_HSEM_Release(HSEM, SHARED_HSEM_ID); }

HSEM 的优势是信号量状态在硬件里,两颗核都能看到,不会被某颗核的软件异常卡死。如果一颗核在持锁过程中崩溃,另一颗核可能无限等待,所以持锁临界区要尽量短,不要把复杂逻辑和函数调用放在锁内。

5.3 消息通知:轮询还是中断

上面的环形缓冲区例子用的是轮询方式,M4 循环检查wr != rd。轮询实现简单、出错概率低,但有两个缺点:一是 CPU 空转耗电;二是数据从写入到被对方发现存在延迟。

轮询适合数据量小、周期短、对功耗不敏感的场景。如果希望 M4 在数据到达时立刻被唤醒,可以用中断通知。具体做法有两种:M7 在写完wr指针后,通过 GPIO 或内部事件触发 M4 的一个 EXTI 中断;或者利用共享标志位加 M4 侧定时器定期检查,相当于低频轮询加中断补偿。

工程上我建议第一版双核通信先用轮询跑通,验证共享内存地址、缓存属性、数据格式都没问题之后,再换中断通知优化实时性。不要一上来就两个核互相发中断,否则出现问题时分不清是方向错误、数据错误还是时序错误。

6. 双核外设分配:不是所有外设都能随意共用

H747 的外设资源非常丰富,但这不代表可以把同一个外设同时给两个核用。设计双核项目时,外设要按归属划分清楚。

一个实用的划分原则是:每个外设只归属于一个核,由该核做初始化、中断处理和关闭操作。如果对方核需要使用该外设的数据,通过前面定义的核间通信通道转发,而不是直接操作外设寄存器。

为什么要这样做?举例说明:假设 USART1 的接收中断被配置给了 M7,但 M4 侧的代码误操作了 USART1 的中断使能位,两颗核的中断控制器同时响应同一外设中断,轻则造成重复处理,重则导致两个核互相踩寄存器配置,最终通信错乱。

从实际项目角度,建议按功能域划分外设:

外设类别建议归属典型功能
高级定时器M4电机 PWM、编码器接口
ADC 高速采样M4电流/电压采样
以太网 MACM7工业以太网协议
USB

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询