串口知识复盘

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

053.6.9 串口知识复盘

/*
 * 整体目的:复盘UART方式1的收发流程、9600波特配置,以及ASCII字符与原始数值字节的区别。
 * 整体设计:主循环查询TI并发送a~z和字符串;串口中断读取接收SBUF,再按命令控制外设。
 * 对比重点:switch故意让1、2等命令使用0x01、0x02,却让3、4使用'3'、'4';
 *           前者要求串口助手以HEX发送01/02,后者要求以文本发送字符3/4(即0x33/0x34)。
 * 时钟依据:11.0592MHz晶振、定时器1为12T、SMOD=0时,TH1/TL1=0xFD对应9600波特。
 * 硬件依据:LED、蜂鸣器和课程所用继电器模块均采用低电平有效控制。
 */

#include "reg52.h"   /* 引入8052特殊功能寄存器声明,使程序能使用P1、SCON、SBUF、RI等名称。 */
#include <intrins.h>  /* 引入Keil C51内部函数声明,软件延时需要调用_nop_()。 */

typedef unsigned char uchar;  /* 为8位无符号字符型建立简写,用于UART字节和接收变量。 */
typedef unsigned int uint;    /* 为C51的16位无符号整型建立简写,用于循环和毫秒参数。 */

uint count = 0;  /* 定义a~z发送循环使用的全局计数器,取值范围为0~25。 */
uchar recv;      /* 保存UART接收的一个原始字节,供switch按数值或ASCII编码匹配。 */

sbit LED1 = P1^0;  /* 将LED1绑定到P1.0;输出0时点亮第一只低电平有效LED。 */
sbit LED2 = P1^1;  /* 将LED2绑定到P1.1;输出0时点亮第二只LED。 */
sbit LED3 = P1^2;  /* 将LED3绑定到P1.2;输出0时点亮第三只LED。 */
sbit LED4 = P1^3;  /* 将LED4绑定到P1.3;输出0时点亮第四只LED。 */
sbit BEEP = P1^6;  /* 将蜂鸣器控制端绑定到P1.6;输出0使PNP驱动管导通。 */
sbit JDQ1 = P2^0;  /* 将低电平触发继电器模块的IN绑定到P2.0。 */

/*
 * 函数目的:执行由xms指定的毫秒级软件忙等待,控制测试文本的重复频率。
 * 时间前提:计数值按11.0592MHz和当前Keil C51指令耗时生成;时钟或优化变化会影响精度。
 */
void Delay_xms(uint xms)                  /* 定义软件延时函数,xms表示计划等待的毫秒数。 */
{                                        /* 延时函数体开始,忙等期间仍可响应已允许的中断。 */
    uchar data i, j;                      /* 定义两个8位内层计数器,并放入片内直接寻址data RAM。 */

    while (xms)                           /* 每完成一轮约1ms等待,就把剩余毫秒数减1。 */
    {                                     /* 一毫秒延时循环开始。 */
        _nop_();                           /* 执行空操作指令,用于补齐延时生成器计算的时间。 */
        i = 2;                             /* 初始化外层8位计数器。 */
        j = 199;                           /* 初始化内层计数器,其递减过程构成主要延时。 */
        do                                /* 使用后判断循环,保证循环体至少执行一次。 */
        {                                 /* do循环体开始,此处只消耗CPU时间。 */
            while (--j);                  /* 先递减j,非0便继续空转;分号表示循环体为空。 */
        } while (--i);                    /* 一组j循环结束后递减i,非0时继续下一组。 */
        xms--;                             /* 一轮约1ms完成,将剩余等待次数减1。 */
    }                                     /* xms归零后退出延时。 */
}                                         /* 延时函数结束并返回调用者。 */

/* 函数目的:使用TI完成一个字节的查询发送,限制CPU写SBUF的速度。 */
void UART_Send_Byte(uchar send_byte)       /* send_byte保存本次需要发送的8位数据。 */
{                                         /* 单字节发送流程开始。 */
    SBUF = send_byte;                      /* 写发送SBUF,启动UART硬件输出一帧。 */
    while (TI == 0);                       /* TI为0时原地等待;分号表示循环体为空。 */
    TI = 0;                                /* 硬件置位TI后由软件清0,为下一帧做准备。 */
}                                         /* 当前字节发送到规定时刻,函数返回。 */

