起因

串口是 51 单片机的"Hello World":打印调试信息、给 WiFi/蓝牙模块发 AT 指令、和上位机通信都靠它。这篇是最小可用的串口程序——上电发送几条字符串,PC 发来的字符原样回显(echo)。调通它,就掌握了 STC89C52 串口的全部要素。

需求

  • STC89C52,晶振 11.0592MHz
  • 串口参数:9600 波特率、无奇偶校验(8N1)
  • 上电发送字符串;PC 发来的字符原样回显

技术实现原理

波特率从哪来:定时器 1 的溢出率

51 没有独立的波特率发生器,串口方式 1 的波特率时钟就是定时器 1 的溢出率再分频

1
波特率 = (2^SMOD / 32) × (FOSC / 12) / (256 - TH1)

因果链是这样的:

  1. 晶振 FOSC 先经 12 分频(标准 51 的一个机器周期 = 12 个时钟周期),得到定时器的计数时钟;
  2. 定时器 1 配成方式 2(TMOD=0x20,8 位自动重装):计到 256 溢出、TH1 自动重装,溢出率 = FOSC/12/(256-TH1)。选方式 2 就是因为它不需要中断重装初值,才能产生稳定的周期;
  3. 溢出脉冲再经过一个 16 或 32 分频器(由 PCON 的 SMOD 位选择,SMOD=1 时 16 分频,即"波特率加倍"),喂给串口;
  4. 把公式倒过来,就有了代码里的 TC_VAL = 256 - FOSC/16/12/BAUD(SMOD=1 时)。

为什么非要用 11.0592MHz 的晶振

这是"结构 → 误差 → 选型"的典型因果链:

  • 11.0592MHz ÷ 12 = 921600,恰好是 9600 × 96 = 115200 × 8,常用波特率都能整除,初值是整数,波特率误差为 0
  • 换成常见的 12MHz 晶振:算 9600 时 TC_VAL = 256 − 12M/12/32/9600 ≈ 256 − 3.26,只能取整为 3,实际波特率 10417,误差 +8.5%,远超 UART 约 2~3% 的采样容限,结果就是收到一堆乱码;
  • 12MHz 晶振下改用 2400 波特率,误差只有 0.16%,才能稳定通信。

所以"串口收到乱码"这个 BUG 在原理层面的根因,绝大多数就是波特率误差:要么晶振选错,要么 TH1 / SMOD 与公式不配套——找到这个根因,解决方案(换 11.0592M 晶振,或按公式重算初值)是自然导出的。

波特率的上限

TH1 最小只能取 1,于是有了最高速度限制(这就是代码注释里"请提前计算"的原因):

  • 波特率加倍(SMOD=1): Max_Baud = FOSC/12/16
  • 波特率不加倍(SMOD=0):Max_Baud = FOSC/12/32

例如:22.1184MHz晶振,波特率加倍时,最大波特率=22118400/12/16=115200

SCON = 0x50 与 TI/RI 的规矩

  • SCON=0x50:串口方式 1(一帧 10 位 = 起始 + 8 数据 + 停止,波特率可变)+ REN=1 允许接收;
  • TI(发完一帧置位)、RI(收到一帧置位)都是硬件置位、必须软件清零——不清零会反复进中断,这是 51 串口最经典的坑;
  • 本程序是"中断收 + 查询发":发送用 while(!TI) 等完成,进入前要先把 ES 关掉,否则 TI 置位瞬间又跳进中断服务函数,两边抢着操作 SBUF/TI(代码里 ES = 0 的注释"让串口安心工作"就是这个原因)。中断服务函数里对 TI 直接清零,就是为了不打扰查询发送。

程序源码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
/*----------------------------------------------- 
  名称:串口通信 
  修改:无 
  内容:连接好串口波特率设置为9600,无奇偶校验 
        晶振11.0592MHz 
        接上串口,可以接收到自己发送的字符 
------------------------------------------------*/  
  
#include<reg52.h> //包含头文件,一般情况不需要改动,头文件包含特殊功能寄存器的定义                          
  
