简介:面向STM32H745双核开发入门者,这份基于CubeMX 6.0与FreeRTOS的完整工程代码,配套《用cubemx6.0玩转NUCLEO-H745ZI开发板(一)》博文,能解决双核模板搭建、任务分配与启动流程梳理等常见困惑。包体约102.35MB,共1286个文件,以C源文件与头文件为主,另含ICF链接脚本、汇编启动文件、静态库与说明文档,目录清晰便于模块化检索。已有3132人学习下载,适合正在研究H7系列双核协同的开发者。解压后可直接查看双核初始化代码、FreeRTOS任务配置以及主从核启动逻辑,尤其适合对照源代码理解Cortex-M7与Cortex-M4核间通信机制;所附数学库还可支撑信号处理实验,配合原博文逐步实操,能更快掌握双核调度、调试技巧与编译排错思路,节省大量从零搭建工程的时间。 ST先别急着点 CubeMX 的生成按钮。STM32H745 这颗料,从拿到手到第一个双核 FreeRTOS 程序真正在板子上跑起来,中间隔着的不是“两个核”的距离,而是对“异构双核”这件事的理解距离。很多人把 H745 当成两颗独立的 Cortex-M 芯片封装在一个封装里,于是用单核的思维去配置,结果 CPU2 永远跑不起来,或者两个核各跑各的,共享数据乱成一锅粥。这篇就是一次性把 CubeMX 6.0 下 H745 双核 FreeRTOS 的入门路径捋清楚,包括启动顺序、RAM 分配、核间通信和必须避开的坑。
1. M7主内、M4主外:H745双核架构里必须先弄清楚的三个事实
1.1 两个核不是对等的:谁主频高,谁该干什么
H745 和 F7、H7 单核系列最大的不同,是它板载了两颗异构内核:一颗 Cortex-M7,最高主频 480MHz,另一颗 Cortex-M4,最高主频 240MHz。注意“异构”这个词。M7 和 M4 的指令集有交集但不完全兼容,M7 多了双精度浮点、更多的流水线级数和更强的总线吞吐;M4 虽然弱一些,但胜在低功耗和足够处理大多数外设类任务。
所以这颗芯片的设计意图从一开始就不是“两个核一起做同一件事”,而是“让 M7 干重活,让 M4 干杂活”。比较合理的分法是这样:M7 跑复杂的算法逻辑,比如音频编解码、图像预处理、电机控制环路的中心计算;M4 管外设交互,比如按键扫描、LED 指示、通讯帧的收发解析。千万不要把两个核放在同一个任务里去抢同一份算力,那样只会把缓存一致性和总线仲裁的问题全部引爆。
1.2 不是上电后两个核一起跑:CPU2 必须被 CPU1 释放
STM32H7 双核系列里,CPU1(M7)和 CPU2(M4)的复位行为是完全不同的。芯片上电后,默认只有 CPU1 从 0x08000000 开始取指运行,CPU2 处于复位保持状态,不会自动去取它的程序。CPU2 什么时候开始跑,取决于 CPU1 软件里是否清了 CPU2 的复位信号。
在 H745 的 RCC 控制器里,有一个专门的寄存器位控制 CPU2 的复位释放,STM32Cube HAL 里封装出来就是HAL_RCC_CPU2_RESET_RELEASE()。你在 CubeMX 里生成的 CPU1 工程,如果选了“双核”支持,代码里会有这行调用,但默认是被条件编译包裹的。很多人第一次跑 CPU1 程序,发现 CPU2 一点动静都没有,其实就是没执行这个释放动作。
还有一个坑:哪怕你释放了 CPU2 复位,CPU2 的程序也得先加载到它自己的 RAM 或 Flash 区域,并且把 PC 指针指过去。H745 里 CM4 的启动地址和 CM7 是分开映射的,CPU2 从 0x08000000 开始是读不到自己代码的。正确做法是:CPU1 的 linker 脚本里划出一块 RAM 放 CPU2 的镜像,或者直接把 CM4 的工程擦写到 0x08100000 的 Flash 分区,CPU1 把地址告诉 CPU2,再释放复位。这一整套逻辑,CubeMX 6.0 已经帮你把骨架搭好了,但你要理解它,否则 debug 阶段会一头雾水。
1.3 外设和内存不是天然共享的:谁访问什么,总线矩阵说了算
H745 的总线矩阵比单核 MCU 复杂得多。RAM 被分成 D1、D2、D3 三个域,每个域各有各的 SRAM 块,而且部分外设和 SRAM 默认只能被某一个 CPU 访问。
举个例子,D1 域的 AXI SRAM(0x24000000)是 M7 的主战场,M4 如果要访问它,必须通过总线矩阵的交叉访问路径,而这条路径是否开启、是否有延迟,取决于你在 CubeMX 里的总线配置。D2 域的 SRAM1/SRAM2/SRAM3(0x30000000 区段)才是两个核都能比较方便访问的共享区。
实际上,H745 双核协作的常规套路是:共享数据放在 D2 SRAM,M7 和 M4 都映射同一块物理地址,通过硬件信号量或者 Mailbox 做互斥和通知。这也决定了你在 CubeMX 里分配内存时,不能把所有 RAM 都一股脑配给 CPU1 工程,而是要把共享区单独划出来,两个工程链接到同一物理地址。
2. CubeMX 6.0里搭建双核FreeRTOS工程的完整动作
2.1 器件选型和工程创建的隐藏细节
CubeMX 6.0 对多核支持已经很成熟,但前提是你得选对芯片型号。打开 CubeMX,在 MCU Selector 里搜 STM32H745,注意系列里有单核型号和双核型号,比如 STM32H745AIH6、STM32H745ZIT6U 等,它们都是双核,而若是 STM32H750 这类成员则可能是单核,务必查看 datasheet 确认是 Dual Core。
选择型号后,正常新建工程。在 Project Manager 页面里,Toolchain / IDE 建议选择 STM32CubeIDE 或者 MDK-ARM,这两个工具对双核调试支持都比较好;如果你用 IAR,也可以,但要留意 IAR 的多核调试插件版本。
这里有个关键:CubeMX 6.0 在生成多核工程时,会自动生成CM7 和 CM4 两个独立子工程,每个子工程里会有自己独立的 main.c、FreeRTOS 配置和 linker 文件。不要试图在同一个工程里同时编译两个核,这是很多人刚上手时容易犯的错误——把两颗核的代码放在一个工程里,编译过了但是下载后完全不知道跑的是哪个核。
2.2 时钟树配置:两个核的主频要分开设
时钟树是 H745 双核工程里最容易出错也最好玩的地方。在 CubeMX 的 Clock Configuration 页面里,你会看到两个 CPU 时钟配置条目:CPU1 CLK和CPU2 CLK。
打开时钟树后,先把 PLL1 配置成 480MHz 供 CPU1 使用,再把 PLL2 配置成 240MHz 供 CPU2 使用,最后分别把RCC_HCLK1_CLOCKSOURCE和RCC_HCLK2_CLOCKSOURCE这类的时钟源选项指向对应的 PLL。
实测中有一个高频问题:如果你把 CPU1 和 CPU2 的时钟配置成同一个 PLL 输出,而两个核的主频需求不同,CubeMX 会直接报错或者生成出来的时钟参数不合法。所以强烈建议两个 PLL 分开,各自锁各自的频。
时钟树配置完,建议顺手看一眼RCC的D2CFGR寄存器配置界面,确认 D2 域(也就是 M4 所在域)的时钟已经使能。很多双核程序跑不起来,不是程序逻辑问题,而是 D2 域的时钟没开或者分频不对,M4 连指令都取不到。
2.3 FreeRTOS 在双核里的配置方式
CubeMX 里配置 FreeRTOS,路径在Middleware and Software Packs -> FREERTOS。这里有一个重要选项:Interface。下拉菜单里有CMSIS_V1和CMSIS_V2,建议直接选CMSIS_V2。V2 对 Cortex-M7 和 M4 的底层适配更完善,任务切换和中断处理都更贴合 ARMv7-M 架构,而且 CubeMX 6.0 生成的代码默认对 V2 的兼容性也更好。
然后勾选Enable FreeRTOS,在任务配置里添加你的任务函数。双核 FreeRTOS 的任务归属,CubeMX 里并没有一个直接“分配 CPU”的下拉框——任务层面的分配是在代码层面实现的,但 CubeMX 帮你把内核初始化代码和默认任务都铺好了。
H745 的双核 FreeRTOS 其实有两种理解方式:
- 独立双系统:两个核上各自跑一个独立的 FreeRTOS,M7 上跑一套调度器,M4 上跑另一套调度器,任务之间不共享调度信息。
- AMP(非对称多处理)模式:两个核上跑同一个 FreeRTOS 框架,但每个核执行不同的任务集。
CubeMX 默认生成的是第一种“双独立系统”,因为它最简单也最可控——两个核互不干扰,任务调度各自独立。等你把独立双系统跑通了,再考虑用 OpenAMP 做更复杂的 AMP 方案,大部分入门项目其实用不着。
2.4 双工程的内存与链接脚本规划
这是整个双核开发里技术含量最高的一步。CubeMX 生成的两个工程,各自带一份链接脚本,默认的 RAM 分配很保守——CM7 工程占用了大部分 D1 AXI SRAM,CM4 工程占用 D2 SRAM。
但双核协作的核心是共享内存,你需要手动留出一块 D2 SRAM 作为共享区,且这块内存必须在两个链接脚本里都出现。
在 CM7 工程的链接脚本(.ld 或 .sct)里,加入:
/* 在链接脚本中划分共享内存段 */ .shared_ram (NOLOAD) : { . = ALIGN(4); *(.shared_ram) *(.shared_ram.*) } > RAM_D2_SHARED然后在代码里把你定义的共享数据结构放到这个段里:
__attribute__((section(".shared_ram"))) volatile uint32_t shared_flag; __attribute__((section(".shared_ram"))) volatile uint16_t sensor_data[64];两个工程要链接到同一物理地址,这个地址就是你配置的 D2 SRAM 里某个固定地址。CM7 和 CM4 里的指针变量,只要都指向0x30040000这类在共享区内的地址,读写就是同一份数据。
3. 从Hello World到双核协作:HSEM、Mailbox与共享内存的选择
3.1 先想清楚:两个核之间为什么要通信机制
两个核本身是并行跑的,如果你只是想让它们各干各的互不打扰,那不需要任何通信机制,两边程序独立编译、独立运行就够了。但实际项目里几乎没有“互不打扰”这回事:M7 算出来的结果,总得让 M4 拿去控制外设;M4 采集到的数据,总得交给 M7 做决策——这就引入了双核通信。
H745 硬件上提供了三种典型的协作手段:HSEM(硬件信号量)、Mailbox和共享内存。这三种不是互斥的,实际工程里经常组合使用。理解它们各自的定位,比会调用 API 更重要。
3.2 HSEM:别让两个核同时碰同一片共享内存
HSEM 是 H7 系列双核专属的硬件信号量模块。它解决的核心问题很简单:当 M7 和 M4 都要修改同一个共享变量时,谁能保证不会互踩?
裸机程序里互斥通常靠关中断,但双核系统里,M7 关中断管不住 M4,M4 关中断也管不住 M7。所以需要硬件层面的“锁”——HSEM 就是这层锁。它的用法很简单,CubeMX 配置好 HSEM 外设后,代码里直接用 HAL 库:
if (HAL_HSEM_FastTake(HSEM_ID_0) == HAL_OK) { // 只有一个核能走到这里,另一个核会在这里等待或重试 shared_flag = 1; HAL_HSEM_Release(HSEM_ID_0, 0); }注意到HSEM_ID_0是信号量编号,H745 的 HSEM 有 32 个信号量,你可以给不同的共享资源分配不同的信号量。入门的经验是:宁可多分配几个 HSEM,也不要两个共享资源共用一个信号量,否则会引发不必要的阻塞等待。
3.3 Mailbox:用来通知,不负责搬运数据
Mailbox 在 STM32H7 上是一个硬件外设,它的工作方式跟邮箱差不多:你可以往里面写入一个最长 8 字节的短消息,对方读出来后触发中断。它和 HSEM 的本质区别是,Mailbox 偏“通知”,HSEM 偏“互斥”。M7 算完一帧数据,往 Mailbox 里塞一个“数据就绪”的消息,M4 收到中断后去共享内存里取数据,路径就是这样。
CubeMX 里配置 Mailbox 后,初始化流程一般会生成两个核各自的初始化和中断处理代码。实际使用中,M7 侧的发送函数大致是:
// CPU1 侧发送一条短消息到 CPU2 HAL_MAILBOX_Send(&hmailbox, MAILBOX_ID_0, (uint32_t*)&msg, MAILBOX_TIMEOUT_MS);M4 侧则在 Mailbox 中断回调函数里处理收到的消息。要注意的是 Mailbox 消息长度极短,适合发状态标志和命令编码,不适合发大数据包。数据本身放共享内存,Mailbox 只负责“让你知道数据来了”。
3.4 选型建议:消息小用 Mailbox,数据大用共享内存加 HSEM
这是我在几个实际项目里沉淀下来的选择逻辑,给你做参考:
| 通信场景 | 推荐方案 | 原因 |
|---|---|---|
| 偶尔发送控制命令(开机、停止、切换模式) | Mailbox | 短消息、低延迟、硬件中断唤醒、无需轮询 |
| 周期性传输传感器数据(几十到几百字节) | 共享内存 + HSEM | 传输效率高,且 HSEM 保证互斥,避免双写冲突 |
| 大数据块(图像帧、波形数据) | D2 SRAM 共享区 + HSEM + Mailbox 通知 | 数据放共享内存,Mailbox 做“数据就绪”通知,HSEM 保证读写期间的独占权 |
如果你的项目只是入门学习,先用 Mailbox 互相发几个“hello”,再用共享内存同步几个变量,跑通之后你对双核通信的体感就完全不一样了。
4. 双核FreeRTOS跑通之后,我踩过的那些坑
4.1 坑一:CPU2 程序烧进去了,为什么跑不起来?
这是绝大多数人遇到的第一个问题。现象是:CPU1 的 FreeRTOS 任务跑得好好的,打印信息都正常,但 CPU2 的代码就是不执行。查了半天,发现 CPU2 那边的程序确实烧进去了,Flash 里的数据也对着,可就是 CPU2 不干活。
**根因在启动顺序。**CPU2 的复位释放必须由 CPU1 执行,而且释放前要保证 CPU2 的代码已经加载到它自己的执行地址(无论是 Flash 还是 RAM)。如果你在 CPU1 的 main() 里没有调用HAL_RCC_CPU2_RESET_RELEASE(),那一切都是白搭。CubeMX 生成的代码里,这个释放动作通常在SystemClock_Config()之后、main()主循环之前,由条件编译#ifdef CORE_CM4包裹着。
另外还有一种情况:你在调试器里单独给 CPU2 烧录程序时,用的是 CM4 工程的 elf,调试器把 PC 设到了 CM4 的复位向量,这个没问题;但当 CPU2 由 CPU1 释放复位时,CPU2 会读取它自己的向量表,如果向量表指向的地址上没有正确的栈顶和复位向量,CPU2 会直接 HardFault。
我的排查手段很简单:在 CPU2 工程的main()最前面把一个 GPIO 拉高,然后用示波器看这个 GPIO 有没有电平变化。没有变化就说明 CPU2 压根没进 main(),问题大概率在启动和复位;有变化说明任务调度或外设初始化有问题。把启动问题和代码逻辑问题先分开,排查效率会高很多。
4.2 坑二:M7 开了 D-Cache 之后,共享内存的数据全是“旧的”
H745 的 M7 内核有 D-Cache 和 I-Cache,M4 没有。这本来没什么,但当你用共享内存做双核通信时,Cache 一致性问题会立刻浮出水面。
M7 写共享内存时,数据先写进 D-Cache,而不会立刻写回物理 RAM;M4 读共享内存时,读到的是物理 RAM 里的旧数据。反过来,M4 往共享内存写数据,M7 读的时候可能读的是 D-Cache 里缓存的旧副本。
解决思路有两个:
- 最简单:直接把共享内存区域的 Cache 关闭。在 MPU 配置里,把共享内存区域设置为
Device或Non-cacheable。CubeMX 里有 MPU 的图形化配置界面,在System Core -> MPU里新增一个 region,覆盖你的共享内存地址范围,配置为Normal memory, Non-cacheable。这种方式不影响性能(毕竟共享区数据量不会太大),而且彻底避免脏数据。 - 进阶做法:开启 Cache,但每次读写共享内存前手动 Clean/Invalidate。即写之前
SCB_CleanDCache(),读之前SCB_InvalidateDCache()。这个做法能保留 Cache 的整体性能,但很容易漏调用,一旦漏了,数据错误是随机复现且极难定位的。
我给你的建议是:入门阶段直接用 MPU 把共享区设置成 Non-cacheable,先把通信逻辑跑通,不要急着一上来就折腾 Cache 优化。
4.3 坑三:两个核的中断优先级设置不一致,FreeRTOS 同步直接失效
FreeRTOS 在 Cortex-M 上依赖 SysTick 和 PendSV 来实现任务调度,而 SysTick 的优先级必须设置为 FreeRTOS 允许的最大优先级(configMAX_SYSCALL_INTERRUPT_PRIORITY对应的值)。这个东西在单核里很简单,但双核系统里容易出问题的地方是:CPU1 和 CPU2 的中断优先级分组(Priority Group)必须保持一致。
如果 CPU1 配置成中断优先级分组为 4(4 位抢占优先级),而 CPU2 分组为 0(0 位抢占优先级),那 SysTick 在两边的实际优先级含义就完全不同,FreeRTOS 的时间片滚动和任务切换会出现诡异的偏差——抓逻辑分析仪能看到 M4 侧任务切换周期忽快忽慢。
解决办法就是在两个工程的SystemClock_Config()或HAL_Init()里统一设置中断优先级分组,例如都用HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)。这个看起来很小的配置,是双核实时系统稳定的关键之一。
4.4 坑四:FreeRTOS 的堆栈溢出检测,双核下要分别开
FreeRTOS 的堆栈溢出检测在单核里是一个 debug 利器,在双核里你记得在两个核的 FreeRTOSConfig.h 里都打开。你的 CM4 工程和 CM7 工程是独立的两个固件,各自维护自己的 FreeRTOS 内核,configCHECK_FOR_STACK_OVERFLOW必须分别设为 1 或 2。
我碰到过一次奇怪的现象:M7 侧的任务一直正常,M4 侧偶尔跑十几分钟就 HardFault。打开 M4 侧的堆栈溢出检测后,用了不到十分钟就在vApplicationStackOverflowHook()里抓到了罪魁祸首——一个在 M4 侧声明的大局部数组,把任务栈直接干穿了。定位速度比盲猜快了一个数量级。
OpenOCD 或 ST-LINK 调试时,建议同时连接两个核的调试会话,观察两边任务的栈高水位线。CubeIDE 里支持同时调试两个工程,创建 debug configuration 时,把 CM7 和 CM4 的调试启动都添加到同一个 session 里(Debug Configurations -> 多工程调试),这样可以同时看两个核的线程栈、变量和断点。
5. 我可以直接抄的:一个最小双核 FreeRTOS 实验的完整动作序列
第一步,CubeMX 新建 STM32H745 工程,时钟树配好 PLL1 和 PLL2,分别给 CPU1 480MHz、CPU2 240MHz。
第二步,两个核都开启 FreeRTOS(CMSIS_V2),各创建一个简单的任务,比如 CM7 上建一个Task_Print_M7,CM4 上建一个Task_Print_M4,任务体里就是延时翻转 GPIO,或者向串口打印不同的字符。
第三步,把共享内存区划到 D2 SRAM,比如地址0x30040000,配置 MPU 为 Non-cacheable,在两边代码里声明同一个volatile结构体指针,指向这块地址。
第四步,配置一个 Mailbox(CubeMX 里 Mailbox 外设配置界面很直观,两边选好核归属),通信测试逻辑:M7 每秒通过 Mailbox 发一条命令,M4 收命令后往共享内存写一个递增计数,M7 再把这个计数打印出来。
第五步,编译 CM4 工程 → 编译 CM7 工程 → 先把 CM4 的固件烧录到其执行地址(如果放到 RAM 可以省去,如果放 Flash 就按 0x08100000 规划) → 再烧 CM7 的固件 → 复位运行。看到两边串口都在按预期打印,且共享计数稳定递增,你的 STM32H745 双核 FreeRTOS 入门程序就算真正跑通了。
这个最小实验看起来简单,但它把你带过了所有关键的门槛:双核启动顺序、时钟域、内存规划、FreeRTOS 双独立调度、Mailbox 通知、共享内存和 Cache 策略。后续你再接外部传感器、通信协议栈、电机控制,都是在这个骨架上做加法。
我最初因为没搞懂 CPU2 复位释放和 D-Cache 一致性这两个点,整整折腾了两天多,代码每次跑起来都像是随机出差错。现在把这些经验整理出来,你照着做,应该能在一晚上以内把第一版双核 Hello World 调通。如果中途卡住,别急着怀疑硬件,拿示波器量一量 GPIO,打开调试器同时看两个核的 PC 指针,你会很快找到问题所在。
本文还有配套的精品资源,点击获取