“中断”,几乎是每个硬件工程师绕不开的第一道坎。我这些年带新人、做面试,最常问的一个问题就是:“你说说看,什么是中断?”得到的回答大多是背概念,比如“CPU暂停当前任务,转去处理突发事件,处理完再回来”,听起来没毛病,但太干。今天我想用一个能记一辈子的例子,把中断从概念到代码、再从代码到PCB这条线彻底讲透。因为中断真不只是软件工程师关心的事,它背后还牵扯着硬件电路、PCB布局、系统实时性,是硬件工程师判断一块板子靠不靠谱的底层思维。无论你刚入门还是做了几年硬件,把这套逻辑理清楚,后面写代码画板子都会顺手很多。
1. 中断到底是什么:一个电话打断,比你想象的更复杂
1.1 生活比喻之外,单片机里的完整流程
先来一个最经典、也最容易理解的例子。你在办公室写周报,写到一半电话响了。你会怎么做?停下手头的活,记住自己刚写到哪一行,然后接电话,处理完电话里的问题,挂掉电话,再回到工位上继续写。这个场景和CPU的中断机制几乎一一对应。
单片机里的中断流程,实际上是这样走的:
- 电话铃响,对应“中断请求信号”,也就是外设或者外部引脚给CPU拉了一个电平变化。
- 你停下手头的活,对应“CPU暂停当前正在执行的主程序”。
- 你记下自己写到哪一行,对应“保护现场”,也就是把当前执行的地址、寄存器状态压入堆栈。
- 接电话交谈、处理事情,对应“执行中断服务函数”,行业内叫ISR。
- 挂电话,对应“清中断标志”,告诉CPU这件事你处理完了。
- 你回到工位继续写周报,对应“恢复现场”,CPU回到刚才被中断的地方,接着往下执行。
这个比喻能覆盖百分之八九十的中断流程概念。但单片机里的实际场景,比接电话复杂得多,比如来了两个电话你接哪个、接电话途中又来一个新电话怎么处理、下次再开会忘了写哪行怎么办,这些在中断里都有对应的机制,后面我会挨个讲。
1.2 为什么不用轮询?中断的意义与代价
有人会问:我不用中断,程序里反复检查某个引脚有没有变化,或者一直查串口接收标志位,不行吗?这叫轮询。轮询在简单场景下确实能用,但有两个致命问题。
第一个问题是浪费CPU。你让一个主频几百兆的CPU干等一个可能几十毫秒才来一次的信号,大部分算力都空转了。第二个问题是实时性差。如果你的主程序里有延时、有耗时操作,轮询可能根本赶不上处理突发事件。举个实际例子,一个编码器信号在高速转动时,脉冲间隔可能只有几微秒,你用轮询去查,一个主循环还没跑完,信号已经丢了好几拍。
中断的意义,就是把“主动问”变成“被通知”。信号来了硬件自动通知CPU,CPU可以随时放下手里的事去处理。但中断不是免费的,它也有代价。CPU响应中断需要时间,从响应到跳转,要经过压栈、取向量、跳转这些操作,再加上中断服务函数本身执行的时间,都会占用CPU。比如STM32F103在72MHz主频下,一次外部中断从触发到进入ISR一般需要十几个到几十个CPU周期,算下来大概是几百纳秒到几微秒不等。如果中断触发频率特别高,ISR又做得太长,CPU大部分时间都在忙着切换任务,主程序反而跑不动。这个“中断开销”的概念,是硬件工程师评估系统实时性时必须心里有数的。
2. 中断相关的基本概念,面试题里最常出现的那几个
2.1 中断向量、中断号与中断服务函数
很多初学者啃芯片手册时,看到一长串以IRQ结尾的枚举就蒙了,比如STM32里的EXTI0_IRQn、TIM2_IRQn。这些东西到底是什么意思?
在Cortex-M系列内核的单片机里,每一个中断源都对应中断向量表里的一个入口。向量表就相当于一张“通讯录”,CPU一收到中断信号,就去查这张表,找到该跳转的地址。每个中断源都有一个编号,这个编号在芯片数据手册和启动文件里写得清清楚楚,中断号其实就是这张“通讯录”里的索引。
以STM32F103为例,部分常见中断向量如下:
| 中断向量名 | 中断号示例 | 对应外设/功能 |
|---|---|---|
| WWDG_IRQn | 0 | 窗口看门狗 |
| EXTI0_IRQn | 6 | 外部引脚中断0 |
| TIM2_IRQn | 28 | 定时器2全局中断 |
| USART1_IRQn | 37 | 串口1全局中断 |
写代码时,中断服务函数的名字不是随便起的,必须和启动文件里的向量表对齐。你写错名字,中断触发了也跳不到你的函数,程序就会跑飞。这也是新手最容易踩的坑之一,函数名少个字母,编译不报错,但中断就是不进。
2.2 抢占优先级与子优先级:谁先跑,谁后跑
前面说“两个电话同时响接哪个”,对应到中断系统里就是优先级的问题。
以Cortex-M3/M4内核为例,NVIC支持中断优先级分组,优先级分为抢占优先级和子优先级两组概念。抢占优先级的意思是,当一个高抢占优先级的中断正在执行时,它可以打断正在执行的低抢占优先级中断,这就是中断嵌套。子优先级不能打断,它只在多个同抢占优先级的中断同时到达时决定谁先响应。
我接触过不少项目,中断一多,优先级没配好,各种神奇问题就出现了:A中断还没执行完,B中断进来把共享数据改了;低优先级的中断永远抢不到CPU;最要命的是,某个中断服务函数里用了延时而这个中断又被更高优先级的中断打断,整个时序全部乱掉。我的经验是,一个项目里中断优先级不要规划得太花哨。简单项目统一用一个优先级分组,开局先把所有中断设成同级,靠子优先级区分先后,跑通了再根据实时性要求慢慢调。单片机中断优先级永远是“够用就好”,别为了炫技搞太复杂。
2.3 ISR的三个铁律:短、快、不碰共享数据
中断服务函数有自己的一套规矩,我在带新人时反复强调,总结下来就是三条铁律。
第一,ISR要尽量短。ISR里只做最重要的事,比如置一个标志位、拷贝一个字节到缓冲区,真正复杂的逻辑放到主循环里做。你在ISR里停留时间越长,其他中断被耽误得越久,系统实时性就越差。第二,ISR里千万不要调用delay、printf、malloc这类函数。这些函数要么会阻塞等待,要么本身不可重入,在中断里调用极容易把系统搞死。我见过太多新人在USART中断接收函数里直接调用printf打印调试信息,结果串口还开着,中断却又来了,直接死锁。第三,如果ISR和主程序共享同一个变量,这个变量必须加volatile修饰。原因在于编译器优化时可能把变量值暂存在寄存器里,主程序改了值,ISR读到的却是旧值;或者ISR改了值,主程序一直以为没变。这个问题后面实战部分还会详细说。
3. 中断不只在代码里:中断引脚背后的PCB设计门道
3.1 外部中断的电路设计:按键、边沿、抖动的纠缠
很多人学中断是从按键开始的,按键中断也是最容易出问题的场景之一。原因就是机械按键在按下去的瞬间,金属触点会反复接触弹开几次,这个过程叫抖动。抖动会产生一串宽度极窄的脉冲,如果直接接在中断引脚上,一个按键动作会触发好多次中断。
在原理图和PCB设计阶段,就要把这个问题解决掉。最常见的做法是RC滤波电路,用一个电阻和一个电容组成低通滤波,把窄脉冲吸收掉。比如10kΩ电阻串联100nF电容到地,时间常数算下来约1ms,基本能把机械按键的抖动毛刺滤干净。同时,按键还要接上拉或下拉电阻,确保没有按键动作时引脚电平固定,不会因为悬空导致噪声误触发。
PCB布局上,这个RC滤波网络和上拉电阻要尽量靠近MCU的中断引脚摆放。我见过一些板子,干扰源离MCU很近,但滤波电容离得很远,走线从干扰源脚下绕过一整圈再到MCU引脚,结果滤波器形同虚设,按键一按,隔壁的PWM信号就串进来误触发中断。好的布局思路是:干扰源在板边,滤波器件紧贴MCU引脚,中间走线短而粗,尽量避开晶振、电感这类强辐射元件。
3.2 中断信号线的布局布线:从连接器到MCU的距离
如果你的中断信号来自板外的连接器、光耦、编码器或者另一块板子,布线时就要格外小心了。我调过一块电机驱动板,编码器Z信号(零位信号)一接上就疯狂触发中断,查了半天发现是这条信号线在PCB上走了将近十厘米,还贴着开关电源的MOS管和电感。编码器信号本身边沿很陡,高速切换时会产生振铃,再加上电源噪声耦合,MCU的输入引脚根本分不清高低电平。
处理这类信号的思路有几种。一是从源头加匹配电阻,在发送端串一个22Ω到33Ω的小电阻,可以明显抑制信号反射和过冲。二是在接收端加一个施密特触发器或者RC滤波,把边沿变缓、毛刺滤掉。三是在PCB布局上,中断信号线尽量远离开关电源、晶振、高频PWM走线,有条件的话让信号线两侧用地线包围,也就是加“保护地”走线。
特别提醒一点,高速编码器这类脉冲信号如果传输距离较长,最稳的方案是用差分信号,比如RS422差分编码器,抗共模干扰能力远远强于单端信号。如果硬要用单端信号,线长别超过二三十厘米,否则就要考虑加一级总线缓冲器或者整形电路。
3.3 电源去耦和地回路:莫名其妙的误中断多数是这个原因
很多时候,中断引脚周围的电路画得干干净净,信号源也正常,但MCU就是会误进中断。这时候你就要怀疑电源和地了。
数字电路在电平翻转的瞬间,会从电源抽取一个瞬态电流。如果MCU电源引脚旁的去耦电容放得不够近、容量不够,电源电压就会出现毛刺。这个毛刺通过地回路传到中断引脚的内部门限电路,就能触发一次外部中断。这是典型的“电源噪声导致逻辑错乱”。
去耦电容的摆放有个基本原则:0.1μF的陶瓷电容要尽量靠近每个电源引脚,距离不超过两三毫米最佳,电容和电源引脚之间不要打过孔,最好在同一层直接相连。大容量的电解电容可以放得远一点,但也要保证充电回路足够宽。地平面同样重要,中断引脚走线不要跨越分割的地平面,模拟地和数字地尽量做到单点连接,避免形成巨大的地环路。
还有一个容易被忽略的问题:板子电源输入端的电压跌落或者短时中断,也会让MCU复位或者误判引脚状态。如果你的产品有低功耗唤醒功能,靠中断唤醒MCU,那么电源跌落时产生的毛刺极有可能把系统从睡眠中“假唤醒”。这种问题在原理图阶段就要考虑清楚,电源入口要加TVS管和足够的储能电容,保证轻微跌落下MCU不掉电、不误触发。
4. 中断和DMA怎么选:以串口、CAN为例的工程实践
4.1 串口中断接收:爽快,但要注意这些坑
串口接收是中断用得最多的场景之一。用中断接收的好处很明显,CPU不用一直盯着接收标志位,数据来了自动通知。做AT指令应答、遥控器解码这类小数据量交互,中断接收完全够用。
但中断接收也有坑,最典型的是数据处理速度跟不上。如果你的串口波特率是115200,每秒钟大约能接收11520字节,算下来每字节间隔不到87微秒。如果在中断服务函数里做太多事,比如解析数据、调用其他函数,下一次中断来了还没来得及退出,数据就会溢出丢失。我在调试一块NB-IoT模组的板子时,就吃过这个亏。模组上电瞬间会连续输出一大串日志和配置信息,我的中断接收函数里做了简单的数据拼接,结果日志一多就丢字节。
正确的做法是,中断服务函数里只做一件事:把收到的字节丢进接收缓冲区,置一个标志位,剩下的事情全部放到主循环里慢慢处理。缓冲区用环形队列实现,这样即使主循环偶尔卡顿几十毫秒,数据也不会丢。
4.2 DMA加空闲中断:处理不定长数据的省心方案
如果数据量很大,比如固件升级、文件传输,中断接收就会让CPU忙于搬数据。这时候就该DMA出场了。
DMA就是“数据搬运工”,外设接收到数据后,DMA直接把数据搬到内存,不用CPU一行一行地参与。串口接收不定长数据时,一个常见的做法是“DMA加空闲中断”:DMA负责接收数据,串口空闲中断负责在总线空闲时告诉CPU“这一帧数据收完了”。
具体流程是:初始化DMA接收,使能串口空闲中断,DMA处于持续接收状态。当一帧数据发送完成后,总线上出现空闲状态,串口触发空闲中断,这时CPU读取DMA剩余长度,算出实际收到的数据长度,处理完这一帧后再重新启动DMA。这样一帧数据只打扰一次CPU,效率远高于逐字节中断。
用这个方法有几个关键细节。第一,DMA缓冲区的长度必须大于可能收到的最大帧长,否则会溢出。第二,要处理好DMA的循环模式和普通模式,普通模式下接收满后DMA会自动停止,循环模式下则会从头继续写,容易覆盖旧数据,所以一般用普通模式加空闲中断。第三,空闲中断的参数要合理设置,有的芯片支持配置空闲检测的时间,设太短会在帧内数据延迟稍大时误判。
4.3 一个经典问题:LIN模式下串口发送会触发接收中断吗
群里经常有人问:“用LIN模式,串口发送出去的数据会触发接收中断吗?”答案是会的,而且这是很多新人第一次接触LIN总线时必然踩的坑。
LIN是单线总线,典型接法是把MCU的UART_TX和UART_RX都接到同一个LIN收发器上,总线上的数据同时会被接收端采样。当你发送一帧数据时,这帧数据也会“原路返回”到自己的接收寄存器里。如果在这期间开启了接收中断,MCU会以为自己收到了数据,进而触发接收中断,把你发出去的字节又读回来一遍。
这在实时性要求高的LIN通信场景下会造成数据混乱。解决办法有几个:第一种是发送期间屏蔽接收中断,等发送完成后再打开;第二种方法是在接收中断里判断收到的字节是不是自己发出去的,如果是就丢弃,但这种方式浪费CPU而且容易出错;第三种方法是很多MCU支持关闭回环模式或者配置发送时接收不使能,从硬件上避免自回环。工程上我建议优先用第一种,简单直接,逻辑清晰。
这个问题的本质,其实是“半双工总线的自我回环”。如果你做RS485通信,也面临类似的坑,只是RS485收发器需要方向控制,发送和接收严格分时,反而更不容易出错。
5. 中断相关Bug现场:现象、原因与排查方法
5.1 中断里干重活,程序为什么“卡死”
我曾经处理过一个设备频繁死机的案例,现象是设备运行一会儿就死掉,看门狗也救不回来。排查了很久,最后定位到串口接收中断里做了一大段协议解析,其中还有循环等待某些标志位。结果在一次通信异常时,ISR里等待的标志位一直不来,CPU就卡在中断里出不来,主循环和看门狗全部停摆。
中断服务函数里干重活,轻则丢数据、丢中断,重则死机重启。如果你的程序出现“偶尔卡死”且卡死前正在通信,先查ISR里有没有耗时的循环等待、有没有调用阻塞函数。正确的做法是,ISR里只收数据、置标志位,协议解析永远放在主循环。实在需要在中断里处理紧急事件,也一定要加超时退出机制,不能无限等下去。
5.2 按键按一下却进了好几次中断
另一个常见bug是按键中断触发次数不对。明明按了一次,系统却像被按了好几次,时而多执行一次,时而少执行一次。
原因前面说过,机械按键的抖动没处理好。排查思路分三步:第一步看硬件,用示波器抓按键按下瞬间的引脚波形,如果看到密密麻麻的毛刺,说明RC滤波没起作用或者参数不合适。第二步看软件,如果硬件已经滤波,软件里也建议加一次性延时消抖,比如检测到边沿后延时10到20毫秒再去读电平,双保险。第三步看代码逻辑,检查中断触发方式是上升沿、下降沿还是低电平触发,有的按键电路常闭常常开,触发沿选反了也会表现为“时灵时不灵”。
我在PCB上还遇到过一种情况,按键确实做了RC滤波,但滤波电容的位置离按键插针太远,引入的走线寄生电感把滤波效果抵消了。这种问题原理图看不出来,只能在Layout阶段规避,所以再次强调,滤波器件要紧贴受保护的引脚。
5.3 中断标志没清,程序疯狂重入
“中断只触发一次,之后就再也不响应了”,这个问题多半是中断标志没清。很多MCU外设的中断标志一旦置位,就会持续向CPU申请中断。如果你的ISR没有在开头清除标志,CPU会不断重入同一个中断,表面上看起来像是程序卡死在这个中断里。
处理方法很简单,进入ISR后第一时间清除中断标志,然后再做数据处理。但不同外设清除方式不一样,有些是写0清除,有些是读寄存器清除,有些必须先读状态寄存器再清除,细节去看芯片参考手册。我见过有人把标志清除放在ISR最后,结果执行到清标志之前又来了一个中断,标志被重新置位,然后死循环重入,憋了半天才找到原因。
5.4 共享变量冲突:优化一开,Bug就来
还有一类很隐蔽的bug,程序不开优化一切正常,一开-O2优化就莫名其妙出错。这种问题十有八九是中断和主程序共享的变量没加volatile。
我解释一下原因:编译器优化时,会把一些变量暂时放在CPU寄存器里,而不是每次使用都从内存读。如果主程序里有一个循环在等一个变量变化,而这个变量是在中断里被改变的,编译器优化后发现这个循环体里没有修改该变量,就直接把它当成不变的常量优化掉了,那你等一万年也等不到变化。
解决办法很简单:所有在中断和主程序之间共享的变量,定义时加volatile修饰,比如volatile uint8_t rx_flag。这个关键字就是在告诉编译器,这变量随时可能被外部改变,别给我优化掉。另外,如果共享变量是多字节的,比如32位变量,还要考虑访问的原子性,必要时在临界区里读取或写入,避免出现读到一半被中断打断的“撕裂”情况。
下面把常见中断问题整理成一个速查表,方便排查时对照:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 程序随机死机复位 | ISR耗时过长、死循环等待 | 简化ISR,去掉阻塞调用,增加超时机制 |
| 按键触发次数不对 | 机械抖动、触发沿错误 | 加RC滤波和软件消抖,核对触发沿 |
| 中断执行一次后不再响应 | 中断标志未清除 | ISR开头清标志,查参考手册 |
| 高优先级中断频繁打断低优先级 | 优先级配置不合理 | 按实时性需求重新规划抢占优先级 |
| 开优化后程序出错 | 共享变量缺volatile | 加volatile,必要时临界区保护 |
| 发送数据引发接收中断 | 半双工总线自回环 | 发送时屏蔽RX中断或关闭回环模式 |
6. 从理论到PCB,中断这条线怎么学才通
6.1 先跑通三个实验:按键、定时器、串口
如果你正准备系统性地学中断,我建议先别急着看一堆理论,直接上手跑实验。三个实验跑通,中断的底子就打牢了。
第一个实验是按键外部中断。用开发板接一个按键,按下点亮LED,再按一次熄灭。这个实验要搞明白三个细节:按键按下时电平怎么跳变、用上升沿还是下降沿触发、软件和硬件怎么配合消抖。第二个实验是定时器中断。用定时器产生周期1ms的中断,在中断里累加计数,让LED每500ms翻转一次。这个实验要理解定时器的预分频系数、自动重装载值怎么计算,以及如何通过标志判断溢出。第三个实验是串口中断接收。PC发一串字符给单片机,单片机收到后用中断方式原路返回。这个实验要理解串口接收标志位、数据寄存器读操作对标志的影响,以及DMA和中断怎么配合。
三个实验都跑通了,你已经能处理绝大部分中断应用场景。这时候再往前走一步,把每个实验对应的原理图拿出来,自己在纸上重新画一遍,标出每个引脚的滤波电阻、上拉电阻、去耦电容,这就是从代码往硬件方向过渡的开始。
6.2 从一块自己的小板开始:把中断电路画到PCB上
很多硬件新人对中断的理解停留在“写代码使能中断”这一步,从来不关心中断引脚在PCB上是什么状态。但实际产品里,中断能否可靠触发,往往取决于PCB设计。我建议你尽早进入“自己画板子”这个环节。
找一个小项目,比如做一个带按键、OLED屏幕、旋转编码器的桌面小仪表。原理图上,按键和编码器的中断引脚必须加上下拉电阻和RC滤波,MCU每个电源引脚旁放0.1μF去耦电容,晶振下方铺地铜皮。PCB布局时,按键和编码器尽量靠近MCU,中断信号线走短,不要在板子边缘绕圈。打样回来后,用示波器实测按键按下时的波形,看看有没有抖动毛刺,再用信号发生器给中断引脚灌一个高速脉冲序列,看看MCU会不会漏中断。
这一步做完,你才能真正理解“中断的可靠性设计”是什么意思。它不是一句口号,而是具体到每一颗电阻、每一厘米走线的事情。
6.3 面试和笔试里,中断相关的常见套路
最后聊聊求职面试。无论你面硬件工程师、嵌入式工程师还是单片机工程师,中断几乎是必考话题。面试官问中断,其实是在考察三样东西:基础概念、工程经验、系统思维。
基础概念类问题,比如“中断和轮询有什么区别”“中断服务函数的执行流程是什么”,这类问题把本文第一节的内容吃透就能回答。工程经验类问题,比如“中断里能不能调用printf”“多字节变量在中断里为什么可能出错”“如何用DMA加空闲中断接收不定长数据”,需要你有实际调试经验支撑。系统思维类问题,比如“给你一个实时性要求很高的电机控制系统,中断优先级怎么规划”“外部中断信号传输距离长,如何设计前端电路”,这种题没有标准答案,但考察的是你能不能把中断放到整个硬件系统里去思考。
面试时给面试官讲清楚“你曾经遇到过一个中断相关的bug,最后怎么定位怎么解决的”,比背十道题都管用。所以平时做项目时,遇到问题多记录多总结,这些东西才是你真正能拿出手的硬通货。
做完前面这一串实验和设计,再看市面上那些从理论到PCB的硬件课程,你会发现自己的学习节奏清晰多了,不至于东一榔头西一棒子。
我个人在这些年实践中最深的体会是:中断不是一个孤立的软件知识,它把CPU内核、外设寄存器、定时时序、通信协议和PCB布局全都串在了一起。前期在这些细节上考虑得越细,后期调试的日子就越清闲。如果你现在刚开始学中断,建议花一天时间把文章里的例子在开发板上跑一遍,再拿一块旧板子对照原理图查一查中断引脚的去耦和滤波。相信我,这一天的投入比刷一百遍概念都值。