/******************************************************************* 
请提前计算一下所选晶振能达到的最高速度,波特率不能超过最高速度 
(1) 波特率加倍(SMOD=1):  Max_Baud = FOSC/12/16 
(2) 波特率不加倍(SMOD=0):Max_Baud = FOSC/12/32 
例如:22.1184MHz晶振,波特率加倍时,最大波特率=22118400/12/16=115200 
*******************************************************************/  
#define FOSC    11059200        //振荡频率  
#define BAUD    9600            //波特率  
#define SMOD    1           //是否波特率加倍  
#if SMOD  
    #define TC_VAL  (256-FOSC/16/12/BAUD)  
#else  
    #define TC_VAL  (256-FOSC/32/12/BAUD)  
#endif  
  
typedef unsigned char uint8;  
typedef unsigned int uint16;  
  
code const char str1[] = "The string is 来自单片机!\r\n";       
code const char str2[] = "Author: Louis \r\n";  
code const char str3[] = "多多指教~~\r\n";  
  
//const  限定一个变量不允许被改变。在一定程度上提高程序安全性和可靠性  
//code 存储区间为 程序存储区  
  
/***************函数声明*******************/  
void InitUART(void);  
void SendOneByte(uint8);  
void SendrStr(const uint8 *ptr);  
  
/****************主函数********************/  
void main(void)  
{  
    uint8 i=0;  
    InitUART();  
  
    while(str2[i]!='\0')  
    {  
    SendOneByte(str2[i++]);   
    }  
  
    SendrStr(&str1[0]);  
    SendrStr(&str1[0]);  
    while(1);  
}  
  
/****************中断服务函数***************/  
void UART_ISR(void) interrupt 4  
{  
    uint8 RX_Data;  
    //只响应"接收"中断,"发送"中断来了就直接抹掉  
    if(RI)  
   {  
     RI = 0;    //串口中断标志不能自己清除,需要手动清除  
     RX_Data=SBUF;  
     SendOneByte(RX_Data);  
   }  
   else  
     TI = 0;        //串口发中断是发送完缓冲区数据之后产生  
}  
  
/****************串口初始化函数*************/  
void InitUART(void)  
{  
    TMOD = 0x20;    //定时器1,模式2工作模式       
    SCON = 0x50;    //串口工作模式1,允许REN   /* SCON: 模式 1,  8-bit UART, 使能接收         */  
    TH1 = TC_VAL;  
    TL1 = TH1;  
    PCON = 0x80;    //发送速率加倍  
    ES = 1;  
    EA = 1;  
    TR1 = 1;  
}  
/**************串口发送字符函数*************/  
void SendOneByte(uint8 c)  
{  
    ES = 0;         //禁止中断,让串口安心工作啊  
    SBUF = c;  
    while(!TI);     //等待发送完毕  
    TI = 0;         //清TI中断  
    ES = 1;         //打开中断  
}  
/**************串口发送字符串函数*************/  
void SendrStr(const uint8 *ptr)  
{  
      
    for(;*ptr!='\0';ptr++)  
    {  
       SendOneByte(*ptr);  
    }  
}  

踩坑记录与注意事项

  • 收到乱码:先查波特率误差(晶振、TH1、SMOD 三者是否配套),再查接线——TXD/RXD 要交叉接,PC 侧用 USB 转 TTL(经 MAX232 的才是 RS232 电平,不能直连);
  • PCON=0x80 别漏:SMOD 漏配,实际波特率只有目标的一半,必然乱码;
  • TH1 和 TL1 都要赋值:方式 2 首次计数从 TL1 开始,只设 TH1 第一次溢出周期就不对(本代码 TL1 = TH1 是正确写法);
  • RI/TI 手动清零:进中断先判断是 RI 还是 TI,处理完立即清,否则死循环进中断;
  • 卡死在 while(!TI):通常是 TR1 忘了置 1 或波特率配错,TI 永远不来,程序就停在这一行。