一、中断是什么,为什么需要中断
CPU在执行主程序的过程中,当出现某种紧急事件时,CPU会暂停当前正在执行的程序,转去执行处理该事件的程序(中断服务程序),处理完毕后再返回原来被暂停的位置继续执行。这就是中断的核心逻辑。
对比"轮询"方式,中断的优势非常明显:轮询需要CPU不断查询外设状态,大量时间浪费在等待上;而中断允许CPU在事件未发生时专注处理主任务,事件到来时自动跳转,实时性和效率都更高。在51单片机中,中断还承担着故障处理、分时操作等关键角色。
51单片机有5个中断源:
| 中断源 | 引脚/来源 | 触发条件 |
|---|---|---|
| 外部中断0(INT0) | P3.2 | 低电平或下降沿 |
| 外部中断1(INT1) | P3.3 | 低电平或下降沿 |
| 定时器0中断(T0) | 内部 | T0计数溢出 |
| 定时器1中断(T1) | 内部 | T1计数溢出 |
| 串口中断 | P3.0/P3.1 | 接收/发送一帧完成 |
二、中断控制寄存器详解
与中断直接相关的寄存器有4个:TCON(中断请求标志与触发方式)、SCON(串口控制)、IE(中断允许)、IP(中断优先级)。
TCON --- 中断请求与控制
TCON字节地址88H,可位寻址。与中断相关的位如下:
- IT0(TCON.0):INT0触发方式,1=下降沿,0=低电平
- IE0(TCON.1):INT0请求标志,硬件置1,响应后自动清0
- IT1(TCON.2):INT1触发方式
- IE1(TCON.3):INT1请求标志
- TR0/TR1:定时器启动控制位(高4位中的低2位)
- TF0(TCON.5):T0溢出标志,响应后硬件自动清0
- TF1(TCON.7):T1溢出标志
SCON --- 串口中断标志
SCON地址98H,其中 RI (接收中断标志)和 TI (发送中断标志)必须由软件手动清零,硬件不会自动清除。
IE --- 中断允许控制
| 位 | 名称 | 功能 |
|---|---|---|
| IE.7 | EA | 总中断开关,1=开 |
| IE.4 | ES | 串口中断允许 |
| IE.3 | ET1 | T1中断允许 |
| IE.2 | EX1 | INT1中断允许 |
| IE.1 | ET0 | T0中断允许 |
| IE.0 | EX0 | INT0中断允许 |
IP --- 中断优先级
51单片机有两个优先级,IP寄存器中对应位=1则设为高优先级。默认优先级顺序为:INT0 > T0 > INT1 > T1 > 串口。高优先级中断可以打断低优先级中断,形成二级嵌套。
三、中断向量与C51的 interrupt 关键字
每个中断源在程序存储器中有固定的入口地址,称为中断向量:
| 中断源 | 入口地址 | C51中断号 |
|---|---|---|
| 外部中断0 | 0003H | 0 |
| 定时器0 | 000BH | 1 |
| 外部中断1 | 0013H | 2 |
| 定时器1 | 001BH | 3 |
| 串口中断 | 0023H | 4 |
在Keil C51中,用 interrupt n 声明中断服务函数,编译器会自动将函数链接到对应的中断向量地址。例如:
c
void External0_ISR(void) interrupt 0 // 对应外部中断0
void Timer0_ISR(void) interrupt 1 // 对应定时器0
void UART_ISR(void) interrupt 4 // 对应串口中断
关于 using 关键字 :using 用于指定中断函数使用哪个工作寄存器组(0~3)。如果不指定,C51默认在中断入口切换到寄存器组0。对于简单的单级中断场景,不写 using 完全可以正常工作;只有当主函数和中断函数都要频繁访问寄存器组、对性能有极致要求时,才需要考虑用 using 分离寄存器组,但这样需要你自行保证寄存器组使用的正确性。
四、代码实现
4.1 外部中断:按键控制LED翻转
这是最基础的外部中断应用。INT0接按键,按下时触发中断,翻转LED状态。
c
#include <reg51.h>
sbit LED = P1^0;
void External0_ISR(void) interrupt 0
{
LED = ~LED; // 翻转LED状态
}
void main()
{
IT0 = 1; // INT0下降沿触发
EX0 = 1; // 使能INT0中断
EA = 1; // 开总中断
LED = 0; // 初始熄灭(假设低电平点亮)
while(1);
}
配置流程只有三步:设置触发方式(IT0)、使能该中断(EX0)、开总中断(EA)。中断服务函数中不要写耗时操作,简单翻转即可。
关于按键抖动:机械按键在按下和释放时会产生5~10ms的电平抖动。如果使用下降沿触发,抖动期间的多个边沿可能导致中断重复触发。在中断服务函数中做简单延时消抖即可:
c
void External0_ISR(void) interrupt 0
{
unsigned char i, j;
for(i = 0; i < 20; i++)
for(j = 0; j < 200; j++); // 约5~10ms延时
if(P3^2 == 0) // 确认按键仍处于按下状态
{
LED = ~LED;
}
}
判断条件 P3^2 == 0 在下降沿触发方式下表示"确认引脚仍为低电平",能够过滤掉抖动产生的误触发。
4.2 定时器中断:精确定时与LED闪烁
定时器中断是51单片机中最常用的中断类型。以12MHz晶振、T0工作在方式1(16位定时)为例,实现500ms定时翻转LED。
初值计算:方式1是16位计数器,最大计数值65536。12MHz晶振的机器周期为1μs,要实现50ms定时,需要计数50000次,初值 = 65536 − 50000 = 15536 = 0x3CB0。
c
#include <reg51.h>
sbit LED = P1^0;
void Timer0_ISR(void) interrupt 1
{
static unsigned char count = 0;
TH0 = 0x3C; // 重装初值 0x3CB0
TL0 = 0xB0;
count++;
if(count >= 10) // 10 × 50ms = 500ms
{
count = 0;
LED = ~LED;
}
}
void main()
{
TMOD &= 0xF0; // 清除T0控制位
TMOD |= 0x01; // T0方式1(16位定时)
TH0 = 0x3C; // 定时50ms初值
TL0 = 0xB0;
ET0 = 1; // 使能T0中断
EA = 1; // 开总中断
TR0 = 1; // 启动定时器
while(1);
}
注意 TH0/TL0 的初值重装写在中断服务函数内部,因为方式1不是自动重装载模式,每次溢出后计数器归零,必须手动重装。
4.3 串口中断:中断接收 + 查询发送
串口中断标志位RI和TI必须软件清零。以下是一个典型的串口收发框架(9600波特率、11.0592MHz晶振):
c
#include <reg51.h>
#define BUF_SIZE 64
unsigned char rxBuf[BUF_SIZE];
unsigned char rxCount = 0;
void UART_Init()
{
TMOD = 0x20; // T1方式2(8位自动重装)
TH1 = 0xFD; // 9600bps @ 11.0592MHz
TL1 = 0xFD;
TR1 = 1; // 启动T1
SM0 = 0; // 串口方式1(8位UART)
SM1 = 1;
REN = 1; // 允许接收
ES = 1; // 使能串口中断
EA = 1; // 开总中断
}
void UART_ISR(void) interrupt 4
{
if(RI)
{
RI = 0; // 软件清接收标志
if(rxCount < BUF_SIZE - 1)
{
rxBuf[rxCount++] = SBUF;
}
}
if(TI)
{
TI = 0; // 软件清发送标志(若使用查询发送,此处可不处理)
}
}
void UART_SendByte(unsigned char dat)
{
SBUF = dat;
while(!TI); // 查询等待发送完成
TI = 0; // 清发送标志
}
接收逻辑放在中断中,每收到一个字节就存入缓冲区并清RI标志;发送则用查询方式,写入SBUF后等待TI置位再清除。这种"中断接收 + 查询发送"的组合在大多数应用场景下足够高效,也避免了收发中断互相干扰的问题。
五、总结
51单片机中断系统的核心可以归纳为"三看":
一看触发条件 :外部中断通过TCON的IT0/IT1选择电平或边沿触发;定时器中断靠T0/T1计数溢出;串口中断靠RI/TI标志。二看使能开关 :IE寄存器中先开对应中断位,再开EA总开关,两者缺一不可。三看标志清零:外部中断和定时器中断的标志位由硬件自动清除,而串口的RI和TI必须软件手动清0,这是最容易踩的坑。
中断函数用 interrupt n 声明,n对应中断号。中断服务函数应当尽量短小,避免长时间占用CPU。对于按键等有抖动的信号源,在中断内加入简单消抖判断即可有效防止误触发。掌握这三点,51单片机的中断编程就抓住了主线。