主控芯片既要跑复杂的图形界面或网络协议栈,又要维持电机电流环、编码器等强实时任务时,往往会出现中断延迟抖动、主循环被高优先级任务挤占、片上资源分配困难等问题。STM32H747 这类双核异构 MCU,正是解决这类矛盾的硬件基础,但上手后会发现:它的难点不在“频率高”,而在于两个内核如何启动、如何共用内存、如何同步外设权限。这篇文章将围绕 STM32H747 在实际嵌入式项目中容易踩坑的技术点展开,结合启动引导、通信机制、外设分配、调试排查等内容,整理成一套可直接指导开发的系统笔记。
1. STM32H747 双核融合的意义与应用场景
1.1 STM32H747 到底是一颗什么样的芯片
从名字上理解,STM32H747 是 STM32H7 系列中的一员,但它并不是一颗普通的 Cortex-M 微控制器,而是一颗双核异构 MCU。芯片内部集成了一个 Cortex-M7 内核和一个 Cortex-M4 内核,典型工作频率分别是 480MHz 和 240MHz,两个内核可以运行不同的程序,甚至在两个内核上分别运行不同的 RTOS。相比一颗高主频单核 MCU,这种设计的核心价值在于“物理隔离”:重量级任务和强实时任务不再抢同一个 CPU,而是各占一个物理内核。
很多人会把“双核”下意识理解为类似 PC 上“两个核心同时调度同一个操作系统”的 SMP 模型。实际上 STM32H747 更偏向 AMP(非对称多处理)架构,两个核心没有共享的操作系统实例,彼此之间需要通过约定的内存区域和事件通知机制协作。这种架构给嵌入式工程带来了更多可能性,也要求开发者具备更强的系统设计意识。
ST 在 H747 上提供了比较丰富的硬件资源,包括大容量 Flash 和 SRAM、LTDC 显示控制器、DMA2D 图形加速器、FMC 外部存储控制器等。但“算力翻倍”并不意味着项目开发难度减半,真正需要花心思的是两个核心之间的互斥保护、启动顺序、数据同步和固件更新策略。
1.2 为什么实际项目需要双核 MCU
过去在工业控制领域,特别是伺服驱动器、机器人控制器、高性能电源等场景中,通常会采用“DSP + MCU”或“MCU + FPGA”的方式:一颗芯片负责实时控制,另一颗芯片负责通信、监控、显示或上位机交互。这样的方案成熟可靠,但成本较高、硬件设计复杂、板级面积大,两个芯片之间的通信链路易受干扰。
STM32H747 的出现让设计者有机会在单颗 MCU 内完成这些分工。Cortex-M7 内核主频高,适合承担复杂运算、图形界面、文件系统、物联网协议栈;Cortex-M4 内核虽然主频低一些,但对大部分控制环路来说已经足够,而且可以从体系结构上避免被 M7 上复杂应用干扰。双核之间通过内部共享内存交换数据,不存在外置 SPI 或 UART 通信那样的低速瓶颈。
此外,H747 提供了相对完善的安全与低功耗特性,例如硬件加密、随机数发生器、独立看门狗、多种低功耗模式,使得它既可以做高性能计算平台,也可以在某些场景下通过关闭其中一颗核心来达到功耗控制目标。
1.3 适合用 STM32H747 的典型场景
综合工程经验来看,STM32H747 比较适合下列几类场景:一是工业控制器,M7 负责 Ethernet、USB、文件系统与人机交互,M4 负责高速电流环或伺服控制;二是带屏的物联网网关或数据采集设备,M7 运行 RTOS + GUI,M4 负责不依赖系统调度的固定周期采样;三是需要同时运行多种协议的通信设备,一个核心跑一种协议栈,可避免不同类型中断之间的激烈竞争;四是科研教学中的多核嵌入式系统课程,它很适合用来演示双核通信、缓存一致性、异构启动等技术原理。
需要提醒的是,使用 H747 之前要先评估项目是否真的需要双核。如果只是一个普通传感器采集任务,用一颗高性能单核 MCU 会更简单,也能避免双核调试带来的复杂度增加。
2. 开发环境准备与工程结构说明
2.1 工具链与开发板选型
开发 STM32H747,官方推荐使用 STM32CubeIDE、Keil MDK 或 IAR Embedded Workbench,三者都支持 Cortex-M7 与 Cortex-M4 双核调试。STM32CubeMX 负责图形化生成工程代码,可以配置时钟树、外设、DMA、FreeRTOS 和双核相关选项。需要注意版本问题:不同芯片批次、不同固件包版本生成的外设驱动和启动文件可能存在差异,建议始终根据你手上开发板对应的官方包版本生成工程,不要盲从网络博客中过时的配置。
硬件方面可以选用 ST 官方开发板,例如 NUCLEO-H747ZI 或 STM32H747 探索板。这类板子通常板载 ST-LINK 调试器,只需一根 USB 线就能下载和调试两个核心。如果你用的是自研硬件,至少要确保调试器支持 Cortex-M7 和 Cortex-M4,并且能识别芯片的两个内核。量产阶段通过 UART 或 USB DFU 下载固件时,也需要提前规划双核固件的合并和烧录方案。
2.2 理解双核工程的结构
双核工程与普通单核工程最大的不同是:一个完整应用需要编译出两个独立的二进制文件,分别对应 core 1 和 core 2 工程。通常 STM32CubeIDE 项目中会创建 CM7 和 CM4 两个子工程,CM7 对应主核,CM4 对应从核。
从软件职责看,CM4 工程更像一个独立的小型单片机项目,有自己的 main 函数、HAL 初始化、中断向量表和链接脚本。而 CM7 工程除了实现主核业务外,还要负责引导 CM4、检查从核固件状态、在必要时完成从核固件的二次加载。因此,在实际操作时,很多团队会先完成 CM4 工程,把它编译成 bin 或 hex,再放入 CM7 工程的 Flash 地址规划中;CM7 上电后先做自身初始化,再跳转到 CM4 固件所在地址。
这种主从式工程结构非常适合从零理解双核开发,因为所有协作逻辑都由开发者自己控制,不会像 SMP 那样由操作系统隐藏并行细节。缺点是需要开发者对内存布局有更高敏感度,稍不注意就会出现两个工程地址范围重叠、符号冲突、数据互相覆盖的问题。
2.3 建议的工程目录组织方式
我建议在项目最开始就使用清晰的目录分隔,避免把所有代码堆在一起。下面是一个相对合理的参考结构:
demo_h747/ ├── CM4/ │ ├── Core/ │ │ ├── Inc/ │ │ ├── Src/ │ │ └── Startup/ │ ├── link_script/ │ └── build/ ├── CM7/ │ ├── Core/ │ │ ├── Inc/ │ │ ├── Src/ │ │ └── Startup/ │ ├── link_script/ │ └── build/ ├── shared/ │ ├── include/ │ │ ├── shm_protocol.h │ │ └── board_resources.h │ └── src/ └── tools/ ├── merge_firmware.py └── flash_map.mdCM7 与 CM4 各自保留独立源码,共享部分放在shared目录中,例如两个核心都会引用的共享消息结构体、内存地址宏、外设资源编号等。tools目录用于存放固件合并、地址校验脚本,这对于后续工厂量产很有价值。
3. STM32H747 的架构与存储域拆解
3.1 双核心与内存访问的基本关系
分析双核 MCU,不能只看 CPU 指令集,还要看总线拓扑和内存归属。STM32H7 系列芯片把内部资源划分为多个域,这样做的目的是让不同外设和内存挂载到合适的总线上,兼顾访问速度与功耗控制。
STM32H747 上的两个核心并不对称。Cortex-M7 通常作为主核,对整个系统的 Flash、外部存储控制器、显示控制器等资源拥有更强的掌控能力;Cortex-M4 更像一个实时加速处理器,可以独立运行控制程序,也可以访问被明确允许的 RAM 区域和外设。由于处理器复位后往往只有一个核心从主 Flash 启动,另一个核心需要主核辅助引导,这一特点直接影响上电执行流程。
这里需要特别留意的是,M7 拥有紧耦合内存 TCM,它对 M7 访问延迟极低,许多开发者喜欢把中断服务函数或关键数据结构放进 TCM,但 TCM 通常不是全芯片共享资源。若 M4 也要访问同一份数据,就不能把数据放在 M7 私有内存区域里,而应放在两个 CPU 都能访问的 SRAM 上。这个区别看起来简单,实际项目里很容易被忽略,导致出现“M7 能读到数据、M4 读不到”的异常。
3.2 D1、D2、D3 域与低功耗管理
在 STM32H7 数据手册中,资源会按 D1、D2、D3 域进行描述。D1 域通常由 Cortex-M7 主处理器和其高带宽外设组成,D2 域可以理解为第二个处理器域,D3 域则带有更多低功耗相关逻辑。默认情况下,CPU 复位后并不是所有域都会被打开,开发者如果没有激活目标外设所在域的时钟和电源,即使代码看起来已经调用了外设初始化函数,外设也可能没有任何反应。
这意味着在双核项目中,初始化代码不再像单片 MCU 那样“一次全部打开”,而要有清晰的域管理概念。比如当你使用某个挂载在 D2 域的定时器时,M7 访问之前需要确认对应总线时钟已经被使能;当你想让 M4 把一个 DMA 搬运结果写到共享内存时,也需要保证共享内存所在域在低功耗模式下不会被意外断电。
从调试经验看,H7 系列外设“没反应”的问题,很多并不出在寄存器配置,而出在域时钟开关与复位状态上。因此遇到症状时,可以先回查当前外设属于哪个电源域,是否使能了对应域时钟,而不是反复修改寄存器值。
3.3 RAM 类型与共享内存的选择
STM32H747 内部有多个 SRAM 块,但它们的访问特性差别很大。有些 SRAM 挂在较高带宽的 AXI 总线上,适合 DMA、显示缓冲等大数据吞吐场景;有些 SRAM 与内核紧耦合,延迟低但容量较小;还有一些 SRAM 位于低功耗域,在系统休眠时需要特别处理。
在规划共享内存时,建议从两个维度筛选:第一,M7 与 M4 是否都能访问;第二,共享内存是否需要被 DMA 和外设访问。如果共享缓冲只是两个 CPU 之间传递控制参数,选择一块访问速度满足应用需求的 SRAM 即可;如果共享缓冲还要与 DMA 数据传输联动,就要考虑总线带宽、Cache 一致性、对齐要求等问题。
设计时还会遇到 Flash 分配问题。M7 与 M4 各自运行独立固件,通常把 M4 固件写在 Flash 的一个独立扇区或 Bank 中。启动时 M7 通过某种方式将 M4 复位向量加载到正确地址,使其开始执行。两个内核的 Flash 区域必须要隔离清楚,不能让 M4 程序区覆盖 M7 的程序区,也不能因为 Flash 擦写操作导致另一个内核正在执行的代码被破坏。
3.4 内存映射信息的获取方式
不同型号的 H7 芯片内部 SRAM 大小和映射并不完全一致,即使是同一系列的不同封装,外设数量和内存边界也可能不同。编写本文时无法给出一个适用于所有子型号的绝对地址表,因为硬件细节永远以官方数据手册和参考手册为准。
最稳妥的做法是打开你的 STM32 工程,在链接脚本或 memory 分区文件里查看实际使用的 RAM/Flash 地址范围,再结合数据手册中的 memory map 确认哪些区域允许两个 CPU 同时访问。很多工程师习惯把地址宏放到shared/include头文件中统一管理,比如M4_FW_START_ADDR、SHM_BUFFER_ADDR、SHM_BUFFER_SIZE,这样在 CM7 和 CM4 两个工程中引用同一套定义,能减少地址不一致导致的问题。
4. 双核启动流程与引导配置
4.1 为什么需要关心双核启动顺序
在单核单片机中,启动流程通常由复位向量、启动文件和 main 函数确定,开发者并不需要过多介入。但在 STM32H747 上,两个核心最终都要运行,系统必须明确谁先生成,谁后运行。
常见的启动策略是:M7 为主核,默认从 Flash 启动并执行主流程;M7 初始化自身系统时钟、电源和外设后,再检查 M4 固件是否存在于指定地址,如果存在,就设置好 M4 的起始地址并解除 M4 复位,使 M4 开始运行。这种方式便于实现主核统一管理、便于在工厂中把 M4 固件和 M7 固件合并成一个镜像。
另一种方式是配置选项字节,让系统复位后 M4 直接从某个 Flash 地址启动。这种方式在产线烧录和固件更新策略上会更复杂,因为两个核心都希望获得独立的启动控制权,容易出现“两核都尝试格式化 Flash”的冲突。多数开发板和示例工程默认采用 M7 引导 M4 的方式,对学习也更友好。
4.2 从向量表理解 M4 的启动入口
ARM Cortex-M 内核的启动机制比较简单:复位后内核会从向量表取出前两个 32 位数据,第一个作为主栈指针 MSP,第二个作为复位向量 Reset_Handler,然后跳转到该地址执行。M7 引导 M4 时,本质上也是利用这个机制。
假设 M4 固件被烧录在 Flash 的0x08100000地址开头,那么0x08100000处存放的是 M4 初始栈顶地址,0x08100004处存放的是 M4 复位函数地址。M7 程序只要读取这两个字,再配合芯片提供的从核复位控制,就能让 M4 像一个普通 Cortex-M 芯片那样启动。
有一类很容易出现的问题:M4 固件被放在一个错误的分区,或者 M4 固件的链接脚本没有把向量表放在区段起始位置,导致 M7 按约定地址读到的前两个数值并不正确。这时系统可能出现 M7 正常运行,但 M4 完全没有执行或者跑飞的现象。调试时建议查看 M4 固件的链接脚本,同时用调试器读取 Flash 起始地址中的内容,确认向量表是否真的是 M4 的向量表。
4.3 M7 跳转 M4 的代码示例
下面是一个简化的 M7 引导 M4 的代码流程,重点展示实现思路。实际项目中,应根据 STM32CubeMX 生成的双核引导代码替换其中的复位与跳转操作,不要直接把伪代码原样复制到没有适配的工程里。
/* 文件路径:CM7/Core/Src/m4_boot.c */ #define M4_FW_START_ADDR 0x08100000U /* M4 固件在 Flash 中的起始地址 */ typedef void (*Jump_Func)(void); void Jump_To_M4_Core(void) { uint32_t m4_sp; uint32_t m4_reset_pc; Jump_Func jump_to_m4; if (M4_FW_START_ADDR == 0U) { return; } /* 读取 M4 向量表前两个字 */ m4_sp = *(volatile uint32_t *)M4_FW_START_ADDR; m4_reset_pc = *(volatile uint32_t *)(M4_FW_START_ADDR + 4U); /* 简单有效性检查,防止地址异常造成 HardFault */ if ((m4_sp == 0U) || (m4_reset_pc == 0U)) { return; } jump_to_m4 = (Jump_Func)m4_reset_pc; /* 跳转前关闭全局中断,避免中断上下文错乱 */ __disable_irq(); /* * 在 CubeMX 生成的工程中,通常会有 CM4 核复位控制逻辑。 * 你需要先设置 CM4 的启动地址,再释放 CM4 的复位, * 也可以直接调用官方生成的 boot 函数。 * 此处仅保留跳转示意。 */ jump_to_m4(); /* 正常情况下不会回到这里 */ while (1U) { } }需要注意的是,实际 H7 的从核启动常常涉及芯片内部的复位控制寄存器,不能只做一个普通函数指针跳转。M7 必须确保 M4 的复位状态被正确释放,同时要让 M4 的异常向量表指向 M4 固件所在地址。如果使用 DSP 库、浮点运算单元或系统定时器,M4 启动代码也需要在进入用户 main 函数前完成相应初始化。
4.4 链接脚本与扇区规划
双核项目中对链接脚本的修改一定要谨慎。CM4 工程的 Flash 起始地址与长度必须与 CM7 工程中预留的 M4 区域一致,RAM 区域也不能与共享内存区域混淆。比如规定0x08100000到0x08140000属于 M4 固件区,那么 M4 链接脚本的FLASH起始地址应设置为0x08100000,长度不要跨越到别的分区。
在 CM7 工程中,还可以定义固定的符号来保存 M4 固件长度与校验值。量产阶段读取这些字段可判断 M4 固件是否被合法烧写,避免在空片或烧写中断状态下盲目跳转。使用内部 Flash 时还要特别注意,不要在 M4 正在运行时擦写 M4 所在扇区,否则 M4 会立即失去指令流,产生不可预知的故障。
5. 双核通信方案:从共享内存到 OpenAMP/RPMsg
5.1 双核之间需要交换哪些数据
确定双核通信方案前,先要梳理两个核心之间的数据流。工业控制项目中常见的通信内容包括:M7 向 M4 发送使能指令、转速目标、控制参数;M4 向 M7 反馈当前状态、故障码、采样数据;两个核心可能共用同一个传感器数据缓冲,也可能需要互相通知事件。不同类型的消息适合不同通信方式,并没有一种通用方案能覆盖所有场景。
如果数据量很小,频率很高,例如 10kHz 电流环的任务通知,比较复杂的大协议栈可能并不合适;如果 M7 需要把大批量文件或图像内容传给 M4,那么共享大缓冲区加 DMA 会更有效。比较好的设计思路是把“实时性要求高、数据长度固定”的部分设计成独立共享通道,把“异步、命令类”的消息走消息邮箱,这样既保留低延迟,也保证系统易维护。
5.2 共享内存的基本模型
共享内存是双核通信中最基础的手段。它本质上是在两个 CPU 都能访问的内存区域中划分一块缓冲区,定义清楚数据结构与读写状态。最简单的模型包括三个要素:数据区、状态标志、方向约定。
例如,可以向下面这样定义共享协议:
/* 文件路径:shared/include/shm_protocol.h */ #ifndef SHM_PROTOCOL_H #define SHM_PROTOCOL_H #include <stdint.h> #define SHM_MAGIC_WORD 0x48473437U /* "HG47" 的 ASCII 组合,用于校验 */ #define SHM_CMD_BUFFER_SIZE 16U typedef enum { SHM_STATE_IDLE = 0, SHM_STATE_M7_WRITE_DONE, SHM_STATE_M4_PROCESS_DONE } ShmState_t; typedef struct { uint32_t magic; /* 共享内存标识 */ volatile ShmState_t state; /* 状态标志 */ uint32_t cmd_id; /* 命令 ID */ uint32_t seq; /* 序号,用于丢包检测 */ uint32_t param[4]; /* 通用参数 */ uint8_t data[SHM_CMD_BUFFER_SIZE]; /* 自定义数据 */ } SharedCmd_t; #endifM7 侧写入数据并更新状态:
SharedCmd_t *shm_cmd = (SharedCmd_t *)SHM_BUFFER_ADDR; shm_cmd->cmd_id = CMD_START_MOTOR; shm_cmd->seq++; shm_cmd->param[0] = target_speed; shm_cmd->state = SHM_STATE_M7_WRITE_DONE;M4 侧轮询状态并对数据进行处理:
SharedCmd_t *shm_cmd = (SharedCmd_t *)SHM_BUFFER_ADDR; if (shm_cmd->state == SHM_STATE_M7_WRITE_DONE) { /* 解析命令并执行控制 */ execute_motor_command(shm_cmd->cmd_id, shm_cmd); /* 处理完成后,把状态切回 IDLE 或写完后状态 */ shm_cmd->state = SHM_STATE_M4_PROCESS_DONE; }这种“状态位 + 数据区 + 轮询”的方式非常直观,是所有共享内存通信的基础。但它也存在明显问题:如果 M7 正在写数据时 M4 突然读到半新半旧的数据,就会产生不一致。因此在复杂通信中,要引入锁、乒乓缓冲或硬件信号量来保护共享区。同时,如果 M7 和 M4 都启用了 Cache,还需要考虑缓存一致性问题,否则可能出现调试器里看到内存已更新、CPU 却仍然读到旧缓存的情况。
5.3 进入共享临界区的安全手段
跨核共享内存与单核中的线程安全有所不同。单核内可以通过关中断或者使用 RTOS 的互斥量来保护临界区,但在两个独立 CPU 之间,一个核关闭中断并不能阻止另一个核心访问共享区域。因此,STM32H7 提供了硬件信号量 HSEM 等机制,用于在两个内核之间建立互斥。
当需要修改共享结构体时,可以先获取硬件信号量,成功后再进行操作,操作完成后释放信号量。如果获取失败,说明另一个核心正在操作数据,当前核心可以等待或重试。使用 HSEM 能避免“两个核同时对同一个缓冲区执行写操作”的风险。不过 HSEM 的使用也有讲究,它只能保护软件设计中对共享资源的访问顺序,无法自动保证 DMA 与外设操作的一致性,开发者仍然需要设计合理的访问协议。
除了加锁机制,也可以采用“单写者”原则:某一段共享数据只允许 M7 写,M4 只读;另一段数据只允许 M4 写,M7 只读。单写者模型在大部分实时控制场景中足够用,而且能绕开很多棘手的并发问题。如果通信双方都需要修改同一个复杂结构体,才需要引入更严格的锁机制。
5.4 OpenAMP 与 RPMsg 通信框架
完全手动实现共享内存协议,优点是灵活、延迟低且可控,缺点是各类标志、锁、缓存维护逻辑需要自行验证,项目越大越容易出错。ST 官方在部分软件包中提供了 OpenAMP 支持,它能帮助用户建立基于 RPMsg 的消息通信通道。
RPMsg 是一种用于远程处理器消息传递的协议,常见于 AMP 多核系统。它构建在共享内存和硬件中断通知机制之上,提供相对抽象的发送和接收接口。调用方不用关心消息具体放在哪个内存地址,也不需要手动管理复杂的就绪标志,框架会维护通信通道。相比手写共享内存,OpenAMP/RPMsg 功能更完整,更适合传输不规则命令、配置包或日志。
但使用 OpenAMP 也需要付出系统资源代价:代码体积更大、初始化流程更复杂、内存占用更多。对于那些只需要传一个 32 位速度值的应用,未必比一个简单的共享内存变量更有优势。因此在工程上,常见做法是混合使用:底层实时控制数据用共享内存直接传递,上层复杂命令和固件管理消息走 OpenAMP/RPMsg。
下面的表格简单对比两个方案的特点:
| 维度 | 手写共享内存 | OpenAMP/RPMsg |
|---|---|---|
| 实时性 | 高,可由应用直接控制 | 中上,取决于配置 |
| 代码复杂度 | 低,适合小型数据交换 | 中高,需要中间件初始化 |
| 功能完整性 | 需自行实现锁和协议 | 提供标准化消息通道 |
| 调试难度 | 较高,需要自己排查同步问题 | 相对统一,但中间件内部逻辑复杂 |
| 适用场景 | 高频、固定长度控制数据 | 多变命令、配置、日志同步 |
6. M7 与 M4 的分工与外设分配策略
6.1 常见任务分配方式
理论上一颗芯片里的两个 CPU 可以执行任何代码,但实际项目应根据总线资源、外设归属和实时性需求设计分工。
一种典型方式是“应用核 + 控制核”。M7 运行 RTOS,承担 Ethernet、USB、LCD 显示、SD 卡文件系统等低速复杂业务;M4 单独运行一个带固定周期中断的控制代码,执行电流采样、编码器读取、PWM 更新。这样可以保证控制环路的执行时间非常稳定,不会被 M7 上的高延迟任务或频繁中断干扰。
另一种方式是“主协议核 + 采集核”。M7 负责运行主要通信协议和用户指令解析,M4 负责多路 ADC 采集、传感器故障判断和硬件保护。如果 M4 运行在较独立的环境中,即使 M7 因为网络异常出现任务堆积,仍然能保持系统安全监控。
还有一个常见的派生用法:一颗核负责高可靠性隔离,另一颗核负责需要频繁更新或第三方调试的功能。这种分配通常不是性能需求,而是工程管理需求。开发者可以把未稳定代码限制在一个核内,减少不稳定代码对另一个核安全逻辑的影响。
6.2 外设资源分配原则
STM32H747 上同一个外设通常只有一个寄存器实例,例如项目里只有一个指定 UART、一个指定 TIMx 等。两个 CPU 不能同时对一个外设寄存器执行配置,否则会导致前一核写入的值被后一核覆盖。因此,外设分配应当尽量做到“谁初始化、谁使用”,而不是“两边都能访问”。
例如 UART 打印日志,可以只分配给 M4 或 M7,用不同串口输出各自日志。如果串口资源紧张,可以在硬件上使用一个带 DMA 的缓冲结构,但必须保证 DMA 配置和缓冲区的消费者逻辑是清晰分层的。中断服务函数也不能随意同时挂在两个内核上,需要确定该中断到底由哪个 CPU 响应。
ST 芯片通常允许通过中断控制器或者相关配置,将外设中断路由到指定核心。分配外设时,建议建立一张“外设资源表”,把每个外设、归属核、主要用途、是否共享、是否启用中断等信息全部记录。这个表格可能不会直接写进代码,但它能有效阻止后期并行开发过程中两个工程师同时修改同一外设配置。
6.3 中断与实时性设计
双核系统中,中断延迟是实时性重要指标。M4 核控制环路如果希望获得确定中断响应,就要尽量减少 M4 上的其他高优先级中断,避免锁中断时间过长。M7 上可以比较宽容地运行 GUI 或网络栈,但仍要避免 M7 的误配置影响 M4。
两个 CPU 都有各自独立的总线和调试单元,但最终仍共享内存带宽和外设总线。当一个核频繁启动大量 DMA 搬运大块数据时,另一个核可能出现总线延迟或者共享内存竞争,这种“软影响”不容易从代码逻辑上发现。测量时会表现为控制环路偶尔多出几百纳秒到几微秒延迟,因此在高实时系统中需要合理分配 DMA 和内存访问优先级。
7. 实战参考:M7 做显示与指令管理,M4 做控制循环
7.1 项目需求与功能拆分
本节以一个简化的双核协作系统为例,说明如何把理论应用到工程结构中。假设项目需要实现一个带屏幕的电机控制器:屏幕上显示实时转速、电流和故障信息,用户通过按键或触控设定目标转速。M7 负责显示与用户交互,M4 负责读取编码器并更新 PWM 输出。
之所以这样拆分,是因为屏幕上如果直接由控制核驱动,LCD 刷屏和显示缓冲搬运可能对控制周期造成不确定性。而 M4 只负责窄而固定的控制流程,更容易通过逻辑分析仪观察实时性。
系统之间通过共享内存传递三种信息:命令信息,M7 发送“启动”“停止”“目标速度”给 M4;状态信息,M4 定时向 M7 回传“当前速度”“电流”“错误码”;握手信息,用于确认共享通道是否正常、固件版本是否匹配。
7.2 共享结构体设计
项目中,可以将共享结构体分为两个方向,尽量避免同时读写:
typedef struct { volatile uint32_t cmd_valid; /* 置 1 表示 M7 已发送新命令 */ uint32_t cmd_code; int32_t target_speed; uint32_t cmd_timeout_ms; } M7ToM4_t; typedef struct { volatile uint32_t ack_valid; /* 置 1 表示 M4