/* 函数目的:遍历以'\0'结尾的C字符串,并复用单字节函数逐字符发送。 */
void UART_Send_Str(uchar *send_str)        /* send_str接收待发送字符串的首地址。 */
{                                         /* 字符串发送函数体开始。 */
    while (*send_str != '\0')              /* 当前字符不是NUL结束标志时继续循环。 */
    {                                     /* 当前正文字符的发送分支开始。 */
        UART_Send_Byte(*send_str++);       /* 发送当前字符,再把指针移动到下一个字符。 */
    }                                     /* 遇到'\0'时退出,不发送结束标志本身。 */
}                                         /* 字符串发送完成并返回。 */

/* 函数目的:配置UART和波特率发生器,循环发送测试数据并等待串口接收中断。 */
void main()                                /* 定义Keil C51程序入口。 */
{                                         /* 主函数体开始。 */
    SCON = 0x50;                           /* 选择UART方式1、允许接收,并将TI和RI初始化为0。 */
    PCON &= 0x7F;                          /* 清SMOD选择波特率不加倍,同时保留PCON其他位。 */
    TMOD &= 0x0F;                          /* 清除定时器1高4位配置,保留定时器0低4位。 */
    TMOD |= 0x20;                          /* 设置定时器1为内部定时、模式2自动重装。 */
    TH1 = 0xFD;                            /* 保存自动重装值253,用于产生9600波特时基。 */
    TL1 = 0xFD;                            /* 设置第一次计数初值,使首轮周期与后续一致。 */
    TR1 = 1;                               /* 启动定时器1,开始给UART提供波特率时钟。 */
    ES = 1;                                /* 打开串口中断分开关,使RI或TI能提出中断请求。 */
    EA = 1;                                /* 打开CPU总中断门控,使串口请求可以被响应。 */

    while (1)                              /* 无限循环,持续发送并允许中断随时处理接收数据。 */
    {                                     /* 一轮主动发送开始。 */
        for (count = 0; count < 26; count++)  /* count取0~25,生成26个小写字母。 */
        {                                  /* 当前字母发送循环体开始。 */
            UART_Send_Byte('a' + count);   /* 利用ASCII连续性生成'a'~'z'并可靠发送。 */
        }                                  /* 26个字母发送完毕。 */

        UART_Send_Byte('\n');               /* 发送ASCII换行LF(0x0A)。 */
        UART_Send_Str("i am wfeng!\r\n");   /* 发送测试字符串和CR+LF行结束序列。 */
        Delay_xms(1000);                   /* 忙等待约1秒,降低整段数据的重复频率。 */
    }                                      /* 本轮结束后回到while顶部继续发送。 */
}                                          /* 主函数语法边界;无限循环使其不会正常返回。 */

/*
 * 函数目的:读取一个串口命令字节,并演示数值常量与字符常量对switch匹配结果的影响。
 * 输入提醒:01、02、05~09按HEX发送;字符3、4按文本发送才能分别匹配'3'和'4'。
 */
void UART_Routine(void) interrupt 4        /* 声明串口中断服务函数,对应向量地址0x0023。 */
{                                          /* 中断处理开始,TI和RI共用这个入口。 */
    if (RI == 1)                           /* RI为1说明UART已接收完一个有效数据帧。 */
    {                                      /* 接收命令处理分支开始。 */
        RI = 0;                            /* 按配套程序顺序由软件清RI,为下一帧做准备。 */
        recv = SBUF;                       /* 读取接收SBUF,把原始8位字节保存到recv。 */
        switch (recv)                      /* 将recv依次与各case编译后的8位常量比较。 */
        {                                  /* switch分支表开始。 */
            case 0x01:                     /* 匹配数值字节0x01,不匹配文本字符'1'(0x31)。 */
                LED1 = 0;                  /* 拉低P1.0,点亮LED1。 */
                break;                     /* 完成本分支后退出switch。 */
            case 0x02:                     /* 匹配数值字节0x02,不匹配文本字符'2'(0x32)。 */
                LED2 = 0;                  /* 拉低P1.1,点亮LED2。 */
                break;                     /* 完成本分支后退出switch。 */
            case '3':                      /* 匹配ASCII字符'3'的编码0x33,不匹配数值0x03。 */
                LED3 = 0;                  /* 拉低P1.2,点亮LED3。 */
                break;                     /* 完成本分支后退出switch。 */
            case '4':                      /* 匹配ASCII字符'4'的编码0x34,不匹配数值0x04。 */
                LED4 = 0;                  /* 拉低P1.3,点亮LED4。 */
                break;                     /* 完成本分支后退出switch。 */
            case 0x05:                     /* 匹配HEX发送的数值字节0x05。 */
                P1 |= 0x0F;                /* 将P1低4位置1,使四个低电平有效LED全部熄灭。 */
                break;                     /* 完成全灭命令后退出switch。 */
            case 0x06:                     /* 匹配HEX发送的数值字节0x06。 */
                BEEP = 0;                  /* 拉低P1.6使PNP驱动管导通,蜂鸣器发声。 */
                break;                     /* 完成蜂鸣器开启后退出switch。 */
            case 0x07:                     /* 匹配HEX发送的数值字节0x07。 */
                BEEP = 1;                  /* 拉高P1.6使PNP驱动管截止,蜂鸣器停止。 */
                break;                     /* 完成蜂鸣器关闭后退出switch。 */
            case 0x08:                     /* 匹配HEX发送的数值字节0x08。 */
                JDQ1 = 0;                  /* 向低电平触发模块输出有效电平,使继电器吸合。 */
                break;                     /* 完成继电器吸合后退出switch。 */
            case 0x09:                     /* 匹配HEX发送的数值字节0x09。 */
                JDQ1 = 1;                  /* 撤销低电平触发,使课程所用继电器模块释放。 */
                break;                     /* 完成继电器释放后退出switch。 */
            default:                       /* 收到不能匹配上述任一常量的字节时进入这里。 */
                break;                     /* 不改变外设状态,直接退出switch。 */
        }                                  /* 命令分支选择结束。 */
    }                                      /* 接收事件处理结束。 */
}                                          /* 串口中断服务结束并返回先前程序。 */

