多核启动流程:从复位到双核齐飞

说实话,多核启动这个话题,我当年刚接触时也绕了不少弯路。你想想看,一个芯片里两个核心,谁先跑?谁后跑?怎么保证它们不打架?这些问题搞不清楚,后面调试起来真是欲哭无泪。

今天我就把STM32多核启动的完整流程掰开揉碎了讲给你听。嗯,咱们从BootROM的行为开始说起。

BootROM:芯片出厂就写好的“第一行代码”

芯片上电后,第一个执行的代码不是你的应用程序,而是BootROM。这是芯片出厂时固化在ROM里的程序,你改不了它,但必须理解它。

BootROM干了三件大事:

  1. 初始化硬件基础环境:时钟、电源、内存控制器这些最基本的玩意儿
  2. 检查启动模式:从Flash启动?从系统存储器启动?还是从SRAM启动?
  3. 加载并跳转到用户代码:找到你的程序入口,把控制权交出去

我在项目中遇到过一个问题:客户说芯片上电后不跑程序。查了半天,结果是BootROM阶段检测到启动引脚电平不对,直接进了系统存储器模式。说白了,就是硬件设计时启动配置引脚没处理好。

核心要点:BootROM只负责把核心0唤醒。核心1?它还在睡大觉呢。

核心0与核心1的启动顺序:谁先谁后?

这个问题其实很简单:核心0永远是老大

为什么?因为BootROM只认核心0。上电后,核心0从地址0x00000000取复位向量,然后开始执行。核心1的复位向量虽然也存在,但没人去触发它。

我个人的习惯是把核心0叫做“主核”,核心1叫做“从核”。主核负责系统初始化、外设配置、任务调度。从核呢?等着主核给它分配任务。

具体流程是这样的:

步骤 核心0(主核) 核心1(从核)
1 从BootROM跳转到用户代码 保持WFI状态(等待中断)
2 初始化系统时钟、内存、外设 继续睡觉
3 加载核心1的启动代码到指定地址 还在睡觉
4 发送SEV指令唤醒核心1 被唤醒,开始执行启动代码
5 继续执行自己的应用程序 初始化自己的栈和局部变量

你可能会问:核心1怎么知道自己该从哪里开始跑?答案就在核心1的复位向量里。主核需要把核心1的入口地址写到特定位置,然后通过硬件机制触发核心1的复位序列。

小技巧:调试时我习惯在核心0的启动代码里加个GPIO翻转,在核心1的启动代码里也加一个。用逻辑分析仪一看,两个核心的启动时序一目了然。

Secondary Core Boot:从核启动的完整实现

好了,理论说完了,咱们来点实际的。下面这段代码展示了如何在STM32H7系列上启动核心1。

/* 核心0:主核启动代码 */
void SystemInit(void)
{
    /* 1. 初始化系统时钟 */
    HAL_Init();
    SystemClock_Config();
    
    /* 2. 初始化必要外设 */
    MX_GPIO_Init();
    MX_DMA_Init();
    
    /* 3. 准备核心1的启动参数 */
    /* 核心1的入口函数地址 */
    uint32_t core1_entry = (uint32_t)&Core1_Main;
    /* 核心1的栈顶地址 */
    uint32_t core1_stack = (uint32_t)_core1_stack_end;
    
    /* 4. 写入核心1的启动信息 */
    /* 注意:这个地址是芯片手册指定的 */
    *((uint32_t *)CORE1_BOOT_ADDRESS) = core1_entry;
    *((uint32_t *)CORE1_STACK_ADDRESS) = core1_stack;
    
    /* 5. 发送SEV指令唤醒核心1 */
    __SEV();
    __WFE();  /* 短暂等待,确保核心1已启动 */
    __WFE();
    
    /* 6. 核心0继续执行自己的任务 */
    while(1)
    {
        /* 主核任务循环 */
    }
}

/* 核心1:从核入口函数 */
void Core1_Main(void)
{
    /* 核心1自己的初始化 */
    HAL_Init();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    /* 核心1的任务 */
    while(1)
    {
        /* 从核任务循环 */
    }
}

这段代码看起来简单,但有几个坑我得提醒你:

我曾经踩过的坑:

  • 核心1的栈空间必须提前分配好,而且不能和核心0的栈重叠。我见过有人两个核心共用栈空间,结果跑着跑着就崩了。
  • 核心1的入口函数不能使用核心0已经初始化过的外设,除非你做了同步保护。说白了,两个核心同时操作同一个UART,数据就乱套了。
  • SEV指令发送后,核心1不会立刻启动。它需要时间完成自己的复位序列。我习惯在SEV后面加两个WFE,给核心1一点缓冲时间。

启动过程中的同步机制

两个核心都跑起来了,怎么保证它们步调一致?这就涉及到同步问题了。

我常用的方法有三种:

  • 轮询标志位:核心0设置一个全局变量,核心1不断检查这个变量。简单粗暴,但浪费CPU周期。
  • 事件信号:用SEV/WFE指令对。核心0发事件,核心1等待事件。效率高,但只能做简单的同步。
  • 硬件信号量:STM32H7系列有硬件信号量外设。这是最靠谱的方式,适合复杂的同步场景。

我个人习惯在启动阶段用事件信号,因为简单。等两个核心都跑起来了,再切换到硬件信号量做任务同步。

避坑指南:千万别在核心1的启动代码里访问核心0正在初始化的外设。我曾经犯过这个错——核心0在配DMA,核心1突然去读DMA的寄存器,结果DMA配置到一半被中断,整个系统直接死机。

启动流程的调试技巧

调试多核启动,说实话比单核麻烦多了。我分享几个实战经验:

  1. 用GPIO做时间戳:每个核心的关键步骤都翻转一个GPIO。用示波器一看,启动时序对不对一目了然。
  2. 共享内存做日志:在共享RAM里开一个环形缓冲区,两个核心都往里面写日志。主核通过UART把日志打印出来。
  3. 分步调试:先让核心0单独跑,确认没问题了再启动核心1。别一上来就双核全开,出了问题都不知道是谁的锅。

我记得有一次调试一个双核音频处理项目,核心1总是启动失败。查了两天,最后发现是核心1的栈空间地址写错了,指向了核心0的栈区域。你想想看,两个核心共用栈空间,不出问题才怪。

嗯,多核启动这块内容就讲到这里。说白了,核心0是管家,核心1是打工人。管家先把家里收拾好,再叫打工人起来干活。这个思路捋清楚了,启动流程也就不难理解了。