恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
80C51串口从寄存器到波特率:SCON、PCON与初始化全解析
首页
资讯中心
/
80C51串口从寄存器到波特率:SCON、PCON与初始化全解析
80C51串口从寄存器到波特率:SCON、PCON与初始化全解析
发布时间:2026/10/17 13:15:12
串口这东西基本是51学习路上第一个有“双向对话”感的外设。第七天学习笔记二这部分我原本只想把串行口控制寄存器SCON和特殊功能寄存器PCON的位定义背下来结果顺着内部结构一直摸到工作方式、波特率计算和初始化流程整整花了两天时间。这篇就按我实际捋通的顺序把80C51串行口内部结构、SCON、PCON、四种工作方式、波特率计算以及串口使用之前的初始化完整讲一遍适合刚学完定时器、正准备开始调串口的读者。为什么值得认真对待因为串口看起来只是调几个寄存器但SCON、PCON、TMOD、TH1/TL1这些寄存器是联动的。一个位设错表现往往是“能发送但接收乱码”“完全没反应”“自发自收正常但跟电脑连不上”。这篇内容就是帮你把这几处联动关系彻底捋清楚。1. 动手配置串口之前先弄明白数据是怎么流出去的1.1 串口在51内核单片机里的“三大件”很多人直接跳进代码写SCON结果出了问题根本不知道从哪里排查。我建议先看硬件框图。80C51串行口的内部结构可以分成三块发送数据缓冲器、接收数据缓冲器以及移位控制逻辑。发送侧和接收侧在物理上是独立的但都叫SBUF这就引出了第一个经典误区。发送SBUF只能写、不能读接收SBUF只能读、不能写。你在C语言里写SBUF dat;是往发送缓冲放数据执行dat SBUF;是从接收缓冲取数据。用同一个名字纯粹是方便编程硬件上两套东西各自干活。很多初学者以为一个SBUF又能发又能收实际上串口比你想的“专一”得多。1.2 两个缓冲区的工作节奏完全不同发送侧的逻辑是你往SBUF写入一个字节后硬件立即把它送入内部的发送移位寄存器一个位一个位移出去SBUF空出来等着下一次写入。接收侧则是接收移位寄存器先把每一位收齐8位完整拼成一个字节后整体装入接收SBUF同时把RI标志置1。所以发送和接收的节奏在时间上是错开的。发送时你写完SBUF千万别立刻再写第二个字节要等TI置位表明上一帧发完否则会覆盖当前还没发完的数据。很多初学串口的人栽在这里连续往SBUF塞两个字节结果只发出一个。接收时也要在RI置位后尽快读取SBUF不然下一帧数据来了上一帧还没读走就会丢。1.3 串口和定时器T1为什么总是绑定出现80C51的串口本身没有独立的波特率发生器方式1和方式3的移位时钟必须借用定时器T1的溢出脉冲经过分频后作为发送/接收的移位时钟。这就是为什么所有串口例程里都必须先配置T1。我第一次看到这个设计时觉得“这也太省了吧”反过来说这也解释了为什么串口初始化和前面学的定时器初始化是焊死的。当然也有个别增强型51芯片把定时器T2单独拨给串口用那就是后话了。标准80C51内核你只需要记住想要串口以可变的波特率工作T1必须跑起来而且多半被设置成方式28位自动重装。这个问题下面讲波特率计算时会再展开。2. SCON寄存器逐位拆解谁控制谁谁报告状态2.1 工作方式选择位SM0/SM1四个方式的入口SCON的字节地址是0x98支持位寻址这点很关键因为后续可以用位操作直接改单个标志位。最高两位SM0和SM1负责选择串口的四种工作方式00方式0同步移位寄存器模式01方式18位UART波特率可变10方式29位UART波特率固定11方式39位UART波特率可变绝大多数实际项目只用方式1和方式3。方式0主要是扩展IO用方式2因为波特率固定实用性反而不如方式3。设置时直接用SCON 0x50这样的字节赋值也很常见但如果是用位操作就是SM0 0; SM1 1;。2.2 REN、TB8、RB8在收发链路里的真实分工REN是接收允许位置1才允许串口接收数据。注意SCON复位值是0x00也就是说默认状态只发不收很多“为什么收不到数据”的问题就出在忘了把REN置1。TB8和RB8都只在方式2、方式3里有用。发送时你把地址/数据标志写进TB8硬件会自动把它作为第9位发出去接收时第9位会被硬件存进RB8。多机通信的判断逻辑就靠这两位配合SM2实现。方式0和方式1里这两位也可以写但基本不起作用RB8在方式1里会被硬件的停止位状态覆盖。2.3 TI与RI两个标志位以及“软件清零”这条硬规矩TI是发送完成标志RI是接收完成标志。一个字节发送完毕或接收完毕硬件自动把相应标志置1。但这俩标志不会自动清零必须你手动在代码里写0。这个“软件清零”的设计是很多人摸不着头脑的地方。为什么不让硬件自动清因为CPU和串口是异步工作的硬件如果自动清了CPU可能还没反应过来。它把标志置1后一直保持就是要等CPU来认领。你在查询方式里可以while(!TI);然后TI0;也可以直接进串口中断后先判断是TI还是RI处理完再清。忘记清RI的后果很经典如果开了中断RI一直保持1串口中断会一直触发程序就卡死在ISR里了。3. PCON里的SMOD位波特率翻倍的开关3.1 PCON其实管着一堆和串口无关的事PCON的特殊之处在于它是个杂项寄存器字节地址0x87不可位寻址只能整字节操作。它的D7位是SMOD波特率加倍控制D6和D5是通用标志位GF1和GF0可以用来存一些自定义状态最低两位PD和IDL是省电模式控制位。实际写串口初始化时你真正关心的只有SMOD。PD和IDL那两个位如果不是做低功耗产品基本上用不到。但是要小心PCON不可位寻址所以想置SMOD只能用PCON | 0x80;这种按位或的方式不要直接写成PCON 0x10;否则会把其他位干掉。3.2 SMOD1时波特率翻倍的原理SMOD名字就叫“串口波特率加倍位”作用非常直接SMOD0时定时器T1的溢出脉冲要经过32分频后作为移位时钟SMOD1时这个分频系数变成16等于时钟频率翻了一倍。所以同一套T1初值下SMOD1的波特率正好是SMOD0的两倍。这里有一个很实用的换算规律。如果你想要9600波特率但T1初值算出来不是整数一个常见补救办法就是让SMOD1然后把目标波特率当成19200重新计算TH1。因为分频减半后同样的T1初值产生的波特率翻倍反过来你可以在SMOD1的前提下用更低的T1溢出率达到目标波特率误差往往更小。3.3 查表配置时SMOD位的取舍网上很多波特率配置表会同时给出SMOD0和SMOD1两组TH1值。我建议除非波特率很高比如19200以上否则统一用SMOD0。原因有三条SMOD0时T1溢出率较低功耗和中断压力都小一些。11.0592MHz晶振配9600时SMOD0的TH1正好是0xFD没有任何误差。如果你的代码其他地方偷偷改了PCONSMOD0时对波特率的影响相对没那么敏感。高波特率场景下SMOD1的优势是能让T1溢出率保持在合理范围因为T1溢出率太高会导致每次中断间隔太短如果还开着T1中断CPU负担会很重。4. 四种工作方式怎么选同步、8位、9位背后的应用场景4.1 方式0不经过TXD/RXD的同步移位模式方式0不是UART它是同步移位寄存器模式数据从RXD引脚逐位移出或移入TXD引脚输出移位时钟。波特率固定是系统时钟的1/12不受T1影响。这种模式最常见的用途是外接74HC164这类移位寄存器实现IO扩展或者接74HC165做输入扩展。由于它收发的每一帧没有起始位和停止位严格来说不适合做芯片间的标准串口通信。我只在扩展数码管位选线的时候用过方式0通信场景里基本没人拿它当串口用。4.2 方式1最常用的8位UART全双工方式1的帧格式是1位起始位 8位数据位 1位停止位总共10位。波特率由T1的溢出率决定可灵活配置。这是绝大多数“51和电脑串口通信”的默认选择也对应串口调试助手里的“8-N-1”设置。方式1的收发都是全双工的发送和接收互不干扰这一点比很多初学者的直觉强。你以为“同时收发会互抢”实际不会发送走发送移位寄存器接收走接收移位寄存器各自独立。唯一要注意的是面对同一组引脚方向由硬件根据REN和当前收发状态自动切换你不需要在代码里改引脚方向。4.3 方式2/方式3第9位数据的价值以及SM2的作用方式2和方式3的帧格式都是11位1位起始位 8位数据 1位可编程的第9位 1位停止位。区别在于方式2波特率固定为振荡频率的1/64或1/32而方式3的波特率由T1决定。第9位数据最经典的应用就是多机通信。主机发送地址帧时把TB8置1发送数据帧时把TB8清0每个从机都设置SM21这样从机只有在收到第9位为1的帧时才会触发RI收到第9位为0的纯数据帧时直接忽略。从机发现自己被点名后再清SM2去接收后续数据帧。工作方式帧格式波特率来源典型用途方式08位同步fosc/12移位寄存器扩展IO方式110位1起始8数据1停止T1溢出率 / 32或16常规点到点UART通信方式211位含第9位fosc/64或fosc/32固定波特率多机通信方式311位含第9位T1溢出率 / 32或16可调波特率多机通信实际项目里如果只是两个单片机互相传数据方式1就够了。只有当你需要地址/数据区分的时候才考虑方式2或方式3而且一般选方式3因为波特率可调更灵活。5. 波特率计算不玄乎从定时器溢出到分频的完整链路5.1 波特率到底由谁决定波特率就是每秒传输多少个二进制位。串口发送不是CPU手动控制引脚电平翻转而是靠移位时钟驱动。T1工作在方式2时每溢出一次就产生一个脉冲这个脉冲经过32或16分频后变成移位时钟每来一个移位时钟脉冲就移出一位。所以波特率链条是晶振频率 → T1计数频率 → T1溢出率 → 分频 → 移位时钟。T1计数频率通常是fosc/12但1T增强型芯片不适用这个要看你手头单片机的手册。如果按标准80C51算T1的溢出率就是fosc / (12 × (256 - TH1))。5.2 方式1的波特率公式推导与TH1计算方式1波特率公式波特率B (2^SMOD / 32) × T1溢出率。展开写就是B (2^SMOD / 32) × fosc / (12 × (256 - TH1))。反推TH1TH1 256 - (fosc × 2^SMOD) / (384 × B)。下面的例子里以11.0592MHz晶振、SMOD0、目标波特率9600为例TH1 256 - 11059200 / (384 × 9600) 256 - 11059200 / 3686400 256 - 3 253。256换算成十六进制就是0xFD。这就是网上所有9600波特率例程里写的0xFD的由来。因为T1是方式2自动重装所以TH1和TL1都要填同一个值。5.3 为什么11.0592MHz晶振成了标准答案11.0592MHz不是一个随便定的数字它存在的意义就是为了让常见波特率的TH1初值是整数。11059200 ÷ 384 28800这个数能被9600整除。换成12MHz晶振情况就尴尬了TH1 256 - 12000000 / 3686400 256 - 3.255 252.745。四舍五入取253后实际波特率是(12000000 / 12) / (256 - 253) / 32 10416.7和目标9600差了约8.5%。这个误差足以让串口数据出现乱码尤其是连续发长帧时。所以做串口通信时我强烈建议直接换11.0592MHz晶振别在12MHz上硬调。顺便说当SMOD1时同样的TH10xFD但波特率会变成19200。这个规律很多人不知道它的好处是同一套T1初值可以快速在两种主流波特率之间切换只要单独改PCON的SMOD位就行。5.4 方式2/3波特率的不同之处方式2因为不需要T1波特率固定B (2^SMOD / 64) × fosc。SMOD0时是fosc/64SMOD1时是fosc/32。方式3虽然帧格式和方式2一样但波特率完全按方式1的公式算也就是靠T1产生。这里给个常用初值速查表以11.0592MHz晶振、SMOD0为准目标波特率TH1初值实际波特率误差48000xFA48000%96000xFD96000%144000xFE144000.08%19200需SMOD10xFD192000%如果你是12MHz晶振还想要接近9600的波特率可以把目标波特率改成10417然后在上位机端也手动设成10417但这治标不治本换晶振才是最干净的做法。6. 串口初始化六步走以9600波特率为例6.1 初始化前的设计规划写代码前先在纸上确定四件事晶振频率、目标波特率、数据格式、收发方式。以最常见的“11.0592MHz晶振、9600、8-N-1、查询收发”为例配置清单如下串口工作方式方式1SCON的SM00SM11允许接收REN1波特率SMOD0TH1TL10xFDT1工作方式方式2自动重装TMOD高4位设为0x2如果你手头是增强型1T单片机公式里的分频系数要先查手册不能直接套这里的12分频。6.2 标准初始化流程与代码void uart_init(void) { TMOD 0x0F; // 保留T0原有配置只清掉T1相关位 TMOD | 0x20; // T1设为方式28位自动重装 TH1 0xFD; TL1 0xFD; // 初值相同方式2自动重装 PCON 0x7F; // SMOD0波特率不加倍 SCON 0x50; // 方式1REN1允许接收 TR1 1; // 启动T1波特率发生器开始工作 ES 1; // 开串口中断如果只用查询可以不开 EA 1; // 开总中断 }这里有个细节必须强调TMOD处理一定要用“先清后置”不要直接写TMOD 0x20;。如果前面定时器T0已经初始化成方式1了整体赋值会直接把T0配置冲掉这是多定时器场景下最高发的小错误。发送和接收的基本查询函数长这样void uart_send_byte(unsigned char dat) { while (!TI); // 等上一帧发送完成 TI 0; // 清发送完成标志 SBUF dat; // 写入发送缓冲硬件开始发送 } unsigned char uart_recv_byte(void) { while (!RI); // 等接收完成 RI 0; // 清接收完成标志 return SBUF; // 读取接收缓冲 }注意发送函数的清TI位置。有人喜欢在写SBUF之前清TI严格来说如果在while之后清说明TI本来就是1也可以。但为了统一习惯我推荐发送前先等TI为1、然后清零、再写SBUF这个顺序能避免漏掉上一帧没发完的边界情况。6.3 初始化完先别急着发数据做一次回环自测我每次调串口第一步不是连电脑上位机而是做回环测试。方法特别简单把单片机的TXD引脚和RXD引脚直接用杜邦线短接然后让程序发一串数据再尝试接收如果发出去的东西能被自己原样收回来说明串口硬件通路、中断、标志位逻辑都没问题。回环测试没过就别急着去查上位机、电平转换这些外部因素。回环测试过了但连不上电脑那问题基本在外部电平不匹配、USB转串口模块接触不良、波特率设置不一致。这个排查顺序能帮你把问题范围缩小一大半。6.4 常见错误与调试经验从实践来看串口调试最常踩的坑集中在几个位置忘记置REN1导致只能发不能收表现是“发出去的电脑能看到但电脑发的单片机没反应”。SCON复位值是0x00如果初始化代码里有清SCON的操作后面又没有重新设置REN接收就会失效。中断方式下ES和EA没同时打开或者开了ES忘了EA程序一直不进入串口中断。收到的数据每个字节都比预期多一个FF或00多半是电平转换模块的接线方向搞反了或者TXD/RXD交叉接错。波特率配置看起来是对的但乱码先检查PCON有没有被别处改成SMOD1再看晶振是不是真的11.0592MHz最后看USB转串口模块是不是劣质的仿品。我个人实际调串口时还有一个习惯用串口调试助手发送的时候先发单个字符‘U’收到正确回显后再发长字符串测试连续性。这样能快速区分是“单字节都错”还是“长字节流丢帧”前者指向波特率和格式问题后者指向缓冲和时序问题。串口这部分我最大的体会是能跑通的例程很多但能把它背后SCON、PCON、T1、TH1这几个寄存器之间的联动关系讲清楚的很少。你要是能自己在纸上把波特率公式推一遍再照着上面的流程做一次回环自测后面写任何串口应用都会顺手很多。