第三章 多核启动流程:CPU0启动、CPU1/CPU2/CPU3启动、核间同步机制
各位同学,今天我们来聊聊TC3xx的多核启动。说实话,这块内容我第一次接触时也绕了不少弯路。多核启动不像单核那么简单——你想想看,四个核同时跑起来,谁先谁后?怎么保证不乱套?
我个人习惯把多核启动比作一场交响乐。CPU0是指挥,其他核是乐手。指挥先上台,然后一个个招呼乐手加入演奏。嗯,这个比喻虽然简单,但道理是通的。
3.1 CPU0的启动——指挥先上台
TC3xx上电后,只有CPU0会自动从复位向量开始执行。其他三个核呢?它们都处于HALT状态,等着CPU0来唤醒。
CPU0的启动流程大致是这样的:
- 硬件初始化:时钟、PLL、看门狗这些基础配置
- 内存初始化:DDR、SRAM的初始化
- 中断向量表设置:BTV(基址表向量)的配置
- 启动其他核:通过软件触发CPU1/2/3的启动
关键点:CPU0必须在启动其他核之前,完成所有全局资源的初始化。我在项目中遇到过,有人让CPU1先跑起来去访问一个还没初始化的外设——结果就是死机。这个坑我踩过,你们别踩。
来看一段典型的CPU0启动代码:
/* CPU0 启动代码片段 */
void core0_main(void)
{
/* 1. 关闭看门狗 */
IfxScuWdt_disableCpuWatchdog(IfxScuWdt_getCpuWatchdogPassword());
IfxScuWdt_disableSafetyWatchdog(IfxScuWdt_getSafetyWatchdogPassword());
/* 2. 初始化时钟 */
initClock();
/* 3. 初始化内存 */
initMemory();
/* 4. 设置中断向量 */
IfxCpu_Irq_installInterruptHandler(&isr_handler, 0);
/* 5. 启动其他核 */
startCore1();
startCore2();
startCore3();
/* 6. CPU0进入主循环 */
while(1)
{
/* CPU0的任务 */
}
}
3.2 CPU1/CPU2/CPU3的启动——乐手依次入场
CPU0通过写特定的寄存器来唤醒其他核。具体来说,是操作ECSP(Endinit Critical Section Protection)保护的寄存器。
每个核的启动地址存放在哪里?在BMHD(Boot Mode Header)里。CPU0读取BMHD,然后设置其他核的启动地址。
| 核 | 启动地址寄存器 | 状态寄存器 |
|---|---|---|
| CPU1 | PC1 | CORE1_STAT |
| CPU2 | PC2 | CORE2_STAT |
| CPU3 | PC3 | CORE3_STAT |
启动其他核的代码示例:
/* 启动CPU1 */
void startCore1(void)
{
/* 设置CPU1的启动地址 */
Ifx_Ssw_StartCore(1, (uint32)core1_main);
/* 等待CPU1确认启动 */
while(!Ifx_Ssw_isCoreRunning(1))
{
/* 等待 */
}
printf("CPU1 started successfully\n");
}
个人经验:我建议在启动每个核之后,都加一个确认机制。别一股脑全启动完再去检查状态。曾经有个项目,CPU2启动失败了,但代码没检查,结果CPU3去访问CPU2的资源时直接挂了。排查了三天才找到原因。
3.3 核间同步机制——别让乐手各吹各的
多核跑起来之后,最大的问题就是同步。四个核同时访问同一个变量怎么办?谁先谁后?
TC3xx提供了两种硬件级别的同步机制:Spinlock和Semaphore。
3.3.1 Spinlock——自旋锁
Spinlock说白了就是一个忙等锁。当一个核获取了锁,其他核就只能在那转圈等。适合短时间的临界区保护。
TC3xx的Spinlock是通过LDST(Load Store)指令实现的原子操作。
/* Spinlock使用示例 */
volatile int spinlock = 0;
void critical_section(void)
{
/* 尝试获取锁 */
while(__sync_lock_test_and_set(&spinlock, 1))
{
/* 自旋等待 */
}
/* 临界区代码 */
shared_data++;
/* 释放锁 */
__sync_lock_release(&spinlock);
}
注意:Spinlock不适合长时间等待。我曾经见过一个案例,某个核在临界区里做了大量计算,其他三个核全在那空转,CPU利用率直接拉满。说白了,Spinlock只适合保护几十个指令周期的操作。
3.3.2 Semaphore——信号量
Semaphore比Spinlock高级一些。它允许任务在等待时进入睡眠状态,而不是空转。TC3xx的Semaphore是基于硬件计数器的。
TC3xx有8个硬件Semaphore单元,每个单元有一个计数器和状态寄存器。
/* Semaphore使用示例 */
void semaphore_example(void)
{
/* 获取信号量 */
Ifx_Semaphore_acquire(sem_id, 1);
/* 访问共享资源 */
process_shared_data();
/* 释放信号量 */
Ifx_Semaphore_release(sem_id, 1);
}
| 特性 | Spinlock | Semaphore |
|---|---|---|
| 等待方式 | 忙等(自旋) | 可睡眠 |
| 适用场景 | 短时间临界区 | 长时间等待 |
| 硬件支持 | 原子指令 | 专用硬件单元 |
| 资源消耗 | 高(CPU空转) | 低 |
3.4 启动代码分析——庖丁解牛
好了,理论讲完了,我们来分析一份完整的启动代码。这是我从一个实际项目中提取出来的,稍微简化了一下。
/* 完整的多核启动流程 */
void startup(void)
{
uint32 core_id = IfxCpu_getCoreId();
if(core_id == 0)
{
/* CPU0: 全局初始化 */
disableWatchdogs();
initPll();
initMemory();
initInterrupts();
/* 创建同步信号量 */
Ifx_Semaphore_create(&start_sem, 0);
/* 启动其他核 */
startCore(1, (uint32)core1_main);
startCore(2, (uint32)core2_main);
startCore(3, (uint32)core3_main);
/* 等待所有核就绪 */
for(int i = 1; i < 4; i++)
{
Ifx_Semaphore_acquire(start_sem, 1);
}
/* 所有核就绪,开始主任务 */
run_main_tasks();
}
else
{
/* CPU1/2/3: 本地初始化 */
initLocalPeripherals();
/* 通知CPU0已就绪 */
Ifx_Semaphore_release(start_sem, 1);
/* 等待CPU0的同步信号 */
wait_for_sync();
/* 执行核专属任务 */
run_core_tasks();
}
}
避坑指南:我曾经在启动代码里犯过一个低级错误——CPU0启动其他核之后,没等它们初始化完就开始分配任务。结果CPU1还在初始化DMA,CPU0就把DMA通道分配出去了。嗯,从那以后我养成了一个习惯:启动完所有核之后,一定要加一个同步屏障。
最后说一句,多核启动的调试比单核麻烦得多。我建议你们在开发初期,先让所有核跑一个简单的LED闪烁程序,确认每个核都能正常启动和运行。然后再逐步添加复杂功能。这样出了问题,至少能确定是启动阶段的问题,还是业务逻辑的问题。
下一章我们会讲内存保护单元(MPU)的配置,这是多核安全的关键。到时候见。