单片机与上位机的串行通信
写在前面
这篇博客主要记录下单片机是如何通过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种方式:
SM0 | SM1 | 波特率 |
---|---|---|
0 | 0 | fosc/12(主振频率/12) |
0 | 1 | 可变 |
1 | 0 | fosc/32(主振频率/36)、fosc/64 |
1 | 1 | 可变 |
这里关于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; //开启定时器1SM0 = 0; SM1 = 1; //串口工作方式1REN = 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); //检测是否发送完毕,发送完毕后自动置1TI=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; //串口方式1TMOD = 0x21; //定时器1使用方式2自动重载,定时器0用作按键扫描TH1 = 0xFD; //9600波特率对应的预设数,定时器方式2下,TH1=TL1TL1 = 0xFD;TH0 = 0x4C; //50msTL0 = 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; //10msTL0 = 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; //开启定时器1TH0 = 0x4C; //50msTL0 = 0x00; TR0 = 1; ET0 = 1; SM0 = 0; SM1 = 1; //串口工作方式1EA = 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; //10msTL0 = 0x00;scankey();
}
目前程序中我觉得不足的地方是发送代码中的while(!TI),这里会把单片机一直占用住,按照之前按键扫描延时尽量不用delay的惯例,这里的while等待我觉得也有不妥,但是不确定是不是我自己想多了,还需要以后深入学习才能得出结论。
如有错误,欢迎评论指正,本人也是边学边总结,一方面检验自己是否真的理解,另一方面如有错误理解也能及时发现及时改正。
2020/3/19日补充:
为什么串口的波特率与定时器有关?
最近再次看回这篇博客不禁思考,这两者有什么联系吗?为什么要用定时器1来控制波特率为什么不能用定时器2,百度了一下发现原来51单片机串口的波特率是与定时器1的溢出率有关,这一点在上面计算波特率的时候的公式里面有体现。定时器的溢出率顾名思义就是与定时器的溢出速率有关,大概意思可能是定时器溢出一次的时间,那么晶振频率如果为11.0592MHz,时钟周期就是1/11.0592,机器周期为12/11.0592,则单片机定时器+1的时间为12/11.0592us,溢出率=溢出一次的时间=计数次数*机器周期,所以通过改变定时器的初值就能改变定时器的溢出率,也就改变了串口的波特率。
波特率这里也顺带解释一下,就是串口每秒能接受的比特数bit,因为串口是一位一位数据按顺序发送,波特率9600就是串口每秒钟能接收的bit数为9600位,如果上位机的波特率大于9600,那么通信就会失败,因为单片机来不及接收这么多的数据量。所以串口通信要求上下位机的波特率要一致,才能保证数据传送不出错。
单片机与上位机的串行通信相关推荐
- proteus中使用虚拟串口实现单片机和上位机通讯
祝大家身体健康哈,肺炎愈来愈多,希望看到这篇文章的旁友都能健健康康! 今天写一下proteus里如何使用虚拟串口仿真单片机和上位机通讯,所需要的软件有:(1)Virtual Serial Port D ...
- 单片机的上位机简单开发(1)
单片机的上位机简单开发(1) 使用的上位机开发工具为Visual Studio 2019 1.界面设计 1.1创建应用 1.2 控件 在Form1.cs(设计)界面下,点右边点击工具箱,找到Label ...
- 单片机的上位机简单开发(4)
单片机的上位机简单开发(4) 界面设计 增加了外部的自定义温度插件,chart图表插件 1.自定义温度插件 打开项目,右键单击工具箱中任意一个控件,弹出右键菜单如下: 单击"选择项" ...
- 【上位机与下位机通信】使用WIFI模块ESP8266连接单片机与上位机通信
文章目录 前言 一.ESP8266模块与STM32连接 二.单片机代码 三.总结 前言 承接上文WIFI上位机部分:[上位机]通过WIFI上位机与网络调试助手通信绘制曲线,现阶段实现了STM32单片机 ...
- 安卓wifi调试助手(单片机wifi上位机)
例程 在网上找例程 最好找个日期新一点的, 因为,太老的工程,不容易编译过. Android WIFI调试助手源码2.0 这个例程,可以很方便地改装成 wifi 开发板(或wifi相关产品) 的上位机 ...
- 编程(代码、软件)规范(适用嵌入式、单片机、上位机等)
目录 前言 第1章 文件 1.1 头文件 1.2 定义文件 第2章 注释规范 2.1 共性注释规范 2.2 文档注释规范 2.3 C语言风格注释规范 第3章 排版规范 3.1 缩进与对齐 风格 3.2 ...
- 英飞凌TC264D单片机——匿名上位机蓝牙串口发送通信协议
代码参照匿名通信协议b站教程完成 Blutetooth.h #ifndef SRC_APPSW_TRICORE_USER_BLUETOOTH_H_ #define SRC_APPSW_TRICORE_ ...
- 单板计算机作用上位机,SCB-1单板机的基本操作
SCB-1单板机的基本操作 1. 键盘操作 SCB-1单板机键盘参见附录三.键盘操作参见附录五. 1)状态设置键 MON 与 USE 监控系统采用设置待命状态的方法实现一键两 ...
- c# 火狐浏览器怎么嵌入窗体中_「C#上位机必看」你们想要的练手项目来了
最近有越来越多做电气的小伙伴开始学习C#来做上位机开发,很多人在学习一段时间后,都有这种感觉,似乎学到了很多知识,但是不知道怎么应用,因此我找了一个真实的上位机小项目,让大家来练练手.这篇文章主要对这 ...
最新文章
- 不可不看的干货——机器人自主系统的技术构建:感知、决策和执行
- python函数作为参数例题_笨办法学Python 习题 19: 函数和变量
- .net core HttpClient 使用之掉坑解析(一)
- node.js之文件读写模块,配合递归函数遍历文件夹和其中的文件
- freemarker使用说明_SpringBoot+Swagger2集成详细说明
- 12岁女孩自学成才考上亚利桑那大学,博士母亲的家庭教育造就「天才少女」...
- MongoDB(1)--简单介绍以及安装
- 二十年的编程,教会我的五件事!
- linux sudoers_Linux –将用户添加到Sudoers列表
- 什么是通讯作者?和第一作者的区别有哪些?
- lasted是什么意思_lasted是什么意思_lasted怎么读_lasted翻译_用法_发音_词组_同反义词_继续存在( last的过去式和过去分词 )-新东方在线英语词典...
- web工程引用其他java工程_并读取spring配置文件_SpringBoot项目实战(8):四种读取properties文件的方式...
- Error: Cannot find module ‘webpack‘
- 整除分块 B - Make Divisible
- SDN相关组织——ONF
- C++中switch用法的意义
- 【开发利器】中国国内可用API合集
- for update
- 浅析多元回归中的“三差”:离差(Deviation)、残差(Residual)与误差(Error)
- Python:variable in function(argument、function) name should be lowercase 处理方式
热门文章
- RoboCup仿真3D底层通信模块介绍(二)
- 时间序列平稳性检验(ADF)和白噪声检验(Ljung-Box)
- Harbor开源项目有奖征文活动开启
- azure 配置vpn_ASP.NET和Azure中配置中的私有配置数据和连接字符串的最佳做法
- QTableWidget实现复制粘贴
- 块存储、文件存储、对象存储这三者的差别
- kali安装flash player
- Excel删除指定列(VB)
- 小米重大变革:成立十个一级部门大量启用80后 向雷军汇报
- [练习]QQ/微信 表情收藏-测试用例的编写 [简洁思路]