第三章 多核启动流程:CPU0启动、CPU1/CPU2/CPU3启动、核间同步机制

各位同学,今天我们来聊聊TC3xx的多核启动。说实话,这块内容我第一次接触时也绕了不少弯路。多核启动不像单核那么简单——你想想看,四个核同时跑起来,谁先谁后?怎么保证不乱套?

我个人习惯把多核启动比作一场交响乐。CPU0是指挥,其他核是乐手。指挥先上台,然后一个个招呼乐手加入演奏。嗯,这个比喻虽然简单,但道理是通的。

3.1 CPU0的启动——指挥先上台

TC3xx上电后,只有CPU0会自动从复位向量开始执行。其他三个核呢?它们都处于HALT状态,等着CPU0来唤醒。

CPU0的启动流程大致是这样的:

  1. 硬件初始化:时钟、PLL、看门狗这些基础配置
  2. 内存初始化:DDR、SRAM的初始化
  3. 中断向量表设置:BTV(基址表向量)的配置
  4. 启动其他核:通过软件触发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提供了两种硬件级别的同步机制:SpinlockSemaphore

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)的配置,这是多核安全的关键。到时候见。