一、串口知识复盘

1.1 数据发送和接收

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

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

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

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

TXD必须接对方的RXD,RXD必须接对方的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:

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

TI和RI共用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产生波特率:

/*
 * 片段目的:配置定时器1模式2,为UART方式1提供9600波特所需的稳定溢出时基。
 * 使用前提:系统晶振为11.0592MHz、定时器1为12T模式。
 */
PCON &= 0x7F;  /* 清SMOD,选择波特率不加倍并保留PCON其他位。 */
TMOD &= 0x0F;  /* 清除TMOD高4位,即先移除定时器1原有配置。 */
TMOD |= 0x20;  /* 把高4位设为0010B:内部定时、模式2自动重装。 */
TH1 = 0xFD;    /* 保存以后每次TL1溢出后自动装入的重装值253。 */
TL1 = 0xFD;    /* 让第一次计数也从253开始,保证首轮周期一致。 */
TR1 = 1;       /* 启动定时器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/s。0xFD不是固定代表某个波特率;晶振频率、单片机的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码实验

主循环首先执行:

/* 片段目的:利用ASCII小写字母连续编码,依次发送a~z。 */
for (count = 0; count < 26; count++)  /* count取0~25,共形成26个字符偏移量。 */
{                                    /* 当前字母发送循环体开始。 */
    UART_Send_Byte('a' + count);     /* 在'a'的编码0x61上加偏移,函数内部等待并清TI。 */
}                                    /* 当前字节完成后count递增,继续下一个字母。 */

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

/* 片段目的:对比单字符转义常量与以双引号表示的完整C字符串发送。 */
UART_Send_Byte('\n');              /* 发送一个LF控制字符,其字节值为0x0A。 */
UART_Send_Str("i am wfeng!\r\n"); /* 发送正文及CR、LF;字符串末尾隐藏的'\0'只作边界。 */

接收区选择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对应 a,62对应 b,7A对应 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标签代表不同字节,不能在同一switch中把它们当作等价写法。 */
case 0x03: /* 匹配原始数值字节0x03,串口助手应在HEX模式发送03。 */
case '3':  /* 匹配ASCII字节0x33,串口助手应在文本模式发送字符3。 */
  • 1.1.3.2.1 例题: 字符发送实验

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

/* 片段目的:让文本方式发送的字符3和4分别控制第三、第四只LED。 */
case '3':      /* recv等于ASCII编码0x33时匹配,即手机或串口助手发送文本字符3。 */
    LED3 = 0;  /* 拉低P1.2,点亮第三只低电平有效LED。 */
    break;     /* 退出switch,防止继续执行字符4分支。 */
case '4':      /* recv等于ASCII编码0x34时匹配。 */
    LED4 = 0;  /* 拉低P1.3,点亮第四只LED。 */
    break;     /* 完成字符4命令后退出switch。 */

相应现象如下:

串口助手发送设置输入内容实际发出的字节匹配结果
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帧决定“怎样逐位传输”,SBUF和 RI/TI负责“怎样与程序交换数据”,而ASCII或HEX决定“人怎样输入和观察这些字节”。