第十六章 UART通信
16.1 UART 概述
16.1.1 前置基础
想要理解和使用 UART,需要先了解一些通讯领域的术语,如下 。
(1)串行通讯和并行通讯
串行通讯和并行通讯是数据传输的两种主要方式,两者的区别如下。



(2)单工通讯和双工通讯
单工和双工是通讯领域中用于描述数据传输方向的术语,它们定义了两个设备之间的
数据通信方式。
单工通信 ,只允许数据在一个方向上传输,即数据只能从发送端传输到接收端,接收
端无法向发送端传输数据。简单来说,就是一种"单向"通信模式,可以类比电视广播。
双工通信,允许数据在两个方向上传输,其下又分为两种类型:半双工和全双工 。
半双工通信 允许数据在两个方向上传输,但不能同时进行。在任何时刻,数据只能在
一个方向上传输。这意味着通信的两端可以轮流发送和接收数据,但不能同时进行。可以
类比对讲机。
全双工通信 允许数据同时在两个方向上传输。这种通信方式最为高效,因为它允许通
信双方同时发送和接收数据,可以类比电话。
(3)同步通讯和异步通讯
同步通讯和异步通讯的区别在于发送方和接收方如何对数据进行协调统一,详细内容
如下图所示。

16.1.2 UART 定义
UART(Universal Asynchronous Receiver/Transmitter)是一种异步、全双工的串行通信
接口,常用于微控制器与计算机、其他微控制器或外部设备之间的数据交换,下图是
UART 通信所需的信号线,其中 Tx 用于发送数据,Rx 用于接受数据。

16.1.3 UART 通讯协议
16.1.3.1 数据格式
在 UART 通信中,数据是逐帧(Frame)发送的,每个数据帧通常包括起始位、数据
位、校验位(可选)和停止位,具体结构如下图所示。

(1)空闲状态
协议规定,在空闲状态下,也就是没有数据传输时,应为高电平。
(2)起始位
起始位表示一个数据帧的开始,起始位为低电平(区别于空闲状态)。
(3)数据位
传输的主体内容,位于起始位之后,长度可以是 5 到 9 位,一般都是 8 位。低电平表
示 0,高电平表示 1。
(4)校验位(可选)
用于校验当前帧的正确性,校验算法可以是奇校验或偶校验,该算法的思想如下。
奇校验(odd parity):如果数据位中 1 的数目是偶数,则校验位为 1,如果 1 的数目
是奇数,校验位为 0,目的是保证数据位+校验位中的 1 的总个数是奇数。
偶校验(even parity):如果数据为中 1 的数目是偶数,则校验位为 0,如果 1 的数目
为奇数,校验位为 1,目的是保证数据位+校验位中的 1 的个数是偶数。
(5)停止位
停止位表示数据帧的结束,通常为 1 位或 2 位,停止位为高电平。
16.1.3.2 发送方和接收方的约定
为保证 UART 通信能够正常工作,发送方和接收方必须提前做好如下约定。
(1)波特率
波特率(Baud Rate)用于表示数据的传输速率,发送方和接收方必须约定好传输速率,
才能保证数据被正确的发送和接收。
需要注意波特率(Baud Rate)和比特率(Bit Rate)的区别,比特率表示每秒传输的位
(bit)数,而波特率表示每秒传输的符号(symbol)数。但是串口通信中,只有 0 和 1 这
两个符号,因此 1 个符号用 1 位就能表示,所以此处的波特率和比特率是等价的。
(2)数据位
发送方和接收方需要明确数据位的位数。
(3)校验位
发送方和接收方需要明确是否有校验位,如果有,需要明确校验算法是哪个。
(4)停止位
发送方和接收方需要明确停止位的位数。
16.2 单片机 UART 使用说明
STC89C52 系列单片机内部集成了一个功能强大的全双工串行通信口,以下是相关引
脚。

该通信口具有四种工作模式,如下。

