中断编程概念
中断编程概念
软件中断编程的四大步骤
IRQ=Interrupt ReQuest(中断请求)

任何支持中断的系统都必须解决这四个基本问题,这是基于中断机制本身的内在需求
- 异常向量表主要解决路由问题
建立一个从中断源到处理代码的映射关系,这样当中断信号到来时,CPU才能知道该跳转到哪里去执行处理代码
不同系统常见的异常向量表
x86的IDT表ARM的向量表MIPS的异常向量基址寄存器
- 保护现场解决状态保存问题
CPU正在执行某段代码,中断一来打断了这个执行流,如果不把当前状态保存起来,这些状态就会在中断处理过程中被覆盖掉。
—从操作系统设计的角度看,保护现场体现了"隔离"和"抽象"两个核心原则
- 隔离意味着不同的执行流互不干扰,各自维护自己的状态。
- 抽象意味着底层的复杂机制对上层是透明的,程序员写程序时不需要考虑"程序会不会被中断打断"的问题
- 中断处理函数就是实际处理问题
- 具体的业务逻辑就是中断处理函数要完成的工作
- 这是整个中断处理流程中唯一与具体应用相关的部分,也真正的业务实现地方
- 恢复现场就是是恢复问题
回到被中断的地方继续执行,好像中断从来没有发生过一样
中断编程相关概念
-
IRQ 号
操作系统/硬件用来标识“哪一路中断”的整数标识符。通常由中断控制器分配或映射给具体中断源(串口、网卡、定时器等)
-
中断向量(interrupt vector)
CPU/中断架构用来索引中断服务入口的编号或直接是跳转地址
-
ISR(中断服务例程)
直接在中断上下文运行的处理函数。要尽量短——通常只做关键/时间敏感工作
-
中断控制器
连接中断源与 CPU 的硬件,负责中断的屏蔽/使能、优先级、路由、发送中断信号
不同架构的中断编程区别
在具体平台上,这四个步骤不是全部需要中断编程来实现的,硬件,编译器,操作系统会参与来自动完成一些配置。
具体来说
- 硬件的参与程度
💬
ARM Cortex-M这样的微控制器内核,硬件自动完成了大量的现场保护工作,甚至包括栈的切换。💬
x86或MIPS这样的架构,硬件只做最少的工作,比如保存程序计数器和状态标志,其他的寄存器都需要软件来保存。
- 编译器的作用
💬现代的C编译器非常智能,比如函数通过interrupt关键字标注,它就会自动生成保护和恢复现场的代码。
编译器主要在汇编层面做更多工作
- 操作系统的抽象层次
💬
Linux这样成熟的操作系统提供了完整的中断管理框架,从硬件抽象到设备驱动接口都考虑周全💬一个简单的
RTOS可能只提供基本的中断注册机制,更多的工作需要自己来做
- 架构本身的设计哲学
💬
CISC架构如``x86倾向于用复杂的硬件提供更多功能,所以x86`有专门的中断门、任务门等机制。💬
RISC架构如MIPS和ARM倾向于保持硬件简单,把复杂性留给软件处理,所以它们的中断机制相对简洁。
stm32的中断编程步骤
🎁第一步:建立异常向量表
当你用官方的
HAL库或者标准外设库创建项目时,启动文件startup_stm32f1xx.s已经帮你定义好了完整的中断向量表。该表是一个数组结构,里面包含了所有异常和中断的处理函数地址。
唯一需要做的就是保证中断处理函数名称和启动文件中预定义的名称完全一致
🎁第二步:保护现场
在STM32上,这一步你完全不需要操心,stm32通过硬件和编译器协同完成
ARM Cortex-M系列的硬件在设计时就考虑到了快速中断响应的需求。当
NVIC检测到一个中断信号并决定响应时,硬件会自动启动异常进入序列,在这个序列中,CPU会自动把八个关键寄存器压入当前使用的栈中如果你的中断处理函数用C语言编写,并且函数内部使用了其他寄存器,编译器会自动分析你的代码,在生成的汇编代码中
插入PUSH指令来保存这些额外使用的寄存器。编译器实际生成的汇编代码在函数入口处包含了必要的寄存器保存操作
🎁第三步:中断处理函数
这一步是你真正需要写业务逻辑的部分
void TIM2_IRQHandler(void)
{// 判断是否是更新中断if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET){// 你的业务逻辑// 必须手动清除中断标志TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
<mark>必须直接调用寄存器读写函数或者使用库函数来操作硬件。需要清楚地知道外设有哪些中断标志位,如何判断它们,如何清除它们</mark>
在STM32裸机系统中,你的中断处理函数运行在中断上下文中,它会直接打断当前正在执行的代码。你可以在中断函数中做任何你需要的操作,只要不花费太长时间。
🎁第四步:恢复现场和返回
在STM32上,这一步和保护现场是对称的,同样完全自动化。
硬件在判断中断结束后,自动从栈上弹出之前保存的八个寄存器,恢复程序计数器和程序状态寄存器,CPU就回到了被中断的地方继续执行。
C代码中只需要让函数正常结束或者写一个return语句,剩下的一切都由编译器和硬件自动处理
总结
在STM32裸机开发中:
-
建立向量表由启动文件和链接器自动完成,你只需要遵守命名规范。
-
保护现场由Cortex-M硬件和编译器自动完成,你完全不需要参与。
-
第三步编写中断处理函数需要你手动完成所有细节
判断中断类型、处理业务逻辑、清除中断标志位,直接面对硬件寄存器编程
-
第四步恢复现场由硬件和编译器自动完成,你只需要让函数正常返回。
此外需要初始化操作,也就是手动完成完整的初始化配置
包括外设配置、中断使能、
NVIC配置等等。
Linux的中断编程步骤
🎁第一步:建立异常向量表
Linux内核在启动的早期阶段已经建立好了完整的异常向量表,这个工作发生在内核的arch/arm/kernel目录下的汇编代码中- 内核使用的是
ARM架构统一的异常向量,所有的外部中断都通过IRQ这一个向量进入,然后由内核的中断处理框架进行二级分发。
编写驱动时,这一步的工作体现:调用内核提供的中断注册函数,这个注册动作是必须手动完成的,但向量表本身的建立和维护是内核的事情。
要使用request_irq或者更现代的devm_request_irq来向内核注册你的中断处理函数
这个调用告诉内核说某个特定的硬件中断号应该由你的处理函数来处理
就是内核维护了一个结构体,中断发生,内核查找这个结构体调用
🎁第二步:保护现场
x6818是硬件加内核底层代码完成Linux系统因为运行在更复杂的Cortex-A处理器上,硬件做的相对较少,更多工作由内核的汇编代码来承担。
🎁第三步:中断处理函数
Linux驱动开发中,你的中断处理函数工作在一个更高的抽象层次上。
你不需要手动清除中断控制器的中断标志
当你通过request_irq注册中断时,提供的处理函数原型是固定的:
必须接受两个参数:中断号+注册时传入的设备私有数据指针
在Linux中,中断处理函数同样运行在中断上下文,但这个上下文有很多限制。你不能睡眠,不能调用可能导致睡眠的函数,不能访问用户空间内存。
🎁第四步:恢复现场和返回
你不需要也不能够去控制这个过程,恢复现场的工作由内核的底层代码完成。
你只需要在函数正常返回一个irqreturn_t类型的值,内核会处理后续的所有清理和恢复工作。
核心差异总结:
-
STM32:直面硬件,手动控制细节(清中断标志、配置NVIC),硬件和编译器帮你做现场保护 -
X6818:内核提供高层抽象,你通过API注册和编写处理函数,底层机制(向量表、GIC、现场保护)完全由内核管理ARM + Linux
能让你只写业务逻辑,是因为 Linux 内核为你封装了
中断抽象层 + 驱动注册机制 + 并发安全框架 + 自动现场管理
主流平台表格总结
| 架构 | OS | 需要手动处理 | 系统自动完成 | 抽象层次 |
|---|---|---|---|---|
| ARM 裸机 | 无 | 向量表、保存现场、清标志 | 无 | 最底层 |
| MIPS 裸机 | 无 | CP0 中断配置 | 无 | 最底层 |
| DSP | 部分 RTOS | PIE 表 / 累加器保护 | 少量调度 | 中等 |
| RISC-V 裸机 | 无 | mtvec / trap 分发 | 无 | 中等偏低 |
| ARM + Linux | Linux Kernel | ISR 注册、逻辑处理 | GIC 驱动 / 栈 / 调度 / 并发 | 最高 |
| x86 + Linux | Linux Kernel | 同上 | 同上 | 最高 |
x86 Linux 和 ARM Linux 的中断编程接口几乎完全一致
⚙️ 写 Linux 驱动时,ARM 和 x86的中断写法几乎一样,就是说写裸机或调试 entry-level 内核汇编时,两者差别极大。
linux内核中断编程
重要函数
如果驱动想要访问某个硬件中断资源,必须先向内核申请这个硬件中断资源
在
linux内核中,处理器的任何一个硬件中断资源对于linux内核来说都是一种宝贵的资源
⭐️一旦申请成功,就向内核注册这个硬件中断对应的中断处理函数,
⭐️一旦注册成功,静静等待着硬件中断触发,一旦触发将来内核自动调用注册的中断处理函数
//request_irq函数名不能变,这是内核 API
//注册<中断处理函数>
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);/*@:这个函数运行在中断上下文中,这是一个非常特殊的执行环境。@:在这个上下文中,你不能做会导致睡眠的操作,比如申请内存(使用GFP_KERNEL)、等待信号量、调用schedule等。@:这是因为中断处理需要尽快完成,否则会阻塞整个系统对其他中断的响应。1:用msleep函数延时2:使用mutex_lock获取互斥锁3:用GFP_KERNEL标志分配内存时如果内存不足需要等待这些都会导致睡眠。*/
| 参数 | 含义 | 说明 |
|---|---|---|
irq | 中断号 | 想注册的硬件中断号,可以通过 gpio_to_irq(gpio) 获得GPIO对应的中断号。 |
handler | 中断处理函数 | 指向 irqreturn_t handler(int irq, void *dev) 类型的函数。当中断发生时,会调用该函数。 |
flags | 中断触发类型 | 常用值: IRQF_TRIGGER_RISING 上升沿触发 IRQF_TRIGGER_FALLING 下降沿触发 IRQF_TRIGGER_HIGH 高电平触发 IRQF_TRIGGER_LOW 低电平触发 IRQF_SHARED 表示中断可共享 |
name | 中断名称 | 内核打印或调试用的字符串,通常用设备名或GPIO名称。 |
dev | 私有数据 | 可以传递指针给中断处理函数,通常传递设备结构体或 GPIO 信息,这样在中断处理函数里就能知道是哪个设备产生的中断。 |
linux内核会给每个硬件中断都分配一个软件编号,类似硬件中断的身份证号IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING:指定为双边沿触发
👉返回值
-
0:注册成功 -
<0:失败,返回错误码,例如-EBUSY(中断号已被占用)
👊此函数完成两个工作
1. 申请硬件中断资源
2. 注册中断处理函数
gpio_to_irq函数是建立gpio和中断映射关系的工具、因为在Linux内核中,GPIO子系统和中断子系统是两个相对独立的框架。GPIO编号是用来标识物理引脚的,而中断号是中断控制器用来识别中断源的。当把一个GPIO配置成中断功能时,底层硬件会将这个GPIO连接到中断控制器的某个输入线上。
//中断处理函数
irqreturn_t handler(int irq, void *dev)
//一个函数指针,返回值参数不可变,名字可改
irq:触发中断的中断号dev:request_irq()中传入的私有数据- 返回值
- 中断处理函数执行失败return IRQ_NONE;
- 中断处理函数执行成功return IRQ_HANDLED;
request_irq(irq_up, button_isr, flags, "KEY_UP", &btn_info[0]); request_irq(irq_down, button_isr, flags, "KEY_DOWN", &btn_info[1]);中断触发时,内核会调用
button_isr(irq, dev),但是handler 是同一个函数,无法直接知道是哪块硬件触发了中断,dev提供了区分硬件的途径。如果没有
dev:
- handler 内就只能通过
irq号去查表或比较才能知道是哪块硬件。- 如果多个设备共享同一个中断号,甚至更复杂,根本无法准确区分。
handler 是函数模板,dev 是函数参数
request_irq函数的最后一个形参(void *dev)用来给中断处理函数传递参数 ,而中断处理函数的第二个形参dev来保存传递的参数!
中断处理函数的dev参数务必要和request_irq的最后一个参数保持一致
设备名称(name)是一个字符串,用于在/proc/interrupts文件中显示,方便调试和监控
宏 含义 IRQF_TRIGGER_HIGH高电平触发,GPIO 电平保持高时持续触发(level sensitive) IRQF_TRIGGER_LOW低电平触发,GPIO 电平保持低时持续触发(level sensitive) IRQF_TRIGGER_RISING上升沿触发,GPIO 电平从低→高 时触发(edge sensitive) IRQF_TRIGGER_FALLING下降沿触发,GPIO 电平从高→低 时触发(edge sensitive) 触发方式
- 单边沿触发:
IRQF_TRIGGER_RISING→ 只在松开时触发IRQF_TRIGGER_FALLING→ 只在按下时触发- 双边沿触发:
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING→ 按下和松开都触发中断- 在 handler 里用
gpio_get_value()区分事件- 电平触发(一般用于硬件持续信号,不常用于按键):
IRQF_TRIGGER_HIGH/IRQF_TRIGGER_LOW- 需要额外在 handler 或硬件清除中断,否则会一直触发
当你不再需要中断时,必须调用free_irq来注销
void free_irq(unsigned int irq, void *dev_id);
free_irq() 的主要功能是:
- 注销中断处理函数(handler);
- 释放中断资源;
- 允许该中断号被其他驱动重新申请;
- 如果系统中有共享中断线(
IRQF_SHARED),则只释放匹配dev_id的那个处理函数,而不是整个中断线。
事件驱动模型
| 模型类型 | 执行方式 | 优点 | 缺点 |
|---|---|---|---|
| 轮询模型 | 程序不断检测事件是否发生 | 逻辑简单 | CPU 空转浪费、延迟大 |
| 事件驱动模型 | 事件触发回调执行逻辑 | 高效、响应快 | 实现复杂、需处理并发与同步 |
Linux 驱动开发中:
- 事件 = 外设中断信号*
- 事件驱动模型在驱动中的具体体现就是中断注册机制
🔹 图示:
硬件产生中断信号│▼
内核中断子系统检测到中断(irq)│▼
调用已注册的中断服务函数(ISR)│▼
ISR 处理中断事件(读取寄存器、唤醒任务等)
