串口知识复盘

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

053.6.9 串口知识复盘

#include "reg52.h"
#include <intrins.h>

typedef unsigned char uchar;
typedef unsigned int uint;

uint count = 0;
uchar recv;

sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit BEEP = P1^6;
sbit JDQ1 = P2^0;

void Delay_xms(uint xms) //@11.0592MHz
{
    uchar data i, j;

    while (xms)
    {
        _nop_();
        i = 2;
        j = 199;
        do
        {
            while (--j);
        } while (--i);
        xms--;
    }
}

void UART_Send_Byte(uchar send_byte)
{
    SBUF = send_byte;
    while (TI == 0);
    TI = 0;
}

void UART_Send_Str(uchar *send_str)
{
    while (*send_str != '\0')
    {
        UART_Send_Byte(*send_str++);
    }
}

void main()
{
    SCON = 0x50;
    PCON &= 0x7F;
    TMOD &= 0x0F;
    TMOD |= 0x20;
    TH1 = 0xFD;
    TL1 = 0xFD;
    TR1 = 1;
    ES = 1;
    EA = 1; // 打开总中断

    while (1)
    {
        for (count = 0; count < 26; count++)
        {
            UART_Send_Byte('a' + count);
        }

        UART_Send_Byte('\n');
        UART_Send_Str("i am wfeng!\r\n");
        Delay_xms(1000);
    }
}

/*****************
串口的中断处理函数
*****************/
void UART_Routine(void) interrupt 4
{
    if (RI == 1)
    {
        RI = 0;
        recv = SBUF;
        switch (recv)
        {
            case 0x01:
                LED1 = 0;
                break;
            case 0x02:
                LED2 = 0;
                break;
            case '3':
                LED3 = 0;
                break;
            case '4':
                LED4 = 0;
                break;
            case 0x05:
                P1 |= 0x0F;
                break;
            case 0x06:
                BEEP = 0;
                break;
            case 0x07:
                BEEP = 1;
                break;
            case 0x08:
                JDQ1 = 0;
                break;
            case 0x09:
                JDQ1 = 1;
                break;
            default:
                break;
        }
    }
}

一、串口知识复盘

1.1 数据发送和接收

UART是异步串行通信接口。“异步”表示通信线中不额外传送时钟,发送方和接收方事先约定波特率、数据位、校验位和停止位,再按照各自的时钟对数据进行发送和采样;“串行”表示一个字节不是同时从8根线送出,而是在一根数据线上逐位传输。

51单片机的串口可以同时发送和接收,即全双工通信

51单片机                                  USB转TTL或另一台设备

P3.1 / TXD  ───────────────────────────>  RXD
P3.0 / RXD  <───────────────────────────  TXD
GND         ────────────────────────────  GND

TXD必须接对方的RXDRXD必须接对方的TXD,两边还要共地。这里讨论的是TTL电平UART,不能把单片机引脚直接接到传统RS-232电平接口;RS-232使用不同的电压范围和极性,需要专用电平转换芯片。

视频展示的是串口内部功能框图,它用于说明各模块的作用和信号方向,不等同于开发板上每一只电阻、晶体管的实际原理图。可以把框图整理为下面这条完整链路:

flowchart LR
    CPU[CPU] -->|写SBUF| TB[发送缓冲器]
    TB --> TS[发送移位寄存器与控制器]
    CLK[定时器1溢出与分频电路] --> TS
    TS --> TXD[TXD / P3.1]
    RXD[RXD / P3.0] --> RS[接收移位寄存器与采样控制器]
    CLK --> RS
    RS --> RB[接收缓冲器]
    RB -->|读SBUF| CPU
    TS -->|一帧发送完毕| TI[TI置1]
    RS -->|一帧接收完毕| RI[RI置1]
    TI --> OR[串口中断请求]
    RI --> OR

虽然程序只看到一个特殊功能寄存器名 SBUF,芯片内部实际上有两个独立缓冲器:

写 SBUF ──> 发送缓冲器 ──> 发送移位寄存器 ──> TXD

读 SBUF <── 接收缓冲器 <── 接收移位寄存器 <── RXD

