1、能够理解UART串口通信的基本原理和通信过程。
1)串行通信的初步认识
并行通信:通信时数据的各个位同时传送,可以实现字节为单位通信,但是通信线占用资源太多,成本高。
串行通信:一次只能发送一位,要发送8次才能发送一个字节。
2、通过IO口模拟UART串口通信把通信的底层操作原理弄明白。
首先是对通信的波特率的设定,在这里我们配置的波特率是9600,那么串口调试助手也得是9600。配置波特率的时候,我们用的是定时器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); //ÅäÖò¨ÌØÂÊΪ9600
while (1)
{
while (PIN_RXD); //µÈ´ý½ÓÊÕÒý½Å³öÏֵ͵çƽ£¬¼´Æðʼλ
StartRXD(); //Æô¶¯½ÓÊÕ
while (!RxdEnd); //µÈ´ý½ÓÊÕÍê³É
StartTXD(RxdBuf+1); //½ÓÊÕµ½µÄÊý¾Ý+1ºó£¬·¢ËÍ»ØÈ¥
while (!TxdEnd); //µÈ´ý·¢ËÍÍê³É
}
}
void ConfigUART(unsigned int baud)
{
TMOD &= 0XF0;
TMOD |= 0X02;
TH0 = 256 -(11059200/12)/baud;
}
void StartRXD()
{
TL0 = 256 - ((256-TH0)>>1);
ET0 = 1;
TR0 = 1;
RxdEnd = 0;
RxdOrTxd = 0;
}
void StartTXD(unsigned char dat)
{
RxdBuf = dat;
TL0 = TH0;
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 = 1;
TxdEnd = 1;
}
}
else
{
if(cnt == 0)
{
if(!PIN_RXD)
{
RxdEnd = 0;
cnt++;
}
else
{
TR0 = 0;
}
}
else if(cnt <= 8)
{
RxdBuf >>= 1;
if(PIN_RXD)
{
RxdBuf |= 0x80;
}
cnt++;
}
else
{
cnt = 0;
TR0 = 0;
if (PIN_RXD)
{
RxdEnd = 1;
}
}
}
}
3、学会通过配置寄存器,实现串口通信的基本操作过程。
UART模块介绍
IO口模拟串口通信,让大家了解了串口通信的本质,但是我们的单片机程序却需要不停的检测扫描单片机IO口收到的数据,大量占用了单片机的运行时间。这时候就会有聪明人想了,其实我们并不是很关心通信的过程,我们只需要一个通信的结果,最终得到接收到的数据就行了。这样我们可以在单片机内部做一个硬件模块,让它自动接收数据,接收完了,通知我们一下就可以了,我们的51单片机内部就存在这样一个UART模块,要正确使用它,当然还得先把对应的特殊功能寄存器配置好。
51单片机的UART串口的结构由串行口控制寄存器SCON、发送和接收电路三部分构成,先来了解一下串口控制寄存器SCON。如表11-1表11-2所示。
SCON——串行控制寄存器的位分配(地址0x98、可位寻址)
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
符号 | SM0 | SM1 | SM2 | REN | TB8 | RB8 | TI | RI |
复位值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
SCON——串行控制寄存器的位描述
位 | 符号 | 描述 |
7 | SM0 | 这两位共同决定了串口通信的模式0~模式3共4种模式。我们最常用的就是模式1,也就是SM0=0,SM1=1,下边我们重点就讲模式1,其它模式从略。 |
6 | SM1 | |
5 | SM2 | 多机通信控制位(极少用),模式1直接清零。 |
4 | REN | 使能串行接收。由软件置位使能接收,软件清零则禁止接收。 |
3 | TB8 | 模式2和3中要发送的第9位数据(很少用)。 |
2 | RB8 | 模式2和3中接收到的第9位数据(很少用),模式1用来接收停止位。 |
1 | TI | 发送中断标志位,当发送电路发送到停止位的中间位置时,TI由硬件置1,必须通过软件清零。 |
0 | RI | 接收中断标志位,当接收电路接收到停止位的中间位置时,RI由硬件置1,必须通过软件清零。 |
前边学了那么多寄存器的配置,相信SCON这个地方,对于大多数同学来说已经不是难点了,应该能看懂并且可以自己配置了。对于串口的四种模式,模式1是最常用的,就是我们前边提到的1位起始位,8位数据位和1位停止位。下面我们就详细介绍模式1的工作细节和使用方法,至于其它3种模式与此也是大同小异,真正遇到需要使用的时候大家再去查阅相关资料就行了。
在我们使用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了。
串口通信的发送和接收电路在物理上有2个名字相同的SBUF寄存器,它们的地址也都是0x99,但是一个用来做发送缓冲,一个用来做接收缓冲。意思就是说,有2个房间,两个房间的门牌号是一样的,其中一个只出人不进人,另外一个只进人不出人,这样的话,我们就可以实现UART的全双工通信,相互之间不会产生干扰。但是在逻辑上呢,我们每次只操作SBUF,单片机会自动根据对它执行的是“读”还是“写”操作来选择是接收SBUF还是发送SBUF,后边通过程序,我们就会彻底了解这个问题。
#include <REGX52.H>
void ConfigUART(unsigned int baud);
void main()
{
EA =1;
ConfigUART(9600);
while(1);
}
void ConfigUART(unsigned int baud)
{
SCON = 0x50;
TMOD &= 0x0F;
TMOD |= 0x02;
TH1 = 256 - (11059200/12/32)/baud;
TH1 =TL0;
ET1 = 0;
TR1 = 1;
}
void InterruptUART() interrupt 4
{
if(RI)
{
RI = 0;
SBUF++;
}
if(TI)
{
TI = 0;
}
}
4、了解字符和数据之间的转换依据和方法。
我们用字符格式发送一个小写的a,返回一个十六进制的0x61,数码管上显示的也是61,ASCII码表里字符a对应十进制是97,等于十六进制的0x61;我们再用字符格式发送一个数字1,返回一个十六进制的0x31,数码管上显示的也是31,ASCII表里字符1对应的十进制是49,等于十六进制的0x31。这下大家就该清楚了:所谓的十六进制发送和十六进制接收,都是按字节数据的真实值进行的;而字符格式发送和字符格式接收,是按ASCII码表中字符形式进行的,但它实际上最终传输的还是一个字节数据。这个表格,当然不需要大家去记住,理解它,用的时候过来查就行了。
5、完成通过串口控制流水灯流动和停止的程序。
#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] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char T0RH = 0;
unsigned char T0RL = 0;
unsigned char RxdByte = 0;
unsigned char flag200ms = 0;
unsigned char flagLight = 1;
void ConfigTimer0(unsigned int ms);
void ConfigUART(unsigned int baud);
void FlowingLight();
void main()
{
EA = 1;
ADDR3 = 1;
ENLED = 0;
ConfigTimer0(1);
ConfigUART(9600);
while(1)
{
if(flagLight == 0)
{
LedBuff[6]= 0XFF;
}
else
{
if (flag200ms != 0)
{
flag200ms = 0;
FlowingLight();
}
}
LedBuff[0] = LedChar[RxdByte & 0x0F];
LedBuff[1] = LedChar[RxdByte >> 4];
}
}
void FlowingLight()
{
static unsigned char dir = 0;
static unsigned char shift = 0x01;
LedBuff[6] = ~shift;
if(dir == 0)
{
shift = shift << 1;
if(shift == 0x80)
{
dir = 1;
}
}
else
{
shift = shift >> 1;
if(shift == 0x01)
{
dir = 0;
}
}
}
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;
TMOD |= 0x01;
TH0 = T0RH;
TL0 = T0RL;
ET0 = 1;
TR0 = 1;
}
void ConfigUART(unsigned int baud)
{
SCON = 0X50;
TMOD &= 0X0F;
TMOD |= 0X02;
TH1 = (11059200/12/32)/baud;
TL1 = TH1;
ET1 = 0;
ES = 1;
TR1 = 1;
}
void LedScan()
{
static unsigned char i = 0;
P0 = 0xFF;
P1 = (P1 & 0xF8) | i;
P0 = LedBuff[i];
if (i < 6)
i++;
else
i = 0;
}
void InterruptTimer0() interrupt 1
{
static unsigned char tmr200ms = 0;
TH0 = T0RH;
TL0 = T0RL;
LedScan();
tmr200ms++;
if(tmr200ms >= 200)
{
tmr200ms = 0;
flag200ms = 1;
}
}
void InterruptUART() interrupt 4
{
if(RI)
{
RxdByte = SBUF;
SBUF = RxdByte;
RI = 0;
flagLight = !flagLight;
}
if(TI)
{
TI = 0;
}
}
6、完成通过串口实现蜂鸣器鸣叫的程序。
#include <reg52.h>
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
sbit BUZZ = P1^6;
unsigned char code LedChar[] = {
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[7] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char T0RH = 0;
unsigned char T0RL = 0;
unsigned char RxdByte = 0;
unsigned char flagBuzz = 0;
void ConfigTimer0(unsigned int ms);
void ConfigUART(unsigned int baud);
void main()
{
EA = 1;
ADDR3 = 1;
ENLED = 0;
ConfigTimer0(1);
ConfigUART(9600);
while(1)
{
LedBuff[0] = LedChar[RxdByte & 0x0F];
LedBuff[1] = LedChar[RxdByte >> 4];
}
}
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;
TMOD |= 0x01;
TH0 = T0RH;
TL0 = T0RL;
ET0 = 1;
TR0 = 1;
}
void ConfigUART(unsigned int baud)
{
SCON = 0X50;
TMOD &= 0X0F;
TMOD |= 0X20;
TH1 = (11059200/12/32)/baud;
TL1 = TH1;
ET1 = 0;
ES = 1;
TR1 = 1;
}
void LedScan()
{
static unsigned char i = 0;
P0 = 0xFF;
P1 = (P1 & 0xF8) | i;
P0 = LedBuff[i];
if (i < 6)
i++;
else
i = 0;
}
void InterruptTimer0() interrupt 1
{
TH0 = T0RH;
TL0 = T0RL;
LedScan();
if(flagBuzz == 0)
BUZZ = 1;
else
BUZZ = ~BUZZ;
}
void InterruptUART() interrupt 4
{
if(RI)
{
RxdByte = SBUF;
SBUF = RxdByte;
RI = 0;
if(RxdByte == 'B')
{
flagBuzz = !flagBuzz;
}
}
if(TI)
{
TI = 0;
}
}