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

2023-05-16

写在前面

  这篇博客主要记录下单片机是如何通过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种方式:

SM0SM1波特率
00fosc/12(主振频率/12)
01可变
10fosc/32(主振频率/36)、fosc/64
11可变

  这里关于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等待我觉得也有不妥,但是不确定是不是我自己想多了,还需要以后深入学习才能得出结论。
  如有错误,欢迎评论指正,本人也是边学边总结,一方面检验自己是否真的理解,另一方面如有错误理解也能及时发现及时改正。

2020/3/19日补充:
  为什么串口的波特率与定时器有关?
  最近再次看回这篇博客不禁思考,这两者有什么联系吗?为什么要用定时器1来控制波特率为什么不能用定时器2,百度了一下发现原来51单片机串口的波特率是与定时器1的溢出率有关,这一点在上面计算波特率的时候的公式里面有体现。定时器的溢出率顾名思义就是与定时器的溢出速率有关,大概意思可能是定时器溢出一次的时间,那么晶振频率如果为11.0592MHz,时钟周期就是1/11.0592,机器周期为12/11.0592,则单片机定时器+1的时间为12/11.0592us,溢出率=溢出一次的时间=计数次数*机器周期,所以通过改变定时器的初值就能改变定时器的溢出率,也就改变了串口的波特率。
  波特率这里也顺带解释一下,就是串口每秒能接受的比特数bit,因为串口是一位一位数据按顺序发送,波特率9600就是串口每秒钟能接收的bit数为9600位,如果上位机的波特率大于9600,那么通信就会失败,因为单片机来不及接收这么多的数据量。所以串口通信要求上下位机的波特率要一致,才能保证数据传送不出错。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

