一、前置基础

想要理解和使用UART,需要先了解一些通讯领域的术语,如下 。

串行通讯和并行通讯

串行通讯和并行通讯是数据传输的两种主要方式,两者的区别如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

单工通讯和双工通讯

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

同步通讯和异步通讯

同步通讯和异步通讯的区别在于发送方和接收方如何对数据进行协调统一,详细内容如下图所示。

  • 同步
    在这里插入图片描述
  • 异步
    在这里插入图片描述

二、UART定义

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

在这里插入图片描述

三、UART通讯协议

数据格式

在UART通信中,数据是逐帧(Frame)发送的,(帧:数据传输时的最小单位)每个数据帧通常包括起始位、数据位、校验位(可选)和停止位,具体结构如下图所示:

在这里插入图片描述

下面对每个部分进行详细介绍:

  • 空闲状态 协议规定,在空闲状态下,也就是没有数据传输时,应为高电平。
  • 起始位 起始位表示一个数据帧的开始,起始位为低电平(区别于空闲状态)。
  • 数据位 传输的主体内容,位于起始位之后,长度可以是5到9位,一般都是8位。低电平表示0,高电平表示1。
  • 校验位(可选) 用于校验当前数据帧的正确性,校验算法可以是奇校验或偶校验,该算法的思想如下。 奇校验(odd parity):如果数据位中1的数目是偶数,则校验位为1,如果1的数目是奇数,校验位为0,目的是保证数据位+校验位中的1的总个数是奇数
    偶校验(even
    parity):如果数据为中1的数目是偶数,则校验位为0,如果1的数目为奇数,校验位为1,目的是保证数据位+校验位中的1的总个数是偶数
  • 停止位 停止位表示数据帧的结束,通常为1位或2位,停止位为高电平。

发送方和接收方的约定

为保证UART通信能够正常工作,发送方和接收方必须提前做好如下约定。

  • 波特率(Baud Rate)用于表示数据的传输速率,发送方和接收方必须约定好传输速率,(比如异步通信)才能保证数据被正确的发送和接收。
    需要注意波特率(Baud Rate)和比特率(Bit Rate)的区别,比特率表示每秒传输的位(bit)数,而波特率表示每秒传输的符号(symbol)数。但是串口通信中,只有0和1这两个符号,因此1个符号用1位就能表示,所以此处的波特率和比特率是等价的。
    如果是4进制传输,也就是符号有0 1 2 3,那么就需要两个比特位去表示符号,波特率:比特率 = 1: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为例,介绍该串行通信口的用法。

工作模式需要通过SCON(Serial Control,串行口控制)寄存器中的SM0和SM1两个控制位进行设置,如下图所示:

在这里插入图片描述

设置波特率

因为数据位(8)和停止位(1)已经固定,方式一没有校验位,所以只用配置波特率。

在二进制中波特率就表示每秒数据传输的比特数。
方式1的波特率会受到两个因素的影响,分别是SMOD控制位和定时器1的溢出频率,具体作用如下图所示。
在这里插入图片描述

UART常用的波特率有4800、9600、19200、38400、57600、115200等,接下来小编以9600为例。

设置SMOD控制位

SMOD控制位位于PCON(Power Control,电源控制)寄存器,如下图所示:

在这里插入图片描述

因为该寄存器不可位寻址,所以只能整个寄存器统一配。
小编这里选择0,所以此时的波特率应为(定时器1的溢出率)/ 32。

设置定时器1

由于定时器1当前的作用仅仅是为串口提供时钟信号,因此可不开启定时器中断,只需完成以下配置即可。
(1)选择定时器工作模式
定时器1的工作模式需要通过TMOD寄存器中的C/T(设置定时或计数)控制位,以及M1和M0(设置脉冲计数器的工作模式)两个控制位进行设置,GATE是用来设置是否允许外部引脚来启动定时器(用不到,GATE直接置0),如下图所示:

在这里插入图片描述

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

在这里插入图片描述

由于没有启用定时器中断,因此无法在中断中重新设置定时器脉冲计数器的初始值,所以此处选择模式2(8位自动重装载)最为合理。

设置脉冲计数器初始值

模式2的最大计数为256,因此定时器1的溢出频率等于SYSclk / 12 /(256 - TH1)或者是SYSclk / 6 /(256 - TH1),假如当前MCU工作在12T模式,所以溢出频率就等于SYSclk / 12 /(256 - TH1)。

在这里插入图片描述

因此最终的波特率就应该等于SYSclk / 12 /(256-TH1)/32,假如当前的系统时钟频率为11059200Hz,目标的波特率为9600,那么就能得到以下方程

在这里插入图片描述

所以可计算出TH1的值应为253。

启动定时器

定时器1的启动也无需外部引脚控制,因此应将GATE控制位设置为0,并将TR1控制位设置为1。

发送数据

STC89C52的串口的发送模块示意图如下:

在这里插入图片描述

数据发送的大致流程如下: 开发者将待发送的8位数据写入发送缓冲器(SBUF),此时发送控制器(TxControl)就会开始工作,它会自动为数据添加起始位和结束位,从而构成一个完整的UART数据帧,然后逐位通过TxD引脚输出出去。当完成一个数据帧的输出之后,发送控制器会将发送中断控制位TI置1,(在连续发送多个字节时用来确保上一组写入SBUF的数据已发送完毕再发送下一字节,避免数据丢失)向CPU请求中断,CPU检测到中断请求后就执行相应的中断服务程序。
总结:发送数据只需要将待发送的数据写入SBUF即可。

接收数据

STC89C52的串口的接收模块示意图如下:
在这里插入图片描述

默认情况下,串口并不会接收数据。(若要接受数据就要一直检测RxD引脚电平变化,使功耗增加)如需接收数据,需要先将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。

在这里插入图片描述

  • 读取中断标志位为复位状态
    读取中断标志位RI必须等于0,也就说要保证上一帧数据已经被读取或处理完毕,才能处理当前帧。

总结:接收数据需要先使能接收,也就是将REN控制位置1,然后开启串口中断,并在中断服务程序中读取SBUF。

串口中断注意事项

根据前文的描述,当使用串口发送完一帧数据后,会将发送中断标志位TI置1;当串口接收到一帧数据后,会将接收中断标志位RI置1。需要注意的是两个控制位请求的是同一个中断——串口中断(中断号为4)。

在这里插入图片描述

也就是说发送完一帧数据和接收完一帧数据之后,执行的都是串口中断的中断服务程序,因此,再编写该中段服务程序时,需要注意判断当前中断到底是由发送操作触发的,还是由接收操作触发的,代码示例如下:

/*
 * 串口中断的中断号为4
 */
void Dri_UART_Handler() interrupt 4
{
    /* 检查接收中断标志位RI,如果为1,表示有一帧数据接收完成 */
    if (RI == 1) {
    }

    /* 检查发送中断标志位TI,如果为1,表示有一帧数据发送完成 */
    if (TI == 1) {
    }
}

另外RI和TI标志位,只能由软件复位,(因为硬件无法得知数据是否处理完毕)也就是需要在中断服务程序中将其设置为0。

五、实操

需求描述

使用UART与PC进行通信,通过PC向单片机发送命令,控制LED的亮灭。

硬件设计

当前需求是实现PC单片机的串口通讯,但是现在的PC基本都不再提供串口,因此需要使用一个USB转串口的芯片来实现PC与单片机的通讯,如下图所示:

在这里插入图片描述

软件设计(一):单字节命令

具体要求

当PC向单片机发送字符A时,单片机需要令LED亮起,并向PC回复:Ok: LED is on。
当PC向单片机发送字符B时,单片机需要令LED熄灭,并向PC回复:Ok: LED is off。
当PC向单片机发送其他字符时,单片机不做任何操作,只需向PC回复:Error: Unknown command。

初始配置

选择串口工作模式

本案例选择模式1,因此需要将SM0和SM1做出如下配置。

SM0 = 0;
SM1 = 1;
设置波特率

本案例波特率选用9600,需要做如下配置。

  • SMOD控制位

按照前文的计算,将SMOD设置为0即可,由于SMOD不可进行位寻址,因此我们需要对其所在的寄存器PCON进行整体赋值。

PCON &= 0x7F;
  • 定时器1

按照前文的计算,定时器1应工作在模式2(8位自动重装载),每次重装载的初始值应为253。具体设置如下:

// 定时器1工作模式
TMOD &= 0x0F;
TMOD |= 0x20;
// 定时器1的初值(0xFD == 253)
TH1 = 0xFD;  
TL1 = 0xFD;
// 启动定时器1
TR1 = 1;
  • 串口接收相关配置 串口默认不接收数据,因此需要先使能接受,另外还需将SCON寄存器中的SM2控制位设置为0,表示接受数据时不校验数据帧的停止位。
REN = 1;
SM2 = 0;
  • 启动串口中断
// 开启中断
EA = 1;
// 开启串口中断
ES = 1;
// 复位中断标志位
RI = 0;
TI = 0;

完成串口的初始化之后,根据需求编写响应的业务逻辑即可。

代码实现

初始化工作:

void Dri_UART_Init()
{
    //1、配置串口工作模式
    SM0 = 0;
    SM1 = 1;  

    // 2、设置波特率
    // 2.1 SMOD置0
    PCON &= 0x7F;
    //2.2 设置定时器1
    // 2.2.1 工作模式:自动装载初值:TMOD高四位置成0010
    TMOD &= 0x0F;
    TMOD |= 0x2F;
    // 2.2.2 初始值(自动装载初值时TL1:用来计数 TH1:用来保存装载初始值)
    TL1 = 253;
    TH1 = 253;
    // 2.2.3 启动定时器
    TR1 = 1;

    // 3、接受数据相关配置
    REN = 1;  //启动跳变检测器
    SM2 = 0;  //校验结束位

    // 4、 串口中断相关配置
    EA = 1;   //总中断
    ES = 1;   //串行口中断
    RI = 0;   //只能软件复位
    TI = 0;
}

接受数据:

    if(RI == 1)
    {
        if(SBUF == 'A')
        {
            P0 == 0X00;  //点亮一排LED灯(共阴)
        }
        else if(SBUF == 'B')
        {
            P0 == 0XFF;  //关灯
        }
        RI = 0;  //手动软件复位
    }

发送数据:

  • 发送单个字符:
void Dri_UART_SendChar(char c)
{
    SBUF = c;
}
  • 发送字符串:
void Dri_UART_SendStr(char* str)
{
    while(*str != '\0')
    {
        Dri_UART_SendChar(*str);
        str++; 
    }
}

问题:随机发送字符,无法发送一个完整的字符串
原因:前一个还没发完,后一个又装载到SBUF里了,会发生覆盖。
优化:设置一个标志位:s_is_sending,1表示正在发送数据,0表示发送完毕,可以发送下一字符了

static bit s_is_sending = 0;

void Dri_UART_SendChar(char c)
{
    while(s_is_sending == 1)
    {}
    s_is_sending = 1;
    SBUF = c;
}

if(TI == 1)
{
    s_is_sending = 0;
    TI = 0;  //手动软件复位
}

初步完整代码:

static bit s_is_sending = 0;

void Dri_UART_Init()
{
    //1、配置串口工作模式
    SM0 = 0;
    SM1 = 1;  

    // 2、设置波特率
    // 2.1 SMOD置0
    PCON &= 0x7F;
    //2.2 设置定时器1
    // 2.2.1 工作模式:自动装载初值:TMOD高四位置成0010
    TMOD &= 0x0F;
    TMOD |= 0x2F;
    // 2.2.2 初始值(自动装载初值时TL1:用来计数 TH1:用来保存装载初始值)
    TL1 = 253;
    TH1 = 253;
    // 2.2.3 启动定时器
    TR1 = 1;

    // 3、接受数据相关配置
    REN = 1;  //启动跳变检测器
    SM2 = 0;  //校验结束位

    // 4、 串口中断相关配置
    EA = 1;   //总中断
    ES = 1;   //串行口中断
    RI = 0;   //只能软件复位
    TI = 0;
}

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++; 
    }
}

//中断服务程序
void Dri_UART_Handler() interrupt 4  //串口中断的中断号是4
{
    if(RI == 1)
    {
        if(SBUF == 'A')
        {
            P0 == 0X00;  //点亮一排LED灯(共阴)
            Dri_UART_SendStr("Ok: LED is on");
        }
        else if(SBUF == 'B')
        {
            P0 == 0XFF;  //关灯
            Dri_UART_SendStr("Ok: LED is off");
        }
        else
        {
            Dri_UART_SendStr("Error: Unknown command");
        }

        RI = 0;  //手动软件复位
    }

    if(TI == 1)
    {
        s_is_sending = 0;
        TI = 0;  //手动软件复位
    }
}

这段代码并不能实现我们想要的功能,会发生中断冲突,因为RI和TI是同一个中断服务程序,在响应RI中断时,TI无法响应,那么标志位s_is_sending就不会改变,程序就会一直阻塞在发送完字符串第一个字符处。
解决思路: 不在接受中断服务里完成发送程序,在main函数里完成。

优化后:
Dri_UART.c:

#include "Dri_UART.h"
#include <STC89C5xRC.H>

static bit s_is_sending = 0;
// 定义缓存
static char s_buffer = 0;

void Dri_UART_Init()
{
    //1、配置串口工作模式
    SM0 = 0;
    SM1 = 1;  

    // 2、设置波特率
    // 2.1 SMOD置0
    PCON &= 0x7F;
    //2.2 设置定时器1
    // 2.2.1 工作模式:自动装载初值:TMOD高四位置成0010
    TMOD &= 0x0F;
    TMOD |= 0x2F;
    // 2.2.2 初始值(自动装载初值时TL1:用来计数 TH1:用来保存装载初始值)
    TL1 = 253;
    TH1 = 253;
    // 2.2.3 启动定时器
    TR1 = 1;

    // 3、接受数据相关配置
    REN = 1;  //启动跳变检测器
    SM2 = 0;  //校验结束位

    // 4、 串口中断相关配置
    EA = 1;   //总中断
    ES = 1;   //串行口中断
    RI = 0;   //只能软件复位
    TI = 0;
}

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_ReceiveChar(char* c)
{
    if(s_buffer)
    {
        *c = s_buffer;
        s_buffer = 0;
        return 1;
    }
    else
    {
        return 0;
    }
}

//中断服务程序
void Dri_UART_Handler() interrupt 4  //串口中断的中断号是4
{
    if(RI == 1)
    {
       s_buffer = SBUF;
        RI = 0;  //手动软件复位
    }

    if(TI == 1)
    {
        s_is_sending = 0;
        TI = 0;  //手动软件复位
    }
}

main.c:

#include "Dri_UART.h"
void main()
{
    char c;
    Dri_UART_Init();
    while(1)
    {
        if(Dri_UART_ReceiveChar(&c))
        {
            if(c == 'A')
            {
                p0 = 0x00;
                Dri_UART_SendStr("Ok: LED is on");
            }
            else if(c == 'B')
            {
                p0 = 0xFF;
                Dri_UART_SendStr("Ok: LED is off");
            }
            else
            {
                Dri_UART_SendStr("Error: Unknown command");
            }
        }

    }
}

软件设计(二):多字节命令

具体要求

当PC向单片机发送字符串on时,单片机需要令LED亮起,并向PC回复:Ok: LED is on
当PC向单片机发送字符串off时,单片机需要令LED熄灭,并向PC回复:Ok: LED is off
当PC向单片机发送其他字符串时,单片机不做任何操作,只需向PC回复:Error: Unknown command

代码实现

发送数据几乎不用改,主要关注收数据。 思路:
1、定义一个计时变量s_idle_count,每收到一个字符重新计时,当超过10ms不再更新表示计时完毕。
2、把s_buffer改成数组。

Dri_UART.c:

#include "Dri_UART.h"
#include <STC89C5xRC.H>
#include "Dri_Timer0.h"

static bit s_is_sending = 0;
// 定义缓存
static char s_buffer[16];
// 定义s_buffer下标
static u8 s_index = 0;


//定义空闲计时时间
static u8 s_idle_count = 0;
//标志位,用来判断字符串是否接受完毕
static bit s_is_complete = 0;

//每一毫秒让s_idle_count加1
void Dri_UART_Timer0Callback()
{
    s_idle_count++;

    if(s_index > 0 && s_idle_count >= 10)
    {
        //数据接受完毕
        bit s_is_complete = 1;
    }
}

void Dri_UART_Init()
{
    //1、配置串口工作模式
    SM0 = 0;
    SM1 = 1;  

    // 2、设置波特率
    // 2.1 SMOD置0
    PCON &= 0x7F;
    //2.2 设置定时器1
    // 2.2.1 工作模式:自动装载初值:TMOD高四位置成0010
    TMOD &= 0x0F;
    TMOD |= 0x2F;
    // 2.2.2 初始值(自动装载初值时TL1:用来计数 TH1:用来保存装载初始值)
    TL1 = 253;
    TH1 = 253;
    // 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_ReceiveChar(char* c)
// {
//     if(s_buffer)
//     {
//         *c = s_buffer;
//         s_buffer = 0;
//         return 1;
//     }
//     else
//     {
//         return 0;
//     }
// }

bit Dri_UART_ReceiveStr(char* str)
{
    if(s_is_complete == 1)
    {
    //把s_buffer里的值拷贝到str
        u8 i;
        for(i = 0; i < s_index; i++)
        {
            str[i] = s_buffer[i];
        }
        //方便后面strcmp比较,因为strcmp以'\0'为比较结束标志
        str[i] = '\0';
        //拷贝完成后将标志位置零
        s_is_complete == 0;
        //将index置零,以便后续继续将数据写入s_buffer
        s_index = 0;
        //返回1,表示已完成接受字符串数据
        return 1;
    }
    //没收到数据或没收完返回0
    return 0;
}

//中断服务程序
void Dri_UART_Handler() interrupt 4  //串口中断的中断号是4
{
    if(RI == 1)
    {
       s_buffer[s_index++] = SBUF;
       s_idle_count = 0;  //每收到一个字符让s_idle_count重新计时
        RI = 0;  //手动软件复位
    }

    if(TI == 1)
    {
        s_is_sending = 0;
        TI = 0;  //手动软件复位
    }
}

main.c:

#include "Dri_UART.h"

// void main()
// {
//     char c;
//     Dri_UART_Init();
//     while(1)
//     {
//         if(Dri_UART_ReceiveChar(&c))
//         {
//             if(c == 'A')
//             {
//                 p0 = 0x00;
//                 Dri_UART_SendStr("Ok: LED is on");
//             }
//             else if(c == 'B')
//             {
//                 p0 = 0xFF;
//                 Dri_UART_SendStr("Ok: LED is off");
//             }
//             else
//             {
//                 Dri_UART_SendStr("Error: Unknown command");
//             }
//         }
//     }
// }


#include <string.h>
#include "Dri_Timer0.h"
//多字节:
void main()
{
    char str[16];
    //先初始化定时器
    Dri_Timer0_Init();
    Dri_UART_Init();
    while(1)
    {
        if(Dri_UART_ReceiveStr(str))
        {
            if(strcmp(str,"on") == 0)
            {
                p0 = 0x00;
                Dri_UART_SendStr("Ok: LED is on");
            }
            else if(strcmp(str,"off") == 0)
            {
                p0 = 0xFF;
                Dri_UART_SendStr("Ok: LED is off");
            }
            else
            {
                Dri_UART_SendStr("Error: Unknown command");
            }
        }
    }
}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