1. 双核 MCU 的架构逻辑:性能与安全为什么能“兼得”
这几年做嵌入式项目,明显感觉到双核 MCU 已经不是“旗舰专属”了。随手翻一下主流厂商的产品线,意法半导体的 STM32H7 系列里带双核的型号、恩智浦的 i.MX RT1170、瑞萨的 RA6M5 / RZ 系列,还有 Silicon Labs 带 Secure Vault 的双核无线 SoC,几乎都在强调同一件事:Dual-Core MCUs Blend High Performance and Enhanced Security。也就是说,双核不再是单纯堆算力,而是把“跑得快”和“防得住”放到同一个芯片里解决。
这里说的“高性能”和“增强安全”,并不是简单地一个核干活、另一个核闲着。对真正做产品的人来说,双核意味着你可以用架构手段同时满足实时控制、复杂通信、安全启动、安全通信这几类互相打架的需求。如果你正在纠结“一个高性能单核跑 Linux + 实时扩展”还是“两颗 MCU 分开管”,那么双核 MCU 很可能是一个更优的中间态。这篇文章我会从架构、软件分工、安全设计、实际落地和调试经验几个层面,把这东西讲透。
1.1 对称多核与异构多核的根本区别
双核 MCU 和多核应用处理器有个关键差异:大多数双核 MCU 走的是异构多核(AMP,Asymmetric Multi-Processing)路线,而不是我们平时听说的对称多核(SMP)。
SMP 的意思是两个一模一样的核心,共同共享一份内存和一套操作系统调度器。Linux 的 SMP 就是这种,多核之间通过调度器自动分配任务,代码写起来相对“无感”。但 MCU 领域的双核,尤其强调异构,比如 Cortex-M7 配 Cortex-M4,或者 Cortex-M33 配 Cortex-M55。这类芯片两个核心的主频可能一样,但性能差异明显,例如 M7 带缓存和双发射,M4 则更省电、更可预测。它们在硬件上就不平衡,所以软件层面也做不到“随便哪个核跑都行”。
AMP 架构下,每个核心运行独立的执行环境,可以跑不同的 RTOS、甚至一个裸机一个 RTOS。两个核心之间通过硬件邮箱(Mailbox)、共享内存、中断来通信。这种设计的好处在于:核心之间天然隔离,一个核崩了或者被攻击,另一个核还能继续工作。这和安全设计的目标高度重合。
1.2 “性能核”与“安全核”的分工模式
双核 MCU 上最常见的分工,是“性能核跑业务,安全核跑安全”。用一颗大核处理以太网协议栈、GUI、复杂算法或者电机控制环路,另一颗小核专门负责安全启动校验、密钥管理、安全协议(比如 TLS 握手运算)、安全存储等。
这种做法在行业里有个很形象的名字,叫“安全岛”(Secure Island)。性能核即使被恶意数据打穿,攻击者拿到的也只是这个核的内存空间。密钥、证书、安全存储区域如果放在另一个核对用的安全内存里,并且在硬件层面做了访问权限隔离,那么攻击者就没法顺着总线直接偷到密钥。再加上安全核往往配有独立的硬件加解密引擎和真随机数发生器(TRNG),TLS 握手开销也不会拖累业务核。
这样的分工不是拍脑袋想出来的,而是安全需求层层倒逼的结果。比如你要做一款支持 MQTT/TLS 的工业网关,业务核如果要同时跑 Modbus 主站、边缘协议转换和 TLS,负担会非常重。而把 TLS 记录层处理放在安全核,让业务核通过 IPC 调用安全服务,整个系统的实时抖动反而更低,安全边界也更清晰。
1.3 选型时先看内核安全扩展
双核 MCU 不是只要有两个核就够了,安全增强很大程度上取决于内核本身支持哪些安全扩展。
Cortex-M33 和 Cortex-M55 的核心自带 TrustZone-M 扩展,可以在硬件层面把内存、外设、中断划分为安全和非安全两个世界。Cortex-M7 和 M4 则没有 TrustZone,只能靠 MPU 做相对粗粒度的保护。RISC-V 双核方案则要看是否带有 PMP(Physical Memory Protection)和校验扩展,比如某些厂家采用一个带 P 扩展的高性能核 + 一个带有安全特性的小核。
选型时建议先把“安全需求等级”定下来:如果只是防误操作、防程序跑飞,MPU 级别就够了;如果产品需要合规,比如 PSA Certified Level 2 或更高,那就尽量选带 TrustZone 或者独立安全子系统的芯片。不要只看主频和 Flash,安全特性往往是双核 MCU 拉开产品档次的地方。
2. 把双核性能真正榨干的软件设计
双核 MCU 的软件架构和单核完全不是一个套路。很多人拿到双核芯片,第一反应是“两个核跑两个点灯程序”,然后发现根本没有发挥出双核的价值。真正有用的做法,是从系统工程角度重新规划任务归属和核间通信。
2.1 任务划分的原则:别把双核当单核用
我在做项目时,任务划分会按照三个维度来评估:实时性要求、计算密度、安全等级。
实时性要求高的任务,比如电流环、编码器采样、PWM 输出,应该放在延迟可预测的核上,最好代码路径短、无动态内存分配、没有不可屏蔽中断长时间占用。计算密度高的任务,比如图像处理、FFT、加密算法,放在带 DSP 扩展或主频更高、带缓存的大核。安全等级高的任务,比如固件验证、密钥派生,放到安全核或安全世界中单独处理。
举个例子:一个双轴伺服控制器使用 M7 + M4 架构,我把两个电流环都放在 M4 上,运行 16kHz 控制频率,代码量不大但周期极短;M7 则跑 EtherCAT 从站协议栈、状态机和上位机命令解析。有人会问,M7 明明更快,为什么不用 M7 跑控制环?因为控制环对确定性要求极高,M4 没有复杂缓存,Cache miss 引入的抖动远比 M7 小。这是嵌入式里“更快不等于更合适”的典型场景。
2.2 IPC 与共享内存:双核通信的“高速公路”
两个核之间通信,最底层的方式是硬件 Mailbox 加中断。Mailbox 本身只能传一个很短的寄存器值,一般用来发事件通知。真正传数据,要走共享内存。
共享内存的设计有几个关键点:
第一,数据 buffer 通常用无锁环形队列(Ring Buffer),避免两个核心使用同一个锁,那个锁放哪里都会成为瓶颈和死锁来源。环形队列的读写指针用原子操作更新,配合内存屏障,基本可以做到无锁。
第二,要小心缓存一致性。M7 这类带缓存的大核,在访问共享内存时默认行为可能造成数据不同步。常见做法是,把共享内存区域配置成非缓存(Normal Non-cacheable),或者使用硬件一致性接口。最稳妥的搞法是查参考手册,单独为共享内存区建立 MPU 或 TrustZone 配置,并明确使用 non-cacheable 属性。否则就会出现“A 核写了一个值,B 核隔了好久才看到”这种极其难查的问题。
第三,通信协议不要设计得太复杂。我一般会在共享内存头部放一个 version 字段、一组数据长度和序列号,然后紧跟有效载荷。B 核收到 Mailbox 中断后,先读取序列号,校验长度,再处理数据。这样即使 A 核更新频率很快,B 核也能通过丢帧策略主动丢弃旧数据,保证实时性。
2.3 双核启动与同步
双核 MCU 的启动流程和单核很不一样。大多数芯片上电后,只有一个核心从 BootROM 开始运行,另一个核心则处于复位状态,或是在等待被释放。比如 STM32H747 内部默认是 M7 上电运行,M4 保持复位;i.MX RT1170 则可以根据启动配置选择哪个核作为主核。
软件上要遵循“主核先起,从核后起”的顺序。主核需要完成全局时钟、电源域、DDR/外部存储器初始化,把从核的固件从 Flash 加载到指定的 RAM 地址,然后释放从核的复位,并从核的入口地址编译时需要固定到对应地址。
两个核心开始运行后,要定义一套“握手协议”。最常见的方式是:从核初始化完自己的外设后,向共享内存中写入版本号,然后给主核发一个 Mailbox 中断;主核收到中断后,再把自己的状态同步过去。不要一上来就交互数据,先确认两个核心的时钟、外设都稳定,否则后面出问题很难排查。
2.4 性能调优:负载均衡不等于平均分
不少人在双核上做负载均衡,以为两核跑 50% + 50% 就是完美。实际经验是,稳态的系统更怕的是瞬态过载。我一般会保证主核的最高负载不超过 70%,从核不超过 80%,剩下一点余量用来处理突发任务和安全服务调用。如果某个核长期跑满,考虑把一部分非实时任务重新分配,或者用消息队列缓存起来,而不是硬塞给另一个核。
另外,中断分配要格外重视。两个核虽然可以各自接收到外设中断,但每个外设的中断只能配置到某一个核。设计时要先画一张表格,把外设、中断、DMA、所在核全部列清楚。如果中断目标核配置错了,轻则性能下降,重则因两个核同时访问同一外设而冲突。这个表在调试时也会非常有价值。
3. 增强安全不是“加壳”,而是系统级设计
很多人理解的“安全”是最后给代码加个加密算法,或者固件里加个密码校验。真正的产品级安全,是从信任根、启动、运行、更新、存储全链路设计出来的。双核 MCU 给了你一个难得的硬件基础,但如果你不会用,等于白买。
3.1 信任根与安全启动
安全启动是系统安全的第一道门。它的目标很简单:确保芯片上跑的固件是开发者签名发布的版本,不是被篡改的版本。
双核场景下,启动链通常比单核多一层。拿 M7 + M4 的组合举例,流程是:
BootROM 加载并验证一级 Bootloader 的签名;一级 Bootloader 验证 M7 应用固件;M7 应用固件去加载 M4 固件前,仍然需要先验证 M4 固件的签名。注意,这一步不能省,不能让 M7 直接读取未经验证的 M4 固件到共享内存。否则攻击者可以通过更新 M4 固件绕过整个安全体系。
签名验证一般使用非对称算法,比如 ECDSA P-256 或 RSA-2048。公钥放在 OTP(一次性可编程)区域或者带读保护的安全 Flash 中,硬件上禁止调试接口读出。私钥永远只存在于构建服务器上,不能出现在工程师本地电脑里。
这里的核心是建立“信任链”:从芯片 BootROM 这个不可变信任根开始,每一级都验证下一级,任何一环断开,系统都不进入正常执行。
3.2 安全分区与权限隔离
安全启动保证“进来的代码是可信的”,但运行时还要防止恶意数据或者内存越界破坏关键数据。双核 MCU 提供了两种隔离手段:MPU 和 TrustZone。
MPU 是每个 Cortex-M 核心都有的内存保护单元,可以按区域配置访问权限。MPU 能挡住普通软件 bug 和无意的非法访问,但它的配置是由 CPU 自己管理的,CPU 跑飞后 MPU 也形同虚设。TrustZone 则是硬件强制的隔离,安全世界和非安全世界的总线事务被物理隔离,非安全代码根本没法访问安全内存和设备。
如果你的双核芯片支持 TrustZone,我强烈建议把安全核本身放在安全世界,把业务核放在非安全世界。再配合“安全内存区域只在安全世界可见”的配置,业务核即使被攻破,也拿不到安全密钥。如果你用的是不带 TrustZone 的双核 M7/M4,至少要保证安全核的私有内存、密钥区通过 MPU 配置成特权模式访问,同时关闭调试口的非法访问。
3.3 硬件加密引擎与密钥存储
双核 MCU 上几乎所有安全通信都要用到硬件加密引擎,比如 AES、SHA、RSA/ECC、真随机数发生器(TRNG)。这些外设的使用要注意一点:优先把它们分配给安全核,或者通过 TrustZone 配置成安全外设。业务核需要加密服务时,不直接操作硬件加密寄存器,而是调安全核提供的 API,通过 IPC 把明文发过去,再拿回密文。
这样设计的好处是,业务核即使被恶意代码控制,也没法直接读取硬件加密引擎里的密钥。密钥存储在安全核私有的 Key Storage 区域,通常是 OTP 里的自定义字段,或者 Secure Flash 区域。每次上电,用设备唯一密钥对密钥进行解密,放在安全 RAM 中。全程不让业务核接触。
在 TLS 连接场景,我看到很多产品直接把证书私钥放在主控的外部 Flash 里。这在双核安全架构里一定是反面教材。正确做法:私钥存放在安全存储区域,TLS 握手时由安全核完成签名/解密操作,或者使用硬件 crypto 库调用完成。如果芯片支持,把密钥放在 Security Subsystem 中,连安全核 CPU 都读不到,只能通过硬件调用完成加解密。
3.4 安全固件更新
“产品能升级”是一件既必需又危险的事。缺少安全机制的升级通道,等于给攻击者开了一个后门。
双核设备升级时,要注意两个核的固件必须作为一个整体处理,不能出现“主核升级了新版本,从核还跑着旧版本”的不一致状态。我一般会把两个固件打包成一个升级镜像,包含版本号、哈希列表、签名。升级前整体校验,再分别写入两个核的备份区。
回滚保护也很重要。如果新版本被发现有安全漏洞,攻击者可能试图往旧版本回滚,因为旧版本可能存在已知漏洞。解决方式是在 OTP 中记录“最低可接受版本号”,Bootloader 启动时如果发现镜像版本小于这个值,就拒绝执行。
3.5 运行时防护:双核互为看门狗
安全不仅是“启动时安全”,还要在运行时保持。双核架构有一个有价值的安全技巧:让两个核互相监控。
比如安全核可以周期性给业务核发 Ping 指令,业务核收到后返回一个递增序列号。安全核如果连续 N 次没有收到正确回复,就认为业务核已经跑飞,主动触发复位或切换到安全状态。反过来,业务核也可以监控安全核的状态,避免安全核因为死锁导致整个安全服务停摆。
这个机制比单纯用硬件看门狗更灵活,因为看门狗只能检测“有没有喂狗”,无法确认“程序逻辑是否正常”。双核互检可以在应用层做到逻辑级别的监控。我甚至在一个安全要求很高的项目里,让安全核定期检查业务核内存里几个关键变量的 CRC,一旦发现异常,直接进入安全停机流程。
4. 一个真实项目:双核工业控制 + 安全通信模块的落地复盘
这一章拿我最近做的一个项目来复盘。需求是做一个工业边缘控制器,既要支持本地高速 IO 控制和 PLC 协议,又要支持带 TLS 的 MQTT 通信上报数据到云端。控制器还要求支持远程固件升级,不能让调试接口暴露密钥。整体选型最终落在了带 TrustZone 的双核 MCU 上。
4.1 需求与选型:为什么是双核而不是“外挂安全芯片”
刚开始方案评审时,有两个备选:一是单核 MCU + 外部安全芯片,二是双核 MCU 一个核跑业务、一个核做安全。
外部安全芯片的优势是安全认证成熟,比如 ATECC608A,但问题是它和业务核之间走 I2C 通信,传输速率有限。TLS 握手需要频繁使用安全芯片做签名,成了性能瓶颈。双核 MCU 内部安全核和业务核走共享内存加 Mailbox,带宽高几个数量级,而且省掉一路 I2C 和一颗芯片的 BOM 成本。
最终选的型号是带有 Cortex-M33 和 Cortex-M55 双核的 MCU,M33 支持 TrustZone,作为安全核;M55 主频更高,带 Helium DSP 扩展,作为业务核。两颗核都有 512KB 以上的本地 SRAM,共享一段 64KB 的 I/D RAM 作为共享通信区。
4.2 软件架构搭建步骤
整个软件架构分四层:
硬件层:双核、内存映射、Mailbox、硬件加密引擎、TrustZone 配置。
基础层:两个核各自的 SDK 和 RTOS。M55 跑 ThreadX,M33 跑裸机 + 安全服务框架,因为安全核的任务相对简单,裸机能最大程度减少运行时代码,也让安全审计更简单。
服务层:定义了一套 IPC 接口,包括安全启动检查、随机数获取、TLS 握手、固件校验等。
应用层:M55 上跑 Modbus TCP 从站、边缘数据处理、MQTT 客户端。M33 上跑证书管理、TLS 记录层处理、安全升级校验。
搭建顺序建议从下往上:
先配置时钟和内存映射,确保两核能独立启动;然后实现 Mailbox 驱动和共享内存区,跑通最简单的“ping-pong”帧;再实现 IPC 服务调用框架;接着移植业务核上的 RTOS,把应用模块挂上去;最后一步步把安全服务从业务核抽到安全核。
不要一上来就迁 TLS,先把 IPC 和基础服务跑稳,不然出问题都不确定是通信问题还是安全逻辑问题。
4.3 共享内存和 Mailbox 的核心实现
共享内存采用无锁环形队列,队列长度 64 项,每项最大 256 字节。
#define SHM_BUF_SIZE 256 #define SHM_QUEUE_SIZE 64 typedef struct { uint32_t magic; uint32_t seq; uint32_t length; uint8_t payload[SHM_BUF_SIZE]; } shm_message_t; typedef struct { volatile uint32_t head; volatile uint32_t tail; shm_message_t msg[SHM_QUEUE_SIZE]; } shm_ring_t;M55 写入时,只修改 head;M33 读取时,只修改 tail。内存区域在两边都配置成 non-cacheable,避免缓存一致性问题。
Mailbox 只用于发通知:
void m55_send_to_secure(mailbox_regs_t *mb, uint32_t data) { while (mb->SR & MAILBOX_SR_FULL); mb->DR = data; }在 M33 侧收到 Mailbox 中断后,就立即从共享内存队列里取一条消息,解析消息类型,执行对应的安全服务。整个过程确定性强,毫秒级完成。
4.4 参数计算与资源分配
双核项目的关键参数要提前算清楚。我一般会列一张资源分配表:
| 资源 | 分配位置 | 大小 | 说明 |
|---|---|---|---|
| M55 应用固件 | Flash 0x08000000 | 512KB | Modbus、MQTT 相关代码 |
| M33 安全固件 | Flash 0x08100000 | 128KB | 安全服务、密钥处理 |
| 共享消息队列 | AHB SRAM | 64KB | 两个核都能访问 |
| 安全存储区 | OTP + Flash | 32KB | 存放公钥、设备证书 |
任务周期和 IPC 负载也需要估算。假设业务核每秒需要建立一次新的 MQTT 连接,每次 TLS 握手需要安全核执行约 20 次 RSA 操作,每次操作平均 5ms,那么安全核每秒需要投入 100ms 处理握手,负载大约 10%。平时非握手阶段,安全核负载低于 2%。这个余量完全够用。
共享内存大小要按峰值消息率计算。假如业务核最高每秒发送 2000 条控制消息,每条平均 128 字节,那每秒要传输 256KB。64KB 环形队列能缓存约 500 条消息,足够应对瞬态突发。如果估算发现排队延迟超过任务周期,就要考虑增大队列或降低消息频率,而不是盲目提高传输带宽。
4.5 实测结果
实测下来,业务核 M55 跑 Modbus TCP 和 MQTT 的同时,CPU 负载大约 45% 左右。安全核 M33 除了处理 TLS 握手,还要做固件校验,平均负载约 20%。整个系统相比之前单核方案,吞吐量提升了接近一倍,而且 TLS 握手时间因为不再被业务任务抢占,反而更稳定了。
安全验证方面,我们模拟了几种攻击场景:通过调试口直接读取 Flash、篡改应用固件、在 M55 上注入恶意代码尝试访问 M33 私有内存,均无法成功。这也验证了双核 + TrustZone 的架构确实能挡住常见物理和远程攻击路径。
5. 双核调试与踩坑实录
双核项目的开发难度,很大一部分在调试。两个核同时跑,日志乱、断点乱、数据乱,一不留神就是几天查不出原因。下面这些坑,我基本都踩过。
5.1 双核 Debug 的常见坑
第一个坑是日志输出。两个核共用同一个 UART 打印日志,不加保护时会出现字符交错。处理方法是在日志驱动里加互斥锁,或者给两个核分配不同 UART,再或者在日志帧里加入核 ID 和时间戳,便于事后分析。
第二个坑是断点。你用 IDE 连上双核调试器,默认可能只停在主核上。从核还在跑,然后你在共用的库函数里下了断点,两个核同时停,IDE 直接卡死。建议:调试时先用一段时间只在单一核上打断点,确认一个核逻辑没问题,再启用多核同步调试。很多调试器支持 cross-trigger,两个核心可以互相触发暂停,但上手复杂,建议前期不要开。
第三个坑是共享内存查看。如果开了缓存,你在调试器窗口看到的共享内存值可能不是最新值。调试前先确认内存属性,或者在调试器里用内存访问命令绕过缓存。
5.2 共享资源的竞争与死锁
一个典型的死锁场景是:两个核都要访问同一个 SPI Flash,各自驱动里都有软件锁,结果 M7 持锁等待 M4 通过 Mailbox 返回结果,而 M4 又持锁等待 M7 释放 SPI 总线,两边互相等待,系统卡死。
这种问题设计阶段就要避免。合理的做法是:每个外设明确指定唯一的“归属核”,其他核要使用该外设时必须通过 IPC 请求归属核代为完成。不要让两个核同时初始化同一个外设。如果必须共享,优先使用无锁方案或带超时的获取锁机制,同时记录持锁核 ID,方便死锁后从复位信息里排查。
5.3 缓存一致性问题
用带缓存的大核(比如 M7/M55)访问共享内存,最常见的现象是:M55 写了一个消息,M33 收到 Mailbox 中断后读到的却是旧数据。原因就是 M55 的 Cache miss 回写规则导致数据还停留在 Cache 里,没有真正落到 SRAM。
解决办法有三个:
把共享内存区域配置成 non-cacheable,最简单,适合消息量不大且不追求极致性能的场景。
在写共享内存之后,主动执行 Clean 操作;在读取共享内存之前,执行 Invalidate 操作。适合不想降低缓存性能的场景。
使用硬件一致性接口,有些芯片支持 AXI SRAM 与 CPU 缓存的一致性,配置后硬件自动处理,但可用区域有限,需要查手册。
我在项目里推荐优先使用 non-cacheable,因为嵌入式共享内存的数据量通常不大,牺牲一部分缓存收益,换取可预测性和代码简单性,非常划算。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 从核启动后立即 HardFault | 从核固件烧录地址错误 | 确认从核链接脚本中固件入口与启动地址一致 |
| 两个核通信偶发丢帧 | Mailbox 中断优先级过低或队列溢出 | 提高 Mailbox 中断优先级,检查队列 head/tail 更新是否被调度打断 |
| 共享内存读到旧数据 | 缓存未失效/清写 | 配置 non-cacheable,或手动 Cache Clean/Invalidate |
| Tx/Rx 同时断点导致 IDE 崩溃 | 多核断点同步未配置 | 只在一个核上断点,或开启 cross-trigger |
| 安全服务调用偶尔超时 | 主核频繁访问安全核导致排队 | 采用异步 IPC 加超时回退,不要阻塞等待 |
| 升级后设备变砖 | 固件镜像没有做整体校验 | 打包双核镜像统一校验,加 A/B 备份升级 |
最后再分享一个小技巧
做双核项目两年多,我最大的体会是:“双核”听起来是硬件问题,实际是软件问题。真正把一个双核 MCU 用好,不是简单地开两个工程、写两套 main,而是要在一开始就把硬件资源归属、内存映射、核间通信、安全边界全部规划好。如果你正准备做双核项目,一定先画一张“双核资源分配表”,把每个外设、每块内存、每个中断、每条 IPC 通道的归属核标清楚,再动手写代码。这个表就是你后续所有架构决策的基础。
最后再补充一个屡试不爽的调试策略:双核项目不要追求“一步到位”。先让两个核各自跑最简程序,互相之间只发一个 Mailbox 帧确认“我活着”;再把共享内存逐渐加大;最后才接入安全服务和复杂协议。每一步都在双核协同的状态下验证过,再去开发新功能。靠这个笨办法,我避开了很多不可复现的诡异 bug。希望这篇总结能让你少走一些弯路。