因此,读写 SBUF 的含义由操作方向决定:

  • SBUF = data;:把待发送字节写入发送缓冲器,并启动硬件发送。
  • data = SBUF;:从接收缓冲器读出已经收完的字节。
  • 串口可以在发送一个字节的同时接收另一个字节,两条硬件通路互不等同。
  • 接收端具有缓冲能力,但不是无限容量的队列;前一个字节长期不读,后续连续数据仍可能覆盖或丢失。

在本程序使用的串口方式1中,一帧采用常见的 8N1 格式,共10位:

线路空闲    起始位     8个数据位,低位先发                         停止位
   1          0       D0  D1  D2  D3  D4  D5  D6  D7                 1
──────────┐      ┌──────────────────────────────────────────────────────
          └──────┘

例如字符 a 的字节值是 0x61,二进制写成 0110 0001。UART从最低位 D0 开始发送,所以数据位在线路上的先后顺序是:

字节表示(D7→D0):0 1 1 0 0 0 0 1
实际发送(D0→D7):1 0 0 0 0 1 1 0

完整帧:起始位0 | 1 0 0 0 0 1 1 0 | 停止位1

接收控制器先检测起始位,再在约定的时间位置采样每个数据位;收满一帧后,把字节送入接收缓冲器并置位 RI。这也是双方波特率必须一致的原因:采样时刻逐渐偏离位中心,就会读到错误数据或停止位错误。

1.1.1 知识要点
部件或标志作用软件操作
TXD / P3.1串口发送引脚硬件按位输出发送帧
RXD / P3.0串口接收引脚硬件从该引脚采样接收帧
SBUF写操作装入待发送字节SBUF = data;
SBUF读操作取出已接收字节data = SBUF;
TI一帧发送完成标志硬件置1,软件清0
RI一帧接收完成标志硬件置1,软件清0
定时器1在方式1中提供波特率基准设置 TMOD、TH1、TL1、TR1
SCON选择串口模式并控制接收本程序写入 0x50
PCON.SMOD控制波特率是否加倍本程序清0,不加倍

SCON = 0x50 即二进制 0101 0000

名称本程序值含义
7SM00SM1共同选择串口方式1
6SM118位UART、可变波特率
5SM20不启用多机通信筛选
4REN1允许接收
3TB80方式1中不作为第9数据位使用
2RB80接收后可保存停止位状态
1TI0初始化时无发送完成标志
0RI0初始化时无接收完成标志

TIRI共用51单片机的串口中断入口 interrupt 4。硬件置位标志并不代表CPU一定会进入中断,还必须同时满足:

(RI = 1 或 TI = 1) 且 ES = 1 且 EA = 1

进入串口中断后,硬件不会自动替程序清除 RI/TI,软件必须判断中断原因并清零相应标志。本程序用中断处理接收、用轮询等待发送完成;如果以后采用全中断式发送,串口中断函数也要增加 TI 分支,并用发送缓冲区管理连续数据。

配套程序的执行过程是:

  1. 主循环依次发送字符 a~z
  2. 接着发送 \n,再发送字符串 i am wfeng!\r\n
  3. 延时约1秒后重复发送。
  4. 收到外部字节后触发串口中断,switch根据字节值控制LED、蜂鸣器或继电器。
1.1.2 波特率计算初值

波特率表示每秒传输的符号数。在本讲的二进制UART中,一个符号只承载一个比特,所以9600 Baud可以近似理解为9600 bit/s。它不等于每秒9600字节:8N1每个有效字节还包含起始位和停止位,连续传输的理论上限约为:

$ \frac{9600\ \text{bit/s}}{10\ \text{bit/byte}}=960\ \text{byte/s} $

本程序以定时器1方式2产生波特率:

PCON &= 0x7F; // SMOD=0,波特率不加倍
TMOD &= 0x0F; // 清除定时器1的模式位
TMOD |= 0x20; // 定时器1方式2,8位自动重装
TH1 = 0xFD;   // 自动重装值
TL1 = 0xFD;   // 第一次计数的起始值
TR1 = 1;      // 启动定时器1

定时器1方式2中,TL1负责递增计数,溢出后硬件自动把 TH1复制回 TL1。因此只需设置一次 TH1,以后每次溢出都能保持相同周期。

1.1.2.1 常用波特率与定时器

本讲的计算建立在以下前提上:

  • 晶振频率 fosc = 11.0592 MHz
  • 单片机按传统8051的12T定时器时钟计算;
  • 串口方式1;
  • 定时器1采用方式2自动重装;
  • SMOD = 0,波特率不加倍。

