单片机与上位机的串行通信

cathy的头像

这篇博客主要记录下单片机是如何通过TXD、RXD与上位机进行数据交换的。

先介绍下51单片机中与串口通信有关的各种寄存器。

首先,上位机如果要发送数据给单片机,单片机接收到数据之后,会存入到SBUF这个发送/接收寄存器,这个寄存器非常特殊,兼具发送和接收时存放数据的功能。如果是data = SBUF,则会把SBUF接收到上位机发送过来的数据存入到data中;如果是SBUF = data,则会把单片机想要发送的数据即data中的数据送入到SBUF中,然后再通过串口发送到上位机。

在接收数据时,单片机会产生中断,不然单片机不知道什么时候接收完一位数据,这个中断叫做串口中断,服务程序是interrupt4,标志位是RI,所以进入串口中断服务程序时一定要记得把RI清零,不然程序就会一直进入串口中断服务程序。控制串口中断的寄存器叫SCON,它的每一位如下:

“单片机与上位机的串行通信"

SM0、SM1这两位与TMOD中控制定时器0、1的M0、M1类似,SM0和SM1是用来控制串口工作方式的。通过改变SM0、SM1的值可以让串行口工作在4种方式:

“单片机与上位机的串行通信"

这里关于4种工作方式如果展开了讲的话,实在太庞大,所以读者如果有疑惑可以自行百度。这里解释一下用定时器产生固定波特率的问题,我翻看了很多其他同学写的博客,发现他们有很多都不清楚方式1和3中为啥要给TH1、TL1(这里用定时器1举例)一个固定的初值。其实这个固定的初值是很多其他前辈算出来的初值,如果要自己计算也是完全可以的,公式如下(戴胜华教授《单片机原理与应用》):

“单片机与上位机的串行通信"

上式中出现到SMOD1,这一位是由电源寄存器PCON的第七位来控制的,假设我们规定好串行口工作在方式1或3的一个初值,原本波特率为9600,SMOD置1后,波特率翻倍,会变为19200,很好理解。

注意注意:这里的串行口工作方式0123跟定时器工作方式0123不是同一个东西,一定要分开。

串行口工作方式为0123任意一种都能通过使用定时器1工作在方式2来产生相应的波特率。

这里给出常用波特率相对应的定时器初值:

“单片机与上位机的串行通信"

举例假如我要让串口产生9600的波特率,我使用串口工作方式1、3,定时器1工作在方式2,那么公式就等于 波特率(9600)=2^0/32 * fosc/12 * 1/(2^k-初值) ,这里我们晶振频率为11.0592MHz,波特率为9600Hz,注意单位转换,K是计数器的计数位数,定时器方式2为八位自动重装,所以K=8,那么等式就变为了 9600 = 1/32 * 11059200/12 * 1/(256-初值) ,化简一下 1/3 = 1/256-初值,那么初值就要为253,十六进制则是0xFD,则定时器每次放进TL1的初值就要为0xFD,这样就能产生9600的波特率。

再解释一下为什么定时器中的高八位和低八位相同,以定时器1举例,如果定时器1工作在方式2即八位自动重装模式,会用低八位TL1来计数,用高八位来保存计数初值。TL1计数回到0时自动将TH1中的初值送回TL1中,完成自动重装。

回到正题,SCON中REN这一位为允许接收控制位,置0则禁止串口接收数据,置1则反之。TB8和RB8是用于方式2和3中发送和接收数据的第9位,我这里不在过多解释,需要用的时候,再仔细百度。TI是发送中断标志位,发送完毕会自动置1,在发送数据前一定要先清零TI,发送完后可根据TI来判断是否发送完毕。RI则是接收中断标志位,可以根据RI的值来判断单片机是否接受完上位机发送来的数据。

总结一下,串口发送和接收涉及到的寄存器相应的位有:PCON中的SMOD,SCON中的SM0、SM1、REN、TI、RI,TH0、TL0(TH1、TL1),TMOD中的M1、M0(控制定时器的工作方式),IE中的EA、ES(允许总中断、允许串口中断),TCON中的TR0(TR1)。

