3. 双核启动流程与同步机制:Reset Handler、核间通信(IPC)基础、Mailbox与共享内存

双核启动,说白了就是让两个大脑同时醒过来,还得让它们知道彼此的存在。很多初学者以为把两个核的代码烧进去就完事了,结果一跑就乱套。我刚开始做H7双核项目时也踩过这个坑——主核跑起来了,从核还在睡大觉,整个系统就跟瘸了一条腿似的。

3.1 双核启动流程:谁先醒,谁后醒

STM32H7系列的双核架构里,Cortex-M7是主核,Cortex-M4是从核。上电复位后,M7会先执行,M4则被硬件强制保持在复位状态。这个设计是有道理的——M7性能更强,适合做系统初始化的"总指挥"。

核心要点:M7负责初始化系统时钟、电源管理、外设共享资源,然后通过RCC寄存器释放M4的复位信号。

具体流程分三步走:

  1. M7执行Reset_Handler:初始化堆栈、BSS段、数据段,配置系统时钟和PLL。
  2. M7配置RCC->RCC_MPCKSGR:将M4从复位状态释放。
  3. M4开始执行自己的Reset_Handler:初始化自己的堆栈和中断向量表。

嗯,这里要注意——M4的向量表地址必须和链接脚本里定义的一致。我遇到过一例,M4的向量表被放在了错误地址,结果一触发中断就飞了。

/* M7主核代码:释放M4复位 */
int main(void)
{
    HAL_Init();
    SystemClock_Config();  /* 配置系统时钟 */
    
    /* 释放M4从核 */
    __HAL_RCC_CPU2_RESET();
    __HAL_RCC_CPU2_CLK_ENABLE();
    
    /* 等待M4启动完成标志 */
    while (!(IPC->Flag & M4_READY_FLAG))
    {
        /* 轮询等待,实际项目中建议加超时 */
    }
    
    /* 双核就绪,开始分工 */
    ...
}

3.2 Reset Handler的差异:两个核,两套规矩

M7和M4的Reset Handler虽然长得像,但细节上有区别。我习惯把两个核的启动代码分开写,而不是共用一套——省得后面调试时搞混。

项目 M7主核 M4从核
启动顺序 先启动,系统总指挥 后启动,等待M7释放
向量表地址 0x08000000(默认) 0x08020000(需配置)
堆栈初始化 使用主堆栈指针MSP 同样使用MSP,但地址不同
时钟配置 负责所有时钟初始化 直接使用M7配置好的时钟
外设访问 可访问所有外设 只能访问分配给M4的外设

说白了,M4的Reset Handler就是个"轻量版"——它不需要重新配置PLL,直接用M7已经稳定的时钟。我在一个项目中试过让M4也去改PLL配置,结果两个核抢时钟控制权,系统直接挂了。从那以后,我就在代码里加了个硬性规定:时钟相关寄存器,只有M7能碰。

3.3 核间通信(IPC)基础:两个核怎么聊天

双核系统最核心的问题就是:两个核怎么交换数据?怎么同步状态?STM32提供了几种IPC机制,我按实用程度排个序:

  • Mailbox(邮箱):基于硬件寄存器的消息传递,适合发短消息。
  • 共享内存:两个核都能访问的RAM区域,适合传大数据块。
  • 硬件信号量:防止两个核同时访问共享资源。
  • 中断触发:一个核通过硬件中断通知另一个核。

你想想看,如果两个核同时往同一个内存地址写数据,会发生什么?数据会被覆盖,系统行为完全不可预测。这就是为什么IPC必须配合同步机制使用。

我的经验:Mailbox适合传控制命令(比如"开始采集"、"停止传输"),共享内存适合传批量数据(比如音频帧、传感器数据包)。别把两者混着用,容易出逻辑混乱。

3.4 Mailbox机制:硬件级的"小纸条"

Mailbox说白了就是一组硬件寄存器,一个核往里面写数据,另一个核读出来。STM32H7的Mailbox有8个通道,每个通道可以传一个32位的数据。

我曾经在一个工业控制项目里用Mailbox传状态码——M7发"开始测量",M4收到后回复"测量完成"。整个过程不需要复杂的协议,简单粗暴但可靠。

/* M7发送消息给M4 */
void IPC_SendMessage(uint32_t msg)
{
    /* 等待Mailbox空闲 */
    while (HSEM->RLR[0] != 0);
    
    /* 写入消息到共享Mailbox寄存器 */
    MAILBOX->M7_M4_MSG = msg;
    
    /* 触发M4中断 */
    HAL_NVIC_SetPendingIRQ(M4_IPC_IRQn);
}

