串口知识复盘
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:
| 位 | 名称 | 本程序值 | 含义 |
|---|---|---|---|
| 7 | SM0 | 0 | 与 SM1共同选择串口方式1 |
| 6 | SM1 | 1 | 8位UART、可变波特率 |
| 5 | SM2 | 0 | 不启用多机通信筛选 |
| 4 | REN | 1 | 允许接收 |
| 3 | TB8 | 0 | 方式1中不作为第9数据位使用 |
| 2 | RB8 | 0 | 接收后可保存停止位状态 |
| 1 | TI | 0 | 初始化时无发送完成标志 |
| 0 | RI | 0 | 初始化时无接收完成标志 |
TI和RI共用51单片机的串口中断入口 interrupt 4。硬件置位标志并不代表CPU一定会进入中断,还必须同时满足:
(RI = 1 或 TI = 1) 且 ES = 1 且 EA = 1
进入串口中断后,硬件不会自动替程序清除 RI/TI,软件必须判断中断原因并清零相应标志。本程序用中断处理接收、用轮询等待发送完成;如果以后采用全中断式发送,串口中断函数也要增加 TI 分支,并用发送缓冲区管理连续数据。
配套程序的执行过程是:
- 主循环依次发送字符
a~z。 - 接着发送
\n,再发送字符串i am wfeng!\r\n。 - 延时约1秒后重复发送。
- 收到外部字节后触发串口中断,
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 | 每次溢出的计数次数 N | TH1 = 256-N |
|---|---|---|---|
| 1200 | 0 | 24 | 0xE8 |
| 2400 | 0 | 12 | 0xF4 |
| 4800 | 0 | 6 | 0xFA |
| 9600 | 0 | 3 | 0xFD |
| 19200 | 1 | 3 | 0xFD |
上表仅适用于列出的条件。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' | 48 | 0x30 | 00110000 | 字符0,不是数值0 |
'3' | 51 | 0x33 | 00110011 | 字符3,不是数值3 |
'A' | 65 | 0x41 | 01000001 | 大写字母A |
'a' | 97 | 0x61 | 01100001 | 小写字母a |
'b' | 98 | 0x62 | 01100010 | 小写字母b |
'i' | 105 | 0x69 | 01101001 | 小写字母i |
'z' | 122 | 0x7A | 01111010 | 小写字母z |
LF / '\n' | 10 | 0x0A | 00001010 | 换行 |
CR / '\r' | 13 | 0x0D | 00001101 | 回车 |
- 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 | 十进制整数3 | 0x03 |
0x03 | 十六进制整数3 | 0x03 |
'3' | ASCII字符3 | 0x33 |
"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。 */
相应现象如下:
| 串口助手发送设置 | 输入内容 | 实际发出的字节 | 匹配结果 |
|---|---|---|---|
| ASCII | 3 | 0x33 | 匹配 case '3',LED3点亮 |
| HEX | 33 | 0x33 | 同样匹配 case '3' |
| HEX | 03 | 0x03 | 不匹配 case '3' |
| ASCII | 4 | 0x34 | 匹配 case '4',LED4点亮 |
| HEX | 05 | 0x05 | 匹配 case 0x05,四个LED熄灭 |
| ASCII | 5 | 0x35 | 不匹配 case 0x05 |
当前示例故意保留了两类命令:'3'/'4'按ASCII字符匹配,其余 0x01、0x02、0x05~0x09按数值字节匹配。这有助于观察两种发送方式的差异,但正式设计通信协议时,应统一命令格式。例如决定使用ASCII命令,就全部使用 '1'~'9';决定使用二进制命令,就全部使用 0x01~0x09,并在协议文档中明确规定。
排查“发送了3却没有执行”的问题时,不要只看界面里显示的字符,应按以下顺序检查:
- 串口助手当前是ASCII发送还是HEX发送;
- 实际发送字节究竟是
0x33还是0x03; - 程序比较的是
'3'还是0x03; - 接收显示模式只是改变显示方式,不能改变已经收到的字节。
1.2 思维导图知识总结
本讲围绕三个核心问题完成串口复盘:
- 数据如何发送和接收:CPU通过写、读
SBUF使用物理分开的收发缓冲器,硬件按位完成UART帧的发送与采样,TI/RI报告完成状态。 - 9600波特率如何得到
0xFD:在11.0592 MHz、12T、定时器1方式2、SMOD=0的条件下,每次溢出计3个机器周期,再经UART/32分频,得到9600 bit/s。 - ASCII与HEX的本质是什么:线路上传的是同一个个字节,ASCII和HEX只是输入解析与接收显示方式;字符
'3'是0x33,数值3是0x03。
把这三个问题串起来,就能形成完整认识:波特率决定“何时采样”,UART帧决定“怎样逐位传输”,SBUF和 RI/TI负责“怎样与程序交换数据”,而ASCII或HEX决定“人怎样输入和观察这些字节”。