3、多核启动流程:启动顺序与核心配置、启动代码(BootROM)分析、用户自定义启动

多核启动,说白了就是让六个核心(TC3xx 有六个核)从「睡梦」中依次醒来,然后各司其职。很多刚接触多核的朋友会问:「是不是所有核同时上电?谁先跑?谁后跑?」嗯,这里面的门道不少。我当年第一次调多核启动时,就踩过「核 0 还没初始化完,核 1 就开始抢资源」的坑。今天咱们把这事彻底聊透。

3.1 启动顺序与核心配置

TC3xx 的启动顺序,说白了就是「一个老大,带一群小弟」。老大是谁?Core 0(CPU0)。它负责整个系统的初始化,包括时钟、电源、内存控制器,以及最重要的——决定其他核心何时启动

具体顺序是这样的:

  1. 硬件复位:所有核心同时复位,但只有 Core 0 开始执行 BootROM 代码。
  2. Core 0 启动:它从 Flash 地址 0xA0000000 开始取指,执行启动代码。
  3. Core 0 初始化:配置 PLL、时钟树、内存保护单元(MPU)、中断控制器等。
  4. Core 0 唤醒其他核:通过写特定的寄存器(比如 BMHDSCR),让 Core 1~5 开始执行。
  5. 各核独立运行:每个核有自己的程序计数器(PC)和栈指针(SP),跑各自的代码。

你想想看,为什么非得 Core 0 先跑?因为硬件设计上,只有 Core 0 有权限访问某些系统级寄存器。比如 SCU(系统控制单元)的配置,其他核碰都不能碰。我在项目中遇到过有人试图在 Core 1 里直接改 PLL 配置,结果系统直接挂掉——嗯,这是硬件的保护机制,别去挑战它。

核心配置的关键寄存器

  • BMHD(Boot Mode Header):决定启动模式(比如从 Flash 启动、从 CAN 启动等)。
  • SCR(Startup Configuration Register):控制每个核的启动地址和启动使能。
  • CPUx_BOOT_ADDR:每个核的启动地址寄存器,Core 0 通过写这个寄存器告诉其他核「你该从哪开始跑」。

3.2 启动代码(BootROM)分析

BootROM 是芯片出厂时固化的一段代码,你改不了它。它的任务很简单:把系统带到能跑用户代码的状态。我个人习惯把 BootROM 的流程分成三个阶段:

阶段一:硬件自检(HWBIST)

上电后,BootROM 先跑一遍硬件自检。这玩意儿会检查 CPU、内存、总线有没有物理损坏。如果自检失败,它会直接进入 SafeState(安全状态),啥也不干了。我记得有一次开发板怎么都启动不了,最后发现是内存焊锡短路——BootROM 的自检救了我一命。

阶段二:启动模式选择

BootROM 会读取 BMHD 的内容,判断你是要从 Flash 启动、从 CAN 启动,还是从 ASC(异步串口)启动。这里有个细节:BMHD 必须放在 Flash 的特定地址(比如 0x80000000),而且要有正确的校验和。如果校验失败,BootROM 会认为 Flash 是空的,然后进入 Alternative Boot Mode(备用启动模式)。

小技巧:调试阶段,我经常用 CAN 启动模式。这样不用反复烧写 Flash,直接通过 CAN 总线把代码灌进去,省了不少时间。

阶段三:用户代码跳转

BootROM 最后一步,就是把控制权交给用户代码。它会跳转到 BMHD 里指定的起始地址(通常是 0xA0000000)。从这一刻起,你的代码开始接管系统。

BootROM 的代码流程可以用下面这个简化的伪代码表示:

void BootROM_Main(void) {
    // 阶段一:硬件自检
    if (HWBIST_Failed()) {
        Enter_SafeState();
        while(1); // 死循环,别想跑了
    }

    // 阶段二:读取启动模式
    BMHD_Type bmhd = Read_BMHD(0x80000000);
    if (bmhd.Checksum_Invalid()) {
        Enter_AlternativeBootMode();
    }

    // 阶段三:跳转到用户代码
    void (*user_code)(void) = (void (*)(void))bmhd.StartAddress;
    user_code(); // 再也不回来了
}

3.3 用户自定义启动

BootROM 跑完后,剩下的就是你的事了。用户自定义启动,说白了就是你自己决定每个核跑什么代码。这里我分享一个我在项目里常用的启动框架:

第一步:Core 0 初始化全局资源

Core 0 先初始化所有核共享的资源:

  • 时钟系统(PLL、CCU)
  • 内存保护单元(MPU)
  • 中断控制器(ICU)
  • 外设(比如 DMA、CAN、SPI)

我曾经犯过一个错:在 Core 0 还没配好 MPU 之前,就启动了 Core 1。结果 Core 1 访问了一段受保护的内存,直接触发 Trap——嗯,从那以后我学乖了,一定要等 Core 0 把「地盘」划清楚,再让其他核进场

第二步:配置其他核的启动地址

Core 0 通过写 CPU1_BOOT_ADDRCPU2_BOOT_ADDR 等寄存器,告诉其他核「你们该从哪开始跑」。比如:

// Core 0 代码
#define CORE1_START_ADDR 0x70000000
#define CORE2_START_ADDR 0x70001000

void Start_Other_Cores(void) {
    // 写启动地址
    CPU1_BOOT_ADDR = CORE1_START_ADDR;
    CPU2_BOOT_ADDR = CORE2_START_ADDR;

    // 使能 Core 1 和 Core 2
    CPU1_ENABLE = 1;
    CPU2_ENABLE = 1;

    // 等待其他核启动完成
    while (!CPU1_RUNNING || !CPU2_RUNNING);
}

第三步:各核独立初始化

每个核启动后,先初始化自己的私有资源:

  • 自己的栈指针(SP)
  • 自己的局部变量区
  • 自己的中断向量表

这里有个坑:每个核的中断向量表必须独立。如果你让所有核共用同一个中断向量表,那中断来了,谁去响应?系统会乱套。我建议每个核都有一份自己的 trap_vector_tableinterrupt_vector_table

警告:千万不要在 Core 0 初始化完成之前,就启动其他核。否则其他核可能会访问到未初始化的外设,导致不可预知的行为。我曾经见过一个案例,Core 1 在 Core 0 还没配好 DMA 时就启动了,结果 DMA 通道被两个核同时操作,数据全乱了。

第四步:同步与握手

所有核启动完成后,需要一个同步机制。我常用的方法是:

  • 每个核启动后,设置一个全局标志位(比如 core_ready[core_id] = 1)。
  • Core 0 轮询这些标志位,直到所有核都就绪。
  • 然后 Core 0 发一个「开始干活」的信号(比如写一个共享内存变量)。

你想想看,如果没有这个同步机制,Core 0 可能已经开始调度任务了,但 Core 1 还在初始化自己的栈——这不就乱套了吗?

总结

多核启动,核心就三点:

  1. Core 0 是老大,它先跑,负责全局初始化。
  2. BootROM 是引路人,它把系统带到安全状态,然后交给你。
  3. 用户自定义启动是艺术,你得规划好每个核的启动顺序、地址和同步机制。

我个人习惯在项目初期,先画一张「多核启动时序图」,把每个核的启动时间点、初始化内容、同步点都标清楚。这样后期调试时,能省不少心。嗯,今天就聊到这,下一章咱们聊聊任务分配——怎么把活儿合理地分给六个核。