第三章 多核启动流程:从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.c或Ifx_C_Init.c里。它要干的事不少:
- 初始化堆栈指针:给每个核分配独立的栈空间。我建议栈大小至少给2KB,别抠门,不然递归调用时栈溢出很难查。
- 初始化BSS段:把未初始化的全局变量清零。嗯,这里要注意,如果忘了清零,变量初始值可能是随机的。
- 初始化数据段:把已初始化的全局变量从Flash拷贝到RAM。
- 调用构造函数:C++的全局对象构造函数在这里执行。
- 启动其他核:这是多核启动的核心步骤。
我的小技巧:在启动代码里加个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 启动顺序的陷阱与实战建议
讲几个我实际踩过的坑:
- 外设初始化顺序:有些外设依赖其他外设先初始化。比如DMA依赖时钟模块,时钟模块又依赖PLL。搞反了直接挂掉。
- 中断向量表:每个核有自己的中断向量表,必须独立配置。我见过有人把CPU0的向量表地址配给了CPU1,结果中断一来就跑飞。
- 栈空间分配:多核共享RAM,栈空间不能重叠。我建议在链接脚本里明确划分每个核的栈区域。
- 启动超时:如果某个核启动失败,系统可能死锁。加个超时机制,超时后打印错误信息。
我的习惯:在启动代码最后加一个同步点。所有核都跑到这里后,才进入主循环。这样能保证启动阶段完全有序。
说白了,多核启动就像指挥一个交响乐团。BootROM是乐谱,CPU0是指挥,其他核是乐手。指挥得先看谱(BootROM),然后给每个乐手分配任务(启动参数),最后挥棒(ECSP)让大家一起开始演奏。如果哪个环节乱了,整首曲子就毁了。
嗯,这一章的内容就这些。下一章我会讲多核下的中断管理,那可是个更刺激的话题。