单片机接收上位机数据工作过程大致为:定时器产生一定波特率——单片机与上位机通过TXD、RXD开始通信——单片机允许串口中断,允许接收数据——单片机接收到数据,进入串口中断服务程序,并将RI置1,软件将RI清零,读取SBUF。

单片机发送数据到上位机工作过程大致为:定时器产生一定波特率——单片机与上位机通过TXD、RXD开始通信——单片机赋值给TI——单片机发送数据给上位机——上位机接收到数据。

下面这两段程序是在郭天祥《新概念51单片机C语言教程》以及参考其他同学的博客写的。

郭天祥:

#include<reg52.h>
typedef unsigned char uint8;
typedef unsigned int uint16;

uint8 flag,a,i;
uint8 code table[]="I get ";

void init(){
	TMOD = 0x20;			//定时器1工作在方式2,八位自动重装
	TH1 = 0xfd;
	TL1 = 0xfd;
	TR1 = 1;				//开启定时器1
	
	SM0 = 0;				
	SM1 = 1;				//串口工作方式1
	REN = 1;				//接收允许
	EA = 1;					//开总中断
	ES = 1;					//开串口中断
}

void main(){
	init();
	while(1){
		if(flag){
			ES = 0;				//暂时关闭串口中断,防止在处理数据时再次发生串口中断
			for(i=0;i<6;i++){
				SBUF=table[i];	//将I get放入发送寄存器
				while(!TI);		//检测是否发送完毕,发送完毕后自动置1
				TI=0;			//将发送完毕的标志位清零
			}
			SBUF=a;				//将接受到的值发回给主机
			while(!TI);			
			TI=0;
			ES=1;				//重新打开串口中断
			flag=0;
		}
	}
}
void ser()interrupt 4{			//串口中断服务程序
	RI = 0;						//中断标志位
	a = SBUF;					//将接收到的数据存入a中
	flag=1;					
}

结合按键,按一下发送一行字符:

#include <reg51.h>
typedef unsigned char uint8;
typedef unsigned int uint16;
#define key_state0 0
#define key_state1 1
#define key_state2 2
sbit key = P3^2;
uint8 key_value;
bit flag;

uint8 Buf[]="hello world!\n";
 
void delay(uint16 n)
{
	while (n--);
}
 
/*波特率为9600*/
void UART_init(void)
{
    SCON = 0x50;        //串口方式1
    TMOD = 0x21;        //定时器1使用方式2自动重载,定时器0用作按键扫描
    TH1 = 0xFD;    		//9600波特率对应的预设数,定时器方式2下,TH1=TL1
    TL1 = 0xFD;
	TH0 = 0x4C;			//50ms
	TL0 = 0x00;
    TR1 = 1;			//开启定时器,开始产生波特率
	TR0 = 1;
	ET0 = 1;
	EA  = 1;
}
 
/*发送一个字符*/
void UART_send_byte(uint8 dat)
{
	SBUF = dat;       	//把数据放到SBUF中
	while (TI == 0);	//未发送完毕就等待
	TI = 0;    			//发送完毕后,要把TI重新置0
}
 
/*发送一个字符串*/
void UART_send_string(uint8 *buf)
{
	while (*buf != '\0')
	{
		UART_send_byte(*buf++);
	}
}
 
void scankey(){
	static uint8 key_state;
	switch(key_state){
		case key_state0:
			if(!key) key_state = key_state1;
			break;
		case key_state1:
			if(!key){
				UART_send_string(Buf);
				delay(20000);
				key_state = key_state2;
			}
			else{
				key_state = key_state0;
			}
			break;
		case key_state2:
			if(key){
				key_state = key_state0;
			}
			break;
		default:break;		
	}
} 
 
void main()
{
	UART_init();
	while(1)
	{
		if(flag){
			scankey();
		}
	}
}

void timer0_isr() interrupt 1 using 0{
	TH0 = 0xDC;			//10ms
	TL0 = 0x00;
	flag = 1;
}

上面郭天祥那段代码中,只接收了上位机发送的一位数据,我又花了点时间改出了一段程序,分别是可以接受多位数据以及根据上位机送来的数据控制流水灯,两段代码就综合到一起了,注释部分是接收多位数据。

