第三章 多核启动流程:从BootROM到多核初始化

说实话,多核启动这块,我当年刚接触TC3xx时也踩过不少坑。你想想看,一个芯片里好几个核,谁先跑?谁后跑?怎么保证它们不乱套?这些问题搞不清楚,代码写出来就是定时炸弹。

今天我就把TC3xx的启动流程掰开揉碎了讲给你听。从按下复位键那一刻开始,到所有核都跑起来,每一步我都会结合自己的实战经验来说。

3.1 复位后的第一行代码:BootROM在干嘛?

芯片上电或复位后,CPU0(主核)会第一个醒来。它执行的第一个程序,不是你的应用代码,而是芯片内部固化的BootROM。

BootROM干三件事:

  • 硬件自检:检查CPU、内存、总线这些基本单元是否正常。我遇到过一块板子,BootROM卡在自检阶段,最后发现是电源纹波太大。
  • 启动模式选择:根据硬件引脚(比如BMHD)决定从哪加载程序。是从Flash启动?还是从CAN、LIN、以太网?
  • 加载启动头:从启动介质中读取用户程序的启动头(Boot Mode Header),这里面包含了程序入口地址、CRC校验等信息。

关键点:BootROM只运行在CPU0上。其他核(CPU1、CPU2)此时还在睡大觉,处于HALT状态。

我个人习惯在项目初期先确认BootROM能否正常跑完。方法很简单:用调试器连上,看PC指针是否跳到了用户程序的复位向量。如果卡住了,八成是硬件问题。

3.2 用户启动代码:谁在管多核?

BootROM结束后,CPU0跳转到用户程序的复位向量。这时候,真正的多核初始化才刚刚开始。

用户启动代码通常放在cstart.cIfx_C_Init.c里。它要干的事不少:

  1. 初始化堆栈指针:给每个核分配独立的栈空间。我建议栈大小至少给2KB,别抠门,不然递归调用时栈溢出很难查。
  2. 初始化BSS段:把未初始化的全局变量清零。嗯,这里要注意,如果忘了清零,变量初始值可能是随机的。
  3. 初始化数据段:把已初始化的全局变量从Flash拷贝到RAM。
  4. 调用构造函数:C++的全局对象构造函数在这里执行。
  5. 启动其他核:这是多核启动的核心步骤。

我的小技巧:在启动代码里加个LED闪烁,能直观看到程序跑到了哪一步。调试时省心不少。

3.3 多核启动:CPU1和CPU2怎么醒过来?

CPU0跑起来后,需要主动唤醒其他核。TC3xx里有个专门的寄存器叫ECSP(Enable Core Start Pulse),往里面写数据就能触发核的启动。

具体流程是这样的:

步骤 CPU0做的事 其他核的状态
1 初始化自身外设、时钟、中断 HALT(暂停)
2 为CPU1、CPU2准备启动参数(栈指针、入口地址) HALT
3 写ECSP寄存器,发送启动脉冲 收到脉冲,开始执行
4 进入主循环或等待同步 执行各自的启动代码

代码实现大概长这样:

/* 唤醒CPU1 */
IfxScuWdt_clearSafetyEndinit();  /* 解锁安全寄存器 */
SCU_ECSP0.B.START = 1;           /* 发送启动脉冲 */
IfxScuWdt_setSafetyEndinit();    /* 重新锁定 */

/* 唤醒CPU2 */
IfxScuWdt_clearSafetyEndinit();
SCU_ECSP1.B.START = 1;
IfxScuWdt_setSafetyEndinit();

注意:ECSP寄存器属于安全寄存器,操作前必须解锁,操作后立即锁定。我曾经因为忘了重新锁定,导致系统在运行时被意外触发复位,查了两天才找到原因。

3.4 核间同步:别让它们乱跑

多核启动后,最怕什么?怕它们同时访问同一个资源。比如两个核同时写同一个全局变量,结果就是数据错乱。

TC3xx提供了几种核间同步机制:

  • Spinlock(自旋锁):轻量级锁,适合短时间等待。我一般用它保护寄存器配置。
  • Semaphore(信号量):适合任务调度,可以设置超时。
  • Inter-Core Interrupt(核间中断):一个核可以给另一个核发中断,通知它做某事。
  • Shared Memory Barrier(共享内存屏障):保证内存操作的顺序性。

举个实际例子。我在做电机控制项目时,CPU0负责通信,CPU1负责算法,CPU2负责IO。它们共享一个电流采样值。如果不加同步,CPU1读到的一半是旧数据,一半是新数据,算出来的控制量直接飞了。

解决方案是用Spinlock:

/* CPU1:写数据 */
while (AcquireSpinlock(&lock) != 0);  /* 等待锁 */
current_sensor = readADC();
ReleaseSpinlock(&lock);               /* 释放锁 */

/* CPU2:读数据 */
while (AcquireSpinlock(&lock) != 0);
motor_current = current_sensor;
ReleaseSpinlock(&lock);

避坑指南:Spinlock里不要做耗时操作,比如延时、打印。否则其他核会一直空转,浪费CPU性能。我曾经在Spinlock里加了个串口打印,结果系统响应慢得像蜗牛。

3.5 启动顺序的陷阱与实战建议

讲几个我实际踩过的坑:

  1. 外设初始化顺序:有些外设依赖其他外设先初始化。比如DMA依赖时钟模块,时钟模块又依赖PLL。搞反了直接挂掉。
  2. 中断向量表:每个核有自己的中断向量表,必须独立配置。我见过有人把CPU0的向量表地址配给了CPU1,结果中断一来就跑飞。
  3. 栈空间分配:多核共享RAM,栈空间不能重叠。我建议在链接脚本里明确划分每个核的栈区域。
  4. 启动超时:如果某个核启动失败,系统可能死锁。加个超时机制,超时后打印错误信息。

我的习惯:在启动代码最后加一个同步点。所有核都跑到这里后,才进入主循环。这样能保证启动阶段完全有序。

说白了,多核启动就像指挥一个交响乐团。BootROM是乐谱,CPU0是指挥,其他核是乐手。指挥得先看谱(BootROM),然后给每个乐手分配任务(启动参数),最后挥棒(ECSP)让大家一起开始演奏。如果哪个环节乱了,整首曲子就毁了。

嗯,这一章的内容就这些。下一章我会讲多核下的中断管理,那可是个更刺激的话题。