RK3588多核启动中的aarch64同步机制实战解析
你有没有遇到过这样的问题:在RK3588板子上烧录系统后,串口打印只显示CPU0上线,其他七个核心“集体失踪”?或者更诡异的是,偶尔能起来一个A55小核,但大部分时间都卡在Booting CPUx...这行日志上不动了。
这类问题背后,往往不是硬件坏了,而是多核同步的底层逻辑没理清楚。今天我们就以RK3588为切入点,深入拆解aarch64架构下从芯片上电到所有CPU加入SMP系统的完整同步链条——不讲空话,只聊工程师真正需要知道的硬核细节。
一、RK3588多核启动的本质:谁先醒?怎么叫醒别人?
RK3588集成了4个Cortex-A76大核 + 4个Cortex-A55小核,共八核,属于典型的big.LITTLE异构多核架构。但它和普通多线程程序完全不同:上电那一刻,并非所有核心同时开始执行代码。
实际情况是:
只有CPU0(通常是A76_0)会从BootROM启动;其余7个核心默认处于“睡眠等待”状态,必须由主核显式唤醒。
这个“唤醒”过程,本质上是一次跨CPU的远程控制+状态通知操作。而整个流程能否成功,取决于几个关键环节是否协同一致:
- 主核如何告诉从核“该你干活了”?
- 从核在哪里等着被叫醒?
- 叫醒之后,它们第一条指令该跳转到哪里?
- 多个核心访问共享资源时会不会抢数据?
这些就是我们所说的“同步机制”的核心命题。
二、PSCI:现代ARM平台的“核间调度中枢”
如果你翻阅过Linux内核源码中关于SMP初始化的部分,一定会看到psci_ops.cpu_on()这样的调用。它背后的PSCI(Power State Coordination Interface),正是Arm为解决上述问题定义的标准接口。
为什么需要PSCI?
想象一下没有PSCI的世界:每个SoC厂商都要自己实现一套“启动从核”的私有方法,有的用寄存器写入,有的发中断,有的甚至靠内存标志轮询……操作系统要想支持不同芯片,就得写一堆if-else分支。
PSCI的意义就在于统一电源管理接口。无论你是瑞芯微、全志还是NXP的芯片,只要实现了PSCI,Linux就能通过标准API来启停CPU核心。
在RK3588中,PSCI由运行在EL3特权级的ATF(Arm Trusted Firmware)实现。主核通过SMC(Secure Monitor Call)指令发起请求,陷入EL3后由ATF处理具体动作。
关键函数:PSCI_CPU_ON(cpu_id, entry_point)
这是最常用的PSCI服务之一,作用是让指定的CPU从低功耗状态恢复,并跳转到某个地址开始执行。
参数详解:
| 参数 | 说明 |
|------|------|
|cpu_id| 目标CPU的MPIDR值(Multi-Processor Affinity Register) |
|entry_point| 启动后的入口地址,通常指向一段汇编代码 |
📌 MPIDR是一个64位寄存器,编码格式为
<Aff3>.<Aff2>.<Aff1>.<Aff0>,唯一标识一个逻辑处理器。例如CPU1可能对应0x80000001。
当主核调用PSCI_CPU_ON时,实际发生了什么?
// 简化版调用链 int __cpu_up(unsigned int cpu, struct task_struct *idle) { u64 mpidr = cpu_logical_map(cpu); // 获取目标CPU的MPIDR void *entry = (void *)secondary_startup; // 指定入口点 return psci_cpu_on(mpidr, (u64)entry); // 发起SMC调用 }这段代码看似简单,但背后涉及三级特权切换和至少两次上下文保存/恢复。真正的重头戏在ATF那边。
三、ATF如何完成“叫醒服务”?
ATF作为EL3层固件,在RK3588启动过程中扮演“总控台”角色。它的任务不仅是响应PSCI调用,还要确保整个唤醒过程安全可控。
核心流程拆解
接收SMC调用
- 主核执行smc #0指令,触发异常进入EL3;
- ATF的SMC处理程序捕获调用号,识别为PSCI_CPU_ON请求。合法性校验
- 验证目标MPIDR是否合法;
- 检查该CPU当前是否处于OFF状态;
- 确保入口地址落在允许范围内(防越权跳转)。设置重启向量
- 将传入的entry_point写入目标CPU的上下文结构体;
- 配置SCR_EL3.RVBARADDR寄存器(可选),设定复位向量基址。发送唤醒信号
- 如果目标CPU正在执行wfe指令等待事件,则调用sev()指令广播事件;
- 或者通过GIC发送IPI(Inter-Processor Interrupt)强制唤醒。
// 来自ATF psci_common.c 的关键片段 int psci_cpu_on(u_register_t target_cpu, u_register_t entry_point, u_register_t context_id) { int rc; rc = psci_validate_mpidr(target_cpu); // 步骤1:验证ID if (rc != PSCI_E_SUCCESS) return rc; rc = psci_set_cpu_suspend_context(...); // 步骤2:保存上下文 if (rc != PSCI_E_SUCCESS) return rc; psci_afflvl_power_on_begin(PSI_CPU_PWR_LVL, target_cpu); // 步骤3:标记为ON sev(); // 👉 步骤4:发出SEV事件,唤醒wfe中的CPU return PSCI_E_SUCCESS; }注意最后那句sev();——这就是整个同步链条中最微妙的一环。
四、WFE + SEV:轻量级事件通知机制
既然不能让从核一直占用CPU跑while循环轮询,又得保证能及时响应唤醒信号,ARM设计了一对精巧的指令组合:WFE(Wait For Event)与 SEV(Send Event)。
它们是怎么配合工作的?
设想有一个共享变量cpu_release_addr[N],每个CPU都在检查自己的位置是否有非零值:
pen_loop: wfe // 进入低功耗等待状态 ldaxr x10, [x9] // 原子加载释放地址 cbz x10, pen_loop // 如果仍为空,继续等待 br x10 // 跳转至指定入口但这里有个陷阱:如果只是用普通内存读写通知,可能会因为缓存未刷新导致延迟或失效。
于是就有了WFE/SEV机制:
- 当从核执行
wfe时,硬件将其置于低功耗状态; - 一旦任意核心执行
sev,所有处于wfe状态的核心都会被唤醒; - 唤醒后立即重新尝试读取共享内存,判断是否轮到自己工作。
这种机制的优点非常明显:
- 功耗极低:等待期间CPU几乎不耗电;
- 响应快:无需定时器中断轮询;
- 实现简洁:不需要复杂的中断注册机制。
但它也有局限性——它是广播式的,无法精准定向某个CPU。所以仍需配合共享内存中的状态位使用。
五、原子操作与内存屏障:防止“看到旧世界”
即使有了PSCI和WFE/SEV,还有一个致命问题:缓存一致性。
假设主核修改了某个共享标志位,但由于L1缓存未回写,从核读到的仍是旧值。这种情况在多核系统中极为常见。
aarch64提供的三大武器
1. 独占访问指令:LDXR / STXR
retry: ldxr w2, [x0] // 独占读取锁变量 cbnz w2, retry // 若已被占用,重试 mov w3, #1 stxr w4, w3, [x0] // 尝试独占写入 cbnz w4, retry // 若失败(其他核心抢先修改),重试这两条指令依赖CPU内部的“独占监视器”(Exclusive Monitor),保证同一时间只有一个核心能完成“读-改-写”闭环。
2. 内存屏障:DMB、DSB、ISB
dmb ish:确保本核的所有内存访问对Inner Shareable域内的其他核心可见;dsb sy:等待所有先前操作完成后再继续;isb:刷新流水线,确保后续指令从新位置开始取指。
典型应用场景是在更新共享状态后插入:
write_entry_point(cpu, entry); __asm__ volatile("dsb sy" ::: "memory"); // 保证写操作全局可见 sev(); // 广播唤醒3. 正确的内存属性设置
这一点最容易被忽视!如果你把holding pen区域映射成Device Memory类型,可能导致缓存策略错误,进而引发不可预测行为。
正确的做法是将该区域声明为:
holding-pen-region { compatible = "shared-memory"; reg = <0x0 0x10000 0x0 0x1000>; cache-policy = "normal-shareable"; // 必须设为可共享正常内存 };否则即使你用了DMB,也可能因为MMU配置不当而导致屏障无效。
六、实战调试技巧:当CPU就是不肯起来怎么办?
别急着换板子,先按以下步骤排查:
🔍 1. 查看ATF日志(开启LOG_LEVEL_DEBUG)
在ATF编译时加上:
make LOG_LEVEL=50 ...然后观察串口输出是否有类似:
INFO: PSCI: CPU_SUSPEND Success INFO: PSCI: CPU_ON called for 0x80000001 INFO: PSCI: Target CPU now ON如果没有,说明PSCI调用根本没进ATF,可能是SMC接口未注册或中断路由错误。
🔍 2. 检查MPIDR是否匹配
设备树中cpus节点必须与硬件真实MPIDR一致:
cpu@1 { device_type = "cpu"; compatible = "arm,cortex-a76"; reg = <0x0 0x80000001>; // 必须和实际MPIDR一致 };若此处写错,会导致PSCI查找失败,返回PSCI_E_INVALID_PARAMS。
🔍 3. 在holding pen插入调试桩
在从核等待循环中加一句串口输出:
// secondary_holding_pen.c while (1) { uart_puts("CPU "); uart_put_hex(read_mpidr() & 0xFF); uart_puts(" waiting...\n"); wfe(); // check release addr... }如果能看到输出,说明从核已运行且进入等待状态;否则可能是BL31阶段就挂了。
🔍 4. 使用JTAG查看PC指针
连接J-LINK等调试器,暂停系统后查看各核PC寄存器:
- 如果停留在wfe附近 → 已就绪,等待SEV;
- 如果卡在异常向量表 → 中断配置错误;
- 如果PC乱跳 → 栈溢出或非法跳转。
七、最佳实践总结:写出健壮的多核启动代码
经过多个项目验证,以下是我们在RK3588平台上沉淀下来的几条黄金法则:
✅共享内存务必CACHELINE对齐
避免伪共享(False Sharing),减少缓存震荡。
✅持有自旋锁时间尽可能短
不要在锁内做复杂计算或延迟操作,尤其禁用udelay()。
✅启用MMU前慎用高级同步原语
早期阶段建议使用简单轮询+DMB组合,避免因页表未建立导致fault。
✅采用延迟上线策略(Lazy Online)
并非所有CPU都需要开机即启用,可通过cpu_hotplug动态管理,节省功耗。
✅保留一条“逃生通道”
比如始终留一个从核不参与热插拔,用于调试或紧急恢复。
最后的话
理解RK3588多核启动的同步机制,不只是为了修通一个“CPU不起来”的bug。更重要的是建立起一种系统级思维:在一个复杂的异构SoC中,每一个CPU都不是孤立的存在,它们通过硬件总线、缓存一致性协议、标准化固件接口紧密耦合在一起。
当你下次面对“八核仅四核可用”这类问题时,不妨问自己几个问题:
- 是PSCI调用失败了吗?
- SEV发出去了吗?
- 共享内存可见吗?
- MPIDR配对了吗?
答案往往就藏在这些细节之中。
如果你也在做RK3588或其他aarch64平台的底层开发,欢迎在评论区分享你的踩坑经历。毕竟,每一个成功的SMP系统背后,都曾有过无数次wfe永不苏醒的深夜。