目录
串行通信的初步认识
通信按照基本类型可以分为并行通信和串行通信。并行通信时数据的各个位同时传送,可以实现字节为单位通信,但是通信线多占用资源多,成本高。比如前边用到的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串口通信的基本应用
通信的三种基本类型
常用的通信从传输方向上可以分为单工通信、半双工通信和全双工通信三类。
- 单工通信就是指只允许一方向另外一方传送信息,而另一方不能回传信息。比如电视遥控器、收音机广播等都是单工通信技术。
- 半双工通信是指数据可以在双方之间相互传播,但是同一时刻只能其中一方发给另外一方,比如对讲机就是典型的半双工。
- 全双工通信就发送数据的同时也能够接收数据,两者同步进行,就如同电话一样,说话的同时也可以听到对方的声音。
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。
- 配置定时器T1为模式2,即自动重装模式。
- 根据波特率计算TH1和TL1的初值,如果有需要可以使用PCON进行波特率加倍。
- 打开定时器控制寄存器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; //手动清零发送中断标志位
}
}