51单片机串口通信实战:从寄存器配置到多机通信与调试排错
1. 从并口到串口为什么51单片机通信非得走串行这条路搞过51单片机的朋友都知道点亮LED、驱动数码管、读按键这些操作本质上都是对IO口的电平控制程序写起来直来直去一个端口八根线想输出什么就输出什么。但一旦涉及到两块芯片之间传数据或者单片机跟电脑、模块之间交换信息情况就完全不一样了。你不可能为了传一个字节专门拉八根线过去再加上控制线、地线十几根线跑起来板子稍微小一点就布不下了。这时候串行通信就成了绕不开的选择——一根线发一根线收两根线就能把数据传过去布线简单、成本低、抗干扰能力在短距离内也够用。51单片机上的串行通信核心就是那个叫UART的东西全称通用异步收发器。它做的事情说白了就是把并行的八个位一位一位地按顺序挪出去接收端再按同样的节奏一位一位地收回来拼成字节。这里面最关键的词是“异步”意思是发送方和接收方没有一根专门的时钟线来同步节奏全靠双方事先约定好的波特率来各自计时。这就好比你跟朋友约好每隔一秒喊一个字两个人各自看表只要表走得一样准就能正常交流。问题在于两块芯片各自用的晶振可能有微小误差喊上几十个字之后节奏就可能对不上了所以波特率的计算和晶振的选择是串口能不能稳定工作的第一道坎。这篇内容适合谁看如果你已经学过51单片机的基础知识知道怎么配置定时器、怎么写中断服务函数但对串口通信的理解还停留在“知道有这么回事”的阶段那这篇就是给你准备的。我会把串口通信从寄存器配置到实际收发数据的完整流程拆开讲包括波特率到底怎么算、中断和查询两种方式各自适合什么场景、多机通信时那个第九位到底怎么用以及实际调试中那些让人抓狂的问题怎么排查。这些东西在教材上往往一笔带过但真正动手做项目的时候每一个细节都可能卡你半天。2. 串口通信的底层逻辑与51单片机的硬件实现2.1 异步串行通信的帧结构到底长什么样串口传数据不是光着传的每一个字节都被包装成一个“帧”。这个帧的结构是固定的起始位、数据位、校验位、停止位。起始位是一位低电平告诉接收方“注意我要开始发了”。接收方检测到这根线上的电平从高变低就知道一帧数据来了然后按照约定的波特率开始采样。数据位通常是八位从最低位开始发也就是先发bit0最后发bit7。校验位是可选的用来做简单的错误检测可以是奇校验、偶校验或者干脆不要。停止位是一位或者两位高电平表示这一帧结束了。为什么要有起始位因为串口线在空闲的时候是高电平接收方一直在等这个下降沿。没有起始位的话接收方不知道什么时候开始采样整个通信就乱了。这就像你打电话总得先说一句“喂”让对方知道你要说话了起始位就是这个“喂”。停止位的作用是给接收方一个缓冲时间让它把收到的数据处理好准备迎接下一帧。如果停止位太短或者没有接收方可能还没处理完上一帧下一帧的起始位就来了数据就会丢。在51单片机上这些帧结构的参数都是通过SCON寄存器来配置的。SCON的低两位SM0和SM1决定了工作模式模式0是同步移位寄存器模式模式1是八位UART可变波特率模式2是九位UART固定波特率模式3是九位UART可变波特率。实际项目里用得最多的是模式1和模式3模式1就是标准的八位数据位模式3多了第九位通常用来做多机通信的地址位或者校验位。SM2位是多机通信控制位在模式2和模式3下起作用后面讲多机通信的时候会详细说。2.2 波特率计算的来龙去脉与晶振选择的坑波特率这个词听起来挺专业其实意思很简单就是每秒传输的位数。9600的波特率就是每秒传9600个位一个位占的时间大约是104微秒。51单片机的串口波特率来源有两个一个是定时器1的溢出率另一个是系统时钟的分频。模式0和模式2用的是系统时钟分频模式1和模式3用的是定时器1的溢出率。为什么模式1和模式3要用定时器因为这两个模式的波特率是可变的你可以通过调整定时器的重装值来改变波特率灵活性更高。定时器1要工作在模式2也就是八位自动重装模式。为什么非得是模式2因为串口通信对波特率的稳定性要求很高如果定时器每次溢出后需要软件重新赋初值中间会有几个机器周期的延迟这个延迟会导致波特率产生抖动传几个字节可能没问题传多了就会累积误差导致通信失败。模式2的自动重装是硬件完成的溢出瞬间就把重装值加载进去没有软件干预波特率就稳了。波特率的计算公式是这样的波特率 (2^SMOD / 32) × (定时器1溢出率)。定时器1的溢出率 系统时钟频率 / (12 × (256 - TH1))。把这两个公式合起来TH1 256 - (系统时钟频率 × 2^SMOD) / (384 × 波特率)。SMOD是PCON寄存器的最高位叫波特率加倍位置1的时候波特率翻倍。举个例子晶振用11.0592MHz波特率要9600SMOD设为0那么TH1 256 - (11059200) / (384 × 9600) 256 - 3 253也就是0xFD。这个值算出来是整数没有误差。为什么晶振要用11.0592MHz而不是12MHz因为12MHz算出来的TH1不是整数。你用12MHz代入公式TH1 256 - (12000000) / (384 × 9600) 256 - 3.255 252.745只能取253实际波特率就变成了12000000 / (384 × 3) 10416.67跟9600差了8.5%。这个误差在传几个字节的时候可能看不出来但连续传几百个字节累积误差就会导致接收方采样错位数据全乱。11.0592MHz这个频率看起来奇怪但它是专门为串口通信设计的能整除常见的波特率所以做串口通信的项目晶振一定要选11.0592MHz这是血的教训。2.3 SCON和PCON寄存器里每一个位的实际意义SCON寄存器是串口通信的核心控制寄存器八个位各有各的用处。SM0和SM1决定工作模式前面已经说了。SM2是多机通信控制位在模式2和模式3下如果SM2置1接收到的第九位数据R8如果是0就不会触发RI中断只有R8是1的时候才触发。这个机制就是用来做多机通信的地址筛选的。REN是接收允许位置1才能接收数据清零就只能发送不能接收。TB8是发送的第九位在模式2和模式3下使用可以放地址标志或者校验位。RB8是接收到的第九位在模式1下它是停止位在模式2和模式3下它是发送方发过来的第九位。TI是发送中断标志发送完一帧数据后硬件自动置1必须软件清零。RI是接收中断标志接收到一帧数据后硬件自动置1也必须软件清零。PCON寄存器里跟串口有关的就是SMOD位也就是最高位。SMOD置1的时候波特率加倍置0的时候不加倍。这个位在系统复位后默认是0如果你需要更高的波特率比如115200在11.0592MHz晶振下TH1算出来是255也就是0xFF这时候SMOD必须置1才能得到115200的波特率。PCON的其他位跟串口没关系是给电源管理用的一般不用管。这里有个容易踩的坑TI和RI标志位必须用软件清零硬件置1之后不会自动清零。如果你在中断服务函数里忘了清TI或者RI程序就会一直进中断主循环根本跑不起来。我见过不少新手写的串口程序发送一个字节后程序就卡死了查了半天发现是TI没清零中断一直在触发。所以记住进中断第一件事就是判断是TI还是RI然后手动清零。3. 查询模式与中断模式两种收发方式的实战对比3.1 查询模式发送数据的完整流程与代码实现查询模式发送数据是最简单的方式不需要开中断主程序里轮询TI标志位就行。流程是这样的先把数据写入SBUF寄存器然后硬件自动开始发送发送完成后TI置1程序检测到TI为1后手动清零TI然后就可以发下一个字节了。代码大概长这样void UART_SendByte(unsigned char dat) { SBUF dat; while(TI 0); TI 0; }这段代码看起来简单但里面有个细节while(TI 0);这个循环是在等TI变成1如果硬件正常发送一个字节大概需要1毫秒左右9600波特率下这个等待时间是可以接受的。但如果波特率很低比如1200一个字节要传将近10毫秒主程序就被卡在这里10毫秒什么都干不了。所以查询模式适合发送数据量小、对实时性要求不高的场景。查询模式接收数据也是类似的思路轮询RI标志位unsigned char UART_ReceiveByte(void) { while(RI 0); RI 0; return SBUF; }但接收查询有个问题如果数据没来程序就死等在这里其他事情都做不了。实际项目里很少用纯查询方式接收因为串口数据什么时候来是不确定的你不可能让主程序一直等着。所以接收通常用中断方式发送可以用查询也可以用中断。3.2 中断模式收发的配置步骤与中断服务函数写法中断模式是实际项目中最常用的方式。配置步骤大概是这样的先设置SCON寄存器选择模式1或者模式3打开REN允许接收。然后设置定时器1为模式2计算并装入TH1和TL1打开定时器1。接着设置IE寄存器打开ES串口中断允许和EA总中断允许。如果要用波特率加倍还要设置PCON的SMOD位。最后写中断服务函数中断号是4。中断服务函数的框架是这样的void UART_ISR(void) interrupt 4 { if(RI 1) { RI 0; rx_data SBUF; rx_flag 1; } if(TI 1) { TI 0; tx_busy 0; } }这个框架里接收和发送共用一个中断入口所以进中断后要先判断是RI还是TI。接收到的数据存到一个全局变量里然后置一个标志位主程序检测到这个标志位就去处理数据。发送完成中断里把发送忙标志清零主程序就可以发下一个字节了。这种“中断收数据主循环处理”的架构是51单片机串口通信最经典的用法。这里有个细节要注意中断服务函数里不要做太耗时的操作。比如你收到一个字节后在中断里直接去解析协议、控制外设这些操作可能耗时几十微秒甚至更长会阻塞其他中断的响应。正确的做法是中断里只做数据的搬运和标志位的设置真正的处理放到主循环里做。我见过有人在串口中断里直接调用延时函数结果整个系统的中断响应都乱了这种写法一定要避免。3.3 两种模式的性能对比与选型建议查询模式和中断模式各有各的适用场景不能简单说哪个好哪个坏。查询模式的优点是代码简单、不需要考虑中断优先级和重入问题缺点是CPU利用率低发送或接收的时候主程序被阻塞。中断模式的优点是CPU利用率高主程序可以同时处理其他任务缺点是代码复杂一些需要考虑中断嵌套和临界区保护。对比维度查询模式中断模式代码复杂度低几行代码搞定中需要配置中断和写ISRCPU占用高等待时CPU空转低数据来了才响应实时性差等待期间无法响应其他事件好主程序可以处理其他任务适用场景发送少量数据、调试阶段接收数据、多任务系统数据丢失风险低因为一直在等中如果中断被长时间关闭实际项目里我通常的做法是发送用查询模式因为发送是主动行为我知道什么时候要发等一下也无所谓接收用中断模式因为数据什么时候来不确定不能让主程序死等。如果系统对实时性要求很高发送也用中断但要注意发送完成中断和接收中断共用一个入口处理逻辑要清晰。4. 多机通信与第九位数据的神秘用途4.1 多机通信的地址帧与数据帧区分机制51单片机的串口有一个很特别的设计模式2和模式3支持九位数据第九位可以单独设置和读取。这个第九位在多机通信里扮演着关键角色。多机通信的场景是这样的一个主机多个从机所有从机的RXD接在一起接到主机的TXD所有从机的TXD通过一个二极管或者逻辑门接到主机的RXD实现总线式的通信。主机要跟某个从机说话先发一个地址帧所有从机都收到但只有地址匹配的那个从机才会被唤醒其他的继续睡觉。具体怎么实现主机发送地址帧的时候TB8置1表示这一帧是地址。从机的SM2都置1这时候只有接收到的第九位RB8为1的时候才会触发RI中断。所以地址帧来了所有从机都进中断读取地址跟自己比较。匹配的从机把自己的SM2清零准备接收后续的数据帧。不匹配的从机保持SM2为1继续过滤掉数据帧。主机发送数据帧的时候TB8置0只有SM2为0的那个从机才会接收其他的从机因为SM2还是1RB8为0不会触发中断数据就被硬件自动丢弃了。这个机制的好处是不需要软件去判断地址硬件自动过滤效率很高。但有个坑从机在接收到地址帧后如果地址匹配必须及时把SM2清零否则后续的数据帧会被过滤掉。如果地址不匹配SM2保持为1但下一次地址帧来的时候RB8为1还是会进中断所以从机的中断服务函数里要一直判断地址。4.2 多机通信的硬件连接与软件协议设计硬件连接上所有从机的RXD并联接到主机的TXD从机的TXD不能直接并联因为多个从机同时发送会短路。通常的做法是每个从机的TXD通过一个开漏门或者二极管接到主机的RXD或者用RS485收发器做半双工通信。如果是实验环境可以用二极管加下拉电阻的方式简单但不太可靠。实际项目里推荐用RS485差分信号抗干扰能力强传输距离也远。软件协议方面需要约定好地址帧的格式和数据帧的格式。比如地址帧可以用0xFF开头后面跟从机地址数据帧可以用0xAA开头后面跟命令和数据。协议设计要考虑错误处理比如从机收到错误的数据帧怎么办主机发送后没有收到应答怎么办。这些都需要在协议里定义清楚。我一般会在协议里加一个校验和接收方算一下校验和不对就丢弃让发送方重发。4.3 多机通信中的常见问题与调试技巧多机通信调试起来比点对点通信麻烦得多因为总线上挂了好几个设备出了问题不好定位。常见的问题有地址匹配了但数据收不到可能是SM2没有及时清零数据收到了但内容不对可能是波特率误差累积从机同时发送导致总线冲突可能是协议里没有做好发送仲裁。调试的时候我一般先用示波器或者逻辑分析仪抓一下总线上的波形看看地址帧和数据帧的第九位是不是符合预期。如果没有逻辑分析仪可以用一个从机专门做监听把总线上所有的数据都打印出来看看谁在什么时候发了什么。另外从机的发送要加一个随机延时避免多个从机同时响应主机导致冲突。这个延时可以用定时器产生也可以用软件循环但要注意延时的随机性不能所有从机用同一个延时值。5. 串口调试中那些让人抓狂的问题与排查方法5.1 数据乱码的五大原因与逐一排查步骤串口调试最常遇到的问题就是数据乱码收到的跟发出去的不一样。这个问题可能的原因很多我按概率从高到低排一下第一波特率不匹配发送方和接收方的波特率设置不一样或者晶振频率不对导致实际波特率偏差太大。第二地线没接好串口通信需要共地如果两块板子的地没有连在一起电平参考就不一致数据肯定乱。第三TTL电平和RS232电平混用51单片机的串口是TTL电平电脑的串口是RS232电平直接连会烧芯片必须用转换芯片。第四数据位、停止位、校验位的配置不一致比如发送方用8位数据位接收方用7位肯定乱。第五干扰太大线太长或者附近有强干扰源信号畸变导致误码。排查的时候先用示波器看一下发送方的TXD波形确认波特率是不是对的。一个位的时间宽度可以算出来9600波特率下一位是104微秒用示波器量一下起始位的宽度如果差太多就是波特率问题。然后检查地线用万用表量一下两块板子的地是不是通的。再确认电平匹配TTL对TTLRS232对RS232不要混。最后检查配置参数数据位、停止位、校验位、波特率四个参数必须完全一致。5.2 接收不到数据的硬件与软件排查清单接收不到数据比乱码更让人头疼因为连错的都没有。硬件方面要查TXD和RXD是不是接反了这个是最常见的错误发送方的TXD要接接收方的RXD很多人直接TXD接TXD那肯定收不到。串口线是不是断了用万用表量一下通断。接收方的REN位是不是置1了如果REN是0硬件根本不接收。中断是不是开了ES和EA都要置1少一个都不行。软件方面要查中断服务函数是不是写对了中断号是不是4函数名后面有没有加interrupt 4。RI标志是不是被误清了如果在中断里先清了RI再读SBUF可能读到的是旧数据。波特率是不是差太多误差超过5%就可能收不到。还有如果用了查询方式接收程序是不是卡在别的地方了根本没执行到查询RI的代码。我遇到过一个很隐蔽的问题串口接收中断和定时器中断同时开着定时器中断的优先级比串口高定时器中断服务函数执行时间太长导致串口中断被延迟响应数据丢了。这种情况要把串口中断的优先级设高一点或者缩短定时器中断的执行时间。51单片机的中断优先级可以通过IP寄存器设置串口中断的优先级位是PS。5.3 发送数据时程序卡死的几种典型情况发送数据卡死通常是卡在while(TI 0);这个循环里出不来。原因可能是TI标志没有被硬件置1说明发送根本没启动。为什么没启动可能是SBUF没有写入数据或者串口发送功能没有使能。检查一下代码确认SBUF确实被赋值了SCON的配置是不是正确。还有一种可能是TI被意外清零了比如在中断里清了TI但主程序还在等TI这时候TI已经是0了主程序就死等。所以如果用了发送中断主程序里就不要再等TI了两者选其一。另一个卡死的情况是中断服务函数里没有清TI导致中断一直触发主程序根本执行不到。这种问题的现象是程序看起来在跑但主循环里的代码不执行因为一直在进中断。解决方法是进中断后先判断是TI还是RI然后手动清零对应的标志位。还有一种情况是波特率设置得太低发送一个字节要几十毫秒主程序等不及就以为卡死了。比如1200波特率下一个字节要传10毫秒左右如果你在发送后马上又发下一个中间没有足够的间隔就会一直卡在等待TI的循环里。这种情况要么提高波特率要么用中断方式发送。5.4 串口通信常见问题速查表现象可能原因排查方法解决方案数据乱码波特率不匹配示波器量位宽统一波特率换11.0592MHz晶振数据乱码地线未共地万用表量通断连接两地数据乱码电平不匹配检查转换芯片加MAX232等电平转换收不到数据TXD/RXD接反检查接线交叉连接收不到数据REN未置1检查SCON置位REN收不到数据中断未开检查IE置位ES和EA发送卡死TI未清零检查ISR手动清TI发送卡死波特率过低计算传输时间提高波特率或用中断数据丢失中断优先级低检查IP提高串口中断优先级数据丢失中断执行太长检查ISR代码缩短ISR处理放主循环6. 从寄存器操作到实战项目的完整实现路径6.1 一个完整的串口收发程序框架把前面讲的东西串起来一个完整的串口收发程序大概长这样初始化部分配置SCON为0x50模式1允许接收配置TMOD的定时器1为模式2计算TH1和TL1打开定时器1配置IE打开ES和EA如果需要加倍波特率就设置PCON的SMOD位。发送函数用查询方式接收用中断方式中断服务函数里判断RI和TI接收到的数据存入缓冲区置标志位。主循环里检测标志位处理接收到的数据然后通过发送函数回复。#include reg52.h #define FOSC 11059200L #define BAUD 9600 unsigned char rx_buf[64]; unsigned char rx_cnt 0; bit rx_complete 0; void UART_Init(void) { SCON 0x50; TMOD 0x0F; TMOD | 0x20; TH1 256 - (FOSC / 12 / 32 / BAUD); TL1 TH1; TR1 1; ES 1; EA 1; } void UART_SendByte(unsigned char dat) { SBUF dat; while(TI 0); TI 0; } void UART_SendString(unsigned char *str) { while(*str) { UART_SendByte(*str); } } void UART_ISR(void) interrupt 4 { if(RI 1) { RI 0; if(rx_cnt 64) { rx_buf[rx_cnt] SBUF; } if(SBUF \n) { rx_complete 1; } } if(TI 1) { TI 0; } } void main(void) { UART_Init(); UART_SendString(UART Ready\r\n); while(1) { if(rx_complete 1) { rx_complete 0; UART_SendString(Echo: ); UART_SendString(rx_buf); rx_cnt 0; } } }这个框架可以直接拿来用改一下波特率和晶振频率就行。注意TH1的计算公式里用了FOSC/12/32/BAUD这是SMOD为0的情况如果SMOD为1分母要再除以2。6.2 环形缓冲区在串口接收中的实战应用上面的代码用了一个简单的数组做接收缓冲区数据满了就丢弃。实际项目里更推荐用环形缓冲区因为串口数据是异步来的主程序处理速度可能跟不上接收速度环形缓冲区可以平滑这个速度差。环形缓冲区的原理是一个固定大小的数组两个指针一个写指针一个读指针写指针追上读指针表示满了读指针追上写指针表示空了。中断里只负责往缓冲区写数据主循环从缓冲区读数据两边互不干扰。#define BUF_SIZE 128 unsigned char ring_buf[BUF_SIZE]; unsigned char write_idx 0; unsigned char read_idx 0; bit RingBuf_Write(unsigned char dat) { unsigned char next (write_idx 1) % BUF_SIZE; if(next read_idx) { return 0; } ring_buf[write_idx] dat; write_idx next; return 1; } bit RingBuf_Read(unsigned char *dat) { if(read_idx write_idx) { return 0; } *dat ring_buf[read_idx]; read_idx (read_idx 1) % BUF_SIZE; return 1; }环形缓冲区的好处是读写操作都是O(1)的不需要移动数据效率很高。但要注意读写指针的更新在中断和主循环里都可能发生如果主循环正在读的时候中断来了要写可能会冲突。解决办法是在读的时候先关中断读完再开中断或者用原子操作。51单片机没有原子操作指令所以关中断是最简单可靠的方法。6.3 串口通信协议的简单设计与解析实际项目里串口传的不只是单个字节往往是一串有结构的数据。比如一个温湿度采集系统单片机通过串口发送的数据可能是帧头、温度高字节、温度低字节、湿度高字节、湿度低字节、校验和、帧尾。接收方需要按照这个格式解析才能正确提取出温度和湿度。协议设计要考虑几个问题帧头要足够特殊不容易跟数据混淆要有长度字段或者固定长度方便接收方判断帧的边界要有校验防止数据出错。一个简单的协议可以这样设计帧头用0xAA 0x55两个字节这两个字节连续出现的概率很低不容易跟数据混淆。然后是命令字表示这帧数据是干什么的。接着是数据长度告诉接收方后面有多少个字节的数据。再后面是数据本身。最后是校验和把前面所有字节加起来取低八位。接收方收到数据后先找帧头找到后读命令字和长度然后读数据最后算校验和对了就处理不对就丢弃重新找帧头。解析的时候要注意数据是流式来的可能一帧数据分几次才收完也可能一次收到一帧半。所以解析函数要能处理不完整的数据把没收完的数据缓存起来等下一批数据来了再继续解析。这个状态机的写法有点绕但写一次之后就可以复用了。6.4 串口通信在实际项目中的典型应用场景串口通信在51单片机项目里用得非常多几乎每个稍微复杂一点的项目都会用到。比如跟电脑通信把采集到的数据传到电脑上显示或者从电脑接收控制命令。跟蓝牙模块通信实现无线数据传输。跟WiFi模块通信接入网络。跟GPS模块通信获取位置信息。跟指纹模块通信做身份识别。跟显示屏通信用串口屏显示界面。这些场景的共同点是单片机需要跟一个外部模块交换数据而串口是最简单、最通用的方式。以蓝牙模块为例模块的TXD接单片机的RXD模块的RXD接单片机的TXD波特率通常默认是9600。单片机通过串口发送数据蓝牙模块把数据透传到手机或者电脑上。接收也是类似的手机发数据蓝牙模块收到后通过串口传给单片机。这种透传模式不需要关心蓝牙协议的具体细节单片机只需要会串口收发就行非常方便。再比如串口屏屏幕本身是一个带串口接口的显示模块单片机通过串口发送指令告诉屏幕显示什么内容。指令通常是一串十六进制数据比如显示一个数字指令可能是0x5A 0xA5 0x05 0x82 0x00 0x01 0x00 0x01这样的格式。单片机只需要按照屏幕的指令手册把对应的数据通过串口发过去就行。这种方案比单片机直接驱动LCD屏简单得多因为显示逻辑都交给屏幕自己处理了单片机只需要发指令。7. 串口通信的进阶技巧与性能优化7.1 提高串口通信可靠性的几个实用手段串口通信在实验室里跑得好好的到了现场就出问题这种情况太常见了。提高可靠性要从硬件和软件两方面入手。硬件方面线要尽量短如果必须长距离传输用双绞线或者屏蔽线屏蔽层接地。如果距离超过几米TTL电平就不行了要换成RS485或者RS232。电源要干净单片机电源和电机、继电器等大功率设备的电源要分开否则电源波动会导致串口误码。接口处可以加TVS管做静电防护加磁珠滤高频干扰。软件方面加校验是最基本的。简单的校验和只能检测单字节错误复杂一点的用CRC16检测能力强很多。加应答机制发送方发一帧数据后等接收方回复ACK超时没收到就重发。加超时处理接收方等帧头的时候如果超时了就重置状态机重新找帧头。加数据过滤如果连续收到大量错误数据可能是波特率不对或者线接错了程序可以进入一个错误状态闪灯报警。还有一个容易被忽视的点串口通信的时候尽量不要关中断太长时间。如果系统里还有其他中断比如定时器中断关中断期间定时器中断会被延迟可能导致定时不准。如果必须关中断保护临界区关中断的时间要尽量短只保护最关键的几行代码。7.2 串口与定时器中断的优先级协调51单片机有两个中断优先级高优先级和低优先级。默认情况下所有中断都是低优先级通过IP寄存器可以把某个中断设为高优先级。串口中断和定时器中断同时存在的时候如果定时器中断的优先级比串口高定时器中断会打断串口中断。如果定时器中断服务函数执行时间很长串口中断就会被延迟可能导致接收数据丢失。解决方法是把串口中断设为高优先级这样串口数据来了可以立即响应定时器中断被延迟一点影响不大。设置方法是把IP寄存器的PS位置1。但要注意如果串口中断服务函数本身执行时间很长高优先级也会导致其他中断被延迟。所以中断服务函数要尽量短只做数据搬运不做复杂处理。还有一种情况是串口发送和接收共用一个中断入口如果发送中断和接收中断同时触发中断服务函数里要先判断是哪个然后分别处理。如果发送中断的处理时间很长接收中断就会被延迟。所以发送尽量用查询方式不要用中断这样串口中断只处理接收逻辑简单响应也快。7.3 低功耗场景下的串口唤醒机制电池供电的项目对功耗很敏感单片机大部分时间在休眠只有串口有数据来的时候才唤醒。51单片机本身没有专门的串口唤醒功能但可以通过外部中断来实现。把串口的RXD线同时接到一个外部中断引脚上串口空闲的时候是高电平起始位是低电平这个下降沿可以触发外部中断把单片机从休眠模式唤醒。唤醒后单片机再初始化串口准备接收数据。这种方式的缺点是RXD线上的任何下降沿都会唤醒单片机包括干扰信号。所以要在外部中断服务函数里判断一下是不是真的起始位。判断的方法是唤醒后延时半个位的时间再采样RXD线如果还是低电平说明是真正的起始位否则就是干扰继续休眠。这个延时可以用定时器实现也可以用软件循环但要注意延时的时间要跟波特率匹配。休眠的时候串口模块要关掉否则它自己也会耗电。唤醒后重新初始化串口配置波特率打开接收。这个过程需要一点时间所以发送方在发数据之前要先发一个唤醒字节等一段时间再发正式数据给单片机留出初始化串口的时间。这个唤醒字节可以是0xFF因为0xFF的起始位是低电平能触发唤醒而且0xFF本身也是常见的数据不会造成混淆。7.4 串口通信的调试工具与实用技巧调试串口通信最常用的工具是USB转串口模块和串口调试助手。USB转串口模块把电脑的USB接口转成TTL串口方便跟单片机连接。选模块的时候要注意电平3.3V的单片机要用3.3V的模块5V的单片机可以用5V的模块混用可能烧芯片。串口调试助手用来收发数据可以设置波特率、数据位、停止位、校验位还可以用十六进制显示和发送。除了这些基本工具逻辑分析仪是调试串口的神器。它可以把TXD和RXD线上的波形抓下来自动解析成字节还能测量位宽直接算出实际波特率。如果没有逻辑分析仪用示波器也行但只能看波形不能自动解析。我调试串口的时候一般先用示波器确认波形和波特率然后用逻辑分析仪抓完整的数据流看看协议解析对不对。软件方面可以在代码里加一些调试输出把关键变量的值通过串口打印出来。比如接收到的数据、状态机的状态、校验和的计算结果这些信息对定位问题很有帮助。但要注意调试输出本身也会占用串口如果串口同时用于通信和调试可能会冲突。解决办法是用一个单独的调试串口或者用IO口翻转来指示程序运行状态比如进中断翻转一个IO主循环翻转另一个IO用示波器看波形就知道程序有没有跑飞。8. 个人实操经验与踩坑记录串口通信这个东西看教材觉得很简单不就是配置几个寄存器然后收发数据吗但真正动手做项目的时候各种奇怪的问题都会冒出来。我印象最深的一次是做一个无线数据传输的项目单片机通过串口跟无线模块通信实验室里测试一切正常到了现场数据丢包率特别高。查了半天发现是现场有一个大功率的变频器干扰特别强串口线又没有屏蔽数据被干扰得乱七八糟。后来换了屏蔽线加了磁环问题才解决。这件事让我明白串口通信的可靠性不只是软件的事硬件设计同样重要。还有一次是帮别人调一个程序串口接收数据偶尔会多一个字节或者少一个字节。查了很久最后发现是中断服务函数里读SBUF之前先清了RI但清RI和读SBUF之间如果来了新的数据新数据的RI会被清掉导致数据丢失。正确的顺序是先读SBUF再清RI这样即使读的过程中来了新数据RI被硬件置1读完之后再清RI新数据不会丢。这个细节教材上一般不会强调但实际项目里很关键。波特率的计算也是容易出错的地方。有一次用12MHz的晶振跑9600的波特率算出来的TH1是253实际波特率是10416误差8.5%。短距离通信的时候没发现问题但传大文件的时候传了几百个字节后数据就开始乱了。后来换成11.0592MHz的晶振TH1是253实际波特率正好是9600误差0%问题就解决了。所以做串口通信晶振一定要选11.0592MHz这个频率就是为串口通信量身定做的。多机通信的调试也让我吃了不少苦头。第一次做多机通信的时候从机收到地址帧后忘了把SM2清零结果数据帧全被过滤掉了从机一直没反应。查了半天才想起来SM2的作用。还有一次是多个从机同时响应主机总线冲突数据全乱了。后来在协议里加了随机延时每个从机收到主机命令后随机等一段时间再回复冲突就少了很多。这些经验都是在实际项目中一点点积累的书本上不会讲得这么细。最后分享一个小技巧如果串口通信出了问题先不要急着改代码先用示波器或者逻辑分析仪看一下波形。波形能告诉你很多信息波特率对不对、数据位对不对、有没有干扰、地线有没有接好。很多时候看一眼波形就能定位问题比盯着代码看半天有效得多。另外串口调试助手要养成用十六进制显示的习惯因为很多协议是二进制的用ASCII显示会看到一堆乱码用十六进制就能看出规律。这些习惯看起来小但能帮你省很多时间。