目录
一、中断的引用
1.1 轮询方式的局限性
1.2 中断系统简介
二、ARM 中断系统硬件架构
2.1 通用中断控制器 GIC
2.1.1 GIC 中断分类
2.1.2 架构组成
2.2 协处理器 CP15
2.2.1 访问指令
2.2.2 关键寄存器
三、代码实现:中断驱动的按键控制
3.1 向量表管理与初始化
3.2 GPIO 与中断配置
3.3 中断服务函数 (ISR)
3.4 主程序流程
四、软件设计原则:低耦合与 OCP
一、中断的引用
1.1 轮询方式的局限性
在学习中断之前,我们通常使用轮询(Polling)的方式来处理按键等外设输入:
while (1) { if ((GPIO1->DR & (1 << 18)) == 0) // 检测到低电平 { led_nor(); beep_nor(); } delay(0x7FFFFF); // 模拟大量复杂业务 }轮询方式的原理:CPU 周期性地读取 GPIO 引脚状态,判断是否有按键按下。
轮询的致命缺陷:
- 漏查风险:当主程序需要处理大量、复杂且耗时的业务时(例如模拟一个 delay(0x7FFFFF) 的长延时),CPU 无法及时检查按键状态。
- 实时性差:就像“汽车刹车”这种高实时性场景,如果 CPU 正在处理其他任务而无法立即响应刹车信号,后果不堪设想。
因此,我们需要引入中断(Interrupt)。
1.2 中断系统简介
定义:中断是指 CPU 能打断当前正在进行的工作,去处理更为紧急的任务(中断服务函数),并且在处理完后,能自动回到原先的地方继续工作。
中断处理的标准流程:
- 中断请求:中断源(外设)发出信号。
- 中断响应检查:CPU 检查是否响应该中断,以及该中断是否被屏蔽。
- 优先级检查:GIC 判定当前中断的优先级。
- 保护现场:保存被暂停程序的上下文。
- 执行中断服务函数(ISR):处理紧急任务。
- 恢复现场:还原上下文,继续执行原程序。
二、ARM 中断系统硬件架构
2.1 通用中断控制器 GIC
IMX6ULL 使用的是单核 Cortex-A7,其内部集成了 GIC v2.0 控制器。GIC 负责管理所有的中断源,并决定分发给哪个核心处理。GIC逻辑分区如下:
2.1.1 GIC 中断分类
根据 ARM GIC v2.0 规范,中断源被分为三类(共 1020 个 ID):
| 类型 | 全称 | ID 范围 | 描述 |
|---|---|---|---|
| SGI | Software-generated Interrupt (软件中断) | 0 - 15 | 由软件向寄存器 GICD_SGIR 写入数据触发,常用于多核间通信。 |
| PPI | Private Peripheral Interrupt (私有中断) | 16 - 31 | 每个核心独有的中断(如 Generic Timer),必须由指定核心处理。 |
| SPI | Shared Peripheral Interrupt (共享中断) | 32 - 1019 | 外部外设产生的中断(如 GPIO、I2C 等),所有核心共享。 |
注意:这里的 SPI 指的是共享中断,不要和通信协议 SPI 总线混淆。
2.1.2 架构组成
GIC 主要由两部分组成:
- Distributor (分发器):负责检测、排序和分发中断。
- CPU Interface (CPU 接口):负责将分发器发送的中断信号传输给处理器核心。
2.2 协处理器 CP15
在配置中断和系统底层时,离不开协处理器 CP15。它负责系统控制、MMU 配置、Cache 管理以及虚拟化等任务。
2.2.1 访问指令
CP15 包含 c0 到 c15 组寄存器,通过专用指令访问:
- MRC:读 CP15 寄存器。
- MCR:写 CP15 寄存器。
2.2.2 关键寄存器
- MIDR (Main ID Register, c0):存储内核的基本信息。
- SCTLR (System Control Register,c1):系统控制寄存器,其中两个位至关重要:
- Bit 13 (V 位):控制异常向量表的基地址。
- 0:基地址为 0x00000000(软件可通过 VBAR 重映射)。
- 1:基地址为 0xFFFF0000(高地址向量,不可重映射)。
- Bit 12 (I 位):指令 Cache 开关。
- Bit 13 (V 位):控制异常向量表的基地址。
- VBAR (Vector Base Address Register, c12):向量基地址寄存器,用于重新映射异常向量表的基地址。在IMX6ULL中,我们将向量表重映射到 0x87800000。
- CBAR (Configuration Base Address Register, c15):配置基地址寄存器,指向GIC控制器的物理基地址。
三、代码实现:中断驱动的按键控制
本文的核心目标是实现 "按键中断触发 LED 翻转 + 蜂鸣器翻转",遵循 "低耦合、高可扩展" 的设计原则。
3.1 向量表管理与初始化
首先,我们需要定义一个中断服务函数指针数组,用于注册和管理中断。
// 定义中断向量表 typedef void (*irq_handler_t)(void); irq_handler_t Vector_table[160]; // 系统中断初始化 void system_interrupt_init(void) { // 1. 重映射异常向量表基地址 __set_VBAR(0x87800000); // 2. 初始化GIC控制器 GIC_Init(); } // 注册中断处理函数 int system_interrupt_register(IRQn_Type irq, irq_handler_t handler) { if (irq > PMU_IRQ2_IRQn || irq < IOMUXC_IRQn) return -1; if (handler == NULL) return -2; Vector_table[irq] = handler; // 将中断号与处理函数关联 return 0; }3.2 GPIO 与中断配置
在 key_init 中,我们不仅配置 GPIO 为输入,还需要配置 GPIO 的中断触发方式和使能。
- 复用配置:将 UART1_CTS_B 复用为 GPIO1_IO18。
- 电气特性:配置上拉电阻和输入迟滞。
- 中断触发:配置 GPIO1->ICR2 寄存器,选择下降沿触发(按下时电平由高变低)。
- 中断使能:配置 GPIO1->IMR 寄存器,置位对应位以允许中断。
void key_init(void) { // 1. 复用功能配置 IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0); // 2. 电气特性配置 IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF0B0); // 3. 引脚方向设置为输入模式 GPIO1->GDIR &= ~(1 << 18); // 4. 配置中断触发方式 (ICR2寄存器,bit4-5对应GPIO1_18) GPIO1->ICR2 |= (3 << 4); // 下降沿触发 // 5. 中断屏蔽寄存器 (IMR) - 解除屏蔽 GPIO1->IMR |= (1 << 18); // 6. GIC配置 GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn); // 使能该中断 GIC_SetPriority(GPIO1_Combined_16_31_IRQn, 0); // 设置优先级 // 7. 注册回调函数 system_interrupt_register(GPIO1_Combined_16_31_IRQn, key_irq_handler); }3.3 中断服务函数 (ISR)
当中断发生时,CPU会跳转到对应的处理函数。
void key_irq_handler(void) { // 检查GPIO中断状态寄存器 (ISR) // 注意:ISR用于指示中断是否发生,IMR用于使能/屏蔽。 if ((GPIO1->ISR & (1 << 18)) != 0) { // 执行业务逻辑:翻转LED和蜂鸣器 led_nor(); beep_nor(); // 必须手动清除中断标志位 GPIO1->ISR |= (1 << 18); } }3.4 主程序流程
主程序在初始化完成后,进入无限循环,此时CPU可以自由执行其他任务(如 g_delay),而按键的中断请求会被GIC捕获并打断主循环。
int main(void) { system_interrupt_init(); // 初始化中断系统 clock_cg_init(); // 开启时钟 beep_init(); led_init(); key_init(); // 配置按键中断 while (1) { g_delay(0x7FFFF); // 主循环执行耗时任务,但不会阻塞中断响应 } return 0; }四、软件设计原则:低耦合与 OCP
在编写中断驱动代码时,我们遵循了良好的软件工程原则:
- 低耦合:
- 中断模块只负责中断的底层处理(如向量表管理)。
- GPIO模块只负责引脚的输入输出。
- 业务逻辑(按键处理)独立封装。
- 开闭原则 (OCP):
- 对修改关闭:核心的中断分发逻辑(system_interrupt_handler)不需要因为新增设备而修改。
- 对扩展开放:当需要增加一个新的按键或设备时,只需调用 system_interrupt_handler 注册一个新的函数指针,无需改动核心代码。