#include <reg52.h>
#define key_state0 0
#define key_state1 1
#define key_state2 2
typedef unsigned char uint8;
typedef unsigned int uint16;

sbit key = P3^2;
// uint8 table[8];
uint8 key_value;
uint8 flag,i,dat;
bit flag1;								//控制是否开始流水
//uint8 num;			
			
void init(){			
	TMOD = 0x21;						//定时器1工作在方式2,八位自动重装
	TH1 = 0xfd;			
	TL1 = 0xfd;			
	TR1 = 0xfd;							//开启定时器1
	TH0 = 0x4C;							//50ms
	TL0 = 0x00;			
	TR0 = 1;			
	ET0 = 1;			
	SM0 = 0;							
	SM1 = 1;							//串口工作方式1
	EA = 1;								//开总中断
	ES = 1;								//开串口中断
}

void scankey(){
	static uint8 key_state;
	switch(key_state){
		case key_state0:
			if(!key) key_state = key_state1;
			break;
		case key_state1:
			if(!key){
				REN = ~REN;				//允许/禁止接收上位机数据
				key_state = key_state2;
			}
			else{
				key_state = key_state0;
			}
			break;
		case key_state2:
			if(key){
				key_state = key_state0;
			}
			break;
		default:break;		
	}
}

void main(){
	init();
	P1 = 0xff;
	while(1){
		if(!REN) P1 = 0xff;				//不接收上位机数据时,关闭所有灯
		if(flag){
			ES = 0;						//暂时关闭串口中断,防止在处理数据时再次发生串口中断
			// for(i=0;i<8;i++){		//回传多位数据
				// SBUF=table[i];		//发送一位
				// while(!TI);			//检测是否发送完毕,发送完毕后自动置1
				// TI=0;				//将发送完毕的标志位清零
			// }
			SBUF = dat;
			while(!TI);
			TI = 0;
			ES=1;						//重新打开串口中断
			flag=0;	
			//num=0;					//清零接收计数
		}	
	}	
}	
void ser()interrupt 4{					//串口中断服务程序
	if(RI){	
		RI = 0;							//中断标志位
		//table[num++] = SBUF;	
		dat = SBUF;						//将接收到的数据存入dat中
		P1 = SBUF;						//将收到的16进制数赋给P1
		//if(num == 8) 					//收满8位数据,开始回传
			flag=1;
	}
}

void timer0_isr() interrupt 1 using 0{
	TH0 = 0xDC;			//10ms
	TL0 = 0x00;
	scankey();
}

目前程序中我觉得不足的地方是发送代码中的while(!TI),这里会把单片机一直占用住,按照之前按键扫描延时尽量不用delay的惯例,这里的while等待我觉得也有不妥,但是不确定是不是我自己想多了,还需要以后深入学习才能得出结论。

补充:

为什么串口的波特率与定时器有关?
  
最近再次看回这篇博客不禁思考,这两者有什么联系吗?为什么要用定时器1来控制波特率为什么不能用定时器2,百度了一下发现原来51单片机串口的波特率是与定时器1的溢出率有关,这一点在上面计算波特率的时候的公式里面有体现。定时器的溢出率顾名思义就是与定时器的溢出速率有关,大概意思可能是定时器溢出一次的时间,那么晶振频率如果为11.0592MHz,时钟周期就是1/11.0592,机器周期为12/11.0592,则单片机定时器+1的时间为12/11.0592us,溢出率=溢出一次的时间=计数次数*机器周期,所以通过改变定时器的初值就能改变定时器的溢出率,也就改变了串口的波特率。

波特率这里也顺带解释一下,就是串口每秒能接受的比特数bit,因为串口是一位一位数据按顺序发送,波特率9600就是串口每秒钟能接收的bit数为9600位,如果上位机的波特率大于9600,那么通信就会失败,因为单片机来不及接收这么多的数据量。所以串口通信要求上下位机的波特率要一致,才能保证数据传送不出错。

如有错误,欢迎评论指正,本人也是边学边总结,一方面检验自己是否真的理解,另一方面如有错误理解也能及时发现及时改正。

版权声明:本文为CSDN博主「关外蝼蚁」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36415628/article/details/103418519
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱: cathy@eetrend.com)。