在这些条件下,TH1 = 0xFD对应的是 9600 bit/s0xFD不是固定代表某个波特率;晶振频率、单片机的1T/6T/12T模式、串口模式或 SMOD变化后,结果都会变化。

11.0592 MHz常用于经典51串口,是因为它能够被12、32以及多种整数重装周期整除,容易得到误差为0的常用波特率:

目标波特率SMOD每次溢出的计数次数 NTH1 = 256-N
12000240xE8
24000120xF4
4800060xFA
9600030xFD
19200130xFD

上表仅适用于列出的条件。STC89系列还可能支持6T模式或增强型波特率发生器,实际项目应先确认芯片时钟模式和使用的波特率源,不能机械照抄 0xFD

1.1.2.2 复盘公式

第一步:求定时器1的计数频率

按12T模式计算,一个机器周期包含12个晶振周期,定时器每个机器周期加1:

$ f_{timer}=\frac{f_{osc}}{12} =\frac{11.0592\ \text{MHz}}{12} =921.6\ \text{kHz} $

所以定时器真正加1所需的时间为:

$ t_{count}=\frac{1}{921600}\approx1.085\ \mu s $

第二步:求 0xFD到溢出的计数次数

0xFD = 253,8位计数器的模数是256:

$ N=256-TH1=256-253=3 $

计数过程如下:

装入 0xFD
    │ 第1次计数
    ▼
   0xFE
    │ 第2次计数
    ▼
   0xFF
    │ 第3次计数
    ▼
   0x00,产生溢出 ──> 硬件立即把TH1的0xFD重装到TL1

因此定时器1的溢出频率为:

$ f_{overflow}=\frac{f_{osc}}{12(256-TH1)} =\frac{11059200}{12\times3} =307200\ \text{Hz} $

第三步:经过串口波特率分频

串口方式1使用定时器1时:

$ \text{Baud}=\frac{2^{SMOD}}{32}\times f_{overflow} $

SMOD=0时:

$ \text{Baud}=\frac{1}{32}\times307200=9600\ \text{bit/s} $

合并后得到通用公式:

$ \boxed{\text{Baud}=\frac{2^{SMOD}}{32}\times \frac{f_{osc}}{12(256-TH1)}} $

反过来求重装值:

$ \boxed{TH1=256-\frac{2^{SMOD}f_{osc}} {384\times\text{Baud}}} $

代入本讲参数:

$ TH1=256-\frac{11059200}{384\times9600} =256-3=253=0xFD $

视频框图把 /32画成 /2/16两级。SMOD=0时二者相乘仍是 /32,所以也可以交换运算顺序:

$ \frac{11.0592\ \text{MHz}}{12\times2\times16} =28.8\ \text{kHz} $

$ \frac{1}{28.8\ \text{kHz}}\approx34.722\ \mu s $

$ 34.722\ \mu s\times3\approx104.167\ \mu s =\frac{1}{9600} $

这种算法在数值上完全等价,但 34.722 μs已经包含了UART的 /32分频,不能称为“定时器每加1所需的时间”。定时器每加1约需 1.085 μs;计3次溢出约需 3.255 μs,再经过 /32折算后,才得到约 104.167 μs的UART位周期。

1.1.3 ASCII与HEX的本质

串口线上只传输高低电平表示的字节,不会额外携带“这是ASCII”或“这是HEX”的标签。ASCII和HEX通常只是串口助手对同一批字节采用的输入、显示方式:

  • ASCII发送:把键盘输入的字符编码成字节。例如输入 3,发送 0x33
  • HEX发送:把输入文字当成十六进制字节。例如输入 03,发送 0x03
  • ASCII接收显示:把收到的字节按字符解释,例如 0x61显示为 a
  • HEX接收显示:把收到的字节按十六进制数显示,例如 0x61显示为 61
实际字节:         0x61       0x62       0x7A       0x0A
ASCII显示:          a          b          z         换行
HEX显示:           61         62         7A          0A

字节始终相同,变化的是软件如何解释和显示它。

标准ASCII是7位编码,范围为 0x00~0x7F。其中既包含可见字符,也包含换行、回车等控制字符。中文通常采用UTF-8、GBK等多字节编码,不属于基础ASCII范围。