/* M4接收消息(在中断服务函数中) */
void M4_IPC_IRQHandler(void)
{
    uint32_t msg = MAILBOX->M7_M4_MSG;
    
    switch (msg)
    {
        case CMD_START_ACQ:
            StartAcquisition();
            break;
        case CMD_STOP_ACQ:
            StopAcquisition();
            break;
        default:
            break;
    }
}

避坑指南:我曾经在Mailbox里传指针地址,结果M4读到的地址是M7的DTCM区域,M4根本访问不了。记住——Mailbox传的地址必须在两个核都能访问的共享内存区域(比如SRAM4或SRAM5)。

3.5 共享内存:大数据交换的"公共黑板"

共享内存就是一块两个核都能读写的RAM区域。STM32H7里,SRAM4和SRAM5是默认的共享区域,两个核通过AHB总线都能访问。

但问题来了——两个核同时写怎么办?这就需要硬件信号量(HSEM)来保护了。

/* M7写共享内存 */
void SharedMem_Write(uint32_t *buf, uint32_t len)
{
    /* 获取信号量 */
    while (HAL_HSEM_FastTake(HSEM_ID_0) != HAL_OK);
    
    /* 写数据到共享内存 */
    for (uint32_t i = 0; i < len; i++)
    {
        SHARED_BUF[i] = buf[i];
    }
    
    /* 释放信号量 */
    HAL_HSEM_Release(HSEM_ID_0, 0);
}

/* M4读共享内存 */
void SharedMem_Read(uint32_t *buf, uint32_t len)
{
    /* 获取信号量 */
    while (HAL_HSEM_FastTake(HSEM_ID_0) != HAL_OK);
    
    /* 读数据 */
    for (uint32_t i = 0; i < len; i++)
    {
        buf[i] = SHARED_BUF[i];
    }
    
    /* 释放信号量 */
    HAL_HSEM_Release(HSEM_ID_0, 0);
}

嗯,这里有个细节——信号量的获取和释放必须成对出现。我见过有人获取了信号量后,在某个分支里忘了释放,结果另一个核永远拿不到锁,整个系统卡死。所以我现在写代码时,习惯用__attribute__((cleanup))或者RAII风格来自动释放信号量。

3.6 双核同步机制:别让两个核"打架"

同步机制解决的是"谁先做、谁后做"的问题。除了信号量,还有两种常用方式:

  • 轮询标志位:一个核写标志,另一个核轮询等待。简单但浪费CPU。
  • 硬件中断通知:一个核写数据后触发另一个核的中断。效率高,但中断服务函数要写得短。

我个人的习惯是:控制类消息用中断通知,数据类消息用轮询+超时。为什么?因为数据量大时,频繁进中断反而降低吞吐量。

实战建议:在共享内存的头部放一个"状态结构体",包含数据就绪标志、数据长度、校验和。这样两个核通过检查这个结构体就能知道对方的状态,不需要每次都走Mailbox。

3.7 双核启动与同步的完整流程图

下面这张图把整个流程串起来了——从M7上电复位,到M4启动完成,再到两个核通过Mailbox和共享内存协同工作。

双核启动与同步流程图 双核启动与同步流程图 M7上电复位 M7执行Reset_Handler M7配置时钟/外设 M7释放M4复位 M4开始执行 M4初始化向量表/堆栈 M4设置就绪标志 双核通过Mailbox/共享内存协同工作 说明:M7先启动,释放M4后,双核通过IPC机制交换数据

3.8 避坑总结:双核启动的常见问题

做了几个双核项目后,我总结了几条血泪教训:

  • 向量表地址别搞错:M4的向量表必须放在M4能访问的地址,且链接脚本要对应。
  • 共享内存要加缓存一致性处理:M7有L1 Cache,M4没有。如果M7写了共享内存但数据还在Cache里,M4读到的是旧数据。解决办法是操作共享内存前调用SCB_CleanDCache()。
  • 信号量超时机制不能省:万一对方死锁了,你的核不能一直等下去。加个超时,超时后报错复位。
  • 中断优先级要统一规划:两个核的中断优先级是独立的,但如果你用中断做IPC,两边优先级得匹配好,否则可能出现M7中断一直打断M4的情况。

我曾经踩过的坑:在一个音频处理项目里,M7和M4通过共享内存传音频数据。M7写数据前清了Cache,但忘了加内存屏障(DSB指令),结果M4读到的数据还是乱的。加了__DSB()后问题解决。记住——Cache操作后一定要加内存屏障。

双核启动和同步,说白了就是"让两个核有序地跑起来,别抢资源,别互相等死"。掌握了Reset Handler的差异、Mailbox的用法、共享内存的保护机制,你就已经迈过了双核开发的第一道坎。