ARM64 中断的硬件基础
在 ARM 体系里,中断属于异步异常。要理解中断,先要把它和同步异常区分开:
- 同步异常:由当前正在执行的指令直接触发,比如访问非法内存、执行系统调用(SVC 指令),和指令强绑定,异常发生时 CPU 明确知道是哪条指令引起的。
- 异步异常(中断):由外部设备主动发信号给 CPU,和当前执行的指令无关,随时可能发生,CPU 事先并不知道它什么时候到来。
中断的作用是让 CPU 能够响应外部设备的请求,比如键盘按下、网卡收到数据、定时器到期。有了中断,CPU 就不必轮询等待设备状态,可以专心执行当前任务,设备有事时再“打断”它,从而大幅提升系统效率。
① 两个核心中断引脚:nIRQ 和 nFIQ
ARM64 处理器有两个直接连到内核的中断输入引脚,对应两种中断类型:
| 信号 | 类型 | 说明 |
|---|---|---|
| nIRQ | 普通中断请求 | 低电平有效,低电平表示有中断请求,高电平表示无请求。所有通用外设的中断,最终都会汇总到这个引脚。 |
| nFIQ | 快速中断请求 | 同样低电平有效,优先级比 IRQ 更高,用来处理时间敏感的紧急中断(比如安全相关、实时控制)。 |
小细节:引脚名前面的 n 表示“低电平有效”,也就是信号拉低才表示触发中断。
② PSTATE 寄存器的中断开关
PSTATE 寄存器里有两位专门控制中断的开关,相当于 CPU 的中断总闸:
- I 位:控制 IRQ 中断的屏蔽 / 打开
I=1:屏蔽 IRQ 中断,CPU 不会响应 IRQI=0:打开 IRQ 中断,CPU 可以响应 IRQ
- F 位:控制 FIQ 中断的屏蔽 / 打开
F=1:屏蔽 FIQ 中断F=0:打开 FIQ 中断
操作系统里的 local_irq_enable() / local_irq_disable(),本质上就是通过修改 PSTATE 的 I 位来开关中断。
③ 中断控制器:GIC(Generic Interrupt Controller)
现代 SoC(比如树莓派 4B 的 BCM2711)外设越来越多,CPU 内核不可能直接连每个外设,所以需要一个中断控制器(GIC)来做“中转站”。
GIC 的核心作用:
- 汇总中断:把所有外设的中断请求收集起来
- 分发中断:根据优先级、亲和性,把中断请求分发到不同 CPU 核心
- 仲裁优先级:多个中断同时来的时候,决定先处理哪个
- 屏蔽 / 使能:可以单独屏蔽某个外设的中断
树莓派 4B 用的就是 GIC-400,这是 ARM 标准的中断控制器,几乎所有 ARMv8 服务器和嵌入式设备都在用。
GIC 和 CPU 的连接方式:
- 每个 CPU 核心都有自己的 CPU 接口
- GIC 通过 nIRQ 和 nFIQ 引脚,把分发后的中断信号发给对应 CPU 核心
- 外部中断源(外设)都连到 GIC 的分发单元
中断处理的完整流程
① 硬件自动做的事(中断发生时)
当中断信号拉低,CPU 检测到中断请求,会自动完成以下操作,不需要软件干预:
- 保存返回地址:把当前的 PC 值(下一条要执行的指令地址)存到
ELR_ELx寄存器 - 保存状态:把当前的 PSTATE 寄存器值存到
SPSR_ELx寄存器 - 屏蔽中断:自动把 PSTATE 的 I/F 位置 1,防止嵌套中断打断当前处理(也叫“关中断”)
- 跳转向量表:根据当前异常等级和执行状态,跳转到异常向量表中对应的中断入口
- 如果中断发生在用户态(EL0),会跳转到
VBAR_EL1 + 0x480(el0_irq) - 如果中断发生在内核态(EL1),会跳转到
VBAR_EL1 + 0x280(el1_irq)
- 如果中断发生在用户态(EL0),会跳转到
② 软件处理流程(操作系统要做的事)
硬件跳转到向量表的汇编入口后,操作系统需要完成以下步骤:
步骤 1:保存中断现场
- 汇编入口里,首先要把所有通用寄存器(X0~X30、SP)和特殊寄存器的值保存到内核栈的
pt_regs结构体里。 - 为什么要保存?因为中断处理函数会用到这些寄存器,如果不保存,中断返回后原程序的寄存器值就乱了。
步骤 2:跳转到中断处理函数
- 汇编里会调用 C 语言的中断处理入口(比如
irq_handler),这里要做两件关键的事:- 读 GIC 获取中断号:GIC 会把当前正在处理的中断号放到
ICC_IAR1_EL1寄存器里,CPU 读取这个寄存器,就能知道是哪个外设触发的中断。 - 根据中断号分发处理:操作系统里会维护一个中断号到处理函数的映射表(比如
irq_desc),根据中断号找到对应的设备中断处理程序。
- 读 GIC 获取中断号:GIC 会把当前正在处理的中断号放到
步骤 3:设备中断处理程序
- 不同外设的中断处理逻辑完全不同,比如:
- 定时器中断:更新系统时间、触发调度器
- 网卡中断:读取接收的数据、发送下一个数据包
- 键盘中断:读取按键扫描码、转换为字符输入
- 处理完中断后,需要写回 GIC 的
ICC_EOIR1_EL1寄存器,表示中断处理完成,GIC 可以清除中断状态,准备下一次请求。
④ 步骤 4:恢复中断现场,返回原程序
- 中断处理完成后,回到汇编入口,把之前保存在栈里的寄存器值恢复,然后执行
ERET指令:ERET会自动把ELR_ELx的值恢复到 PC,把SPSR_ELx的值恢复到 PSTATE- CPU 回到被中断打断的指令,继续执行
树莓派4B上的传统中断控制器
树莓派 4B(BCM2711)内置了两套中断控制器,但不能同时使用:
- 传统中断控制器:基于寄存器的简单中断路由,是树莓派老平台(BCM2837)的兼容方案,通过读取 / 写入内存映射寄存器来管理中断。
- GIC-400:ARM 标准的通用中断控制器,是树莓派 4B 默认启用的方案,支持多核分发、优先级管理,现代操作系统(如 Linux)都用它。
这里讲一下传统中断控制器,它是一个“简化版”的中断控制器,理解它的工作原理,为后续学习 GIC-400 的铺垫一下基础。
5 组中断源
树莓派 4B 的中断源按来源分成 5 组,全部汇总到传统中断控制器,再分发到 4 个 CPU 核心的 nIRQ/nFIQ 引脚:
| 中断组 | 来源 | 说明 |
|---|---|---|
| ARM 核心中断组 | CPU 内核内部 | PS/PNS/HP/V 定时器中断、PMU 性能监控中断 |
| ARM_LOCAL 中断组 | CPU 本地外设 | 邮箱中断、本地定时器中断、AXI 总线错误中断,只有 CPU 能访问 |
| ARMC 中断组 | CPU/GPU 共享外设 | 定时器、邮箱、DoorBell、VPU 错误中断,CPU 和 GPU 都能访问 |
| VideoCore 中断组 | GPU 外设触发 | 共 62 个 VC 中断,比如 GPU 渲染、视频解码完成中断 |
| PCIe 中断组 | PCIe 设备触发 | 共 57 个 PCIe 中断,比如网卡、USB 控制器中断 |

