51单片机串口通信中的“隐形开关”:为什么你的中断停不下来?
你有没有遇到过这种情况——
只从电脑发了一个字节到51单片机,结果它却像疯了一样不停地回传、打印、甚至卡死?
主程序好像完全跑不起来,CPU一直在某个地方打转……
别急,这很可能不是硬件坏了,也不是代码逻辑错了。
问题的根源,藏在一个小小的标志位里:RI 和 TI。
在51单片机的串口通信实验中,这两个看似不起眼的位,其实是决定系统能否稳定运行的“隐形开关”。而绝大多数初学者踩过的坑,都和它们有关。
一、串口通信靠什么“感知”数据到来?答案是:中断 + 标志位
我们都知道,UART(通用异步收发器)是一种常见的串行通信方式,用于单片机与PC、传感器或其他设备交换数据。但在资源极其有限的8051架构中,CPU不能一直傻等数据到来——那样太浪费时间了。
于是,工程师设计了一种更聪明的方式:中断驱动 + 状态标志机制。
当一帧数据接收完成时,硬件自动设置一个“标记”,告诉CPU:“嘿,有新消息!”
这个“标记”就是RI(Receive Interrupt Flag);
同理,当发送完成后,会置起TI(Transmit Interrupt Flag)。
但关键来了:
🔴51单片机不会自动清除这些标志位!必须由软件手动清零。
如果你忘了这一句RI = 0;或TI = 0;,那么下一次从中断返回后,CPU马上又看到“标记还在”,于是再次跳进中断服务程序……
就这样,无限循环开始了——也就是大家常说的“中断风暴”。
这不是bug,这是设计规则。理解它,才能驾驭它。
二、SCON寄存器:串口的“控制中枢”
所有这一切的背后,核心就是那个地址为0x98的特殊功能寄存器——SCON(Serial Control Register)。
| 位 | 名称 | 功能说明 |
|---|---|---|
| D7 | SM0 | 串口工作方式选择位(配合SM1) |
| D6 | SM1 | 方式选择主控位 |
| D5 | SM2 | 多机通信使能(常用于模式2/3) |
| D4 | REN | 允许接收控制位(必须置1才能接收) |
| D3 | TB8 | 发送第9位(仅模式2/3使用) |
| D2 | RB8 | 接收第9位或停止位 |
| D1 | TI | 发送中断标志:发送完一帧后硬件置1 |
| D0 | RI | 接收中断标志:接收到一帧后硬件置1 |
其中最值得关注的就是最后两位:TI 和 RI。
它们的工作流程是这样的:
- 数据接收完成 → 硬件将RI = 1
- 若已开启串口中断(ES=1)且总中断使能(EA=1)→ 触发中断
- CPU跳转至中断向量地址
0x0023,执行 ISR - 在ISR中读取SBUF获取数据
- 必须执行
RI = 0;手动清零 - 否则下次返回主程序前,中断条件仍满足 → 再次进入ISR → 死循环!
TI也是一样的道理:每次发送完一个字节后,TI被置1,若不清零,后续任何时刻都会“假装”刚发完数据,导致无法继续发送或反复触发中断。
三、一段经典代码背后的“生死线”
来看一个典型的串口初始化与中断处理示例:
#include <mcs51/8051.h> void uart_init() { TMOD |= 0x20; // 定时器1,模式2:8位自动重装 TH1 = 0xFD; // 11.0592MHz晶振,波特率9600 SCON = 0x50; // 模式1,REN=1,允许接收 TR1 = 1; // 启动定时器1作为波特率发生器 ES = 1; // 使能串口中断 EA = 1; // 开启全局中断 } void main() { uart_init(); while(1) { // 主循环可做其他事:扫描按键、驱动LED... } } void serial_isr() __interrupt(4) { if (RI) { unsigned char dat = SBUF; // 读取接收到的数据 SBUF = dat; // 回显给上位机 while (!TI); // 等待发送完成 TI = 0; // ⚠️ 必须清零TI RI = 0; // ⚠️ 必须清零RI! } if (TI) { TI = 0; // 如果单独处理发送完成事件 } }注意这两行:
TI = 0; RI = 0;它们就像“安全闸门”的关闭按钮。少了任何一个,整个系统就可能失控。
💡 小贴士:有些开发者习惯把RI = 0;放在读取SBUF之前,这是错误的!因为一旦清零,就失去了“本次中断是由接收引起”的判断依据。正确的顺序是:先判断 → 再处理 → 最后清标志。
四、为什么共用一个中断向量?如何区分到底是接收还是发送?
细心的人会发现:51单片机只有一个串口中断入口,地址固定为0x0023,中断号为4。
那怎么知道当前是因为接收完成还是发送完成而进入中断呢?
答案就在RI 和 TI 的状态组合上。
| RI | TI | 可能原因 |
|---|---|---|
| 1 | 0 | 接收到了数据 |
| 0 | 1 | 发送完成了数据 |
| 1 | 1 | 接收和发送同时完成(少见但可能发生) |
所以在中断服务程序中,必须通过if(RI)和if(TI)分别判断,进行分支处理。
这也意味着,你可以实现全双工通信:一边接收命令,一边发送响应,互不干扰。
但要注意:不要在中断里加 delay() 延时!
中断应尽可能快地完成任务并退出,避免影响其他中断响应。比如发送等待可以用查询TI的方式,也可以改为中断发送(每次写SBUF后启动中断,完成后再清TI并关闭)。
五、常见“翻车现场”及应对策略
❌ 现象1:收到一个字符,回传十个相同字符
诊断:未清RI导致重复进入接收分支
修复:确保RI = 0;出现在处理逻辑之后、中断退出之前
❌ 现象2:程序启动后直接“卡死”,连LED都不闪
诊断:中断频繁触发,CPU始终陷在ISR中出不来
排查点:
- 是否漏写了RI=0或TI=0
- 是否在中断中调用了阻塞函数(如长延时)
- 是否存在堆栈溢出(过多局部变量)
❌ 现象3:只能发送一次,之后再也发不出数据
诊断:TI未清零,系统认为上次发送还未结束
修复:每次发送完成后务必执行TI = 0;
✅ 高级技巧:使用临时变量提升稳定性
void serial_isr() __interrupt(4) { unsigned char stat = SCON; // 一次性读取SCON状态 unsigned char dat; if (stat & 0x01) { // RI == 1 dat = SBUF; // 存入缓冲区或处理 process_data(dat); } if (stat & 0x02) { // TI == 1 // 发送完成回调 tx_complete_callback(); } SCON &= 0xFC; // 清除RI和TI(bit0和bit1清零) }这种方法可以避免在处理过程中再次发生标志变化带来的竞争风险,尤其适用于高速通信场景。
六、教学之外的深层价值:学会“状态机思维”
很多人学完51单片机串口通信后觉得“不过如此”,但实际上,这段经历真正教会我们的,是一种底层嵌入式开发的核心思维方式——基于状态标志的状态机控制模型。
你在处理RI/TI的时候,本质上是在维护一个微型状态机:
- 初始状态:等待接收
- 事件触发:RI=1 → 进入“接收完成”状态
- 动作执行:读SBUF、处理数据
- 状态复位:RI=0 → 回到初始状态
这种“检测标志 → 执行动作 → 清除标志”的模式,在后来学习RTOS、DMA、I²C、SPI乃至操作系统信号量时都会反复出现。
可以说,搞懂了RI和TI,你就迈出了嵌入式实时系统编程的第一步。
七、写在最后:老古董也有大智慧
如今的STM32、ESP32等现代MCU早已支持DMA、硬件自动清标志、环形缓冲队列等功能,似乎再也不用操心这些细节了。
但正因如此,我们才更需要回过头来理解像51单片机这样“赤裸裸暴露硬件行为”的平台。
它强迫你去思考:
- 中断是怎么来的?
- 标志位是谁设的?谁该清?
- 系统如何知道一件事已经做完?
这些问题的答案,构成了嵌入式系统的地基。
所以,当你下次再做一个串口实验时,不妨多问一句:
👉 “我这句RI = 0;写对位置了吗?”
也许正是这一行代码,决定了你的系统是稳定运行,还是陷入无尽的中断深渊。
📌关键词回顾:51单片机串口通信实验、中断标志位、RI、TI、SCON寄存器、串口中断服务程序、硬件置位软件清零、中断风暴、波特率设置、UART通信、中断优先级、标志位清零、串口调试、中断向量、状态机控制
如果你在实践中还遇到过哪些“诡异”的中断问题,欢迎留言分享,我们一起拆解那些藏在寄存器里的秘密。