51单片机外部中断超详细教程 —— 从寄存器配置到按键消抖,一篇全搞定
🔌 一、从问题开始:主函数一直在死循环,怎么就突然跳到中断函数了?
你可能会思考这样的问题:main()函数中只有一个Delay_xms(1000);死循环,没有对LED灯做任何操作。那么LED为什么会自己亮灭呢?
答案就在于外部中断。当按键按下时,单片机被“打断”,跳转到中断处理函数执行LED = ~LED;,然后返回主程序继续执行。
用生活中的场景来理解就是这样的——
你正在打游戏 → 水壶响了 → 你停止打游戏去倒水 → 倒完水接着打游戏
在这个类比中:
| 生活中的场景 | 单片机中的概念 |
|---|---|
| 打游戏 | while(1)主循环中执行的代码 |
| 水壶响了 | 中断源产生中断请求 |
| 倒水 | 中断处理函数(Interrupt Service Routine, ISR) |
简单来说,中断就是CPU在执行当前程序的过程中,由于某种紧急事件的发生,暂时停止当前程序的执行,转而执行处理该事件的程序,处理完后再返回原来被中断的地方继续执行原来的程序。
在生活中,如果水壶响(中断请求)了,无论你在打游戏还是写作业,都必须去倒水(执行中断服务程序),倒完水之后再接着打游戏(恢复现场)。这正是中断的核心价值所在。
🎯 二、中断源:什么事件能“打断”CPU?
STC89C52单片机提供了8个中断请求源,分别为:
| 中断源 | 名称 | 对应引脚/来源 | 中断向量地址 |
|---|---|---|---|
| 外部中断0 | INT0 | P3.2 | 0003H |
| 定时器0中断 | Timer0 | 内部 | 000BH |
| 外部中断1 | INT1 | P3.3 | 0013H |
| 定时器1中断 | Timer1 | 内部 | 001BH |
| 串口中断 | UART | TXD/RXD | 0023H |
| 定时器2中断 | Timer2 | 内部 | 002BH |
| 外部中断2 | INT2 | 扩展 | 0033H |
| 外部中断3 | INT3 | 扩展 | 003BH |
这些中断源可以分为三大类:外部中断(由外部引脚电平变化触发)、定时器中断(由定时/计数器溢出触发)和串口中断(由数据发送/接收完成触发)。
⚙️ 三、寄存器配置:如何让中断工作起来?
要让外部中断正常工作,需要配置三个寄存器。下图展示了中断系统的工作流程:
从上图中可以看到,中断源首先经过中断允许控制寄存器(IE),然后经过中断优先级控制寄存器(IP),最后进入CPU。我们需要按照这个顺序来配置中断。
3.1 TCON寄存器——设置触发方式
TCON(Timer/Counter Control Register,定时器/计数器控制寄存器)位于地址88H,是可位寻址的寄存器,其低4位控制外部中断的触发方式和中断请求标志。
TCON各位的含义如下:
| 位 | 名称 | 功能说明 |
|---|---|---|
| B7 | TF1 | 定时器T1溢出中断标志(溢出时由硬件置1,响应中断后硬件自动清0) |
| B6 | TR1 | 定时器T1运行控制位(TR1=1启动T1,TR1=0停止T1) |
| B5 | TF0 | 定时器T0溢出中断标志 |
| B4 | TR0 | 定时器T0运行控制位 |
| B3 | IE1 | 外部中断1请求标志(检测到中断时硬件置1,响应后自动清0) |
| B2 | IT1 | 外部中断1触发方式控制位 |
| B1 | IE0 | 外部中断0请求标志 |
| B0 | IT0 | 外部中断0触发方式控制位 |
IT0和IT1决定了触发方式:
-
IT0/IT1 = 1:下降沿触发(下降沿触发方式)。下降沿触发会锁存中断请求,不会出现电平触发时的重复请求问题。
-
IT0/IT1 = 0:低电平触发(电平触发方式)。低电平触发方式下,外部中断源必须保持低电平有效直到CPU响应中断,并在中断服务程序执行完之前撤消低电平,否则将产生另一次中断。
💡 何时选择边沿触发?
下降沿触发(ITx=1):当多个按钮共用一根中断线时尤为有用,因为即便后续按钮松开导致电平回升,也不会多次进入中断。而每个中断信号都会锁存请求,直至被CPU响应。
低电平触发(ITx=0):适用于中断线被多个外设共享的情形,中断线会在任意外设需要服务时强制拉低。只要线上保持低电平,中断就会持续触发,直到软件清除请求。另外,如果想用外部中断将单片机从掉电模式唤醒,必须选用低电平触发(边沿触发无效)。
3.2 IE寄存器——开中断许可
IE(Interrupt Enable Register,中断允许寄存器)位于地址A8H,控制中断的开关。要注意的是,中断允许采用两级控制:先要通过EA打开总中断,然后还要分别打开对应的中断允许位。
IE各位的含义如下:
| 位 | 名称 | 功能说明 |
|---|---|---|
| B7 | EA | 总中断允许控制位(EA=1开放中断,EA=0屏蔽所有中断) |
| B6 | — | 保留位 |
| B5 | ET2 | 定时器T2中断允许位 |
| B4 | ES | 串口中断允许位 |
| B3 | ET1 | 定时器T1中断允许位 |
| B2 | EX1 | 外部中断1中断允许位 |
| B1 | ET0 | 定时器T0中断允许位 |
| B0 | EX0 | 外部中断0中断允许位 |
3.3 IP寄存器——设置中断优先级
IP(Interrupt Priority Register,中断优先级寄存器)用于设置中断源的优先级。51单片机提供两级中断优先级,可实现二级中断嵌套。
IP各位的含义如下:
| 位 | 名称 | 功能说明 |
|---|---|---|
| B7 | — | 保留位 |
| B6 | — | 保留位 |
| B5 | — | 保留位 |
| B4 | PS | 串口中断优先级控制位(PS=1为高优先级) |
| B3 | PT1 | 定时器T1中断优先级控制位 |
| B2 | PX1 | 外部中断1优先级控制位 |
| B1 | PT0 | 定时器T0中断优先级控制位 |
| B0 | PX0 | 外部中断0优先级控制位 |
需要注意的是,IP设置的是执行优先级。当多个中断源同时请求时,CPU会优先响应高执行优先级的中断;但如果没有设置IP,单片机仍会按照查询优先级(即自然优先级)顺序依次查询中断标志。查询优先级是硬件固定的:外部中断0 > 定时器0 > 外部中断1 > 定时器1 > 串口中断 > 定时器2。
🔄 四、中断优先级:多个中断同时来怎么办?
4.1 自然优先级
如果不设置优先级(IP寄存器的各位全为0),中断系统会按自然优先级来响应中断请求。自然优先级的顺序从高到低为:INT0 > Timer0 > INT1 > Timer1 > UART。
4.2 中断优先级的三条原则
中断优先级遵循以下三条原则:
-
CPU同时接收到几个中断时,首先响应优先级最高的中断请求。
-
正在进行的中断过程不能被新的同级或低优先级的中断请求所中断。
-
正在进行的低优先级中断服务,能被高优先级中断请求中断。
4.3 中断嵌套机制
通过设置IP寄存器改变执行优先级后,高优先级中断可以打断低优先级中断,形成中断嵌套。
中断嵌套的实例如下:假设将串口中断设置为高优先级(IP = 0x10),则串口中断可以打断任何其他中断的服务函数实现嵌套,且只有串口中断能打断其他中断的服务函数。若串口中断没有触发,则其他几个中断之间还是保持自然优先级,相互之间无法嵌套。
注意: 如果在中断优先级寄存器IP中设置了某中断为高优先级,那么在任意时刻,一旦高优先级中断被触发,CPU会立刻做出响应,这可能产生中断嵌套。因此,编程者必须在中断服务程序中处理好堆栈深度、防止数据覆盖等问题。
📍 五、中断向量表:中断发生后跳转到哪里?
当中断发生时,CPU会根据中断源的类型,自动跳转到程序存储器中固定的地址——这个地址就是中断向量。
51单片机的中断向量表位于程序存储器的起始位置,即从地址0x0000开始到0x001F。这些地址是固定的,由单片机的硬件设计决定,程序员不能更改。
| 中断源 | 中断号 | 中断向量地址 |
|---|---|---|
| 外部中断0 | 0 | 0003H |
| 定时器0中断 | 1 | 000BH |
| 外部中断1 | 2 | 0013H |
| 定时器1中断 | 3 | 001BH |
| 串口中断 | 4 | 0023H |
| 定时器2中断 | 5 | 002BH |
| 外部中断2 | 6 | 0033H |
| 外部中断3 | 7 | 003BH |
在程序中,我们通过interrupt关键字来指定当前函数是哪个中断源的中断服务程序。例如,void Int0_Routine(void) interrupt 0告诉编译器这个函数是外部中断0的中断服务程序,编译完成后,该函数的入口地址会自动放置在外部中断0对应的中断向量地址0003H处。当外部中断0发生时,CPU会自动跳转到这个地址执行。
Keil Cx51编译器支持多达32个中断函数(中断号0-31),但需要注意的是,中断号0-4对应51单片机标准的5个中断源,中断号5-7则对应STC扩展的外部中断2、3和定时器2中断等。
💡 中断服务函数中不要编写复杂代码:
中断服务函数与普通函数不同,它的入口地址是固定在中断向量表中的,而普通函数由程序动态调用。在
interrupt修饰的函数中:
不能有返回值(必须是void类型)
不能传递参数
尽量不要编写耗时太长的代码,否则会影响后续中断的及时响应
中断ISR里代码越精简越好,因为被中断的主程序可能正在处理对时序要求非常苛刻的任务
🧠 六、中断响应完整流程:从触发到返回
当中断触发时,CPU会按以下流程处理:
步骤1:保存现场
CPU将当前程序计数器PC的值压入堆栈(硬件自动完成)。如果中断服务程序使用了工作寄存器、累加器、标志位等,还需要手动将它们压入堆栈保护,以便执行完中断后恢复原样。
步骤2:中断响应
CPU根据中断源查询中断向量表,自动跳转到对应的中断向量地址,执行中断服务程序。
步骤3:执行中断服务
执行用户编写的中断处理代码。特别注意:如果中断服务程序中没有清除中断标志位,且触发条件依然存在,中断可能会再次触发,导致程序逻辑混乱。
步骤4:恢复现场
将堆栈中保护的现场数据弹出,恢复原来的寄存器状态。
步骤5:中断返回
执行RETI指令,自动弹出堆栈中的PC值,返回到主程序被中断的位置继续执行。
💡 为什么需要“保护现场”?
因为中断的发生是随机的——CPU不知道用户什么时候会按下按键,中断服务程序可能在主程序执行的任何位置被调用。CPU在中断服务程序中使用的寄存器可能在中断前就已经存放了重要的数据。如果不保护现场,中断返回后这些数据就会丢失,程序就会出现逻辑错误。
🔗 七、硬件连接:为什么板子上的按键能触发中断?
从电路原理图中可以看到,按键SW3连接到P3.2(INT0),按键SW4连接到P3.3(INT1)。当按下按键时,P3.2或P3.3引脚被下拉到GND(低电平);当松开按键时,上拉电阻将引脚拉回到高电平。
正是因为P3.2和P3.3引脚被配置为下降沿触发(IT0=1,IT1=1),所以当按下按键的瞬间,引脚从高电平跳变为低电平,产生了下降沿信号,从而触发外部中断。
这种连接方式的优点是:不需要在主循环中不断轮询按键状态,只要有按键按下,单片机就会立即响应中断,实时性非常好。
🚧 八、按键消抖:一个容易被忽视的关键问题
机械按键在按下和释放的瞬间,由于金属弹片的弹性作用,会在几毫秒内产生多次快速的通断现象,这被称为 “按键抖动” 。如果不做消抖处理,按下一次按键可能会触发多次中断,导致按键响应出现混乱。示波器可清晰捕捉到抖动尖峰,其持续时间通常在5~10ms左右。
消抖的两种方法
1. 软件消抖
在中断服务函数中,添加一个短延时,在延时之后再次检测按键状态,确认确实是按键按下,而不是抖动产生的伪信号。
优化后的中断服务程序示例:
void Int0_Routine(void) interrupt 0
{
EX0 = 0; // 暂时关闭外部中断0,防止重入
Delay_xms(10); // 延时消抖
if(P3_2 == 0) // 再次检测确认确实是按键按下
{
LED1 = ~LED1; // 执行中断功能
while(P3_2 == 0); // 等待按键释放(松手检测)
}
EX0 = 1; // 重新打开外部中断0
}
2. 硬件消抖
使用RC滤波电路或施密特触发器对按键信号进行硬件滤波,能从根本上消除抖动信号。常见方案包括74HC08与门芯片构建的滤波电路,其传播延迟约11ns,响应速度快且不占用CPU时间。
💡 两种消抖方式对比:
软件消抖:实现简单,无需额外硬件,但会占用CPU时间,响应有轻微延迟
硬件消抖:响应快,不占用CPU时间,稳定性最好,但需要额外的硬件电路
📊 九、代码逐行解析
#include "reg52.h"
#include <intrins.h>
typedef unsigned char uchar;
typedef unsigned int uint;
sbit LED1 = P1^0; // 定义LED1连接到P1.0口
sbit LED2 = P1^1; // 定义LED2连接到P1.1口
💡
sbit关键字的解释:
sbit是Keil C51编译器特有的关键字,用于定义特殊功能寄存器中的单个位。普通的C语言中没有sbit这个关键字,但在51单片机编程中,通过sbit可以方便地对某个I/O引脚进行位操作。例如,sbit LED1 = P1^0;表示LED1这个变量代表P1口的第0位(即P1.0引脚)。^符号后面的数字表示该寄存器的第几位。
中断初始化代码:
IT0=1; // 配置外部中断0为下降沿触发(IT0是TCON.0位)
EX0=1; // 打开外部中断0中断允许位(EX0是IE.0位)
IT1=1; // 配置外部中断1为下降沿触发(IT1是TCON.2位)
EX1=1; // 打开外部中断1中断允许位(EX1是IE.2位)
EA=1; // 打开总中断允许控制位(EA是IE.7位)
中断服务函数:
void Int0_Routine(void) interrupt 0 // interrupt 0 = 外部中断0
{
Delay_xms(10); // 延时消抖
LED1 = ~LED1; // LED1状态取反
}
void Int1_Routine(void) interrupt 2 // interrupt 2 = 外部中断1
{
Delay_xms(10);
LED2 = ~LED2;
}
🚀 十、总结与实践建议
外部中断是51单片机中非常重要的功能,它让单片机能够及时响应外部突发事件的实时处理。通过学习外部中断,你应该掌握了:
-
中断的概念:CPU暂停当前工作去处理紧急事件,处理完后再返回
-
中断源的类型:外部中断(INT0/1/2/3)、定时器中断、串口中断
-
中断相关寄存器:TCON(触发方式)、IE(中断允许)、IP(优先级)
-
中断响应流程:保存现场→跳转执行→恢复现场→中断返回
-
中断向量表:每个中断源对应固定的入口地址
-
中断优先级:自然优先级和可编程执行优先级的区别
-
按键消抖的必要性:软件消抖或硬件消抖解决按键抖动问题
-
中断嵌套机制:高优先级中断可以打断低优先级中断
-
硬件连接原理:按键如何通过P3.2/P3.3引脚触发外部中断
🛠️ 动手实践一下
-
改变触发方式:将
IT0=1改为IT0=0,观察按键按下时的效果有何不同。你会发现低电平触发下,只要按键保持按下,中断会被反复触发。 -
设置中断优先级:尝试设置IP = 0x04(将外部中断1设为高优先级),然后在两个中断服务函数中各加入一个长延时,观察中断嵌套现象。注意观察高优先级中断是否可以打断低优先级中断的执行。
-
关闭消抖延时:将中断服务函数中的
Delay_xms(10);注释掉,然后快速按一下按键,观察LED灯变化多少次,你会发现抖动导致的多次触发会让LED状态变得“不可预测”。 -
扩展外部中断数量:如果需要更多外部中断源,可以通过I/O扩展芯片(如74HC165)或与门逻辑电路实现。常用的扩展方法包括硬件请求+软件查询、二极管组合逻辑等。
-
注意中断服务程序的效率:在编写中断服务程序时,代码要尽量简洁高效。对于复杂处理,应在中断服务程序中仅设置标志位,而在主循环中处理实际任务,这样可以避免中断服务函数占用过长的时间。
如果你在实验中遇到问题,欢迎在评论区留言交流!
更多推荐

所有评论(0)