11、SMP启动流程详解:从BootROM到RT-Thread SMP启动、汇编代码分析
各位同学,今天我们来聊聊多核启动这个“第一道坎”。
说实话,我第一次接触SMP启动时,被那个“主核先跑,从核等待”的流程搞得晕头转向。后来在调试一块四核Cortex-A7的板子时,从核死活不响应IPI中断,查了三天才发现是启动地址对齐出了问题。嗯,这些坑咱们今天一次性讲清楚。
11.1 从BootROM到RT-Thread:启动的“接力赛”
芯片上电后,谁先跑?当然是BootROM。它固化在芯片内部,负责最底层的硬件初始化。BootROM做完后,会跳转到用户指定的启动地址——通常是Flash或SRAM的起始位置。
这里有个关键点:BootROM只唤醒一个核心,也就是我们说的“主核”(Primary Core)。其他核心(从核)处于WFI或WFE状态,等待主核发信号唤醒。
启动流程概览:
- BootROM → 主核启动,从核休眠
- 主核执行Bootloader(如u-boot)
- Bootloader加载RT-Thread内核镜像
- RT-Thread内核初始化(主核完成)
- 主核唤醒从核,从核进入SMP启动流程
我个人习惯把这一步叫做“接力赛第一棒”。BootROM是芯片厂商写死的,我们改不了。但从Bootloader开始,就完全由我们掌控了。
11.2 主核启动:从Reset Vector到C环境
主核上电后,第一件事就是去Reset Vector取指令。这个地址通常是0x00000000或0xFFFF0000,具体看芯片设计。
我们来看看典型的启动汇编代码。以ARM Cortex-A系列为例:
; 启动代码入口
.section .text.startup
.globl _start
_start:
; 设置栈指针
ldr sp, =__stack_top
; 关闭中断
cpsid if
; 初始化BSS段
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
bss_loop:
cmp r0, r1
strlo r2, [r0], #4
blo bss_loop
; 跳转到C语言入口
bl rt_hw_board_init
这段代码看着简单,但坑不少。我曾经在BSS清零时忘了检查边界条件,结果把栈空间也给清了,系统直接跑飞。你想想看,栈都没了,函数调用怎么可能正常?
避坑指南: 我建议在BSS清零前,先确认链接脚本中__bss_start和__bss_end的地址是否正确。可以用JTAG单步调试,打印这两个地址的值。
11.3 从核启动:主核的“唤醒术”
主核跑起来后,从核还在睡大觉。怎么唤醒它们?这取决于硬件架构。
对于ARM GIC(通用中断控制器)架构,通常使用SGI(软件生成中断)来唤醒从核。具体流程是:
- 主核为每个从核准备一个启动地址(通常是SRAM中的一段代码)
- 主核向从核发送SGI中断,中断号通常是0或1
- 从核收到中断后,从WFI/WFE状态醒来
- 从核读取启动地址,跳转执行
来看看RT-Thread中唤醒从核的代码片段:
void rt_hw_secondary_cpu_up(void)
{
unsigned int cpu_id;
for (cpu_id = 1; cpu_id < CONFIG_SMP_CORES; cpu_id++) {
// 设置从核启动地址
*((volatile unsigned int *)SMP_BOOT_ADDR) = (unsigned int)secondary_startup;
// 发送SGI中断
dsb();
sev();
// 等待从核确认
while (!cpu_online(cpu_id)) {
cpu_relax();
}
}
}
这里有个细节:dsb()和sev()是内存屏障和事件发送指令。为什么需要内存屏障?因为主核写入启动地址后,从核可能因为缓存一致性看不到这个值。说白了,就是保证写入操作对其他核心可见。
注意: 不同架构的唤醒机制差异很大。比如RISC-V使用CLINT(核心本地中断器),x86使用IPI(处理器间中断)。移植时一定要查芯片手册,别想当然。
11.4 从核启动汇编:从核的“独立王国”
从核被唤醒后,执行的代码和主核类似,但有一些关键区别。我们来看secondary_startup的汇编实现:
; 从核启动入口
.section .text.secondary
.globl secondary_startup
secondary_startup:
; 设置从核栈指针
ldr sp, =__secondary_stack_top
; 关闭本地中断
cpsid if
; 初始化MMU(如果需要)
bl mmu_init_secondary
; 使能缓存
bl cache_enable
; 跳转到C语言入口
bl rt_hw_secondary_cpu_init
注意看,从核的栈是独立的——__secondary_stack_top。为什么?因为每个核心都需要自己的栈空间,否则函数调用会互相覆盖。我在项目中就见过有人偷懒,让从核和主核共用栈,结果系统跑着跑着就莫名其妙崩溃了。
另外,从核的MMU初始化通常比主核简单。主核需要建立页表、映射外设地址空间,而从核只需要继承主核的页表设置,或者做少量调整。说白了,从核就是个“拿来主义”,直接用主核搭好的环境。
11.5 RT-Thread SMP启动的“最后一公里”
从核进入C语言环境后,RT-Thread会调用rt_hw_secondary_cpu_init。这个函数负责:
- 初始化从核的中断控制器
- 注册从核到调度器
- 启动从核上的空闲线程
- 等待调度器分配任务
来看看核心代码:
void rt_hw_secondary_cpu_init(void)
{
rt_base_t level;
// 初始化本地定时器
rt_hw_timer_init();
// 使能中断
level = rt_hw_interrupt_disable();
rt_hw_interrupt_enable();
// 注册当前核心
rt_cpu_self()->id = smp_processor_id();
rt_cpu_self()->online = 1;
// 启动调度器
rt_scheduler_start();
// 不会执行到这里
while (1);
}
这里有个容易忽略的点:rt_hw_interrupt_disable()和rt_hw_interrupt_enable()之间的代码必须非常短。为什么?因为从核刚启动时,中断控制器可能还没完全配置好,贸然开中断会导致未定义中断触发。我建议在使能中断前,先把所有中断源屏蔽掉,再逐个使能。
启动完成标志: 当所有从核都执行到rt_scheduler_start()后,SMP系统就正式启动了。此时主核和从核都在调度器的管理下,共同处理任务。
11.6 常见问题与调试技巧
最后,分享几个我在调试SMP启动时遇到的典型问题:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 从核不响应唤醒 | 启动地址未对齐 | 检查启动地址是否4字节对齐 |
| 从核启动后死机 | 栈空间不足或重叠 | 检查链接脚本中栈地址分配 |
| 系统跑飞 | 缓存一致性问题 | 添加内存屏障指令 |
| 中断异常 | 中断控制器未初始化 | 确保从核初始化了GIC |
我曾经遇到一个最诡异的问题:从核启动后,偶尔能正常工作,偶尔直接卡死。查了整整两天,最后发现是电源管理单元(PMU)的时序问题——从核上电后需要等待一定时间才能访问外设。嗯,这种硬件相关的坑,只能靠仔细阅读芯片手册来避免。
调试建议: 我习惯在从核启动代码中加一个GPIO翻转的调试点。如果从核执行到了某个位置,GPIO就会输出一个脉冲。这样用逻辑分析仪一看,就知道从核卡在哪一步了。
好了,SMP启动流程就讲到这里。下一章我们会深入分析调度器在多核环境下的实现,看看RT-Thread是如何在多个核心之间分配任务的。