单片机与上位机的串行通信 的相关文章

  • 用实际的激光雷达和里程计运行gmapping历程

    我用的激光雷达是LMS1XX系列的 xff0c 该系列的激光雷达有ROS下的驱动程序可以到网上下载编译 里程计是自己机器人发布的信息 一开始以为自己得到了这两个信息就能运行gmapping了 xff0c 结果发现自己太天真 上网查了一下原因
  • 无数四轴的航模常识

    KV 值 无刷电机KV值定义为 转速 V xff0c 意思为输入电压增加1伏特 xff0c 无刷电机空转转速增加的转速值 例如KV1000的无刷电机 xff0c 代表电压为11V的时候 xff0c 电机的空转转速为11000转 分 KV值越
  • 5G系统的性能指标

    ITU R制定了5G系统的8个性能指标和3种应用场景 流量密度 xff1a 10Tbit s km 2 连接数密度 xff1a 10 6每平方千米 时延 xff1a 空口1ms 能效 xff1a 相对于4G xff0c 100倍提升 用户体
  • find命令用法小结

    0 前言 不管我决心如何组织文件 xff0c 似乎总有无法找到文件的时候 有时是因为我不记得最初的文件名 xff0c 其他时候 xff0c 我知道名字 xff0c 但我不记得在哪里保存它了 甚至有时我需要一个我最初就没有创建的文件 但是 x
  • 在EBAZ4205 zynq7010上运行AXI_DMA中断回环测试

    在EBAZ4205 zynq7010上运行AXI DMA loop interrupt 整体的布局图 这是上面的一张接口图 下面对每个模块附上截图 AXI DMA的输出mm2s introut s2mm introut接到PS系统的中断触发
  • http 方式请求pos请求和get请求,认证方式 用户名密码

    利用apache包请求get和post请求 xff0c 用户名和密码认证 xff0c 用户名密码填则认证 xff0c 不填测不认证 利用maven构建项目 需要导入一个包 lt dependency gt lt groupId gt org
  • jenkins部署jar包到远程服务器

    jenkins部署jar包到远程服务器 1 安装Publish Over SSH插件2 配置ssh以及jenkins的ssh配置3 jenkins任务的配置4 总结 一 xff0c 安装Publish Over SSH插件 xff0c 进入
  • java把文件流转base64,然后前端展示base64图片

    一 xff0c java端 1 xff0c 项目是基于springboot的 读取本地图片 xff0c 转成base64编码字节数组字符串 xff0c 传到前端 2 xff0c 这种传输图片的方式可以用于Java后台代码生成条形码二维码 x
  • 使用nacos配置教程

    1 xff0c 访问nacos网页 默认nacos网页为 uat环境地址 进入网页 默认账号 nacos 密码 nacos 2 xff0c 配置管理 1 xff0c 页面概述 进来页面如下 xff0c 我已经建立了三个命名空间 xff0c
  • redis实现延时队列的两种方式

    背景 项目中的流程监控 xff0c 有几种节点 xff0c 需要监控每一个节点是否超时 按传统的做法 xff0c 肯定是通过定时任务 xff0c 去扫描然后判断 xff0c 但是定时任务有缺点 xff1a 1 xff0c 数据量大会慢 xf
  • go集成gin处理error

    1 gin的使用 gin在go开发web的占比是挺大的 很好用的web框架 xff0c 简单快速高效 但是呢 xff0c 在使用gin的过程中 xff0c 如何去统一去处理error和数据返回给客户端呢 xff1f 原始的做法如下 xff1
  • Okhttp3最佳使用方式

    Okhttp3最佳使用方式 一 xff0c 使用前准备 要使用okhttp3 xff0c 首先我们要引入相关依赖 xff0c 至于版本的选择 xff0c 就根据自己项目的需求了 span class token tag span class
  • Caused by: java.net.BindException: Address already in use: bind

    这个报错是因为端口号已经被占用了 xff0c 那么就有两种解决方式 xff1a 换端口号或者杀掉这个进程 一 xff0c 换端口号 这个比较简单 xff0c 就是把tomcat的端口号改一下 xff0c 换一个没用的端口号就行了 1 xff
  • copilot平替tabnine解析

    1 copilot 借着最近chatgpt大火的东风 xff0c copilot又重新火了一把 什么是copilot xff1f 直接上wiki百科上的解释 GitHub Copilot是GitHub和OpenAI合作开发的一个人工智能工具
  • idea中scala的类型自动补全与关闭:Type Annotations

    初学时建议全部打开 xff0c 知道当前操作的是什么类型的数据 xff0c 帮助更好理解 打开路径 xff1a Settings gt Editor gt Code Style gt Scala gt Type Annotations 将所
  • idea中利用免费插件翻译源码中出现的单词

    1 idea中安装Translation插件 2 有道智云注册id https ai youdao com 2 1 创建翻译实例 xff1a 信息随便填 2 2 创建应用 xff1a 信息随便填 xff0c 绑定服务时将创建的翻译实例绑定上
  • 安装centos7的时候出现无法识别路经,按ctrl+x无法保存

    修改安装盘卷标号 xff0c 不要有空格或特殊符号修改安装盘内 EFI BOOT grub cfg 第24行 xff0c 修改为对应的卷标号即可
  • mysql启动失败:Redirecting to /bin/systemctl restart mysql.service Failed to restart mysql.service: Unit

    今天出现一个小问题 xff1a root 64 linux01 hive3 1 2 service mysql start Redirecting to bin systemctl restart mysql service Failed
  • spark报错:ERROR SparkContext: Error initializing SparkContext.

    今天刚好spark history server 配好 xff0c 就出现了这个错误 xff1a ERROR SparkContext Error initializing SparkContext 最后一再仔细看 xff0c 才发现是sp
  • git:Clone failed: git: ‘permission denyed‘及Clone failed: git: ‘remote-https‘ is not a git command.

    在centos中 1 Clone failed git 39 permission denyed 39 Could not read from remote repository 问题原因 xff1a git没有设置公钥时 xff0c 将g

随机推荐