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)。