寄存器链路由
传统中断控制器没有复杂的分发逻辑,完全靠状态寄存器的串联路由来判断中断源,核心是 3 类寄存器:
① 中断源寄存器(IRQ_SOURCE0~3)
每个 CPU 核心都有一组 IRQ_SOURCE 寄存器(IRQ_SOURCE0 对应 CPU0,以此类推),用来快速判断中断来自哪个组。以 IRQ_SOURCE0 为例,其中:
Bit[0:7]:对应 ARM 核心 / ARM_LOCAL / ARMC 中断组的中断Bit[8]:表示 VideoCore 中断组有中断触发Bit[9]:表示 PMU 中断触发Bit[11]:表示本地定时器中断触发Bit[30]:表示 AXI 总线错误中断触发
② 中断待定寄存器(PENDING0/1/2)
当中断发生时,对应的待定寄存器会把中断位置 1,用来标记“哪个具体中断源触发了请求”:
PENDING0:对应 VideoCore 中断组的 0~31 号中断PENDING1:对应 VideoCore 中断组的 32~63 号中断PENDING2:对应 ARM_LOCAL / ARMC 中断组的中断,包括定时器、邮箱、DoorBell 等
这些寄存器的串联逻辑如下:
- 先读
IRQ_SOURCE0,判断中断来自哪个组 - 如果
IRQ_SOURCE0[8]为 1,说明中断来自 VideoCore 组,需要继续读PENDING0/PENDING1 - 如果
IRQ_SOURCE0的其他位为 1,说明中断来自 ARM 本地 / 共享外设,需要读PENDING2
ARM内核上的通用定时器
4 个通用定时器
ARMv8-A 架构的 Cortex-A72 内核,内置了 4 个不同用途的通用定时器,每个定时器都有独立的中断源,对应不同的异常等级和使用场景:
| 定时器 | 全称 | 用途 | 中断源 |
|---|---|---|---|
| PS 定时器 | Physical Secure Timer | EL1 安全模式下的物理定时器 | CNT_PS_IRQ |
| PNS 定时器 | Physical Non-Secure Timer | EL1 非安全模式下的物理定时器(我们最常用) | CNT_PNS_IRQ |
| HP 定时器 | Hypervisor Physical Timer | EL2 虚拟化环境下的物理定时器 | CNT_HP_IRQ |
| V 定时器 | Virtual Timer | EL1 下的虚拟定时器(配合虚拟化使用) | CNT_V_IRQ |
教材里以 PNS 定时器(非安全物理定时器)为例,讲解完整的初始化和中断处理流程,因为它是裸机 / 操作系统开发中最常用的定时器。
| 定时器 | 控制寄存器 | 计数值寄存器 | 中断位 (SOURCE0) |
|---|---|---|---|
| PS 安全物理定时器 | CNTPS_CTL_EL0 | CNTPS_TVAL_EL0 | BIT0 |
| PNS 非安全物理定时器 | CNTP_CTL_EL0 | CNTP_TVAL_EL0 | BIT1 |
| HP 虚拟化物理定时器 | CNTHP_CTL_EL2 | CNTHP_TVAL_EL2 | BIT2 |
| V 虚拟定时器 | CNTV_CTL_EL0 | CNTV_TVAL_EL0 | BIT3 |
关键寄存器解析
① CNTP_CTL_EL0:定时器控制寄存器
这是定时器的“总开关”,3 个关键字段:
- ENABLE (Bit[0]):定时器使能位
1:开启定时器,计数器开始递减0:关闭定时器,计数器停止
- IMASK (Bit[1]):中断屏蔽位
0:不屏蔽中断,计数器到 0 时会触发中断1:屏蔽中断,即使计数器到 0 也不会触发中断
- ISTATUS (Bit[2]):中断状态位(只读)
1:表示定时器已经到期,中断处于待处理状态0:表示定时器还在运行,未到期
② CNTP_TVAL_EL0:定时器初值寄存器
- 位宽:32 位(低 32 位有效,高 32 位保留)
- 作用:设置定时器的初值 TimerValue
- 工作方式:计数器会从 TimerValue 开始,以固定频率递减,当减到 0 时触发中断
比如设置 TimerValue=1000000,如果定时器频率是 1MHz,就会 1 秒后触发中断。
③ TIMER_CNTRLn:树莓派 4B 定时器路由寄存器
这是树莓派 4B 特有的寄存器,每个 CPU 内核有一个(TIMER_CNTRL0 对应 CPU0),用来把 4 个定时器的中断路由到 IRQ 引脚:
Bit[0]:路由CNT_PS_IRQ中断到 IRQBit[1]:路由CNT_PNS_IRQ中断到 IRQ(我们的例子用的就是这个)Bit[2]:路由CNT_HP_IRQ中断到 IRQBit[3]:路由CNT_V_IRQ中断到 IRQ
定时器初始化完整流程
步骤 1:设置 CNTP_CTL_EL0 的 ENABLE=1
msr CNTP_CTL_EL0, x0 ; x0的Bit0设为1,开启定时器
作用:打开定时器的总开关,让计数器开始工作。注意此时中断还没使能,需要配合后面的步骤。
步骤 2:设置 CNTP_TVAL_EL0 的 TimerValue
ldr x0, =1000000 ; 初值设为1000000(1秒,假设1MHz频率)
msr CNTP_TVAL_EL0, x0
作用:给定时器设置一个初始的计数值,让它从这个值开始递减。初值的大小,决定了定时器多久后触发中断。
步骤 3:设置 TIMER_CNTRL0 的 CNT_PNS_IRQ=1
ldr x0, =(1 << 1) ; Bit1设为1,路由PNS定时器中断到IRQ
msr TIMER_CNTRL0, x0
作用:把 PNS 定时器的中断信号,路由到 CPU 的 nIRQ 引脚,让 CPU 能收到中断请求。
步骤 4:打开 PSTATE 寄存器的 IRQ 总开关
msr DAIFClr, #(1 << 1) ; 清除PSTATE的I位,打开IRQ中断
作用:打开 CPU 的中断总闸,允许 CPU 响应 IRQ 中断。之前我们保存现场时会把 PSTATE 的 I 位置 1 屏蔽中断,这里初始化时需要打开。
定时器中断处理完整流程
步骤 1:定时器到期,触发中断
CNTP_TVAL_EL0 减到 0 → CNTP_CTL_EL0 的 ISTATUS 位被置 1,定时器中断信号通过 TIMER_CNTRL0 路由到 nIRQ 引脚 → CPU 响应中断。
步骤 2:跳转到 el1_irq 汇编入口
CPU 跳转到异常向量表中 VBAR_EL1 + 0x280 对应的 el1_irq 入口,这是内核态下 IRQ 中断的固定入口。
步骤 3:保存中断上下文
汇编里通过 kernel_entry 宏,把 X0~X30、SP 等寄存器保存到内核栈的 pt_regs 结构体里。目的:保存现场,保证中断返回后原程序能继续运行。
步骤 4:跳转到中断处理函数
汇编里调用 C 语言的 irq_handler 函数,进入中断处理逻辑。
步骤 5:读取 IRQ_SOURCE0 寄存器
uint32_t irq_source = readl(IRQ_SOURCE0);
作用:判断中断来源,看是哪个外设触发的中断。IRQ_SOURCE0[Bit1] 对应 CNT_PNS_IRQ 中断。
步骤 6:判断是否为 PNS 定时器中断
if (irq_source & (1 << 11)) { // 本地定时器中断对应IRQ_SOURCE0的Bit11
// 处理定时器中断
}
作用:确认是定时器中断,不是其他外设的中断。
步骤 7:重新设置 CNTP_TVAL_EL0 的 TimerValue
ldr x0, =1000000
msr CNTP_TVAL_EL0, x0
作用:重置定时器初值,让定时器重新开始计数,实现周期性中断。如果不重置,定时器会一直停在 0,不会再次触发中断。
步骤 8:返回 el1_irq 汇编函数
回到汇编入口,准备恢复现场。
步骤 9:恢复中断上下文
把之前保存在内核栈里的 pt_regs 结构体恢复到寄存器中。
步骤 10:执行 ERET 指令,返回中断现场
ERET 会把 ELR_EL1 的值恢复到 PC,把 SPSR_EL1 的值恢复到 PSTATE,CPU 回到被打断的程序,继续执行。
中断现场
中断发生时,CPU 会突然从当前正在执行的程序(比如用户进程、内核线程)切换到中断处理程序。为了让中断处理完之后,程序能完全回到被打断的那一刻继续运行,必须把当时所有关键的状态信息保存下来,这就叫中断现场。
ARM64 里,需要保存的核心信息就是这 4 类:
- PSTATE 寄存器:CPU 的状态标志(中断屏蔽位、条件码、异常等级等)
- PC 值:被打断时,下一条要执行的指令地址(中断返回地址)
- SP 值:被打断时的栈指针,记录原程序的栈使用情况
- 通用寄存器 X0~X30:程序运行时的变量、参数、返回值都存在这里
struct pt_regs,就是为了统一管理这些信息而定义的栈帧结构。
pt_regs 栈帧
| 栈帧位置 | 字段名 | 对应寄存器 | 含义 |
|---|---|---|---|
| 栈底(低地址) | regs[0] | X0 | 通用寄存器 X0 的值 |
| ... | regs[1]~regs[29] | X1~X29 | 通用寄存器 X1~X29 的值 |
regs[30] | X30(LR) | 链接寄存器,保存函数返回地址 | |
| SP | 原程序的 SP | 被打断时的栈指针 | |
| PC | ELR_EL1 | 中断返回地址,CPU 自动保存在 ELR_EL1 里 | |
| 栈顶(高地址) | PSTATE | SPSR_EL1 | 被打断时的 PSTATE 状态,CPU 自动保存在 SPSR_EL1 里 |
大小计算:每个寄存器 8 字节,一共 31 个通用寄存器(X0~X30)+ SP + PC + PSTATE,总共 34 个寄存器,34 × 8 = 272 字节,和教材里标注的一致。
中断现场保存流程
当中断发生时,CPU 先自动做两件事:
- 把返回地址存到
ELR_EL1 - 把当前 PSTATE 存到
SPSR_EL1
然后跳转到向量表的 el1_irq 汇编入口,开始手动保存现场:
步骤 1:分配栈帧空间
sub sp, sp, #272 // 在当前内核栈上分配 272 字节的 pt_regs 栈帧
这一步把 SP 指针向下移动 272 字节,为栈帧腾出空间。
步骤 2:保存通用寄存器 X0~X30
stp x0, x1, [sp, #0]
stp x2, x3, [sp, #16]
// ... 依次保存到 x29, x30
stp x29, x30, [sp, #232]
按照栈帧布局,把 X0~X30 依次存到栈里的 regs[0]~regs[30] 位置。
步骤 3:保存原程序的 SP
mov x0, sp
add x0, x0, #272 // 计算保存现场前的 SP 值(栈帧分配前的 SP)
str x0, [sp, #240]
这里的 SP_TOP 就是分配栈帧之前的 SP 值,也就是原程序被打断时的栈指针。
步骤 4:保存 PC 和 PSTATE
mrs x0, elr_el1 // 读中断返回地址(PC)
str x0, [sp, #248]
mrs x0, spsr_el1 // 读被打断时的 PSTATE
str x0, [sp, #256]
把 CPU 自动保存到 ELR_EL1 和 SPSR_EL1 的值,存到栈帧里的 PC 和 PSTATE 字段。
保存完成后,整个中断现场就完整地存在内核栈的 pt_regs 结构里了,后续的中断处理函数就可以安全地使用所有寄存器,不用担心破坏原程序的状态。
中断现场恢复流程
中断处理完成后,需要把栈帧里的现场恢复到 CPU 寄存器,然后返回原程序。
步骤 1:恢复 PSTATE 和 PC
ldr x0, [sp, #256]
msr spsr_el1, x0 // 恢复 PSTATE
ldr x0, [sp, #248]
msr elr_el1, x0 // 恢复中断返回地址
把栈帧里的 PSTATE 和 PC 写回 SPSR_EL1 和 ELR_EL1,为 ERET 指令做准备。
步骤 2:恢复原程序的 SP
ldr x0, [sp, #240]
mov x1, x0
读取栈帧里的 SP_TOP,保存到临时寄存器,等通用寄存器恢复完再写回。
步骤 3:恢复通用寄存器 X0~X30
ldp x0, x1, [sp, #0]
ldp x2, x3, [sp, #16]
// ... 依次恢复 x29, x30
ldp x29, x30, [sp, #232]
按照栈帧布局,把 regs[0]~regs[30] 恢复到 X0~X30 寄存器。
步骤 4:恢复 SP 并释放栈帧
mov sp, x1 // 恢复原程序的 SP 值
add sp, sp, #272 // 释放栈帧空间
把保存的 SP_TOP 写回 SP,并把栈指针向上移动 272 字节,释放栈帧。
步骤 5:执行 ERET 指令
eret
ERET 会自动:
- 从
ELR_EL1恢复 PC,跳回被打断的指令 - 从
SPSR_EL1恢复 PSTATE,恢复中断屏蔽位和异常等级
至此,CPU 就完全回到了被中断打断的那一刻,继续执行原程序。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_52240295/article/details/168866272



