4.3 串口通信
4.3.1 通信的概念
通信一词按照传统的理解就是信息的传输与交换。
对于单片机来说,通信则与传感器、存储芯片、外围控制芯片等技术紧密结合,成为整个单片机系统的“神经中枢”;没有通信,单片机所实现的功能仅仅局限于单片机本身,就无法通过其它设备获得有用信息,也无法将自己产生的信息告诉其它设备。如果单片机通信没处理好的话,它和外围器件的合作程度就受到限制,最终整个系统也无法完成强大的功能,由此可见单片机通信技术的重要性。
UART(Universal Asynchronous Receiver/Transmitter,即通用异步收发器)串行通信是单片机最常用的一种通信技术,通常用于单片机和电脑之间以及单片机和单片机之间的通信。
4.3.2 串口通信介绍
串口通信是按照位(bit)发送和接收,串口可以在使用一根线发送数据的同时用另一根线接收数据;这种通信方式使用的数据线少,在远距离通信中可以节约通信成本,但其传输速度比并行传输低。
串口是计算机上一种非常通用的设备通信协议,大多数计算机(不包括笔记本电脑,主要是台式主机)包含两个基于RS-232的串口。串口同时也是仪器仪表设备通用的通信协议。
图4-3-1 标准RS232串口
上面图中的串行接口叫做 RS232 接口,由于现在笔记本电脑都不带这种 9 针串口了,所以和单片机通信越来越趋向于使用USB协议虚拟的串口(就是使用USB转串口协议芯片,实现串口与USB协议互转,比如:CH340)。 标准的RS232接口里,2号引脚是接收数据口RXD,3号引脚是发送数据TXD,对于 RS232 标准来说,它的TXD 和 RXD 的电压, -3V~-15V 电压代表是 1, +3~+15V 电压代表是 0。 因此电脑的 9 针 RS232串口是不能和单片机直接连接的,需要用一个电平转换芯片 MAX232 来完成,单片机上的电压是TTL标准,TTL电平信号规定,+5V等价于逻辑“1”,0V等价于逻辑“0”。
STC90C51RC/RD+系列单片机串口通信对应的专用管脚是P3.0/RxD和P3.1/TxD,由它们组成的通信接口就叫做串行接口,简称串口。
图4-3-1 两个单片机之间串口通信示意图
图中, 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 位数据位后,然后检测到停止位,再准备下一个数据的接收。
下面图片展示了一个完整的串口数据发送接收过程:
图4-3-2 串口数据发送示意图
4.3.3 51单片机的串口寄存器介绍
STC90C51RC/RD+系列单片机内部集成有一个功能很强的全双工串行通信口(P3.0/RxD和P3.1/TxD),与传统8051单片机的串口完全兼容。设有2个互相独立的接收、发送缓冲器,可以同时发送和接收数据。
串行通信设有4种工作方式,其中两种方式的波特率是可变的,另两种是固定的,波特率由内部定时器/计数器产生。
4种工作模式,可通过软件编程对SCON中的SM0、 SM1的设置进行选择。其中模式1、模式2和模式3为异步通信,每个发送和接收的字符都带有1个起始位和1个停止位。
发送缓冲器只能写入而不能读出,接收缓冲器只能读出而不能写入,因而两个缓冲器可以共用一个地址码(99H)。两个缓冲器统称串行通信特殊功能寄存器SBUF。
串口相关的配置寄存器如下:
图4-3-3
完成基本串口通信主要使用的寄存器有4个:
SCON: 串行控制寄存器 (可位寻址)
PCON: 电源控制寄存器 (不可位寻址)
IE : 中断允许寄存器 (可位寻址)
SBUF: 发送/接收缓冲区(双向的)
4.3.3 串行控制寄存器SCON
串行控制寄存器SCON用于选择串行通信的工作方式和某些控制功能,其格式如下:
图4-3-4
TI: 数据发送完成中断请求标志位,由内部硬件自动置位(TI=1),必须用软件复位(TI=0)。
RI: 数据接收完成中断请求志位,由内部硬件置位,即RI=1,必须由软件复位,即RI=0。
SCON的所有位在复位之后全部为"0"。
REN:允许/禁止串行接收控制位。 由软件置位REN,即REN=1为允许串行接收状态,可启动串行接收器RxD,开始接收信息。软件复位REN,即REN=0,则禁止接收。
SM0/FE:当PCON寄存器中的SMOD0/PCON.6位为0时,该位和SM1一起指定串行通信的工作方式,如下表所示。
图4-3-3
SCON寄存器主要配置串口的工作方式,启动串口的接收功能,常用的工作模式是方式1(8位UART)。
串口工作在方式1时,波特率是可变的,可变的波特由定时器/计数器1或独立波特率发生器产生。
示例:
SCON=0x50; //设置为工作方式1,允许串口接收
4.3.3 电源控制寄存器PCON
电源控制寄存器PCON格式如下:
图4-3-4
SMOD: 波特率选择位。当SMOD=1,则使串行通信方式1、 2、 3的波特率加倍;复位时SMOD=0。
SMOD0: 帧错误检测有效控制位,当SMOD0=0时,与SCON寄存器中的SM0/FE位一起指定串行口的工作方式。复位时SMOD0=0。
配置示例:
PCON=0x80; //波特率加倍
4.3.4 串行口数据缓冲寄存器SBUF
STC90C51RC/RD+系列单片机的串行口缓冲寄存器(SBUF)的地址是99H,实际是2个缓冲器,写SBUF的操作完成待发送数据的加载,读SBUF的操作可获得已接收到的数据。两个操作分别对应两个不同的寄存器,1个是只写寄存器,1个是只读寄存器。
示例:
u8 Rx_Byte;
Rx_Byte = SBUF; //接收到的数据保存到变量中
SBUF = Rx_Byte; //将变量保存的数据发送出去
4.3.5 波特率设置
图4-3-5
图4-3-6
图4-3-7 常见的波特率设置
4.3.6 配置串口实现数据收发示例(波特率不加倍)
下面代码配置串口的波特率为9600,波特率不加倍,当前运行代码的单片机晶振是: 11.059200MHZ。
单片机工作在12T模式下(在12T架构下一个机器周期是12个时钟周期,也就是 12/11059200 秒)
主函数里1秒钟向串口发送一个字符串,串口开启了接收中断,如果收到数据就原样将数据再发送出去。
波特率的配置方法,在STC芯片参考手册的串口章节有示例代码(P199),可以参考修改。
(硬件平台说明:CPU是STC90C516RD 、晶振频率12MHZ 、工作在12T模式下、一个机器周期为1us时间)
示例代码:
#include <reg51.h>
/*串口初始化函数*/
void UART_Init(void)
{
PCON=0x00; //波特率不加倍
SCON=0x50; //配置串口工作在模式1(8位数据模式)
EA=1; //打开总中断
ES=1; //打开接收中断
TMOD&=0x0F; //清零T1的控制位
TMOD|=0x20; //配置T1为模式2 (8位自动重装载)
TL1=TH1=256-(11059200/12/32/9600); //计算 T1 重载值 28800
//TH1= TL1=256-(FOSC/12/32/BAUD); //计算公式 FOSC表示晶振频率 BAUD表示波特率
TR1=1; //启动 T1
}
/*串口接收中断*/
void UART_IRQHandler(void) interrupt 4
{
u8 Rx_Byte;
if(RI) //接收到字节
{
RI=0;//手动清零接收中断标志位
Rx_Byte=SBUF; //接收到的数据保存到变量中
UART_SendOneByte(Rx_Byte); //再发回给电脑端
}
}
/*发送一个字符*/
void UART_SendOneByte(u8 c)
{
SBUF = c;
while(TI==0){}
TI = 0;
}
/*发送字符串*/
void UART_SendString(u8 *p)
{
while(*p++!='\0')
{
UART_SendOneByte(*p);
}
}
int main()
{
UART_Init();
while(1)
{
UART_SendString("12345欢迎学习51单片机开发.\r\n");
DelayMs(1000);
}
}
4.3.7 配置51单片机的串口支持printf函数
下面代码中重写了putchar函数支持了标准的printf函数,因为printf函数底层会调用putchar函数进行字节发送。Keil软件上不需要做任何其他设置。putchar函数原型在stdio.h文件中有原型声明。
如果要支持scanf函数,重写getchar函数即可。
(硬件平台说明:CPU是STC90C516RD 、晶振频率12MHZ 、工作在12T模式下、一个机器周期为1us时间)
示例代码:
#include <reg51.h>
/*串口初始化函数*/
void UART_Init(void)
{
PCON=0x00; //波特率不加倍
SCON=0x50; //配置串口工作在模式1(8位数据模式)
//EA=1; //打开总中断
//ES=1; //打开接收中断
TMOD&=0x0F; //清零T1的控制位
TMOD|=0x20; //配置T1为模式2 (8位自动重装载)
TL1=TH1=256-(11059200/12/32/9600); //计算 T1 重载值 11956000
//TH1= TL1=256-(FOSC/12/32/BAUD); //计算公式 FOSC表示晶振频率 BAUD表示波特率
TR1=1; //启动 T1
}
/*发送一个字符*/
void UART_SendOneByte(u8 c)
{
SBUF = c;
while(TI==0){}
TI = 0;
}
/*重写putchar函数为了支持printf函数*/
char putchar(char c)
{
UART_SendOneByte(c);
return c;
}
int main()
{
u8 str[]="我是字符串";
u32 data1=123456;
float data2=123.356;
int data3=0x12345;
UART_Init();
while(1)
{
printf("字符串:%s\r\n",str);
printf("data1:%ld\r\n",data1); //这里的u32是typedef unsigned long u32;
printf("data2:%f\r\n",data2);
printf("十六进制:%#x\r\n",(int)data3);
DelayMs(1000);
}
}
4.3.8 配置串口实现数据收发(12M晶振、波特率加倍)
下面代码配置串口的波特率为4800,单片机晶振的频率为12MHZ。
(硬件平台说明:CPU是STC90C516RD 、晶振频率12MHZ 、工作在12T模式下、一个机器周期为1us时间)
示例代码:
#include <reg51.h>
int main()
{
u8 key;
UART_Init();
while(1)
{
key=Array_Scan();
if(key)
{
UART_SendString("12345欢迎学习51单片机开发.\r\n");
}
}
}
/*
串口初始化函数
单片机采用了12M的晶振
*/
void UART_Init(void)
{
PCON=0x80; //波特率加倍
SCON=0x50; //配置串口工作在模式1(8位数据模式)
EA=1; //打开总中断
ES=1; //打开接收中断
TMOD&=0x0F; //清零T1的控制位
TMOD|=0x20; //配置T1为模式2 (8位自动重装载)
TL1=TH1=0xF3;
//TH1=TL1=256-(FOSC/12/32/BAUD); //计算公式 FOSC表示晶振频率 BAUD表示波特率
TR1=1; //启动 T1
}
/*
串口接收中断
*/
void UART_IRQHandler(void) interrupt 4
{
u8 Rx_Byte;
if(RI) //接收到字节
{
RI=0;//手动清零接收中断标志位
Rx_Byte=SBUF; //接收到的数据保存到变量中
UART_SendOneByte(Rx_Byte); //再发回给电脑端
}
}
/*
发送一个字符
*/
void UART_SendOneByte(u8 c)
{
SBUF = c;
while(TI==0){}
TI = 0;
}
/*
发送字符串
*/
void UART_SendString(u8 *p)
{
while(*p++!='\0')
{
UART_SendOneByte(*p);
}
}
/*
重写putchar函数为了支持printf函数
*/
char putchar(char c)
{
UART_SendOneByte(c);
return c;
}