串口知识复盘
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必须接对方的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产生波特率:
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/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码实验
主循环首先执行:
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对应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 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;
相应现象如下:
| 串口助手发送设置 | 输入内容 | 实际发出的字节 | 匹配结果 |
|---|---|---|---|
| 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决定“人怎样输入和观察这些字节”。