MCU配NVIC写ISR就完事,Linux多出上下半部和threaded_irq——抽象多了,但底层信号拉电平、CPU响应的逻辑没变。本文把两边逐一对齐。
一、中断本质---异步通知的核心逻辑
CPU频率高,外设频率低,两者节奏不同步。CPU轮询等外设是白烧电,中断的思路反过来:CPU大部分时间休眠,外设准备好了主动拉信号线通知CPU来干活。
中断发生后,硬件自动完成压栈 → 查向量表 → 跳转ISR → 执行ISR → 弹栈返回。你只写ISR函数,其余全由硬件处理。
中断系统四个组件:中断源(外设喊CPU)、中断控制器(接线员,汇总信号并做优先级仲裁)、向量表(ISR地址数组,按中断号查表)、ISR(你写的处理函数)。
本节要点:中断本质是外设主动通知CPU的异步机制,硬件自动完成压栈查表跳转,你只管写ISR。
二、MCU侧中断 NVIC配优先级写ISR完事
MCU中断配置分四步:设置优先级分组 → 设置触发类型 → 使能中断通道 → 写ISR函数。
ISR必须遵守铁律:快进快出、先清标志、不调阻塞函数、共享变量加volatile。
/* NVIC配置 + ISR + 主循环 */#include "stm32f4xx.h"volatile uint8_t g_uart_rx_flag = 0; /* volatile:防止O3优化剔除 */void USART1_IRQHandler(void){/* 铁律1: 快进快出,只清标志置位 */if (USART1->SR & USART_SR_RXNE) { USART1->SR &= ~USART_SR_RXNE; /* 铁律2: 先清标志 */ g_uart_rx_flag = 1; /* 铁律4: volatile共享变量 */ }/* 铁律3: 绝不调HAL_Delay()等阻塞函数 */}void nvic_init(void){/* 优先级分组: 抢占4位+子优先级0位 */ NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP4);/* 设置抢占优先级=6 */ NVIC_SetPriority(USART1_IRQn, 6);/* 使能中断通道 */ NVIC_EnableIRQ(USART1_IRQn);}int main(void){ nvic_init();for (;;) {if (g_uart_rx_flag) { /* 主循环处理耗时逻辑 */ g_uart_rx_flag = 0; process_uart_data(); /* 耗时操作放这里 */ } __WFI(); /* 休眠等中断,省电 */ }}
MCU的ISR只清标志、置标志位,实际处理放主循环——这本质上就是手动拆成上下两步。
本节要点:MCU中断就是NVIC配优先级、写ISR快进快出,耗时操作手动下放主循环。
三、Linux侧中断 基本接口与上下半部分离
Linux提供request_threaded_irq()注册中断,内核自动把耗时部分丢进线程执行,不用手动拆。
/* Linux中断注册与释放 */#include <linux/gpio.h>#include <linux/interrupt.h>#include <linux/module.h>static irqreturn_t gpio_hard_isr(int irq, void *dev_id){/* 硬中断上半部:只做最紧急的操作 */struct my_dev *dev = dev_id; dev->irq_count++;return IRQ_WAKE_THREAD; /* 唤醒下半部线程 */}static irqreturn_t gpio_thread_fn(int irq, void *dev_id){/* 下半部线程上下文:可以sleep,做耗时操作 */struct my_dev *dev = dev_id; process_gpio_event(dev);return IRQ_HANDLED;}static int __init my_init(void){int irq; gpio_request(17, "my_gpio"); irq = gpio_to_irq(17);/* 注册threaded_irq:上半部+下半部线程 */ request_threaded_irq(irq, gpio_hard_isr, gpio_thread_fn, IRQF_TRIGGER_RISING, "my_irq", &my_device);return 0;}static void __exit my_exit(void){int irq = gpio_to_irq(17); free_irq(irq, &my_device); /* dev_id必须与注册时一致 */ gpio_free(17);}
Linux提供四种下半部机制:SoftIRQ(中断上下文,不能sleep,内核网络栈专用)、Tasklet(中断上下文,不能sleep,同类不并发)、Workqueue(进程上下文,可以sleep)、Threaded IRQ(进程上下文,可以sleep,大部分场景优先选)。
本节要点:Linux用request_threaded_irq自动拆分上下半部,下半部线程可sleep,比MCU手动拆更优雅。
四、MCU对比Linux 中断处理与并发保护逐一对齐
| | |
|---|
| 中断控制器 | | |
| 注册方式 | | request_threaded_irq() |
| 上半部 | | |
| 下半部 | | SoftIRQ/Tasklet/Workqueue/Threaded IRQ |
| 并发保护 | volatile | atomic_t + spin_lock_irqsave + mutex |
| 可sleep | | |
并发保护是中断的核心难题:ISR和主循环抢同一块数据怎么办?MCU侧用volatile保证可见性,关中断保证原子性,Cortex-M3+还提供__LDREX/__STREX实现无锁原子操作。Linux侧用atomic_t做简单计数,spin_lock_irqsave在获取自旋锁同时关中断,mutex_lock用于可sleep的进程上下文。核心原则一样:中断上下文绝不能sleep。
本节要点:MCU手动拆上下半部,Linux内核帮你拆;并发保护手段不同,但"中断上下文不能sleep"的底线两边一样。
五、注意事项
| | |
|---|
| | |
volatile | | |
| | |
| | |
| | |
| | 硬中断用spin_lock_irqsave关中断,sleep留给下半部 |
free_irq | | dev_id |
tasklet | | tasklet |
本节要点:忘清标志和ISR调sleep是两大杀手,MCU侧查volatile和标志位,Linux侧严格区分中断上下文和进程上下文。
六、总结
- 中断本质两边一样:外设拉信号线,CPU放下手头活去处理
- MCU手动拆上下半部(ISR置flag + 主循环处理),Linux用
threaded_irq内核帮你拆 - 并发保护手段不同,底线相同:中断上下文绝不能sleep
- 理解了MCU的ISR+主循环,Linux的上下半部就是同一套逻辑加了抽象层
中断的逻辑从来没变过——外设喊一声,CPU放下手头活去处理,处理完接着干。MCU那套NVIC配优先级、写ISR就完事,Linux多了几层抽象兼容所有硬件,代价是代码膨胀。理解这个共性,两边切换就是换套API,逻辑还是那套逻辑。