资讯详情

资讯详情

STC单片机串口通信实战:SBUF寄存器与中断服务函数详解

STC 单片机算是很多电子爱好者真正上手的第一颗“能干活”的芯片价格便宜、资料多、烧录简单拿来做串口通信练手再合适不过。但串口这块内容很多人卡在 SBUF 寄存器到底怎么读写、中断服务函数为什么进不去、波特率怎么算才对这几个点上网上教程要么只贴代码不讲原理要么讲了一堆寄存器却跑不起来。这篇内容就是把我自己反复调试 STC 串口时踩过的坑、验证过的配置流程完整梳理一遍从 SBUF 的读写机制讲到中断服务函数的编写最后给出一份可以直接编译烧录的完整代码。不管你是刚接触单片机的新手还是用过一阵子但串口总是调不通的开发者都能从里面找到能直接抄作业的部分。核心关键词串口通信、SBUF 寄存器、中断服务函数会贯穿全文跟着走一遍基本就能把 STC 的串口跑通。1. 串口通信整体设计与方案拆解1.1 为什么 STC 串口值得单独拿出来讲串口通信在单片机开发里的地位有点像螺丝刀在工具箱里的地位——不起眼但几乎每个项目都要用。调试信息输出、模块间通信、和上位机交互串口都是最省事的那条路。STC 系列单片机比如常见的 STC89、STC12、STC15、STC8 等几乎每一款都带至少一个 UART硬件成本为零只要两根线 TX、RX 对接就能通信。但 STC 串口的“坑”也比较集中。第一它用的是传统的 8051 内核架构串口相关的寄存器命名和操作方式和现代 ARM 芯片差别不小SBUF、SCON、TMOD、TH1、TL1 这一套得先理清楚。第二波特率的产生依赖定时器定时器的工作模式、初值计算、是否倍频都直接影响通信是否正常。第三中断服务函数的写法有固定格式写错了编译能过但运行时进不去中断非常隐蔽。我选择从 SBUF 寄存器切入是因为它是整个串口通信的数据枢纽——发送写它、接收读它理解了 SBUF 的读写时序串口的一大半原理就通了。再往上叠加中断服务函数就构成了一个完整的收发闭环。这个方案的优势在于不依赖任何库函数纯寄存器操作代码量小、可移植性强换一款 STC 芯片只要改波特率初值和寄存器名就能复用。1.2 整体方案的分层思路我把整个串口通信拆成三层来看这样调试的时候能快速定位问题出在哪一层。第一层是硬件连接层负责 TX、RX、GND 三根线的正确对接以及电平匹配。STC 单片机通常是 TTL 电平如果直接连电脑需要 USB 转 TTL 模块如果连另一个单片机则 TX 接 RX、RX 接 TX 交叉连接。第二层是寄存器配置层包括串口工作模式选择SCON、波特率发生器配置定时器 TMOD、TH1、TL1、波特率倍频PCON 的 SMOD 位、中断使能IE、EA。这一层是纯软件配置也是最容易出错的地方。第三层是数据收发层发送靠写 SBUF 触发接收靠读 SBUF 取数据中断方式下还要配合 TI、RI 标志位和中断服务函数。这一层决定了数据能不能正确进出。分层的好处是当串口不通时我可以按“线接对了吗→寄存器配对了吗→数据读写逻辑对吗”的顺序逐层排查而不是一上来就盯着代码发呆。这个排查顺序在后面第 4 节的常见问题里还会反复用到。1.3 轮询方式与中断方式的取舍STC 串口收发有两种基本方式轮询和中断。轮询就是死等标志位比如发送时写 SBUF 后一直查 TI 是否为 1接收时一直查 RI 是否为 1。中断则是标志位置位后自动跳进中断服务函数处理。轮询的优点是逻辑简单、不用配中断适合发送少量调试信息、主循环不忙的场景。缺点是等待期间 CPU 被占死什么都干不了接收时如果数据来得突然还容易丢。中断的优点是 CPU 利用率高收发都在后台完成主循环可以干别的事。缺点是要配中断使能、写中断服务函数出错点更多。我的建议是发送用轮询、接收用中断这是实际项目里最常用的组合。发送是我们主动发起的等一会儿无所谓接收是被动的必须用中断及时响应否则数据就丢了。后面的完整代码也是按这个思路写的。2. SBUF 寄存器与波特率的核心细节解析2.1 SBUF 到底是什么一个地址两个寄存器SBUF 这个名字容易让人误解以为它就是“一个”寄存器。实际上在 8051 架构里SBUF 对应两个物理上独立的寄存器一个是发送缓冲寄存器一个是接收缓冲寄存器它们共用同一个地址通常是 0x99。你写 SBUF 的时候数据进的是发送缓冲你读 SBUF 的时候数据来自接收缓冲。硬件根据你是读还是写自动切换到对应的物理寄存器。这个设计很巧妙省了地址空间但对新手来说有个坑你不能通过读 SBUF 来检查自己刚写进去的发送数据。因为读操作访问的是接收缓冲读出来的可能是上次收到的数据跟你刚写的发送数据毫无关系。我见过有人写SBUF A; if(SBUF A)来验证发送结果永远不成立就是踩了这个坑。正确的做法是写 SBUF 后等发送完成标志 TI 置 1TI 置 1 说明数据已经从发送缓冲移到了移位寄存器并发送完毕或至少发送缓冲空了。TI 需要软件清零这一点在中断和轮询里都要注意。2.2 串口工作模式与 SCON 寄存器SCON 是串口控制寄存器8 位每一位都有明确含义。STC 兼容 8051SCON 的位定义基本一致位名称含义D7SM0工作模式选择高位D6SM1工作模式选择低位D5SM2多机通信控制位D4REN接收使能1 允许接收D3TB8发送的第 9 位模式 2/3 用D2RB8接收的第 9 位模式 2/3 用D1TI发送中断标志发送完成置 1需软件清零D0RI接收中断标志接收完成置 1需软件清零工作模式由 SM0、SM1 组合决定最常用的是模式 1SM00SM11即 8 位数据、1 位停止位、无校验的异步通信波特率由定时器产生。模式 0 是同步移位寄存器模式一般用于扩展 IO模式 2 和模式 3 是 9 位数据多用于多机通信或带校验的场合。实际配置时模式 1 加接收使能SCON 的值就是0x500101 0000SM11、REN1。这个值我背了很多年因为几乎每个 STC 串口项目开头都是它。2.3 波特率计算定时器初值怎么算模式 1 的波特率由定时器 1 的溢出率决定公式是波特率 (2^SMOD / 32) × (定时器1溢出率) 定时器1溢出率 fosc / (12 × (256 - TH1))其中 SMOD 是 PCON 寄存器的最高位为 1 时波特率加倍。fosc 是系统时钟频率。把两个公式合起来波特率 (2^SMOD / 32) × fosc / (12 × (256 - TH1))反解出 TH1TH1 256 - (2^SMOD × fosc) / (384 × 波特率)举个实际例子。假设 STC89 用 11.0592MHz 晶振这个频率是串口通信的“黄金频率”因为能被标准波特率整除要 9600 波特率SMOD0TH1 256 - (1 × 11059200) / (384 × 9600) 256 - 11059200 / 3686400 256 - 3 253 0xFD所以 TH1 0xFDTL1 也设 0xFD模式 2 自动重装时 TL1 初值同 TH1。如果 SMOD1 波特率加倍TH1 就是 256 - 6 250 0xFA。这里有个关键点晶振频率选得不对波特率误差会很大。如果用 12MHz 晶振算 9600TH1 256 - 12000000/3686400 ≈ 256 - 3.255 252.7取 253 的话实际波特率约 8928误差超过 7%通信会不稳定甚至乱码。所以做串口尽量用 11.0592MHz 或其倍数这是无数人验证过的经验。2.4 定时器 1 的工作模式配置波特率发生器用定时器 1必须工作在模式 28 位自动重装。为什么因为模式 2 下 TL1 溢出后会自动把 TH1 的值重装进 TL1不需要软件干预溢出率稳定。如果用模式 116 位每次溢出都要软件重装初值中断响应有延迟波特率就不准了。TMOD 寄存器低 4 位管定时器 0高 4 位管定时器 1。定时器 1 模式 2 对应高 4 位是0010即 0x20。如果定时器 0 不用低 4 位设 0TMOD 0x20。如果定时器 0 也在用比如做其他定时就要把两部分的配置或起来。配置顺序我习惯这样先TMOD 0x20再TH1 0xFD; TL1 0xFD然后TR1 1启动定时器最后配 SCON 和中断。顺序不是死的但 TR1 要在 SCON 之前或同时启动保证波特率发生器先跑起来。3. 中断服务函数与完整收发实现3.1 中断使能的配置链条STC 串口中断要生效需要打开三层开关总中断 EA、串口中断使能 ES。这两个都在 IE 寄存器里。EA 是 IE 的最高位0x80ES 是 IE 的 D4 位0x10。所以IE 0x90或者分别EA 1; ES 1;都可以。这里有个常见误区有人只开了 ES 没开 EA或者只开了 EA 没开 ES结果中断死活进不去。我的习惯是写两行EA 1; ES 1;比直接赋值 0x90 更直观也避免把 IE 里其他位误清零。另外如果用的是 STC12、STC15、STC8 这些增强型芯片串口可能有两个UART1、UART2中断向量号不同使能位也可能在扩展寄存器里比如 STC8 的 IE2。这时候要查具体型号的手册不能照搬 STC89 的配置。我下面给的代码以最经典的 STC89 兼容写法为主增强型芯片改一下寄存器名即可。3.2 中断服务函数的固定格式8051 的中断服务函数有固定格式串口中断的向量号是 4所以函数写成void UART_ISR() interrupt 4 { // 处理逻辑 }interrupt 4这个关键字告诉编译器这是中断 4 的服务函数编译器会自动在向量表对应位置放跳转指令。函数名可以随便起但interrupt 4不能错。写错向量号编译能过但中断来了跳不到你的函数表现就是“中断不进”。中断服务函数里要做两件事判断是接收中断还是发送中断然后清标志位。因为 TI 和 RI 共用同一个中断向量进中断后必须靠软件判断是哪个触发的。标准写法void UART_ISR() interrupt 4 { if(RI) { RI 0; // 接收标志清零 rx_data SBUF; // 读接收数据 rx_flag 1; // 置接收完成标志 } if(TI) { TI 0; // 发送标志清零 // 发送完成处理 } }注意 RI 和 TI 都必须软件清零硬件不会自动清。忘了清中断会反复触发程序就卡死在中断里了。这是新手最常犯的错误之一。3.3 发送与接收的完整代码实现下面这份代码是我实际验证过的STC89 系列 11.0592MHz 晶振、9600 波特率、串口模式 1、接收用中断、发送用轮询。可以直接编译烧录。#include reg52.h typedef unsigned char u8; typedef unsigned int u16; u8 rx_data 0; // 接收到的数据 bit rx_flag 0; // 接收完成标志 // 串口初始化9600bps 11.0592MHz void UART_Init() { TMOD 0x20; // 定时器1 模式2 8位自动重装 TH1 0xFD; // 9600bps 初值 TL1 0xFD; TR1 1; // 启动定时器1 SCON 0x50; // 模式1允许接收 EA 1; // 开总中断 ES 1; // 开串口中断 } // 发送一个字节轮询方式 void UART_SendByte(u8 dat) { SBUF dat; // 写SBUF触发发送 while(!TI); // 等待发送完成 TI 0; // 软件清零 } // 发送字符串 void UART_SendString(u8 *str) { while(*str ! \0) { UART_SendByte(*str); } } // 串口中断服务函数 void UART_ISR() interrupt 4 { if(RI) { RI 0; rx_data SBUF; rx_flag 1; } if(TI) { TI 0; } } void main() { UART_Init(); UART_SendString(UART Ready\r\n); while(1) { if(rx_flag) { rx_flag 0; UART_SendByte(rx_data); // 回显收到的数据 } } }这段代码实现了一个“回显”功能单片机收到什么就发回什么。烧录后打开串口助手发一个字符应该能收到同样的字符。如果收不到就按第 4 节的排查表逐项检查。3.4 代码里几个容易忽略的细节第一while(!TI)这个写法依赖 TI 初始为 0。上电复位后 TI 默认是 0没问题。但如果之前发送过数据没清 TI这里就会直接跳过等待导致数据还没发完就写下一个 SBUF数据被覆盖。所以每次发送完必须TI 0。第二中断里if(RI)和if(TI)用的是两个独立的 if不是 if-else。因为理论上 RI 和 TI 可能同时置位虽然少见用 if-else 会漏处理一个。用两个 if 更稳妥。第三rx_data和rx_flag在主循环和中断里都被访问rx_flag用了bit类型。在 8051 上对 bit 变量的读写是原子的问题不大。但如果换成多字节数据比如接收一个数据包就要考虑中断打断主循环读写带来的数据一致性问题通常用关中断保护或者双缓冲解决。第四发送字符串函数里用了指针遍历注意字符串要以\0结尾。如果发的是二进制数据可能含 0x00就不能用这个函数得改成带长度参数的版本。4. 常见问题排查与实操避坑指南4.1 串口不通的逐层排查表串口调不通是最常见的问题我整理了一张排查表按从硬件到软件的顺
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →