UART串口通信

目录

串行通信的初步认识

RS-232通信接口

USB转串口通信

IO口模拟UART串口通信

UART串口通信的基本应用

通信的三种基本类型

UART模块介绍

UART串口程序

 通讯实例与ASCII码

ASCII码字符表


串行通信的初步认识

通信按照基本类型可以分为并行通信和串行通信。并行通信时数据的各个位同时传送,可以实现字节为单位通信,但是通信线多占用资源多,成本高。比如前边用到的P0 = 0xFE;一次给P0的8个IO口分别赋值,同时进行信号输出。习惯上还称P0、P1、P2和P3为51单片机的4组并行总线。

串行通信,如果一个0xFE这样一个字节的数据要传输过去的话,假如低位在前高位在后的话,那发送方式就是0-1-1-1-1-1-1-1-1,一位一位的发送出去的,要发送8次才能发送完一个字节。

STC89C52有两个引脚是专门用来做UART串行通信,一个是P3.0,一个是P3.1,它们还分别有另外的名字叫作RXD和TXD,由它们组成的通信接口就叫作串行接口,简称串口。

图中,GND表示单片机系统电源的参考地,TXD是串行发送引脚,RXD是串行接收引脚。两个单片机之间要通信,首先电源基准得一样,所以要把两个单片机的GND相互连接起来,然后单片机1的TXD引脚接到单片机2的RXD引脚上,即此路为单片机1发送而单片机2接收的通道,单片机1的RXD引脚接到单片机2的TXD引脚上,即此路为单片机2发送而单片机1接收的通道。这个示意图就体现了两个单片机相互收发信息的过程。

当单片机1想给单片机2发送数据时,比如发送一个0xE4这个数据,用二进制形式表示就是0b11100100,在UART通信过程中,是低位先发、高位后发的原则,就让TXD首先拉低电平,持续一段时间,发送一位0,然后继续拉低,再持续一段时间,又发送了一位0,然后拉高电平,持续一段时间,发了一位1……一直到把8位二进制数字0b11100100全部发送完毕。这个一段时间是通信中的另一个重要概念——波特率,也叫作比特率

波特率就是发送二进制数据位的速率,习惯上用baud表示,即发送一位二进制数据的持续时间=1/baud。在通信之前,单片机1和单片机2首先都要明确约定好它们之间的通信波特率,必须保持一致,收发双方才能正常实现通信。

在UART通信的时候,一个字节是8位,规定当没有通信信号发生时,通信线路保持高电平,当要发送数据之前,先发一位0表示起始位,然后发送8位数据位,数据位是先低后高的顺序,数据位发完后再发一位1表示停止位。这样本来要发送一个字节的8位数据,而实际上一共发送了10位,多出来的两位其中一位起始位,一位停止位。接收方原本一直保持的高电平,一旦检测到了一位低电平,那就知道了要开始准备接收数据了,接收到8位数据位后,然后检测到停止位,再准备下一个数据的接收,如图11-2所示。

图11-2是串口数据发送示意图,实际上是一个时域示意图,就是信号随着时间变化的对应关系。比如在单片机的发送引脚上,左边的是先发生的,右边的是后发生的,数据位的切换时间就是波特率分之一秒。

RS-232通信接口

RS-232接口一共有9个引脚,分别定义是:(1)载波检测DCD;(2)接收数据RXD;(3)发送数据TXD;(4)数据终端准备好DTR;(5)信号地线SG;(6)数据准备好DSR;(7)请求发送RTS;(8)清除发送CTS;(9)振铃提示RI。要让这个串口和单片机进行通信,只需要关心其中的2引脚RXD、3引脚TXD和5引脚GND即可。

对于RS-232标准来说,它是个反逻辑,也叫作负逻辑。为何叫负逻辑?它的TXD和RXD的电压,-3V~-15V电压代表是1,+3~+15V电压代表是0。低电平代表的是1,而高电平代表的是0,所以称之为负逻辑。因此计算机的9针RS-232串口是不能和单片机直接连接的,需要用一个电平转换芯片MAX232来完成,如图11-4所示。

USB转串口通信

只需要在电路上添加一个USB转串口芯片,就可以成功实现USB通信协议和标准UART串行通信协议的转换,开发板51使用的是CH340T芯片,如图11-5所示。

图11-5中左下方J1和J2是两个跳线的组合,大家可以在开发板左下方的位置找到,需要用跳线帽把中间和下边的针短接在一起。右侧的CH340T这个电路很简单,把电源、晶振接好后,6引脚和7引脚的DP和DM分别接USB口的两个数据引脚上去,3引脚和4引脚通过跳线接到单片机的TXD和RXD上去。

CH340T的电路里3脚位置加了个4148的二极管,这是一个小技巧。因为STC89C52单片机下载程序时需要冷启动,就是先点下载后上电,上电瞬间单片机会先检测需要不需要下载程序。虽然单片机的VCC是由开关来控制,但是由于CH340T的3脚是输出引脚,如果没有此二极管,开关后级单片机在断电的情况下,CH340T的3脚和单片机的P3.0(即RXD)引脚连在一起,有电流会通过这个引脚流入后级电路并且给后级的电容充电,造成后级有一定幅度的电压,这个电压值虽然只有2~3V左右,但是可能会影响到正常的冷启动。加了二极管后,一方面不影响通信,另外一个方面还可以消除这种不良影响。

IO口模拟UART串口通信

对于UART串口波特率,常用的值是300、600、1200、2400、4800、9600、14400、19200、28800、38400、57600、115200等速率。

直接使用STC-ISP软件自带的串口调试助手,先把串口调试助手的使用给大家说一下,如图11-6所示。第一步要选择串口助手菜单,第二步选择十六进制显示,第三步选择十六进制发送,第四步选择COM口,这个COM口要和自己计算机设备管理器里的那个COM口一致。波特率按程序设定好的选择,程序中让一个数据位持续时间是1/9600秒,那这个地方选择波特率就是选9600,校验位选N,数据位8,停止位1。

 配置波特率的时候用的是定时器T0的模式2。模式2中,不再是TH0代表高8位,TL0代表低8位了,而只有TL0在进行计数,当TL0溢出后,不仅仅会让TF0变1,而且还会将TH0中的内容重新自动装到TL0中。这样有一个好处,就是可以把想要的定时器初值提前存在TH0中,当TL0溢出后,TH0自动把初值就重新送入TL0了,全自动的,不需要程序中再给TL0重新赋值了。

波特率设置好以后,打开中断,然后等待接收串口调试助手下发的数据。接收数据的时候,首先要进行低电平检测while (PIN_RXD),若没有低电平则说明没有数据,一旦检测到低电平,就进入启动接收函数StartRXD()。接收函数最开始启动半个波特率周期,如果在数据位电平变化的时候去读取,因为时序上的误差以及信号稳定性的问题很容易读错数据,所以希望在信号最稳定的时候去读数据。除了信号变化的那个沿的位置外,其他位置都很稳定,那么现在就约定在信号中间位置去读取电平状态,这样能够保证读得一定是正确的。

一旦读到了起始信号,就把当前状态设定成接收状态,并且打开定时器中断,第一次是半个周期进入中断后,对起始位进行二次判断一下,确认一下起始位是低电平,而不是一个干扰信号。以后每经过1/9600秒进入一次中断,并且把这个引脚的状态读到RxdBuf里边。等待接收完毕之后,再把这个RxdBuf加1,再通过TXD引脚发送出去,同样需要先发一位起始位,然后发8个数据位,再发结束位,发送完毕后,程序运行到while (PIN_RXD),等待第二轮信号接收的开始。

#include <REG52.H>

sbit PIN_RXD = P3 ^ 0; // 接收引脚定义
sbit PIN_TXD = P3 ^ 1; // 发送引脚定义

bit RxdOrTxd         = 0; // 指示当前状态为接收还是发送
bit RxdEnd           = 0; // 接收结束标志
bit TxdEnd           = 0; // 发送结束标志
unsigned char RxdBuf = 0; // 接收缓存器
unsigned char TxdBuf = 0; // 发送缓存器

void ConfigUART(unsigned int baud);
void StartTXD(unsigned char dat);
void StartRXD();

void main()
{
    EA = 1;
    ConfigUART(9600); // 配置波特率
    while (1) {
        while (PIN_RXD)
            ;       // 等待接受引脚出现低电平,即起始位
        StartRXD(); // 启动接收
        while (!RxdEnd)
            ;                 // 等待接收完毕
        StartTXD(RxdBuf + 1); // 接收到的数据+1后,发送回去
        while (!TxdEnd)
            ; // 等待发送完成
    }
}

void ConfigUART(unsigned int baud)
{
    // 串口配置函数,baud -> 通讯波特率
    TMOD &= 0xf0;                     // 清零T0控制位
    TMOD |= 0x02;                     // 配置T0为模式2
    TH0 = 256 - 12000000 / 12 / baud; // 计算T0重载值
}

void StartRXD()
{
    // 启动串行接收
    TL0      = 256 - (256 - TH0) >> 1; // 接收启动时的T0定时为半个波特率周期
    ET0      = 1;
    TR0      = 1;
    RxdEnd   = 0; // 清零接收结束标志
    RxdOrTxd = 0; // 设置当前状态为接收
}

void StartTXD(unsigned char dat)
{
    // 启动串行发送,dat为待发送字节数据
    TxdBuf   = dat;
    TL0      = TH0; // T0计数初值为重载值
    ET0      = 1;
    TR0      = 1;
    PIN_RXD  = 0; // 发送起始位
    TxdEnd   = 0; // 清零发送结束标志
    RxdOrTxd = 1; // 设置当前状态为发送
}

void InterruptTimer0() interrupt 1
{
    static unsigned char cnt = 0; // 位接收或发送计数
    if (RxdOrTxd)                 // 串行发送处理
    {
        cnt++;
        if (cnt <= 8) {
            PIN_TXD = TxdBuf & 0x01;
            TxdBuf >>= 1;
        } else if (cnt == 9) // 发送停止位
        {
            PIN_TXD = 1;
        } else { // 发送结束
            cnt    = 0;
            TR0    = 0;
            TxdEnd = 1;
        }
    } else {              // 串行接收处理
        if (cnt == 0) {   // 处理起始位
            if (!PIN_RXD) // 起始位为零时,清零接收缓冲器,准备接收数据位
            {
                RxdBuf = 0;
                cnt++;
            } else { // 起始位不为零,终止接收
                TR0 = 0;
            }
        } else if (cnt <= 8) // 处理8位数据位
        {
            RxdBuf >>= 1; // 低位在先,所以将接收的位向右移
            if (PIN_RXD) {
                RxdBuf |= 0x80;
            }
            cnt++;
        } else // 停止位处理
        {
            cnt = 0;
            TR0 = 0;
            if (PIN_RXD) {
                RxdEnd = 1;
            }
        }
    }
}

UART串口通信的基本应用

通信的三种基本类型

常用的通信从传输方向上可以分为单工通信、半双工通信和全双工通信三类。

  1. 单工通信就是指只允许一方向另外一方传送信息,而另一方不能回传信息。比如电视遥控器、收音机广播等都是单工通信技术。
  2. 半双工通信是指数据可以在双方之间相互传播,但是同一时刻只能其中一方发给另外一方,比如对讲机就是典型的半双工。
  3. 全双工通信就发送数据的同时也能够接收数据,两者同步进行,就如同电话一样,说话的同时也可以听到对方的声音。

UART模块介绍

IO口模拟串口通信,让大家了解了串口通信的本质,但是单片机程序却需要不停地检测扫描单片机IO口收到的数据,大量占用了单片机的运行时间。这时候就会有聪明人想了,其实我们并不是很关心通信的过程,只需要一个通信的结果,最终得到接收到的数据就行了。这样就可以在单片机内部做一个硬件模块,让它自动接收数据,接收完了,通知一下就可以了,51单片机内部就存在这样一个UART模块,要正确使用它,当然还得先把对应的特殊功能寄存器配置好。

51单片机的UART串口的结构由串行口控制寄存器SCON、发送和接收电路三部分构成,先来了解一下串口控制寄存器SCON,如表11-1和表11-2所示。

 在使用IO口模拟串口通信的时候,串口的波特率是使用定时器T0的中断体现出来的。在硬件串口模块中,有一个专门的波特率发生器用来控制发送和接收数据的速度。对于STC89C52单片机来讲,这个波特率发生器只能由定时器T1或定时器T2产生,而不能由定时器T0产生,这和模拟的通信是完全不同的概念。

如果用定时器2,需要配置额外的寄存器,默认是使用定时器1的,本章内容主要就使用定时器T1作为波特率发生器来讲解,方式1下的波特率发生器必须使用定时器T1的模式2,也就是自动重装载模式,定时器的重载值计算公式为:

TH1=TL1=256-晶振值/12/2/16/波特率

 和波特率有关的还有一个寄存器,是一个电源管理寄存器PCON,它的最高位可以把波特率提高一倍,也就是如果写PCON|=0x80以后,计算公式就成了:

TH1=TL1=256-晶振值/12/16/波特率

 公式中数字的含义解释一下,256是8位定时器的溢出值,也就是TL1的溢出值,晶振值在开发板上就是11059200,12是说1个机器周期等于12个时钟周期,值得关注的是16,下面来重点说明。在IO口模拟串口通信接收数据的时候采集的是这一位数据的中间位置,而实际上串口模块比模拟的要复杂和精确一些。它采取的方式是把一位信号采集16次,其中第7、8、9次取出来,如果这三次中其中有两次是高电平,就认定这一位数据是1,如果两次是低电平,就认定这一位是0,这样一旦受到意外干扰读错一次数据,也依然可以保证最终数据的正确性。

“晶振值/12/2/16/波特率”这个地方计算的时候,出现不能除尽,或者出现小数怎么办,允许出现多大的偏差?把这部分理解了,也就理解了晶振为何使用11.0592M了。

串口通信的发送和接收电路在物理上有两个名字相同的SBUF寄存器,它们的地址也都是0x99,但是一个用来作为发送缓冲,一个用来作为接收缓冲。意思就是说,有两个房间,两个房间的门牌号是一样的,其中一个只出人不进人,另外一个只进人不出人,这样的话,就可以实现UART的全双工通信,相互之间不会产生干扰。但是在逻辑上呢,每次只操作SBUF,单片机会自动根据对它执行的是“读”还是“写”操作来选择是接收SBUF还是发送SBUF,后边通过程序就会彻底了解这个问题。

UART串口程序

基本步骤如下:

  1. 配置串口为模式1。
  2. 配置定时器T1为模式2,即自动重装模式。
  3. 根据波特率计算TH1和TL1的初值,如果有需要可以使用PCON进行波特率加倍。
  4. 打开定时器控制寄存器TR1,让定时器跑起来。

这里还要特别注意一下,就是在使用T1做波特率发生器的时候,千万不要再使能T1的中断了。

#include <reg52.h>
    
    void ConfigUART(unsigned int baud);
    
    void main()
    {
      EA = 1;              //使能总中断
      ConfigUART(9600);                 //配置波特率为9600
      while (1);
    }
    /* 串口配置函数,baud-通信波特率 */
    void ConfigUART(unsigned int baud)
    {
      SCON = 0x50;                          //配置串口为模式1
      TMOD &= 0x0F;                         //清零T1的控制位
      TMOD |= 0x20;                          //配置T1为模式2
      TH1 = 256 - (11059200/12/32)/baud;     //计算T1重载值
      TL1 = TH1;                            //初值等于重载值
      ET1 = 0;                             //禁止T1中断
      ES = 1;                             //使能串口中断
      TR1 = 1;                             //启动T1
    }
    /* UART中断服务函数 */
    void InterruptUART() interrupt 4
    {
      if (RI)                              //接收到字节
      {
        RI = 0;                          //手动清零接收中断标志位
        SBUF = SBUF + 1;                  //接收的数据+1后发回,左边是发送SBUF,右边是接收SBUF
      }
      if (TI)                              //字节发送完毕
      {
        TI = 0;                          //手动清零发送中断标志位
      }
    }

 通讯实例与ASCII码

