3、多核启动流程:启动顺序与核心配置、启动代码(BootROM)分析、用户自定义启动
多核启动,说白了就是让六个核心(TC3xx 有六个核)从「睡梦」中依次醒来,然后各司其职。很多刚接触多核的朋友会问:「是不是所有核同时上电?谁先跑?谁后跑?」嗯,这里面的门道不少。我当年第一次调多核启动时,就踩过「核 0 还没初始化完,核 1 就开始抢资源」的坑。今天咱们把这事彻底聊透。
3.1 启动顺序与核心配置
TC3xx 的启动顺序,说白了就是「一个老大,带一群小弟」。老大是谁?Core 0(CPU0)。它负责整个系统的初始化,包括时钟、电源、内存控制器,以及最重要的——决定其他核心何时启动。
具体顺序是这样的:
- 硬件复位:所有核心同时复位,但只有 Core 0 开始执行 BootROM 代码。
- Core 0 启动:它从 Flash 地址 0xA0000000 开始取指,执行启动代码。
- Core 0 初始化:配置 PLL、时钟树、内存保护单元(MPU)、中断控制器等。
- Core 0 唤醒其他核:通过写特定的寄存器(比如
BMHD或SCR),让 Core 1~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_ADDR、CPU2_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_table 和 interrupt_vector_table。
警告:千万不要在 Core 0 初始化完成之前,就启动其他核。否则其他核可能会访问到未初始化的外设,导致不可预知的行为。我曾经见过一个案例,Core 1 在 Core 0 还没配好 DMA 时就启动了,结果 DMA 通道被两个核同时操作,数据全乱了。
第四步:同步与握手
所有核启动完成后,需要一个同步机制。我常用的方法是:
- 每个核启动后,设置一个全局标志位(比如
core_ready[core_id] = 1)。 - Core 0 轮询这些标志位,直到所有核都就绪。
- 然后 Core 0 发一个「开始干活」的信号(比如写一个共享内存变量)。
你想想看,如果没有这个同步机制,Core 0 可能已经开始调度任务了,但 Core 1 还在初始化自己的栈——这不就乱套了吗?
总结
多核启动,核心就三点:
- Core 0 是老大,它先跑,负责全局初始化。
- BootROM 是引路人,它把系统带到安全状态,然后交给你。
- 用户自定义启动是艺术,你得规划好每个核的启动顺序、地址和同步机制。
我个人习惯在项目初期,先画一张「多核启动时序图」,把每个核的启动时间点、初始化内容、同步点都标清楚。这样后期调试时,能省不少心。嗯,今天就聊到这,下一章咱们聊聊任务分配——怎么把活儿合理地分给六个核。