1.1.3.1 ASCII表
字符或控制码十进制十六进制8位二进制说明
'0'480x3000110000字符0,不是数值0
'3'510x3300110011字符3,不是数值3
'A'650x4101000001大写字母A
'a'970x6101100001小写字母a
'b'980x6201100010小写字母b
'i'1050x6901101001小写字母i
'z'1220x7A01111010小写字母z
LF / '\n'100x0A00001010换行
CR / '\r'130x0D00001101回车
  • 1.1.3.1.1 例题: ASCII码实验

主循环首先执行:

for (count = 0; count < 26; count++)
{
    UART_Send_Byte('a' + count);
}

'a'的值为 0x61,循环变量依次加 0~25,因此发送出的字节依次对应字符 a~z。随后程序又发送:

UART_Send_Byte('\n');
UART_Send_Str("i am wfeng!\r\n");

接收区选择ASCII显示时可看到:

abcdefghijklmnopqrstuvwxyz
i am wfeng!

选择HEX显示时,同一段数据的主要字节为:

61 62 63 ... 78 79 7A 0A
69 20 61 6D 20 77 66 65 6E 67 21 0D 0A

其中:

  • 61对应 a62对应 b7A对应 z
  • 0A是换行符 LF,不是字母 i
  • 69才是小写字母 i
  • 20是空格;
  • 字符串末尾的 0D 0A是常见的 CR LF回车换行组合。

HEX显示中的 61表示一个字节 0x61。它不是十进制61,也不是两个ASCII字符 '6''1';只有把HEX输入框中的 61按十六进制解析后,才会生成一个值为 0x61的字节。

1.1.3.2 字符与数字的区别

C语言中的数字常量、字符常量和字符串并不是一回事:

C语言写法含义典型字节值
3十进制整数30x03
0x03十六进制整数30x03
'3'ASCII字符30x33
"3"字符串,由字符3和结束符组成0x33 0x00

所以,下面两个条件不会匹配同一个接收字节:

case 0x03: // 要求收到数值字节0x03,串口助手应使用HEX发送03
case '3':  // 要求收到ASCII字节0x33,串口助手应使用ASCII发送3
  • 1.1.3.2.1 例题: 字符发送实验

视频演示结束时,第三、第四个分支使用字符常量:

case '3':
    LED3 = 0;
    break;
case '4':
    LED4 = 0;
    break;

相应现象如下:

串口助手发送设置输入内容实际发出的字节匹配结果
ASCII30x33匹配 case '3',LED3点亮
HEX330x33同样匹配 case '3'
HEX030x03不匹配 case '3'
ASCII40x34匹配 case '4',LED4点亮
HEX050x05匹配 case 0x05,四个LED熄灭
ASCII50x35不匹配 case 0x05

当前示例故意保留了两类命令:'3'/'4'按ASCII字符匹配,其余 0x01、0x02、0x05~0x09按数值字节匹配。这有助于观察两种发送方式的差异,但正式设计通信协议时,应统一命令格式。例如决定使用ASCII命令,就全部使用 '1'~'9';决定使用二进制命令,就全部使用 0x01~0x09,并在协议文档中明确规定。

排查“发送了3却没有执行”的问题时,不要只看界面里显示的字符,应按以下顺序检查:

  1. 串口助手当前是ASCII发送还是HEX发送;
  2. 实际发送字节究竟是 0x33还是 0x03
  3. 程序比较的是 '3'还是 0x03
  4. 接收显示模式只是改变显示方式,不能改变已经收到的字节。

1.2 思维导图知识总结

本讲围绕三个核心问题完成串口复盘:

  1. 数据如何发送和接收:CPU通过写、读 SBUF使用物理分开的收发缓冲器,硬件按位完成UART帧的发送与采样,TI/RI报告完成状态。
  2. 9600波特率如何得到 0xFD:在11.0592 MHz、12T、定时器1方式2、SMOD=0的条件下,每次溢出计3个机器周期,再经UART /32分频,得到9600 bit/s。
  3. ASCII与HEX的本质是什么:线路上传的是同一个个字节,ASCII和HEX只是输入解析与接收显示方式;字符 '3'0x33,数值3是 0x03

把这三个问题串起来,就能形成完整认识:波特率决定“何时采样”,UART帧决定“怎样逐位传输”,SBUFRI/TI负责“怎样与程序交换数据”,而ASCII或HEX决定“人怎样输入和观察这些字节”。