下面就做一个简单的例程来实现单片机串口调试助手发送的数据,开发板上的数码管上显示出来。

 #include <reg52.h>
    
    sbit ADDR3 = P1^3;
    sbit ENLED = P1^4;
    
    unsigned char code LedChar[] = {   //数码管显示字符转换表
      0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
      0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
    };
    unsigned char LedBuff[7] = {     //数码管+独立LED显示缓冲区
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
    };
    unsigned char T0RH = 0;     //T0重载值的高字节
    unsigned char T0RL = 0;     //T0重载值的低字节
    unsigned char RxdByte = 0;     //串口接收到的字节
    void ConfigTimer0(unsigned int ms);
    void ConfigUART(unsigned int baud);
    
    void main()
    {
      EA = 1;        //使能总中断
      ENLED = 0;      //选择数码管和独立LED
      ADDR3 = 1;
      ConfigTimer0(1);      //配置T0定时1ms
      ConfigUART(9600);     //配置波特率为9600
    
      while (1)
      {      //将接收字节在数码管上以十六进制形式显示出来
        LedBuff[0] = LedChar[RxdByte & 0x0F];
        LedBuff[1] = LedChar[RxdByte >> 4];
      }
    }
    /* 配置并启动T0,ms为T0定时时间 */
    void ConfigTimer0(unsigned int ms)
    {
      unsigned long tmp;     //临时变量
    
      tmp = 11059200 / 12;        //定时器计数频率
      tmp = (tmp * ms) / 1000;     //计算所需的计数值
      tmp = 65536 - tmp;         //计算定时器重载值
      tmp = tmp + 13;          //补偿中断响应延时造成的误差
      T0RH = (unsigned char)(tmp>>8);     //定时器重载值拆分为高低字节
      T0RL = (unsigned char)tmp;
      TMOD &= 0xF0;      //清零T0的控制位
      TMOD |= 0x01;      //配置T0为模式1
      TH0 = T0RH;       //加载T0重载值
      TL0 = T0RL;
      ET0 = 1;        //使能T0中断
      TR0 = 1;    //启动T0
    }
    /* 串口配置函数,baud为通信波特率 */
    void ConfigUART(unsigned int baud)
    {
      SCON = 0x50;            //配置串口为模式1
      TMOD &= 0x0F;     //清零T1的控制位
      TMOD |= 0x20;     //配置T1为模式2
      TH1 = 256 - (11059200/12/32)/baud;     //计算T1重载值
      TL1 = TH1;       //初值等于重载值
      ET1 = 0;        //禁止T1中断
      ES = 1;        //使能串口中断
      TR1 = 1;        //启动T1
    }
    /* LED动态扫描函数,需在定时中断中调用 */
    void LedScan()
    {
      static unsigned char i = 0;     //动态扫描索引
    
      P0 = 0xFF;            //关闭所有段选位,显示消隐
      P1 = (P1 & 0xF8) | i;     //位选索引值赋值到P1口低3位
      P0 = LedBuff[i];        //缓冲区中索引位置的数据送到P0口
      if (i < 6)            //索引递增循环,遍历整个缓冲区
        i++;
      else
        i = 0;
    }
    /* T0中断服务函数,完成LED扫描 */
    void InterruptTimer0() interrupt 1
    {
      TH0 = T0RH;     //重新加载重载值
      TL0 = T0RL;
      LedScan();      //LED扫描显示
    }
    /* UART中断服务函数 */
    void InterruptUART() interrupt 4
    {
      if (RI)     //接收到字节
      {
        RI = 0;     //手动清零接收中断标志位
        RxdByte = SBUF;     //接收到的数据保存到接收字节变量中
        SBUF = RxdByte;     //接收到的数据又直接发回,叫作-"echo",
                      //用以提示用户输入的信息是否已正确接收
      }
      if (TI)     //字节发送完毕
      {
        TI = 0;     //手动清零发送中断标志位
      }
    }

ASCII码字符表

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值