目录
[阶段 1:上电 → 复位处理(_reset_handler)](#阶段 1:上电 → 复位处理(_reset_handler))
[阶段 2:main 里做了什么](#阶段 2:main 里做了什么)
[阶段 3:按键按下 → 真正中断触发(硬件自动做)](#阶段 3:按键按下 → 真正中断触发(硬件自动做))
[阶段 4:进入汇编 _irq_handler](#阶段 4:进入汇编 _irq_handler)
[阶段 5:C 语言中断分发函数(完整代码)](#阶段 5:C 语言中断分发函数(完整代码))
[阶段 6:从中断 C 函数返回汇编](#阶段 6:从中断 C 函数返回汇编)
[告诉 GIC:中断处理完了](#告诉 GIC:中断处理完了)
[五、中断服务函数表 vs 异常向量表(彻底区分)](#五、中断服务函数表 vs 异常向量表(彻底区分))
[六、最容易踩的 7 个坑(必须注意)](#六、最容易踩的 7 个坑(必须注意))
一、中断源三大分类
-
SGI(Software Generated Interrupt)软件触发中断
- 中断号:0~15
- 来源:软件写寄存器触发,不是硬件
- 用途:多核 CPU 间相互打断、任务调度
-
PPI(Private Peripheral Interrupt)私有外设中断
- 中断号:16~31
- 来源:每个 CPU 自己独有的硬件
- 例如:CPU 内部定时器、CPU 看门狗
-
SPI(Shared Peripheral Interrupt)共享外设中断
- 中断号:32~159
- 来源:所有外部设备(GPIO、UART、LCD、定时器等)
- 本次学习的中断 GPIO1_IO18 就属于这一类
二、相关专业名词解析
| 名词 | 全称 | 核心作用 |
|---|---|---|
| IRQ | 普通中断请求 | 所有外设共用唯一硬件中断入口 |
| GIC | 通用中断控制器 | 管理 160 个中断、提供中断号、接收结束信号 |
| VBAR | 向量基地址寄存器 | 告诉 CPU 向量表存放地址(0x87800000) |
| CPSR | 当前程序状态寄存器 | 保存 CPU 模式、中断开关、状态标志 |
| SPSR | 备份程序状态寄存器 | 中断时自动保存 CPSR,返回时恢复 |
| LR(R14) | 链接寄存器 | 保存中断返回地址,需硬件修正 |
| PC(R15) | 程序计数器 | 指向当前取指指令,中断时强制跳转 |
| CP15 | 系统协处理器 | 配置 VBAR、读取 GIC 基地址、控制 Cache |
三、异常向量表
.global _start
_start:
ldr pc, =_reset_handler // 0x00 复位
ldr pc, =_undef_handler // 0x04 未定义指令
ldr pc, =_software_handler // 0x08 软中断 SWI
ldr pc, =_prefetch_abort_handler // 0x0C 取指令失败
ldr pc, =_data_abort_handler // 0x10 读/写数据失败
nop // 0x14 保留
ldr pc, =_irq_handler // 0x18 所有外设中断统一入口
ldr pc, =_fiq_handler // 0x1C 快速中断
关键点(极其重要)
- 这 8 个入口是 ARM 架构硬件规定死的,顺序不能变
- 每个入口占 4 字节
- 所有外部设备(GPIO、UART 等)全部共用同一个入口:0x18 的 irq_handler
- 硬件只认这张表,不认你的 C 语言中断表
四、中断完整真实流程
阶段 1:上电 → 复位处理(_reset_handler)
_reset_handler:
cpsid i // 关中断,初始化期间不允许被打断
ldr sp, =0x81000000 // 设置 SVC 模式栈
// 配置 CP15:打开 I-Cache、打开向量表重映射
mrc p15, 0, r1, c1, c0, 0
orr r1, r1, #< 12) // 打开 I-Cache
bic r1, r1, #(< 13) // 清除 V 位,让 VBAR 生效
mcr p15, 0, r1, c1, c0, 0
cps #0x12 // 切换到 IRQ 模式
ldr sp, =0x82000000 // 设置 IRQ 模式专用栈
cps #0x1f // 切换到 System 模式
ldr sp, =0x83000000 // 设置 C 语言用的栈
cpsie i // 打开总中断!
bl main // 进入 C 语言 main
- 关中断 :初始化不能被打断
- 设置独立栈 :给每个模式设置独立栈
- SVC 栈:0x81000000
- IRQ 栈:0x82000000
- System 栈:0x83000000
- 配置 CP15:打开 VBAR 重映射,清除 SCTLR.V (bit13),否则 VBAR 设置无效
- 开总中断:cpsie i,打开 CPSR.I 位,从此 CPU 可以响应外设中断
阶段 2:main 里做了什么
int main(void)
{
system_irq_init(); // GIC 初始化 + VBAR 重映射
ccm_ccgr_enable(); // 打开所有时钟
led_init();
beep_init();
key_irq_init(); // 配置 GPIO1_IO18 为中断输入
// 重点:把"中断号"和"处理函数"绑定
request_irq(GPIO1_IO18_IRQ, gpio1_io18_handler);
while(1){} // 等待中断触发
}
核心动作
- VBAR=0x87800000,向量表放在这里
- GIC 初始化,打开 GIC 控制器
- request_irq 注册函数,把 GPIO1_IO18 对应的处理函数存入数组
- while (1),等待硬件触发中断
阶段 3:按键按下 → 真正中断触发(硬件自动做)
当你按键 → GPIO1_IO18 产生中断信号 → 发给 GIC → GIC 发给 CPU硬件自动做 4 件事:
- 把 CPSR 复制到 SPSR_irq
- 把 当前 PC 下一条地址 存入 LR_irq
- 把 CPU 模式 强制切换到 IRQ 模式
- 强制 PC 跳转到 VBAR + 0x18,也就是跳转到 _irq_handler
阶段 4:进入汇编 _irq_handler
_irq_handler:
-
修正返回地址
sub lr, lr, #4
- 为什么?ARM 三级流水线,中断到来时 PC 已预取。硬件保存的 LR 是 下下条指令地址,不减 4,返回会跳错地址、跑飞、死机
| 异常类型 | 进入时的 lr 值 | 需修正量 | 目的 |
|---|---|---|---|
| IRQ(普通中断) | PC + 4 | 减 4 | 返回被中断的指令,确保主程序正常续行 |
| FIQ(快速中断) | PC + 4 | 减 4 | 返回被中断的指令,快速中断专用修正 |
| 预取中止(Prefetch Abort) | PC + 4 | 减 4 | 返回被中止的指令,重试取指操作,恢复程序执行 |
| 数据中止(Data Abort) | PC + 8 | 减 8 | 返回被中止的指令,可重新访问内存,修复数据访问异常 |
| SVC(软中断) | PC + 4 | 不减 | 返回下一条指令,软中断执行完成后正常续行 |
| 未定义指令(Undefined) | PC + 4 | 不减 | 返回下一条指令,跳过未定义指令,避免程序卡死 |
| 复位(Reset) | 无定义 | 无 | 不复位返回,复位为系统启动入口,无返回逻辑 |
-
保存现场(压栈 R0~R12 + LR)
stmfd sp!, {r0-r12, lr}
- 中断里会用到这些寄存器,必须保存,否则主程序寄存器被破坏
-
读取 GIC 基地址(CP15)
mrc p15, 4, r1, c15, c0, 0
- 从 CP15 拿到 GIC CPU 接口基地址,这是 IMX6ULL 规定的读取方式
-
偏移到 GIC 寄存器区域
add r1, r1, #0x2000
-
读取 IAR 寄存器,获取中断号
ldr r0, [r1, #0xC]
- IAR 偏移是 0xC,读取后 R0 = 真实中断号,只有 GIC 能告诉你:到底是谁触发的中断
-
把中断号和 GIC 地址压栈
stmfd sp!, {r0,r1}
-
切换到 System 模式(0x1F)
cps #0x1f
- IRQ 模式栈小,不能直接调用 C 函数,C 语言函数必须在 System 或 SVC 模式
-
再次保存现场(进 C 函数前)
stmfd sp!, {r0-r12, lr}
-
调用 C 语言统一中断入口
bl system_irq_handler
- 跳转到 C 函数,中断号 R0 会作为参数传进去
阶段 5:C 语言中断分发函数(完整代码)
// 两张中断表(核心全局变量)
static irq_handler_t irq_handler_array[160]; // 普通外设中断服务函数表
static irq_handler_t irq_gpio_handler_array[160]; // GPIO 专用中断服务函数表
// 所有中断统一 C 入口(汇编跳转至此)
void system_irq_handler(IRQn_Type irq_num)
{
switch(irq_num)
{
// GPIO1 组合中断(0~15、16~31 引脚共用两个中断号)
case GPIO1_Combined_0_15_IRQn:
case GPIO1_Combined_16_31_IRQn:
{
// 判断是否是 GPIO1_IO18 引脚触发中断
if (GPIO1->ISR & (1< 18))
{
// 调用注册好的 GPIO1_IO18 处理函数
irq_gpio_handler_array[GPIO1_HANDLER_BASE + 18]();
// 清除 GPIO 中断标志位(必须!否则反复进中断)
GPIO1->ISR |= (< 18);
}
}
break;
// 普通外设中断(UART、Timer、LCD 等)
default:
if(irq_handler_array[irq_num]) // 判断是否注册了处理函数
irq_handler_array[irq_num](); // 调用对应的中断处理函数
break;
}
}
// 中断注册 API(绑定中断号与处理函数)
int request_irq(int irq_num, irq_handler_t handler)
{
// 参数校验:中断号范围、处理函数不为空
< IOMUXC_IRQn) || (irq_num > GPIO_IRQ_MAX))
return -1;
if(!handler)
return -1;
switch (irq_num)
{
// GPIO1 0~15 引脚(共用 GPIO1_Combined_0_15_IRQn 中断号)
case GPIO1_IO0_IRQ ... GPIO1_IO15_IRQ:
GIC_EnableIRQ(GPIO1_Combined_0_15_IRQn); // 使能 GIC 对应中断
GIC_SetPriority(GPIO1_Combined_0_15_IRQn, 0); // 设置中断优先级为 0
irq_gpio_handler_array[irq_num - GPIO_IRQ_BASE - 1] = handler; // 绑定函数
break;
// GPIO1 16~31 引脚(共用 GPIO1_Combined_16_31_IRQn 中断号)
case GPIO1_IO16_IRQ ... GPIO1_IO31_IRQ:
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn); // 使能 GIC 对应中断
GIC_SetPriority(GPIO1_Combined_16_31_IRQn, 0); // 设置中断优先级为 0
irq_gpio_handler_array[irq_num - GPIO_IRQ_BASE - 1] = handler; // 绑定函数
break;
// 普通外设中断(直接用中断号绑定)
default:
GIC_EnableIRQ(irq_num); // 使能 GIC 对应中断
GIC_SetPriority(irq_num, 0); // 设置中断优先级为 0
irq_handler_array[irq_num] = handler; // 绑定函数
break;
}
return 0;
}
核心意义
- 根据中断号查找对应的处理函数,实现中断分发
- GPIO 是 组合中断(多个引脚共用一个中断号),必须读 ISR 寄存器判断具体触发引脚
- 必须清除 GPIO 的 ISR 中断标志位,否则会反复触发中断
- 普通外设中断可直接通过中断号查表,调用对应处理函数
阶段 6:从中断 C 函数返回汇编
ldmfd sp!, {r0-r12, lr} // 恢复进入 C 函数前保存的现场
cps #0x12 // 切换回 IRQ 模式(准备后续返回主程序)
ldmfd sp!, {r0, r1} // 恢复中断号(r0)和 GIC 基地址(r1)
告诉 GIC:中断处理完了
str r0, [r1, #0x10]
- 偏移 0x10 是 EOIR(End of Interrupt,中断结束寄存器)
- 不写这句:GIC 会认为中断还没处理完,后果是再也进不了下一次中断
最后一步:真正返回主程序
ldmfd sp!, {r0-r12, pc}^
- 恢复所有寄存器(r0~r12、lr)
- ^ 符号作用:自动把 SPSR_irq 的值写回 CPSR
- 同时完成:恢复原来的 CPU 模式、自动打开中断、PC 跳回被打断的位置(回到 main 函数的 while (1))
五、中断服务函数表 vs 异常向量表(彻底区分)
异常向量表(硬件层)
- 共 8 个入口,ARM 架构硬件规定死,顺序不可变
- 硬件自动跳转,所有外设中断共用同一个 IRQ 入口(0x18)
- 作用:告诉 CPU 中断来了该跳转到哪里(只区分中断类型,不区分具体外设)
中断服务函数表(软件层)
-
核心代码:
irq_handler_array[160]; // 普通外设中断表(160个表项,对应0~159中断号) irq_gpio_handler_array[160]; // GPIO 专用中断表(区分组合中断的具体引脚) -
160 个函数指针,下标 = 中断号(或引脚对应偏移)
-
作用:区分具体是哪个外设 / 引脚触发中断,调用对应的处理函数
两者关系
硬件向量表 → 统一 IRQ 入口(汇编 _irq_handler)→ 读 GIC 中断号 → 查软件中断服务函数表 → 执行对应 C 处理函数
六、最容易踩的 7 个坑(必须注意)
- LR 必须减 4 :
sub lr, lr, #4不可省略,否则返回地址错误,主程序跑飞、死机 - EOIR 必须写 :
str r0, [r1, #0x10]不可省略,否则 GIC 认为中断未结束,无法再次触发中断 - 必须清除外设中断标志:GPIO 的 ISR 寄存器必须写 1 清除,否则会无限触发中断
- 调用 C 函数前必须切 System 模式:IRQ 模式栈空间小,直接调用 C 函数会导致栈溢出
- 不同模式必须用不同栈:SVC、IRQ、System 模式需配置独立栈地址,否则会破坏寄存器数据
- CP15 的 V 位必须清 0 :
bic r1, r1,< 13)不可省略,否则 VBAR 重映射无效,CPU 找不到向量表 - 向量表必须 4 字节对齐、地址连续:ARM 硬件要求,否则向量表无法正常跳转
七、一句话总结整个中断流程
按键触发 → GIC 接收 → 硬件跳向量表 → 汇编保存现场 → 读中断号 → C 语言查表执行处理函数 → 清除外设中断标志 → 告诉 GIC 中断结束 → 恢复现场 → 返回 main 函数 while (1) 等待下一次中断