前言
上一篇的轮询模式,CPU 被绑在 while(1) 里不停查询。本篇引入中断,让硬件主动通知 CPU;再重点讲最常用的内部中断——定时器中断,实现非阻塞的精准定时。
一、什么是中断?一句话理解
CPU 正在执行主程序,某个紧急事件向 CPU 发来请求信号。CPU 立刻暂停当前代码、保存现场,跳转到专门写好的中断服务函数处理;处理完恢复现场,回到刚才的位置继续执行。
类比:你在看书(while(1)),手机响了(中断)。
- 放下书,标记读到第几页(保护现场)
- 接电话处理(中断服务函数)
- 挂断,翻回标记继续看(恢复现场)
核心优势:不是 CPU 主动轮询查事件,而是事件主动通知 CPU,实时响应。
基础名词
- 中断源:触发中断的来源。外部中断(按键、外部电平变化);内部中断(定时器、串口、ADC)
- 中断标志位:硬件自动置 1,代表 “中断来了”;进入中断函数后必须清零,否则重复触发
- 中断优先级:多个中断同时到,优先级高的先执行;高优先级可抢占低优先级(中断嵌套)
- 中断服务函数 ISR:不能传参、不能返回值,尽量简短
- 中断向量表:内存地址表,存各中断源的 ISR 入口地址,硬件自动查表跳转
二、外部中断(按键中断举例)
外部中断靠引脚电平变化触发:
- 下降沿触发:高→低瞬间(按键按下最常用)
- 上升沿触发:低→高瞬间
- 电平触发:持续低 / 高就持续触发(易重复进中断,少用)
机械按键有抖动,硬件加电容消抖,或软件在中断里延时消抖。但中断里不推荐延时,项目一般用标志位,把业务逻辑放回 while(1)。
sbit LED = P1^0;
bit key_flag = 0; // 按键标志位
void Int0_ISR() interrupt 0 // 外部中断0
{
key_flag = 1; // 只打标记,不做复杂操作
}
void main(void)
{
IT0 = 1; // 下降沿触发
EX0 = 1; // 开外部中断0
EA = 1; // 开总中断
while(1)
{
if(key_flag) // 主循环处理业务
{
LED = ~LED;
key_flag = 0;
}
}
}
好习惯:中断只做标记,业务逻辑交给主循环。ISR 一定要短!
三、定时器中断:最常用的内部中断
定时器本质是一个不断自增的计数器,由系统时钟驱动。计数到预设值溢出,硬件自动触发中断。
对比阻塞延时:
- delay_ms():CPU 原地空转,啥也干不了(阻塞)
- 定时器中断:计数器硬件自动计数,CPU 正常跑主程序;到点触发中断(非阻塞)
配置要点
- 预分频:把系统时钟分频,得到计数脉冲
- 自动重装载值:计数器 0 加到设定值 → 溢出 → 置标志位
- 开启定时器:中断使能 + 总中断
代码示例(51 单片机,定时器 0,50ms 定时,20 次 = 1s,LED 每秒翻转):
#include <reg52.h>
sbit LED = P1^0;
bit timer_flag = 0;
unsigned char cnt = 0;
void Timer0_ISR() interrupt 1 // 定时器0中断
{
TH0 = 0x4C; // 重装初值,50ms
TL0 = 0x00;
cnt++;
if(cnt >= 20) // 20*50ms = 1s
{
cnt = 0;
timer_flag = 1;
}
}
void Timer0_Init(void)
{
TMOD |= 0x01; // 模式1
TH0 = 0x4C;
TL0 = 0x00;
ET0 = 1; // 开定时器0中断
TR0 = 1; // 启动定时器
EA = 1; // 开总中断
}
void main(void)
{
Timer0_Init();
while(1)
{
if(timer_flag) // 每秒执行一次任务
{
LED = ~LED;
timer_flag = 0;
}
}
}
四、中断执行完整流程(重点)
五、写中断代码的避坑要点
- ✅ ISR 尽量短小,禁止长延时、复杂循环、printf(耗时)
- ✅ 中断和主循环共用的变量加 volatile,防编译器优化
- ✅ 中断标志位一定清除,不清零会反复进入中断
- ✅ 不要在中断里调用耗时函数,中断只负责打标记
- ✅ 注意中断优先级,避免低优先级任务被一直抢占丢事件
- ❌ 别在中断里做大量业务逻辑,只做标记!
六、小结
- 中断本质:硬件主动通知 CPU,实现异步事件响应。
- 外部中断:响应突发外部信号(按键、传感器)
- 定时器中断:周期性定时任务,替代阻塞延时,项目里最常用





