chu_bingzhi头像

ARM中断处理

ARM64 中断的硬件基础

在 ARM 体系里,中断属于异步异常。要理解中断,先要把它和同步异常区分开:

  • 同步异常:由当前正在执行的指令直接触发,比如访问非法内存、执行系统调用(SVC 指令),和指令强绑定,异常发生时 CPU 明确知道是哪条指令引起的。
  • 异步异常(中断):由外部设备主动发信号给 CPU,和当前执行的指令无关,随时可能发生,CPU 事先并不知道它什么时候到来。

中断的作用是让 CPU 能够响应外部设备的请求,比如键盘按下、网卡收到数据、定时器到期。有了中断,CPU 就不必轮询等待设备状态,可以专心执行当前任务,设备有事时再“打断”它,从而大幅提升系统效率。

① 两个核心中断引脚:nIRQ 和 nFIQ

ARM64 处理器有两个直接连到内核的中断输入引脚,对应两种中断类型:

信号类型说明
nIRQ普通中断请求低电平有效,低电平表示有中断请求,高电平表示无请求。所有通用外设的中断,最终都会汇总到这个引脚。
nFIQ快速中断请求同样低电平有效,优先级比 IRQ 更高,用来处理时间敏感的紧急中断(比如安全相关、实时控制)。

小细节:引脚名前面的 n 表示“低电平有效”,也就是信号拉低才表示触发中断。

② PSTATE 寄存器的中断开关

PSTATE 寄存器里有两位专门控制中断的开关,相当于 CPU 的中断总闸:

  • I 位:控制 IRQ 中断的屏蔽 / 打开
    • I=1:屏蔽 IRQ 中断,CPU 不会响应 IRQ
    • I=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)

② 软件处理流程(操作系统要做的事)

硬件跳转到向量表的汇编入口后,操作系统需要完成以下步骤:

步骤 1:保存中断现场

  • 汇编入口里,首先要把所有通用寄存器(X0~X30、SP)和特殊寄存器的值保存到内核栈的 pt_regs 结构体里。
  • 为什么要保存?因为中断处理函数会用到这些寄存器,如果不保存,中断返回后原程序的寄存器值就乱了。

步骤 2:跳转到中断处理函数

  • 汇编里会调用 C 语言的中断处理入口(比如 irq_handler),这里要做两件关键的事:
    • 读 GIC 获取中断号:GIC 会把当前正在处理的中断号放到 ICC_IAR1_EL1 寄存器里,CPU 读取这个寄存器,就能知道是哪个外设触发的中断。
    • 根据中断号分发处理:操作系统里会维护一个中断号到处理函数的映射表(比如 irq_desc),根据中断号找到对应的设备中断处理程序。

步骤 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 TimerEL1 安全模式下的物理定时器CNT_PS_IRQ
PNS 定时器Physical Non-Secure TimerEL1 非安全模式下的物理定时器(我们最常用)CNT_PNS_IRQ
HP 定时器Hypervisor Physical TimerEL2 虚拟化环境下的物理定时器CNT_HP_IRQ
V 定时器Virtual TimerEL1 下的虚拟定时器(配合虚拟化使用)CNT_V_IRQ

教材里以 PNS 定时器(非安全物理定时器)为例,讲解完整的初始化和中断处理流程,因为它是裸机 / 操作系统开发中最常用的定时器。

定时器控制寄存器计数值寄存器中断位 (SOURCE0)
PS 安全物理定时器CNTPS_CTL_EL0CNTPS_TVAL_EL0BIT0
PNS 非安全物理定时器CNTP_CTL_EL0CNTP_TVAL_EL0BIT1
HP 虚拟化物理定时器CNTHP_CTL_EL2CNTHP_TVAL_EL2BIT2
V 虚拟定时器CNTV_CTL_EL0CNTV_TVAL_EL0BIT3

关键寄存器解析

① 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 中断到 IRQ
  • Bit[1]:路由 CNT_PNS_IRQ 中断到 IRQ(我们的例子用的就是这个)
  • Bit[2]:路由 CNT_HP_IRQ 中断到 IRQ
  • Bit[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被打断时的栈指针
PCELR_EL1中断返回地址,CPU 自动保存在 ELR_EL1 里
栈顶(高地址)PSTATESPSR_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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--