资讯动态

80C51串行口详解:从内部结构到SCON/PCON及波特率计算初始化

发布时间:2026/10/17 1:06:30 来源:尧图企业网站定制
51学习第七天二——80c51串行口内部结构、SCON、PCON、工作方式、波特率计算与初始化一篇讲透串口学51到第七天如果你还停留在点亮LED、驱动数码管、按键扫描这个阶段那今天这一步算是正式踏入“单片机与外界对话”的门槛了。串行口UART是80C51里最常用、也最实用的外设之一调试时打印一个变量、和电脑上位机通信、控制蓝牙/WiFi模块、接GPS模块、甚至和另一块单片机通信全都靠它。这篇文章我按自己的学习路径来写先讲串口内部结构再讲SCON和PCON这两个控制寄存器然后对比四种工作方式手把手算波特率最后给出一套可以直接抄的初始化代码外加我在实际调试中踩过的坑。内容偏基础但都是干货适合正在学51串口、被波特率搞晕、初始化总出问题的同学。1. 串行口硬件结构数据是怎么从单片机“说”出去的很多人学串口上来就背寄存器背完就忘。我建议先花十分钟搞清楚80C51串行口的内部硬件结构知道数据是怎么从CPU走到引脚上的后面SCON里每一位的作用就变得顺理成章了。1.1 结构框图里那几个关键部件80C51串行口在芯片内部主要由这几个部分构成发送数据缓冲器SBUF、接收数据缓冲器SBUF注意是两个独立物理单元、发送移位寄存器、接收移位寄存器、串行控制逻辑以及TXDP3.1和RXDP3.0两个引脚。发送过程是这样的CPU执行SBUF data这条指令把一个字节写入发送SBUF硬件自动在移位时钟驱动下把SBUF里的8位数据从低位到高位逐位搬到TXD引脚上发送完毕后硬件把TI位置1。接收过程相反RXD引脚上的电平变化被采样电路逐位移入接收移位寄存器收够一帧后硬件自动把数据装入接收SBUF然后把RI位置1CPU执行data SBUF就能读出来。有一点非常关键发送SBUF和接收SBUF虽然名字一样、地址也一样都是0x99但它们是两个完全独立的物理寄存器。写0x99只会写入发送SBUF读0x99只会从接收SBUF读出互不干扰。所以一条指令SBUF x和另一条指令x SBUF根本不会冲突。这是初学者最需要建立的概念。1.2 双缓冲机制有什么用很多人不理解为什么要在SBUF之外再加一个移位寄存器。直接让CPU往移位寄存器里写不行吗不行。假设没有中间缓冲CPU正在往移位寄存器里填数据的时候移位时钟恰好启动就会把还没填完的字节发出去产生错帧。有了双缓冲CPU只需要把数据写进SBUF就立刻可以去干别的事了移位寄存器慢慢往外发发完置一个TI通知CPU“我这个字节已经发完了可以再写下一个”。接收侧同理移位寄存器收完一帧后把完整字节送到接收SBUF这个拷贝是瞬时完成的下一帧从RXD进来时不会覆盖上一次的数据因为此时上一帧数据已经在SBUF里等着CPU来读了。这里有个实际意义在主循环里处理串口发送时你不需要用延时让CPU等TXD引脚把数据发完只要检测TI标志位即可这个后面初始化代码部分会具体演示。2. 串行口控制寄存器SCON每一个比特都是总开关SCON是串行口最核心的控制寄存器地址0x98支持位寻址每一位都有一个位地址可以直接用RI 1、TI 0这样的写法操作。下面把它拆开讲清楚。2.1 SCON各位的功能与含义SCON共8位SM0、SM1、SM2、REN、TB8、RB8、TI、RI。SM0和SM1是工作方式选择位组合出四种工作方式00对应方式001对应方式110对应方式211对应方式3。这四位组合是后面讲工作方式的基础选择不同方式直接决定了帧格式和波特率来源。SM2是多机通信允许位。在方式2和方式3下若SM21则只有当接收到的第9位数据RB8为1时RI才置1单片机才接收这一帧RB8为0时直接丢弃。SM20时不管第9位是0还是1都正常接收。这个功能用在多机通信中作为地址帧和数据帧的区分手段。REN是接收允许位靠软件置1来使能接收置0则禁止接收。注意方式0下REN0会关闭移位寄存器的移位功能方式1~3下REN0就是不听RXD引脚上的数据了。TB8是发送的第9位数据位在方式2和方式3中由软件赋值常用于多机通信时发送地址帧置1或数据帧置0。RB8是接收到的第9位数据位也只在方式2和方式3中有效。方式1下如果SM20RB8存放接收到的停止位。TI是发送中断标志位。发送完一帧数据后由硬件置1注意硬件不会自动清零必须由软件清零。TI1有两种响应路径如果打开了串口中断ES1则触发中断否则作为查询标志位软件循环判断。RI是接收中断标志位。接收完一帧数据后由硬件置1同样需要软件清零。2.2 为什么说TI和RI“必须手动清零”是最大的坑不少初学者写串口程序时发现第一个字节发送正常后面就乱套了或者中断一进来就死循环十有八九是没手动清TI/RI。举个例子主循环里你写了这样一个发送函数void UART_SendByte(unsigned char dat) { SBUF dat; while(!TI); // 等待发送完成 }第一次调用时寄存器上电复位TI0数据写完SBUF硬件开始发送发完后TI被置1循环退出函数返回。看起来没问题。但如果这个函数被连续调用第二次、第三次呢第二次进入时TI还是上一次遗留的1while(!TI)瞬间就不等了此时上一帧可能刚刚发完、SBUF虽然可以写入但紧接着写入新数据时如果前一个字节还没完全移出就会导致帧格式出问题。而且更直接的问题是第二次调用SBUF dat可能发生在上一帧数据还未完全发送完毕的时刻导致数据覆盖。正确写法是在等待TI置1之后立刻清零void UART_SendByte(unsigned char dat) { SBUF dat; while(!TI); // 等发送完成 TI 0; // 手动清标志 }有人习惯先清TI再发送void UART_SendByte(unsigned char dat) { TI 0; SBUF dat; while(!TI); }这两种写法都行核心原则就是发送完毕后必须把TI复位不能让它一直为1。RI同理读完数据后必须RI 0不然下一帧进来时RI已经为1程序会误以为又收到新数据而重复处理上一次的数据。3. 特殊功能寄存器PCONSMOD位与波特率倍增的秘密讲完SCON还有一个和串口密切相关的寄存器——电源控制寄存器PCON地址0x87。这个寄存器里和串口有关的只有最高位SMOD它控制波特率是否加倍实际使用中相当重要。3.1 PCON的结构PCON一共8位最高位SMOD是串行口波特率倍增位。单片机复位时PCON各位清零也就是SMOD0。在方式1、方式2、方式3下如果SMOD1波特率在原有基础上翻倍SMOD0则波特率不变。PCON这个寄存器不可位寻址也就是说你无法像操作SCON那样直接写SMOD 1必须整体赋值。比如要把SMOD置1而其他位不动正确写法是PCON | 0x80; // 只把最高位置1为什么这里单独强调一下因为真的有人在Keil里直接写SMOD 1编译报错后一脸懵。和SCON不同PCON不支持位操作指令只能用字节读写。3.2 SMOD倍增的数学含义波特率翻倍本质上是把波特率时钟源的分频系数从64降为32。以方式1和方式3为例波特率公式波特率 (2^SMOD / 32) × 定时器1溢出率SMOD0时2^01除以32SMOD1时2^12除以16。分子从32变成16正好翻倍。方式2的固定波特率公式也一样波特率 (2^SMOD / 64) × 振荡频率注意这里分母是64和方式1/方式3分母不同后面计算的时候别搞混。4. 四种工作方式从同步移位到多机通信到底怎么选80C51串行口有四种工作方式记住一句话方式0用来扩展并行口方式1是基础8位UART方式2是固定波特率9位UART方式3是可变波特率9位UART。平时用得最多的是方式1需要多机通信时用方式3。4.1 方式0同步移位寄存器方式方式0下串行口工作于同步移位寄存器模式TXD引脚输出移位脉冲RXD引脚输入或输出数据。每帧8位数据低位在前波特率固定为振荡频率的1/12。这个方式一般不用于单片机之间的通信而是用来外接移位寄存器芯片扩展并行I/O口比如用74HC164做并行输出扩展用74HC165做并行输入采集。它节省了I/O口但代价是不能长距离传输、抗干扰差适合板内短距离使用。4.2 方式18位UART最常用的通信方式方式1的帧格式是1位起始位低电平 8位数据位低位在前 1位停止位高电平共10位。起始位和停止位由硬件自动添加。波特率由定时器1的溢出率决定公式波特率 (2^SMOD / 32) × 定时器1溢出率其中定时器1溢出率就是定时器1每秒产生的溢出次数。这是最常用的通信方式波特率可变、帧格式标准和电脑串口助手直接通信完全没有问题。4.3 方式29位UART固定波特率方式2的帧格式是1位起始位 8位数据位 1位可编程的第9位TB8/RB8 1位停止位共11位。波特率是固定的由振荡频率决定波特率 (2^SMOD / 64) × 振荡频率比如12MHz晶振、SMOD0时波特率 12MHz / 64 187500bps。这个速率太高和大多数外设通信都不匹配所以方式2在实际项目里很少用除非你的晶振频率恰好能产生目标波特率。4.4 方式39位UART可变波特率多机通信方式3的帧格式和方式2完全一样都是11位但波特率来源变成了定时器1溢出率公式和方式1一样波特率 (2^SMOD / 32) × 定时器1溢出率所以方式3兼具9位帧格式和可调波特率两个优点。多机通信时通常选择方式3配合SCON里的SM2位和TB8/RB8来实现地址帧/数据帧的区分。4.5 四种方式对比速查工作方式帧格式波特率来源典型用途方式08位同步数据振荡频率/12扩展并行I/O口方式1起始位8数据停止位定时器1溢出率常规双机/PC通信方式2起始位8数据TB8/RB8停止位固定频率/64或/32较少使用方式3起始位8数据TB8/RB8停止位定时器1溢出率多机通信、常规通信5. 波特率计算公式、实例与误差分析波特率计算是串口学习里最容易劝退人的环节其实核心就一个公式、一个初值换算。我彻底把它讲透。5.1 核心公式与定时器初值推导方式1和方式3的波特率公式波特率 (2^SMOD / 32) × 定时器1溢出率定时器1溢出率的计算要看定时器工作方式。串口波特率生成最常用的方式是定时器1工作于方式28位自动重装此时定时器1溢出率 定时器1计数频率 / (256 - TH1)如果定时器1的计数频率是振荡频率的1/1212T模式则定时器1溢出率 振荡频率 / 12 / (256 - TH1)代入波特率公式波特率 (2^SMOD / 32) × 振荡频率 / 12 / (256 - TH1)化简后得到12T模式下波特率 (2^SMOD × 振荡频率) / [384 × (256 - TH1)]反过来求TH1TH1 256 - (2^SMOD × 振荡频率) / (384 × 波特率)这个公式一定要自己推导一遍别看它有点绕理解了之后再遇到任何晶振频率、任何波特率你都能自己算初值而不是永远查表。5.2 实战计算11.0592MHz晶振、9600bps最常见的晶振是11.0592MHz为什么选这个频率因为它能精确分频出众多标准波特率。以9600bps、SMOD0、12T模式为例TH1 256 - (1 × 11059200) / (384 × 9600) 256 - 11059200 / 3686400 256 - 3 253 0xFD所以直接把TH1和TL1都填0xFD就能得到9600bps。再验算一下实际波特率波特率 11059200 / [384 × (256 - 253)] 11059200 / 11520 9600正好等于9600零误差。这就是11.0592MHz晶振被称为“串口专用晶振”的原因。如果SMOD1则TH1 256 - (2 × 11059200) / (384 × 9600) 256 - 6 250 0xFA同样能得到9600bps但此时定时器溢出率更高初值更小。5.3 如果只有12MHz晶振怎么办很多开发板用的是12MHz晶振此时算9600bpsTH1 256 - 12000000 / (384 × 9600) 256 - 3.255208 252.7448TH1只能是整数取253实际波特率 12000000 / [384 × (256 - 253)] 12000000 / 11520 10416.67bps误差 (10416.67 - 9600) / 9600 ≈ 8.5%这个误差已经超过UART可容忍的范围了。UART异步通信要求收发双方波特率误差一般在2%~3%以内8.5%的偏差会导致接收端采样点逐渐偏移轻则偶尔乱码重则完全收不到。所以工程实践上只要涉及串口通信尽量选11.0592MHz晶振。12MHz晶振做串口通信不是不行得用非标准波特率或者换其他分频方案但麻烦事多不如直接换晶振。5.4 波特率误差检查表晶振频率目标波特率计算TH1(SMOD0)实际波特率误差11.0592MHz96000xFD96000%11.0592MHz48000xFA48000%11.0592MHz192000xFD(SMOD1)192000%12MHz96000xFD10416.678.5%12MHz48000xF948000%看到没12MHz算4800bps反而没误差因为12000000 / (384 × 4800) 6.51取整后不是精确值这里表格里取的0xF9对应的是初值247实际算下来是有微小误差的我标注一下12000000/(384×(256-247))12000000/3456≈3472bps这是不对的。正确算4800的初值12000000/(384×4800)6.51TH1256-6.51≈249.49取249实际波特率12000000/(384×7)4464bps误差约7%更糟糕。所以再次强调12MHz晶振做标准波特率串口通信非常吃力老老实实用11.0592MHz。5.5 6T模式下的计算增强型51单片机比如部分STC系列可以通过配置将机器周期从12个时钟降为6个甚至1个此时波特率公式里的12要换成6或1。6T模式下方式1/方式3的波特率公式变为波特率 (2^SMOD × 振荡频率) / [192 × (256 - TH1)]11.0592MHz、9600bps、SMOD0TH1 256 - 11059200 / (192 × 9600) 256 - 6 250 0xFA所以同样硬件配置6T模式下的TH1初值和12T模式SMOD1时的初值一样。这一点在实际写代码之前务必确认你的单片机到底工作在12T还是6T模式不然照抄别人代码时初值全对但波特率翻倍串口收到一堆乱码还找不到原因。6. 串口初始化实操完整代码与逐步拆解理论讲完下面给出可以直接抄的初始化代码。我以Keil C51、11.0592MHz晶振、9600bps、方式1、查询方式收发为例。6.1 初始化步骤清单串口初始化可以归纳为固定四步配置定时器1工作方式TMOD 0x0F再TMOD | 0x20让定时器1工作于方式28位自动重装。设置波特率初值TH1 TL1 0xFD然后TR1 1启动定时器。设置串口工作方式SCON 0x50对应SM00、SM11、SM20、REN1即方式1允许接收。必要时设置PCON的SMOD位然后配置中断ES、EA。第2步里有一个细节容易被忽略TMOD的复位值是0x00定时器0和定时器1都被配置为方式013位定时器直接写TMOD0x20会同时把定时器0也改成方式1覆盖你之前对定时器0的配置。所以稳妥写法是先清低四位、再置高四位TMOD 0x0F; TMOD | 0x20;。同理如果你定时器0正用于定时这个细节能省去一堆排查时间。6.2 完整代码示例#include reg52.h void UART_Init(void) { TMOD 0x0F; // 清空定时器1的模式位 TMOD | 0x20; // 定时器1工作于方式28位自动重装 TH1 0xFD; // 波特率初值9600bps 11.0592MHz, SMOD0 TL1 0xFD; // 自动重装值 TR1 1; // 启动定时器1 SCON 0x50; // 方式1REN1允许接收 PCON 0x7F; // SMOD0波特率不倍增 } void UART_SendByte(unsigned char dat) { SBUF dat; while(!TI); // 等待发送完成 TI 0; // 清零发送标志 } unsigned char UART_ReceiveByte(void) { while(!RI); // 等待接收 RI 0; // 清零接收标志 return SBUF; } void UART_SendString(unsigned char *str) { while(*str) { UART_SendByte(*str); } } void main(void) { UART_Init(); while(1) { UART_SendString(Hello UART\r\n); // 等待接收直到有数据到来收到后原样回显 if(RI) { unsigned char tmp UART_ReceiveByte(); UART_SendByte(tmp); } } }这段代码的核心逻辑很简单每轮循环先发一条字符串然后检查RI有没有置位如果有就接收并回显。注意主循环里判断的是RI但UART_ReceiveByte内部又用while(!RI)等待这里如果RI已经为1会直接通过。实际上更简洁的写法是主循环直接判断RI后读取SBUF不一定封装函数看需求灵活处理。6.3 查询方式与中断方式的取舍上面的代码用的是查询方式。查询方式代码简单、逻辑直观适合单任务或主循环不那么繁忙的程序。但存在一个明显问题如果主循环正在执行一段耗时操作比如数码管动态扫描、长时间的延时串口数据来了你没法及时响应数据可能被下一帧覆盖丢失。中断方式则不同。初始化里打开ES和EA在串口中断服务函数里直接处理收到或者发送完成的事件void UART_Init(void) { // 前面的配置照旧 ES 1; // 打开串口中断 EA 1; // 打开总中断 } void UART_ISR(void) interrupt 4 { unsigned char dat; if(RI) { RI 0; dat SBUF; UART_SendByte(dat); // 回显 } if(TI) { TI 0; // 发送完成可在此继续发送下一帧 } }注意串口中断服务函数判断RI和TI的顺序先判RI再判TI。因为在同一时刻只可能有一个标志位置1但严谨起见两个都判断。中断方式的好处是主循环不必频繁轮询RI数据到了硬件自动打断CPU程序响应及时。代价是中断服务函数里尽量不要做耗时操作否则会影响主循环的实时性。6.4 发送字符串的细节串口发送字符串时经常遇到两个小问题。第一字符串末尾忘了加\r\n导致上位机收到的内容不换行显示成一长串。第二使用while(*str)判断字符串结尾要求你传入的确实是以\0结尾的字符串常量或者数组如果传入一个没有结尾符的数组程序会一直发到内存里的任意0为止看起来就是发送了一堆乱码。实际项目里我习惯把发送字符串改为指定长度的发送或者在数组定义时就保证有结尾符code unsigned char msg[] System ready\r\n;如果字符串存放在代码区code区注意传入函数时要声明unsigned char code *str否则指向数据区的指针去读代码区数据拿到的是错误的地址发送内容会变成一堆未知字符。这也是Keil C51里新手经常踩的坑。7. 常见问题与排查实录串口调不通到底哪儿错了串口调试不像点灯出了问题有迹可循。我把自己调试串口时遇到过的几类典型问题整理在下面每个都给出排查思路。这些问题如果没遇到过你可能会觉得“怎么可能会犯这种错”但实际就是在这些细节上翻车的人最多。7.1 收到乱码波特率、晶振、SCON三连查乱码是最常见的问题。先确认收发双方的波特率是否一致。电脑串口助手里选的是9600单片机初始化也是9600这个提前对好。然后确认波特率初值是否匹配晶振频率12MHz晶振配9600bps就是天然8.5%误差乱码正常换11.0592MHz晶振。最后确认SCON配置是否正确方式1应该设0x50如果你误设成0x10REN没置1接收功能没开也会表现为收不到或者读到错误的数。还有一种情况发送端波特率是对的接收端用的不是UART而是逻辑分析仪的默认解码配置解码失败显示乱码。这时候先拿串口助手自收自发验证排除上位机问题后再查单片机。7.2 只能发不能收查REN查引脚查共地接收不到数据先检查REN位是否为1。REN0时接收关闭RI永远不会置1。这个最容易被忽略。然后检查TXD和RXD有没有接反。单片机TXD接对方RXDRXD接对方TXD这是交叉连接的。很多人直接用一根杜邦线把两个TXD连在一起当然通信不了。第三个隐蔽的坑是共地问题。如果两块板子各自独立供电一定要把GND连在一起否则信号没有参考电平UART收到的全是噪声。我自己有一阵子用USB转串口模块调试模块的GND没和开发板共地结果接收全乱码接上GND后一切正常。共地问题在串口通信里出现的频率比你想象的高得多。7.3 发送第一个字节丢失程序上电后第一次调用UART_SendByte发送时如果第一个字节丢了或者变成空数据通常是因为单片机上电复位后TXD引脚电平还没稳定上位机串口已经打开复位瞬间TXD被拉低被视为一个起始位导致上位机误收一个0x00。解决办法是在初始化完成后延时几十毫秒再开始发送数据或者上位机那边在上电后再打开串口。7.4 一帧数据里偶尔多个字节或丢字节这通常和波特率误差有关但误差在2%以内时不应该出现偶发错误。肉眼可见的偶发错误更可能是时钟源不稳定比如板子上用的直插晶振旁边没有负载电容或者用了内部RC振荡器做波特率时钟。内部RC振荡器的精度受温度影响明显做串口通信强烈建议用外部晶振。7.5 收发都正常但上位机显示的是十六进制串口助手显示模式选错也会让人误以为乱码。发送0x41时文本模式显示A十六进制模式显示41。如果上位机和单片机约定的是文本协议显示十六进制当然看不懂。先把显示模式切换回来再核对协议格式别一上来就怀疑波特率。7.6 常见问题速查表现象可能原因排查方法完全乱码波特率不一致收发双方统一波特率完全乱码晶振频率与初值不匹配用11.0592MHz晶振完全乱码未共地连接GND完全乱码12T/6T模式配置不一致确认单片机的时钟模式收不到数据REN0初始化中SCON置0x50收不到数据TXD/RXD接反交叉连接TXD接RXD第一个字节丢失上电复位时序初始化后延时再发送偶发错帧时钟源不稳定改用外部晶振能发不能收对方未打开串口先验证上位机收发发送内容不对指针指向错误存储区区分data区与code区7.7 排查串口的正确顺序如果你实在调不通我建议按这个顺序走一遍用一个USB转串口模块把单片机的TXD和RXD短接环回测试串口助手发送什么单片机收到什么如果不行问题在单片机侧如果行问题在连接线或对方设备。用示波器或者逻辑分析仪看TXD引脚上有没有波形有波形说明数据真的发出去了没有波形则检查定时器TR1是否启动、SBUF是否成功写入。把波特率降到2400或1200再试低速下容错性更强如果低速下正常、高速下乱码说明是波特率误差或信号完整性问题。环回测试这个技巧特别实用它能快速把问题定位到“单片机内部”还是“外部链路”避免在错误的方向上白白浪费时间。8. 写在最后串口调试的一点经验我在学串口的过程中真正花时间最多的不是理解SCON和PCON也不是背波特率公式而是排查那些“看起来正常但实际就差一步”的细节。比如共地、TXD和RXD接反、还有定时器模式被覆盖。这些坑几乎每个人都踩过但网上很少有文章把细节讲全很多教程直接贴代码读者抄完运行不了也不知道差在哪。经验是学串口不要一上来就背寄存器表先理解数据通路再逐个分析控制位然后自己动手算一次波特率初值最后把发送、接收、中断三套代码各写一遍。这样过一遍之后你以后再看到任何一篇串口文章都会觉得特别轻松。后续如果你要用单片机接ESP8266、GPS模块、蓝牙模块这些设备串口就是你操作它们的窗口。把这套基础打牢后面不管是做物联网还是做小型控制系统都会顺畅很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