Raspberry Pi 3B 的 AArch64 内核如何正确切换到 EL1?
直接跳到 el_check_el1 不会切换 Exception Level。如果入口处于 EL2,这条分支只会让程序继续在 EL2 执行。进入 EL1 需要配置目标执行状态和运行环境,再通过 eret 完成切换。
现有代码中,HCR_EL2.RW 和 SPSR_EL2 = 0x3c5 的设置与返回 AArch64 EL1h 的目标一致,但缺少明确的 SCTLR_EL1 初始化和 EL1 的 FP/SIMD 访问配置。仅凭灰屏无法确定故障点,也无法确认 framebuffer 初始化是否完整执行。
先确认切换前后的 CurrentEL
当前的无条件分支会跳过所有切换代码:
b el_check_el1
调试实际的切换路径时,需要移除这条分支,恢复根据 CurrentEL 分派的流程。CurrentEL 的原始值中,EL1 对应 0x4,EL2 对应 0x8;右移两位后分别是 1 和 2。
进入 C 后,可以读取当前级别:
static inline unsigned current_el(void)
{
unsigned long value;
__asm__ volatile("mrs %0, CurrentEL" : "=r"(value));
return (unsigned)((value >> 2) & 3UL);
}
通过 UART 或调试器检查结果。原来的 boot_el 只记录进入切换流程之前的级别,多个核心又会写入同一个变量,因此不能用它确认每个核心切换后的状态。
eret 从 SPSR_EL2 恢复目标状态,并从 ELR_EL2 取得目标地址。SPSR_EL2 指定的 AArch64 状态还必须与 HCR_EL2.RW 一致。Arm 异常模型说明
补齐 EL1 的运行环境
EL2 和 EL1 使用各自的系统控制寄存器。代码能在 EL2 运行,不能说明进入 EL1 后的内存访问、栈和指令权限也已配置正确。
需要检查以下设置:
SCTLR_EL1:明确设置 EL1 的 MMU、缓存、字节序等状态,不能依赖未确认的启动值。CPACR_EL1.FPEN:如果 C 代码或库函数会使用 FP/SIMD 指令,需要允许相应访问。CPTR_EL2:控制低级别的 FP/SIMD 等访问是否陷入 EL2,并按处理器要求处理保留位。SP_EL1:EL1h 使用该栈指针,调用 C 前必须为当前核心设置有效且满足 16 字节对齐要求的栈。VBAR_EL1:安装有效的异常向量表,以便在异常发生时定位原因。
Cortex-A53 的 CPTR_EL2 有必须写为 1 的 RES1 位,因此需要调整原来的 msr cptr_el2, xzr。在该处理器上,可使用 0x33ff 保留这些位,同时清除 TFP 和 TCPAC。Cortex-A53 技术参考手册
修改启动汇编的示例
以下片段用于 Raspberry Pi 3B 的 Cortex-A53,假设当前启动环境采用小端、物理地址执行,且不依赖已经启用的 MMU 或缓存。该片段说明基本切换路径,尚未包含异常向量表实现,也没有经过该项目的硬件验证。
可以替换原来的 el_check、el_check_el2,新增 el1_setup,并保留原有的 el_check_el1 核心分派和各个 entryN_start:
el_check:
mrs x0, CurrentEL
lsr x0, x0, #2
ldr x2, =boot_el
str x0, [x2]
cmp x0, #2
beq el_check_el2
cmp x0, #1
beq el1_setup
b hold
el_check_el2:
// EL1 executes AArch64; stage 2 translation is disabled.
mov x0, #(1 << 31)
msr hcr_el2, x0
// Permit EL1 access to the physical counter and timer.
mrs x0, cnthctl_el2
orr x0, x0, #3
msr cnthctl_el2, x0
msr cntvoff_el2, xzr
// Cortex-A53 RES1 bits; no FP/SIMD trap to EL2.
mov x0, #0x33ff
msr cptr_el2, x0
// Cortex-A53: required RES1 bits, little endian,
// MMU and instruction/data caches disabled.
ldr x0, =0x30d00800
msr sctlr_el1, x0
// Mask D, A, I, F; return to AArch64 EL1h.
mov x0, #0x3c5
msr spsr_el2, x0
adr x0, el1_setup
msr elr_el2, x0
isb
eret
el1_setup:
// Select SP_EL1. entryN_start sets its value below.
msr spsel, #1
// Permit FP/SIMD accesses at EL1 and EL0.
mrs x0, cpacr_el1
orr x0, x0, #(3 << 20)
msr cpacr_el1, x0
isb
// x1 still contains the core index.
b el_check_el1
这里继续使用原来的 entryN_start 设置每个核心的栈。在抵达对应的 mov sp, x0 之前,不应插入函数调用或任何使用栈的代码。若要处理这段早期代码本身发生的异常,应进一步提前设置每核 SP_EL1 并安装异常向量表。
上述 SCTLR_EL1 和 CPTR_EL2 常量针对这里的处理器与启动假设。已有地址映射、缓存或其他执行状态时,需要按实际环境配置,不能直接套用。
为什么没有浮点运算也要检查 FP/SIMD
即使 C 源码没有浮点表达式,编译器仍可能使用 SIMD 寄存器完成结构体复制、清零或其他优化。仅清除 EL2 的相关陷阱,不能说明 EL1 的 CPACR_EL1.FPEN 已经允许访问。
如果异常记录显示 ESR_EL1.EC = 0x07,应检查故障指令和 FP/SIMD 访问配置。这个异常类别与相关访问陷阱有关。Arm ESR_EL1 寄存器说明
也可以暂时用 GCC 的 -mgeneral-regs-only 编译相关 C 代码,辅助判断故障是否与编译器生成的 FP/SIMD 指令有关。该选项不会约束手写汇编,预编译库也需要单独检查。GCC AArch64 编译选项
用异常信息定位灰屏
建议先只运行核心 0,暂缓写入其他核心的启动地址和执行 sev,等单核路径稳定后再恢复多核启动。每个核心都需要单独执行相关系统寄存器初始化。
在 eret 前、EL1 入口、kernel() 入口,以及 lfb_init()、sd_init()、fs_mount() 前后添加 UART 标记,先找出程序最后执行到的位置。
同时安装按 2 KiB 对齐的 EL1 异常向量表。发生同步异常时,至少记录:
ESR_EL1:异常类别及附加信息。ELR_EL1:发生异常的指令地址。FAR_EL1:对相应地址异常有效的故障地址。SPSR_EL1:异常发生前的处理器状态。
0x3c5 虽然屏蔽了 D、A、I、F 对应的异常类别,但不会阻止同步异常。如果仍有陷入 EL2 的可能,也应准备 EL2 的异常记录。
kernel.c 是否需要修改
已展示的 kernel.c 没有明确的 EL2 专用系统寄存器访问,暂时没有依据要求重写它。可以先修正启动汇编,再结合异常地址检查实际调用到的驱动和底层函数。
如果故障指令访问了 EL2 专用寄存器,需要将该操作保留在 EL2 初始化阶段,或改用 EL1 可用的机制;如果是内存访问异常,则应检查 EL1 的映射、地址和设备访问属性。拿到切换后的 CurrentEL、最后一个执行标记和异常记录后,才能判断灰屏的具体原因。
备注:内容仅供参考。
