当前位置:首页>Linux>从MCU到Linux嵌入式中断的底层逻辑

从MCU到Linux嵌入式中断的底层逻辑

  • 2026-10-11 06:17:01
从MCU到Linux嵌入式中断的底层逻辑

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 中断处理与并发保护逐一对齐

维度
MCU
Linux
中断控制器
NVIC,寄存器直配
GIC/IOAPIC,内核抽象
注册方式
向量表写ISR函数名
request_threaded_irq()
上半部
ISR,关中断执行
硬ISR,关抢占执行
下半部
主循环手动轮询flag
SoftIRQ/Tasklet/Workqueue/Threaded IRQ
并发保护volatile
 + 关中断 + __LDREX/__STREX
atomic_t
 + spin_lock_irqsave + mutex
可sleep
ISR中禁止
下半部线程允许

并发保护是中断的核心难题:ISR和主循环抢同一块数据怎么办?MCU侧用volatile保证可见性,关中断保证原子性,Cortex-M3+还提供__LDREX/__STREX实现无锁原子操作。Linux侧用atomic_t做简单计数,spin_lock_irqsave在获取自旋锁同时关中断,mutex_lock用于可sleep的进程上下文。核心原则一样:中断上下文绝不能sleep。

本节要点:MCU手动拆上下半部,Linux内核帮你拆;并发保护手段不同,但"中断上下文不能sleep"的底线两边一样。

五、注意事项

坑
现象
解法
忘清中断标志
ISR死循环反复进入
ISR入口立即写寄存器清标志
volatile
忘加
O3优化后共享变量不更新
ISR与主循环共享变量必加volatile
ISR调耗时函数
其他中断丢响应
耗时操作下放主循环/下半部线程
优先级配反
关键事件被低优先级打断
高优先级数字值更小,仔细核对
共享中断未判源
误触发别人的ISR
ISR入口判断本设备中断标志
Linux硬IRQ调sleep
内核oops死锁
硬中断用spin_lock_irqsave关中断,sleep留给下半部
free_irq
传错dev_id
释放失败或释放错中断
dev_id
必须与request_irq时一致
tasklet
里调sleep
内核崩溃
tasklet
是中断上下文,改用workqueue

本节要点:忘清标志和ISR调sleep是两大杀手,MCU侧查volatile和标志位,Linux侧严格区分中断上下文和进程上下文。

六、总结

  • 中断本质两边一样:外设拉信号线,CPU放下手头活去处理
  • MCU手动拆上下半部(ISR置flag + 主循环处理),Linux用threaded_irq内核帮你拆
  • 并发保护手段不同,底线相同:中断上下文绝不能sleep
  • 理解了MCU的ISR+主循环,Linux的上下半部就是同一套逻辑加了抽象层

中断的逻辑从来没变过——外设喊一声,CPU放下手头活去处理,处理完接着干。MCU那套NVIC配优先级、写ISR就完事,Linux多了几层抽象兼容所有硬件,代价是代码膨胀。理解这个共性,两边切换就是换套API,逻辑还是那套逻辑。

最新文章

随机文章