其中模式 0 为同步通讯,该模式下,TxD 引脚会作为时钟信号线,RxD 作为数据信号
线,该模式下只能实现半双工通讯。
模式 1、2、3 为经典的 UART 通讯,三者的区别在与是否有校验位,以及波特率
是否可变。
模式 1 的一个数据帧包含 1 个起始位,8 个数据位和 1 个停止位。
模式 2 和模式 3 的一个数据帧包含 1 个数据位,8 个数据位,1 个校验位和 1 个停止位。
另外模式 1 和模式 3 的波特率可由定时器 1 进行配置,因而可以自由设置,而方式 2
的波特率直接由系统时钟决定,因而不可自由配置。
下面以方式 1 为例,介绍该串行通信口的用法。
16.2.1 设置工作模式
工作模式需要通过 SCON(Serial Control,串行口控制)寄存器中的 SM0 和 SM1 两
个控制位进行设置,如下图所示。

16.2.2 设置波特率
方式 1 的波特率会受到两个因素的影响,分别是 SMOD 控制位和定时器 1 的溢出频率,
具体作用如下图所示。

UART 常用的波特率有 4800、9600、19200、38400、57600、115200 等,此处以 9600
为例。
(1)设置 SMOD 控制位
SMOD 控制位位于 PCON(Power Control,电源控制)寄存器,如下图所示。

(2)设置定时器 1
由于定时器 1 当前的作用仅仅是为串口提供时钟信号,因此可不开启定时器中断,只
需完成以下配置即可。
(1)选择定时器工作模式
定时器 1 的工作模式需要通过 TMOD 寄存器中的 C/T 控制位,以及 M1 和 M0 两个控
制位进行设置,如下图所示。

C/T 用于设置计数/定时的工作方式,此处应选择定时模式,因此需将 C/T 设置为 0。
M1 和 M0,用于配置具体的工作模式,相关配置如下。

由于没有启用定时器中断,因此无法在中断中重新设置定时器脉冲计数器的初始值,
所以此处选择模式 2(8 位自动重装载)最为合理。
(2)设置脉冲计数器初始值
模式 2 的最大计数为 28=256,因此定时器 1 的溢出频率等于 SYSclk / 12 /(256 - TH1)
或者是 SYSclk / 6 /(256 - TH1),假如当前 MCU 工作在 12T 模式,所以溢出频率就等于
SYSclk / 12 /(256 - TH1)。
(3)启动定时器
定时器 1 的启动也无需外部引脚控制,因此应将 GATE 控制位设置为 0,并将 TR1 控
制位设置为 1。
16.2.3 发送数据
STC89C52 的串口的发送模块示意图如下。
数据发送的大致流程如下:
开发者将待发送的 8 位数据写入发送缓冲器(SBUF) ,此时发送控制器(Tx Control)
就会开始工作,它会自动为数据添加起始位和结束位,从而构成一个完整的 UART 数据帧,
然后逐位通过 TxD 引脚输出出去。当完成一个数据帧的输出之后,发送控制器会将发送中
断控制位 TI 置 1,向 CPU 请求中断,CPU 检测到中断请求后就执行相应的中断服务程序。
总结:发送数据只需要将待发送的数据写入SBUF 即可。
16.2.4 接收数据
STC89C52 的串口的接收模块示意图如下。
默认情况下,串口并不会接收数据。如需接收数据,需要先将 REN(Receive Enable)
控制位置为 1,REN 控制位位于 SCON 寄存器,如下图所示。

当REN 置为 1 后,上图中的 1 到 0 跳变检测器(1-To-0 Transition Detector) 就会开
始工作,具体来讲就是不断检测 RxD 引脚的起始位。当检测到 1 到 0 的跳变后,就会启动
接收控制器(Rx Control) ,接收控制器会将接收到数据逐位移入到输入移位寄存器
(Input Shift REG) ,直到接收到停止位,就算完成了一帧数据的接收。
正常情况下,接下来,接收控制器会将输入移位寄存器(Input Shift REG) 中的数据
加载到读取缓冲器(SBUF) 中,并将读取中断控制位 RI 置 1,向 CPU 请求中断,CPU 检
测到中断请求后就执行相应的中断服务程序,开发者就能在中断服务程序中读取 SBUF 获
取当前帧的数据了。
但是上述操作(加载数据到 SBUF 和 RI 置位)的执行是有条件的,满足条件才会执行,
不满足,那么当前数据帧就会被丢弃,具体条件如下:
(1)结束位正常
开发者可以配置是否检测停止位的有效性(高电平有效)。是否检测是由 SCON 寄存
器中的 SM2 控制位来决定的。SM2=1 时,接收控制器就会检测控制停止位,当 SM2=0 时,
则不会检测停止位,建议将 SM2 设置为 0。

2)读取中断标志位为复位状态
读取中断标志位 RI 必须等于 0,也就说要保证上一帧数据已经被读取或处理完毕,才
能处理当前帧。
总结:接收数据需要先使能接收,也就是将 REN 控制位置 1,然后开启串口中断,并
在中断服务程序中读取 SBUF。
16.2.5 串口中断注意事项
根据前文的描述,当使用串口发送完一帧数据后,会将发送中断标志位 TI 置 1;当串
口接收到一帧数据后,会将接收中断标志位 RI 置 1。需要注意的是两个控制位请求的是同
一个中断------串口中断(中断号为 4)。
也就是说发送完一帧数据和接收完一帧数据之后,执行的都是串口中断的中断服务程
序,因此,再编写该中段服务程序时,需要注意判断当前中断到底是由发送操作触发的,
还是由接收操作触发的,代码示例如下。
cs
/*
* 串口中断的中断号为 4
*/
void Dri_UART_Handler() interrupt 4
{
/* 检查接收中断标志位 RI,如果为 1,表示有一帧数据接收完成 */
if (RI == 1) {
}
/* 检查发送中断标志位 TI,如果为 1,表示有一帧数据发送完成 */
if (TI == 1) {
}
}
另外 RI 和 TI 标志位,只能由软件复位, 也就是需要在中断服务程序中将其设置为 0,
如下
cs
/*
* 串口中断的中断号为 4
*/
void Dri_UART_Hander() interrupt 4
{
/* 检查接收中断标志位 RI,如果为 1,表示有一帧数据接收完成 */
if (RI == 1) {
RI = 0;
}
/* 检查发送中断标志位 TI,如果为 1,表示有一帧数据发送完成 */
if (TI == 1) {
TI = 0;
}
}
16.3 需求描述
使用 UART 与 PC 进行通信,通过 PC 向单片机发送命令,控制 LED 的亮灭。
16.4 硬件设计
当前需求是实现 PC 单片机的串口通讯,但是现在的 PC 基本都不再提供串口,因此需
要使用一个 USB 转串口的芯片来实现 PC 与单片机的通讯,如下图所示。

16.5 软件设计:单字节命令
16.5.1 具体要求
当 PC 向单片机发送字符 A 时,单片机需要令 LED 亮起,并向 PC 回复:Ok: LED is
on。
当 PC 向单片机发送字符 B 时,单片机需要令 LED 熄灭,并向 PC 回复:Ok: LED is
off。
当 PC 向单片机发送其他字符时,单片机不做任何操作,只需向 PC 回复:Error:
Unknown command。
16.5.2 实现思路
实现当前需求,需要先对串口进行初始化,具体操作如下。
(1)选择串口工作模式
本案例选择模式 1,因此需要将 SM0 和 SM1 做出如下配置。
cs
SM0 = 0;
SM1 = 1;
(2)设置波特率
本案例波特率选用 9600,需要做如下配置。
(1)SMOD 控制位
按照前文的计算,将 SMOD 设置为 0 即可,由于 SMOD 不可进行位寻址,因此我们
需要对其所在的寄存器 PCON 进行整体赋值,如下。
cs
PCON &= 0x7F;
(2)定时器 1
按照前文的计算,定时器 1 应工作在模式 2(8 位自动重装载),每次重装载的初始值
应为 253。具体设置如下。
cs
// 定时器 1 工作模式
TMOD &= 0x0F;
TMOD |= 0x20;
// 定时器 1 的初值
TH1 = 0xFD;
TL1 = 0xFD;
// 启动定时器 1
TR1 = 1;
(3)串口接收相关配置
串口默认不接收数据,因此需要先使能接受,另外还需将 SCON 寄存器中的 SM2 控制
位设置为 0,表示接受数据时不校验数据帧的停止位。
cs
REN = 1;
SM2 = 0;
(4)启动串口中断
cs
// 开启中断
EA = 1;
// 开启串口中断
ES = 1;
// 复位中断标志位
RI = 0;
TI = 0
完成串口的初始化之后,根据需求编写响应的业务逻辑即可。
16.5.3 完整代码
(1)Dri_UART.h
cs
#ifndef __DRI_UART_H__
#define __DRI_UART_H__
#include <STC89C5xRC.H>
#include "Util.h"
/**
* @brief 串口初始化方法,需要先调用
*
*/
void Dri_UART_Init();
/**
* @brief 通过串口发送一个字符串
*
* @param ch 要发送的字符串
*/
void Dri_UART_SendStr(char *str);
/**
* @brief 通过串口接收一个字符
*
* @param p_ch 要接收的字符指针
*
* @return 0 为读取失败,1 为读取成功
*/
bit Dri_UART_RecvChar(char *p_ch);
#endif
(2)Dri_UART.c
cs
#include "Dri_UART.h"
#define BAUD_RATE 9600
#define T2TEMP 256 - (FOSC / NT / 32 / BAUD_RATE)
char buffer;
// 发送状态,1:正在发送,0:未在发送
static bit is_sending;
void Dri_UART_Init()
{
// 1.设置串口工作模式
SM0 = 0;
SM1 = 1;
// 2.设置波特率
// 2.1 设置 SMOD 控制位
PCON &= ~0x80;
// 2.2 设置定时器 1
TMOD &= 0x0F;
TMOD |= 0x20;
TH1 = 0xFD;
TL1 = 0xFD;
TR1 = 1;
// 3.串口接收相关配置
REN = 1;
SM2 = 0;
// 打开中断总开关和串口中断开关
EA = 1;
ES = 1;
TI = 0;
RI = 0;
is_sending = 0;
buffer = 0;
}
void Dri_UART_SendChar(char ch)
{
// 如果有数据在发送,等待发送完成
while (is_sending);
is_sending = 1;
SBUF = ch;
}
void Dri_UART_SendStr(char *str)
{
while (*str) {
Dri_UART_SendChar(*str);
str++;
}
}
bit Dri_UART_RecvChar(char *p_ch)
{
// 如果缓冲区有数据,则将值符传给 p_ch,并清空缓冲区
if (buffer) {
*p_ch = buffer;
buffer = 0;
return 1;
} else {
return 0;
}
}
/**
* @brief 串口中断函数,进入这个函数有两个触发条件:发送完成和接收完成
*
*/
void Dri_UART_Func() interrupt 4
{
// 收到命令,将数据放入缓冲区
if (RI == 1) {
buffer = SBUF;
RI = 0;
}
// 发送完成,将发送状态置为 0
if (TI == 1) {
is_sending = 0;
TI = 0;
}
}
(3)main.c
cs
#include <STC89C5xRC.H>
#include "Dri_UART.h"
void main()
{
char command;
Dri_UART_Init();
while (1) {
if (Dri_UART_RecvChar(&command)) {
if (command == 'A') {
// 点亮 LED
P0 = 0x00;
Dri_UART_SendStr("Ok:LED is on");
} else if (command == 'B') {
// 熄灭 LED
P0 = 0xFF;
Dri_UART_SendStr("Ok:LED is off");
} else {
// 报错
Dri_UART_SendStr("Error:Unknown command");
}
}
}
}
16.6 软件设计:多字节命令
16.6.1 具体要求
当 PC 向单片机发送字符串 on 时,单片机需要令 LED 亮起,并向 PC 回复:Ok: LED
is on
当 PC 向单片机发送字符串 off 时,单片机需要令 LED 熄灭,并向 PC 回复:Ok: LED
is off
当 PC 向单片机发送其他字符串时,单片机不做任何操作,只需向 PC 回复:Error:
Unknown command
16.6.2 完整代码
1)Dri_UART.h
cs
#ifndef __DRI_UART_H__
#define __DRI_UART_H__
#include <STC89C5xRC.H>
#include "Util.h"
/**
* @brief 串口初始化
*
*/
void Dri_UART_Init(void);
/**
* @brief 发送字符串
*
* @param str 待发送字符串
*/
void Dri_UART_SendStr(char *str);
/**
* @brief 接收字符串
*
* @param str 用于接收字符串的数组
* @return bit 0 为读取失败,1 为读取成功
*/
bit Dri_UART_ReceiveStr(char str[]);
#endif /* __DRI_UART_H__ */
2)Dri_UART.c
cs
#include "Dri_UART.h"
#include "Dri_Timer0.h"
#define BAUD_RATE 9600
#define T2TEMP 256 - (FOSC / NT / 32 / BAUD_RATE)
#include <STDIO.H>
/ 0:未在发送 1:正在发送
static bit s_is_sending = 0;
static char s_buffer[10] = {0};
static u8 s_index = 0;
static bit s_is_complete = 0;
static u8 s_idle_count = 0;
void Dri_UART_Timer0Callback()
{
s_idle_count++;
if (s_index > 0 && s_idle_count >= 10) {
s_is_complete = 1;
s_idle_count = 0;
}
}
void Dri_UART_Init()
{
// 1.选择工作模式
SM0 = 0;
SM1 = 1;
// 2.波特率
// 2.1 SMOD
PCON &= 0x7F;
// 2.2 Timer1
// 2.2.1 工作模式
TMOD &= 0x0F;
TMOD |= 0x20;
// 2.2.2 初始值
TH1 = 0xFD;
TL1 = 0xFD;
// 2.2.3 开启
TR1 = 1;
// 3.接受相关
REN = 1;
SM2 = 0;
// 4. 中断相关
EA = 1;
ES = 1;
RI = 0;
TI = 0;
// 5. 注册空闲监测回调
Dri_Timer0_RegisterCallback(Dri_UART_Timer0Callback);
}
void Dri_UART_SendChar(char c)
{
while (s_is_sending == 1);
s_is_sending = 1;
SBUF = c;
}
void Dri_UART_SendStr(char *str)
{
while (*str != 0) {
Dri_UART_SendChar(*str);
str++;
}
}
bit Dri_UART_ReceiveStr(char *cmd)
{
u8 i;
if (s_is_complete) {
for (i = 0; i < s_index; i++) {
cmd[i] = s_buffer[i];
}
cmd[s_index] = '\0';
s_index = 0;
s_is_complete = 0;
return 1;
} else {
return 0;
}
}
void Dri_UART_Handler() interrupt 4
{
if (RI == 1) {
// 接收数据的逻辑
s_idle_count = 0;
s_buffer[s_index++] = SBUF;
RI = 0;
}
if (TI == 1) {
// 发送数据的逻辑
s_is_sending = 0;
TI = 0;
}
}
(3)main.c
cs
#include "Dri_UART.h"
#include <STRING.H>
void main()
{
char command[10] = {0};
Dri_UART_Init();
while (1) {
if (Dri_UART_ReceiveStr(command)) {
if (strcmp(command, "on") == 0) {
P0 = 0x00;
Dri_UART_SendStr("Ok:LED is on");
} else if (strcmp(command, "off") == 0) {
P0 = 0xFF;
Dri_UART_SendStr("Ok:LED is off");
} else {
Dri_UART_SendStr("Error:Unhnown command");
}
}
}
}