51单片机串口

该部分的笔记来自视频教程链接https://www.bilibili.com/video/BV1bt4y197NR/?spm_id_from=333.788&vd_source=b91967c499b23106586d7aa35af46413

一、51单片机串口基础介绍

51单片机串口_第1张图片
51单片机串口_第2张图片
51单片机串口_第3张图片
一般的应用层的协议中采用和校验或CRC校验,而奇偶校验还是解决基本通信中的帧格式中的校验。
51单片机串口_第4张图片

51单片机串口_第5张图片
51单片机串口_第6张图片

发送和接收缓冲寄存器都叫 SBUF 且共享逻辑地址 99H ,但在物理上是两个独立的寄存器。相当于是一个房间的前门和后门。

51单片机串口_第7张图片

后面只介绍模式1。

与串口相关的功能寄存器:

51单片机串口_第8张图片
对于 SCON ,主要用到的就是 SM0、SM1、REN、TI 和 RI ,其他几位用的不多。

对于 PCON,只用到了 SMOD 这一位,剩下的几位与串行口无关,与单片机的功耗(如进入掉电模式)有关。当 SMOD 为1时,设定的波特率会翻倍。

51单片机串口_第9张图片

对于多机通信控制,实际上也多是在应用层的通信协议中自定义多机地址来解决,而很少使用 SM2 这种方式。

对于发送中断标志位 TI 和接收中断标志位 RI ,一定要用软件来清 0 。

对于波特率的计算,可以参考下面,

51单片机串口_第10张图片

先确定波特率,再利用公式计算出 T1溢出率。再由 T1溢出率得到定时时间,再由定时时间得到配置定时器的初值。

如果想要做串行通信,一般推荐 11.0592 MHz 的晶振。因为使用 11.0592 MHz 的晶振,再计算定时器的初值时,计算的结果将会是一个整数。


51单片机串口_第11张图片

51单片机串口_第12张图片

二、安装虚拟串口

该软件已上传至 CSDN 资料库(内含安装视频)(资料和安装视频链接: https://www.bilibili.com/video/BV1u54y1s7B3/?spm_id_from=333.337.search-card.all.click&vd_source=b91967c499b23106586d7aa35af46413)。

安装好后,打开该软件,那如何使用这个软件呢?
51单片机串口_第13张图片
点击添加端口,

在这里插入图片描述

在 Proteus 仿真中,搜索并添加器件 COMPIL,

51单片机串口_第14张图片
与单片机相连接。
51单片机串口_第15张图片
双击该元器件,将该器件的 COM 口设置为 COM2。

51单片机串口_第16张图片
为什么要这样连接:
51单片机串口_第17张图片

最后,设定的为COM2口、波特率为 4800,数据位为8位,无奇偶校验位,1位停止位。

51单片机串口_第18张图片

而 COM3 则在电脑上的 STC-ISP 软件上进行选择。

51单片机串口_第19张图片
上面工具准备好之后,就可以编程代码来进行实现了。

一、单片机串行口发送数据到上位机的编程实现之查询方式实现一帧数据的发送

首先,使用 STC-ISP 软件生成指定波特率的代码。

51单片机串口_第20张图片

#include <reg52.h>
#include "delay.h"

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI  RI  (REN是允许接收)
									//		0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式
	TL1 = 0xFA;			//设置定时初始值
	TH1 = 0xFA;			//设置定时重载值
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
}

void main()
{
		UartInit();
		while(1)
		{
				// 将数据写到发送缓冲寄存器 SBUF 后,会自动发送出去。
				SBUF = 0x88; // 10位,异步串口通信     0      1000 1000      1 
			                //                    起始位     数据位     停止位
				// 当发送到停止位时,会将 TI 置 1
				while(!TI); // TI == 0 时,会一直等在这
				TI = 0;     // 手动清 0
				Delay_Xms(1000);
		}
}

将波特率修改为 11.0592 MHz,

51单片机串口_第21张图片

后点击运行,在 STC-ISP 软件中的接收缓冲器中选择 HEX 模式。

51单片机串口_第22张图片

在左侧栏的虚拟仪器中可以添加终端来进行查看。

51单片机串口_第23张图片
这个可以代替 STC-ISP 这个软件的串口接收器,因此需要将其的 RXD 连接单片机的 TXD, TXD 连接单片机的 RXD。同时,也需要进行参数设置( 双击 COMPIM )。

在这里插入图片描述

之后,点击运行,将会弹出窗口

51单片机串口_第24张图片

如果没有窗口弹出或者将窗口给 × (关闭)掉了,就采用下面的解决方法:

51单片机串口_第25张图片

如果没有这个选项卡,则 先停止仿真,

51单片机串口_第26张图片
之后,应该就可以了。

最后,如果接收到的是乱码,可能是因为显示设置的问题,双击或右键窗口

51单片机串口_第27张图片

勾选 Hex 显示模式,否则,是以文本模式进行显示。

如果在程序中将 0x88 修改为 ‘a’ ,则可以尝试下,

在这里插入图片描述

以文本模式进行显示:

51单片机串口_第28张图片
将其封装成函数,

发送一个字节:

51单片机串口_第29张图片

发送一个字符串:

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}

二、单片机串口发送一串数据到上位机(使用中断的方式实现)及printf串口输出重定向的实现

这里将波特率改为 9600 ,
51单片机串口_第30张图片

代码如下:

#include <reg52.h>
#include "delay.h"

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式
	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
	EA  = 1;        //打开串口中断
}

void main()
{
		UartInit();
		while(1)
		{
				// 将数据写到发送缓冲寄存器 SBUF 后,会自动发送出去。
				SBUF = 0x86; 
				Delay_Xms(1000);
		}
}

void usart_isr() interrupt 4
{
		if(TI)
		{	
				TI = 0;     // 手动清 0
		}
}	

串口重定向:keil中的 printf 不能打印到串口,因此,需要做一个重定向,做法是重写 putchar 这个函数。标准C语言中,该函数是被输出到电脑屏幕的。

如何重写呢?

char putchar(char c)
{
	sendByte(c);
	return 0;
}

printf 通过调用该函数来实现向串口输出数据。

此外,使用 printf 函数时,还需要添加头文件 #include

调用时,可如下所示。

在这里插入图片描述

更为方便的是可以使用格式化输出参数。

51单片机串口_第31张图片
为了方便显示,可以加上回车换行符。

运行结果,如下:
51单片机串口_第32张图片
51单片机串口_第33张图片
有问题。这是格式化输出的问题。

参考手册

51单片机串口_第34张图片

51单片机串口_第35张图片

所以,修改代码,如下。

在这里插入图片描述

之后,输出正常。

总结下:基于 sendByte 函数实现的串口发送(sendString、putchar、printf)是利用的查询的方式实现的。

三、单片机串行口从上位机接收(中断的方式)一帧数据的编程并通过(上面讲解的中断和查询发送的方式)发送给电脑,然后通过串口助手显示的实现方法

12MHz,确实误差较大。

#include <reg52.h>

unsigned char recv_data;

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值

	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
	EA  = 1;        //打开总中断
}

void main()
{
		UartInit();
		while(1);
}

void usart_isr() interrupt 4
{
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
			  recv_data = SBUF; 
				recv_data = recv_data + 1;
				SBUF = recv_data;
		}	
		if(TI)  
		{
				TI = 0;
		}
}	

这里,发现一个问题,就是在仿真中,

51单片机串口_第36张图片

如果这根线是连着的话,是看不到实验效果的。

51单片机串口_第37张图片

还有,就是在这个实验中,也要使用 HEX模式进行显示。

可以接着将上面的代码改为查询发送的方式,这样就需要多定义一个变量作为标志位。

完整代码如下:

#include <reg52.h>

unsigned char recv_flag = 0;
unsigned char recv_data;

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值

	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
	EA  = 1;        //打开总中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void main()
{
		UartInit();
		while(1)
		{
			if(recv_flag == 1)
			{
					recv_flag = 0;
					recv_data = recv_data + 1;
					sendByte(recv_data);
			}
		}
}

void usart_isr() interrupt 4
{
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
			  recv_data = SBUF; 
				recv_flag = 1 ;
		}	
}	

运行效果与上图一样。

此外,还可以进行扩展,即根据接收的指令执行响应。

51单片机串口_第38张图片

运行效果如下:

51单片机串口_第39张图片

此外,也可以使用字母来作为 switch 的选择条件,

在这里插入图片描述
这时,就需要采用文本发送和接收的方式来进行。

51单片机串口_第40张图片

但是,这种方式还是太过于简单,因为工程上会传很多数据。

四、单片机串行口从上位机接收(中断的方式)一串数据的编程

关键:以一个特定的字符作为结束符。

#include <reg52.h>


#define MAX_REV_NUM 10 

unsigned char recv_flag = 0;

unsigned char recv_length = 0;  // 接收字符数的实际长度

unsigned char recv_buf[MAX_REV_NUM]; // 接收缓冲区, MAX_REV_NUM 要大于接收的字符数

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
  
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
	EA  = 1;        //打开总中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}

void main()
{
		unsigned char i;
	
		UartInit();
		while(1)
		{
			if(recv_flag == 1)
			{
					recv_flag = 0;
					for(i = 0; i<recv_length; i++)
					{
						sendByte(recv_buf[i]);
					}
			}
		}
}

void usart_isr() interrupt 4
{
		static unsigned char recv_cnt = 0;
		unsigned char temp;
	
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
				temp = SBUF;
				if(temp != 0xFF)// 必须以0xFF作为此次发送的结束符
				{
					if(recv_cnt < MAX_REV_NUM)
					{
						recv_buf[recv_cnt++] = temp;
					}
				}
				else
				{
						recv_flag = 1 ;
						recv_length = recv_cnt ;
						recv_cnt = 0;
				}
		}	
}	

这个程序是有瑕疵的,比如,在要发送的数据后面连续发送两个结束符,就会出现问题。

51单片机串口_第41张图片
我分析,应该是串口中断抢占 while 循环,主循环只来的及发送一个 01,就被第二个 0D 给终止了,所以说该程序是有瑕疵的。

后面,将采用串行口定时中断实现超时接收一串数据的编程,将更加的实用。

五、串行口定时中断实现超时接收一串数据的编程

51单片机串口_第42张图片

5.1 编程思路

假设有两个数据包,第一个数据包有四个数据帧,第二个数据包有五个数据帧。
51单片机串口_第43张图片

那如何利用超时检测来判断第一个数据包是否已经被接收完毕呢?

首先是根据波特率来计算出接收一个字节(数据帧)所需要的时间。一个字节有 10 位,那在9600的波特率下接收完这一个字节的时间就是

51单片机串口_第44张图片
从而在第二帧数据与第一帧数据相差的时间不会超过一个数据帧的时间长度,也就是 1.042 ms 。

当接收完第一个数据包的最后一个数据帧后,如果超过 1.042 ms, 没有数据帧发送来,就说明第一个数据包接受完毕,准备接收第二个数据包或结束接收。

51单片机串口_第45张图片
在程序中实现时,可以在当接收到第一个数据帧后,开启定时器来进行计数,当接收到下一个数据后,就将计数器清零。(有点像喂狗的过程),这样当没有数据发送过来,计数器就不会被清零,从而超过设定的数值。(一般是3-5倍,计算的1.042ms,所以是3-5ms)

5.2 程序代码

main.c

#include <reg52.h>
#include "time.h"

#define MAX_REV_NUM  10  // 最大接收数据量为 10

unsigned char start_timer = 0;

unsigned char recv_flag = 0;

unsigned char recv_timer_cnt = 0; // 定时器启动时间计数

unsigned char recv_buf[MAX_REV_NUM]; // 接收缓冲区, MAX_REV_NUM 要大于接收的字符数

unsigned char recv_cnt = 0;

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
  
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
	EA  = 1;        //打开总中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}

void clear_recvBuffer(unsigned char *buf)
{
	unsigned char i;
	for(i = 0; i < MAX_REV_NUM; i++)
	{
		buf[i] = 0;
	}
}

void main()
{
		unsigned char i;
	
		Timer0_Init();
		UartInit();
		EA = 1;
		while(1)
		{
			if(recv_flag == 1)
			{
					recv_flag = 0;
				  start_timer = 0; // 关定时器
					sendString(recv_buf);
					clear_recvBuffer(recv_buf);
			}
		}
}

void usart_isr() interrupt 4
{
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
				start_timer = 1; // 1、接收第一帧数据的时候,打开软件定时器去计数

				if(recv_cnt < MAX_REV_NUM)
				{
						recv_buf[recv_cnt++] = SBUF; // 2、接收数据到数据缓冲区,注意缓冲区的大小和范围
				}		
				else
				{
						recv_cnt = MAX_REV_NUM;
				}
				recv_timer_cnt = 0;	// 3、当接收到下一个数据后,就将计数器清零,喂狗。
		}	
}	

time.h

#ifndef _TIME_H_
#define _TIME_H_

#include <reg52.h>

#define MAX_REV_TIME 5 

// 函数的声明
void Timer0_Init(void);		//1毫秒@11.0592MHz

#endif

time.c

#include "time.h"

extern unsigned char recv_cnt;

extern unsigned char start_timer ;

extern unsigned char recv_flag;

extern unsigned char recv_timer_cnt ; // 定时器启动时间计数

void Timer0_Init(void)		//1毫秒@11.0592MHz
{

	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	
	TL0 = 0x66;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值

	TF0 = 0;					//清除TF0标志
	ET0 = 1;

	TR0 = 1;					//定时器0开始计时
}

void timer_isr() interrupt 1
{
		TR0 = 0;				//定时器0开始计时
		
		if(start_timer == 1)
		{
			  recv_timer_cnt++;  // 1、累加定时时间计数器
			  if(recv_timer_cnt > MAX_REV_TIME) // 2、判断定时时间是否超过了设定的最大的阈值,超过则说明等待一段时间后没有新的数据到,我们判定一个数据包接收完毕 
				{
					recv_timer_cnt = 0; // 3、清除定时计数器,处理数据(在主循环中),清除buffer(放到数据处理之后)
					recv_cnt = 0;
					recv_flag = 1;
				}
		}
	
		TL0 = 0x66;				//设置定时初始值
		TH0 = 0xFC;				//设置定时初始值

		TR0 = 1;				//定时器0开始计时
}

六、判断数据帧头(非即时接收,匹配接收缓冲区的方式)来接收一串数据的串口通信程序编写

在上面程序的基础上进行完成。

如何选择数据的帧头呢?

一般有两种情况,一种是所要接收的数据是可预测的,此时可以人为的选择帧头,使得帧头和真正有价值的数据位完全不匹配。另外一种是所要接收的数据是不可预测的,具有随机性。此时,可以采用多个数据帧来构成帧头,即增加特征字节的长度来构成帧头。

本次实现的功能如下:

自定义一个数据帧头为 0x55 ,0xAA ,0x55,然后解析数据位,

51单片机串口_第46张图片

其他数据为无效数据包,会被丢掉。

一般的用户自定义协议数据包会包含:

51单片机串口_第47张图片
程序源码如下:

main.c

#include <reg52.h>
#include "time.h"
#include "led.h"

#define MAX_REV_NUM  10  // 最大接收数据量为 9 ,最后一位是 '\0'

unsigned char start_timer = 0;

unsigned char recv_flag = 0;

unsigned char recv_timer_cnt = 0; // 定时器启动时间计数

unsigned char recv_buf[MAX_REV_NUM]; // 接收缓冲区, MAX_REV_NUM 要大于接收的字符数

unsigned char recv_cnt = 0;

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
  
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
	EA  = 1;        //打开总中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}

/* 将接收数据缓冲区清零 */
void clear_recvBuffer(unsigned char *buf)
{
	unsigned char i;
	for(i = 0; i < MAX_REV_NUM; i++)
	{
		buf[i] = 0;
	}
}

void uart_service(unsigned char *buf)
{
		unsigned char recv_move_index = 0; // 查找的索引
	
		if(recv_flag)
		{
				recv_flag = 0;
				start_timer = 0; // 关定时器
			
				sendString(buf);
				
				/* 如果接收到的数据是 0x55 0xAA 0x55 0x01 0x02 0x55 0xAA 0x55 0x02 0x01 ,在第一个就响应退出了 */
				/* 接收到的一串数据中只要满足 0x55 0xAA 0x55 0x02 0x01 或 0x55 0xAA 0x55 0x01 0x02 就响应 */
				while((recv_cnt >= 5) && (recv_move_index <= 4)) // 5 + 4 = 9,保障数组不会溢出,而第9位一定是'\0',所以从第四位就停止处理了
				// 原程序是 while((recv_cnt >= 5) && (recv_move_index <= recv_cnt)),因为当 recv_move_index > 5 时,buf会溢出,程序会无响应,特修改。
				{
						if((buf[recv_move_index + 0] == 0x55) && (buf[recv_move_index+1] == 0xAA) && (buf[recv_move_index+2] == 0x55 ))
						{
								if((buf[recv_move_index+3] == 0x01) && (buf[recv_move_index+4] == 0x02))
								{
										LED1 = 0;
										break;
								}
								if((buf[recv_move_index+3] == 0x02) && (buf[recv_move_index+4] == 0x01))
								{
										LED1 = 1;
										break;
								}
								/* 源程序是在这写了break; 但是存在假帧头的问题即:55 AA 55 AA 55 01 02 66 77 不满足就直接退出了,不会接着往下匹配了 */
						}
						recv_move_index++;
				}
			
				recv_cnt = 0;
				clear_recvBuffer(buf);
			}
}

void main()
{
		unsigned char i;
	
		led_init();
		Timer0_Init();
		UartInit();
		EA = 1;
		while(1)
		{
			uart_service(recv_buf);
		}
}

void usart_isr() interrupt 4
{
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
				start_timer = 1; // 1、接收第一帧数据的时候,打开软件定时器去计数

				if(recv_cnt < MAX_REV_NUM - 1)
				{
						recv_buf[recv_cnt++] = SBUF; // 2、接收数据到数据缓冲区,注意缓冲区的大小和范围
				}		
				else
				{
						recv_cnt = MAX_REV_NUM - 1;
				}
				recv_buf[recv_cnt] = '\0';
				recv_timer_cnt = 0;	// 3、当接收到下一个数据后,就将计数器清零,喂狗。
		}	
}	

time.h

#ifndef _TIME_H_
#define _TIME_H_

#include <reg52.h>

#define MAX_REV_TIME 5 

// 函数的声明
void Timer0_Init(void);		//1毫秒@11.0592MHz

#endif

time.c

#include "time.h"

extern unsigned char recv_cnt;

extern unsigned char start_timer ;

extern unsigned char recv_flag;

extern unsigned char recv_timer_cnt ; // 定时器启动时间计数

void Timer0_Init(void)		//1毫秒@11.0592MHz
{

	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	
	TL0 = 0x66;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值

	TF0 = 0;					//清除TF0标志
	ET0 = 1;

	TR0 = 1;					//定时器0开始计时
}

void timer_isr() interrupt 1
{
		TR0 = 0;				//定时器0开始计时
		
		if(start_timer == 1)
		{
				recv_timer_cnt++;  // 1、累加定时时间计数器
			  if(recv_timer_cnt > MAX_REV_TIME) // 2、判断定时时间是否超过了设定的最大的阈值,超过则说明等待一段时间后没有新的数据到,我们判定一个数据包接收完毕 
				{
					recv_timer_cnt = 0; // 3、清除定时计数器,处理数据(在主循环中),清除buffer(放到数据处理之后)
					
					recv_flag = 1;
				}
		}
	
		TL0 = 0x66;				//设置定时初始值
		TH0 = 0xFC;				//设置定时初始值

		TR0 = 1;				//定时器0开始计时
}

总结:这个程序是在上个程序的基础功能之上(定时中断实现超时接收一串数据), 为了识别是否为有效数据,加了帧头,也就是多了几个个特定字符(为了增加随机性, 让数据中不出现冲突),然后在接收到数据后,通过逐个判断接收缓冲区中的内容,通过锁定帧头位置,从而获取我们所需要的数据,最后判断处理。此外,为了程序更加稳定,在视频程序的基础上做了两处修改,

一 是考虑数组结束标志:增加了 recv_buf[recv_cnt] = ‘\0’;

二 是考虑数组溢出问题:修改为 while((recv_cnt >= 5) && (recv_move_index <= 4))

三 是考假帧头的情况:在匹配帧头并执行条件时,再 break 退出。

程序上传至CSDN(判断数据帧头(非即时接收,匹配接收缓冲区的方式)来接收一串数据的串口通信程序编写.zip)。

七、串口中断中即时解析数据帧头的通信程序

上面是在接收完数据之后才开始解析数据(在 while 循环中),但是这种方式并不适合于实时性要求高的项目。可以使用在中断中边接收边解析的方式。

自定义一个数据协议如下,

51单片机串口_第48张图片
状态机的编程思想(switch…case…语句)。

记录遇到的问题:是关于蜂鸣器的。因为将 led 的值赋值给了 beep ,导致蜂鸣器响的始终不对。。。从而让我怀疑了人生。折腾了一下午,服气了。。。

main.c

#include <reg52.h>
#include "time.h"
#include "led.h"
#include "beep.h"
#include "uart.h"

unsigned char recv_flag = 0;

extern unsigned char recv_buf[MAX_REV_NUM];

void main()
{
		led_init();
		beep_init();
		Timer0_Init();
		UartInit();
		EA = 1;
		while(1)
		{
				if(recv_flag)
				{
						recv_flag = 0;
						sendString(recv_buf);
						clear_recvBuffer(recv_buf, MAX_REV_NUM);
				}
		}
}

time.c

#include "time.h"

extern unsigned char recv_cnt;
extern unsigned char recv_flag;

extern unsigned int led_data;
extern unsigned int led_cnt;

extern unsigned int beep_data;
extern unsigned int beep_cnt;
extern unsigned char beep_flag;

void Timer0_Init(void)		//1毫秒@11.0592MHz
{

	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	
	TL0 = 0x66;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值

	TF0 = 0;					//清除TF0标志
	ET0 = 1;

	TR0 = 1;					//定时器0开始计时
}

void timer_isr() interrupt 1
{
		TR0 = 0;				//定时器0开始计时
		
		if(led_cnt < led_data)
		{
				led_cnt++;
				LED1 = 0;
		}
		else
		{
				LED1 = 1;
		}
		
//		if(beep_flag)
//		{
			if(beep_cnt > 0)
			{	
					beep_cnt--;
					BEEP = 0;
			}
			else
			{
//					beep_flag = 0;
					BEEP = 1;
			}
//		}
		
//		if(beep_cnt < beep_data)
//		{
//				beep_cnt++;
//				BEEP = 0;
//		}
//		else
//		{
//				BEEP = 1;
//		}
	
		TL0 = 0x66;				//设置定时初始值
		TH0 = 0xFC;				//设置定时初始值

		TR0 = 1;				//定时器0开始计时
}

time.h

#ifndef _TIME_H_
#define _TIME_H_

#include <reg52.h>
#include "led.h"
#include "beep.h"

// 函数的声明
void Timer0_Init(void);		//1毫秒@11.0592MHz

#endif

uart.c

#include "uart.h"

extern unsigned char recv_flag;

unsigned char recv_buf[MAX_REV_NUM]; // 接收缓冲区, MAX_REV_NUM 要大于接收的字符数

unsigned char recv_cnt = 0;

unsigned char machine_step = 0;

unsigned int led_data;
unsigned int led_cnt;

unsigned char beep_flag = 0;
unsigned int beep_data;
unsigned int beep_cnt;

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
  
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}

/* 将接收数据缓冲区清零 */
void clear_recvBuffer(unsigned char *buf, unsigned char len)
{
	unsigned char i;
	for(i = 0; i < len; i++)
	{
		buf[i] = 0;
	}
}

void usart_isr() interrupt 4
{
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
				switch(machine_step)			
				{
					case 0:									// 状态一
						recv_buf[0] = SBUF;
						if(recv_buf[0] == 0xAA)
						{
								machine_step = 1; // 在状态二接收下一帧的数据
						}
						else
						{
								machine_step = 0;
						}
						break;
						
					case 1:
						recv_buf[1] = SBUF;		// 状态二
						if(recv_buf[1] == 0x55)
						{
								machine_step = 2; 
								recv_cnt = 2;
						}
						else
						{
								machine_step = 0;
						}
						break;
						
					case 2:
						recv_buf[recv_cnt] = SBUF;
						recv_cnt++;
						if(recv_cnt > 4) // 三帧数据接收完毕
						{
								machine_step = 3;
						}
						else
						{
								machine_step = 2;
						}
						break;	
						
					case 3:	
						recv_buf[recv_cnt] = SBUF;
						if(recv_buf[recv_cnt] == 0x0D) // 将数据一次性接收过来
						{
								switch(recv_buf[2])
								{
										case 1:
											led_data = recv_buf[3];
											led_data = led_data << 8;
											led_data = led_data + recv_buf[4];
											led_cnt  = 0; // 目的是使 LED 点亮上述接收的数据时间
										break;
											
										case 2:
											beep_data = recv_buf[3];
											beep_data = beep_data << 8;
											beep_data = beep_data + recv_buf[4];
//											if(beep_flag == 0)
//											{
//												beep_flag = 1;
												beep_cnt  = beep_data;
//											}
//											beep_cnt  = 0;
										break;
										
										default:
											break;
								}
								machine_step = 0;
								recv_cnt = 0;
								recv_flag = 1; // 接收完一串数据,标志位置1
						}
						else // 重新接收
						{
								machine_step = 0;
								recv_cnt = 0;
						}
					break;	
					
					default:	
						break;	
				}
			
		}	
}	

uart.h

#ifndef _UART_H_
#define _UART_H_

#include <reg52.h>

#define MAX_REV_NUM  10  // 最大接收数据量为 9 ,最后一位是 '\0'

// 函数声明
extern void UartInit(void);		//[email protected]

extern void sendString(unsigned char *dat);

extern void clear_recvBuffer(unsigned char *buf, unsigned char len);

#endif

beep.c 和 led.c 省略。

总结:该代码上传至 CSDN 资料库,该部分代码并没有延续上一个程序的定时中断实现超时接收一串数据的编程方式,而是通过判断帧头和帧尾的方式。

程序还存在 Bug ,比如:sendString 函数的问题。比如

51单片机串口_第49张图片

这是因为 sendString 函数判断到 ‘\0’ 就退出的原因,但是程序执行是没有问题的。

结论:该代码思路常规应用(知道对方大概会发过来约定好的协议数据)是没有问题的。

八、串口中断即时解析用户自定义通讯协议的编程实现——接收数据字节固定的情况

主要实现的功能是在前面的代码(中断即时解析用户自定义通讯协议的编程)之上,增加和校验或异或校验。

修改的地方有:把原先在中断中对 数据包中有效数值 的处理放到主程序的 while 循环中。

接收缓冲区是不需要保存帧头、帧尾以及校验等这些数据的。直接在中断中进行判断和解析即可。

51单片机串口_第50张图片
main.c

#include <reg52.h>
#include "time.h"
#include "led.h"
#include "beep.h"
#include "uart.h"

unsigned char recv_flag = 0;

extern unsigned char recv_buf[MAX_REV_NUM];

unsigned int led_data;
unsigned int led_cnt;

unsigned int beep_data;
unsigned int beep_cnt;

void main()
{
		led_init();
		beep_init();
		Timer0_Init();
		UartInit();
		EA = 1;
		while(1)
		{
				if(recv_flag)
				{
						recv_flag = 0;
						sendString(recv_buf);
					
						switch(recv_buf[0])
						{
								case 1:
									led_data = recv_buf[1];
									led_data = led_data << 8;
									led_data = led_data + recv_buf[2];
									led_cnt  = 0; // 目的是使 LED 点亮上述接收的数据时间
								break;
									
								case 2:
									beep_data = recv_buf[1];
									beep_data = beep_data << 8;
									beep_data = beep_data + recv_buf[2];
									beep_cnt  = beep_data;
								break;
								
								default:
									clear_recvBuffer(recv_buf, MAX_REV_NUM);
									break;
						}	
				}
		}
}

uart.c

#include "uart.h"

extern unsigned char recv_flag;

unsigned char recv_buf[MAX_REV_NUM]; // 接收缓冲区, MAX_REV_NUM 要大于接收的字符数

unsigned char recv_cnt = 0;

unsigned char machine_step = 0;


void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
  
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}
	
/* 将接收数据缓冲区清零 */
void clear_recvBuffer(unsigned char *buf, unsigned char len)
{
	unsigned char i;
	for(i = 0; i < len; i++)
	{
		buf[i] = 0;
	}
}

void usart_isr() interrupt 4
{
		unsigned char recv_data;  // 接收到的数据不再放在接收缓冲区中,而是直接进行处理。
	
		static unsigned char sum_check; // 用于进行 和 校验
		static unsigned char xor_check; // 用于进行异或校验
	
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
			
				recv_data = SBUF;
			
				switch(machine_step)			
				{
					case 0:									// 状态一
						
						sum_check = 0;
						xor_check = 0;
					
						if(recv_data == 0x55)
						{
								machine_step = 1; // 在状态二接收下一帧的数据
						}
						else
						{
								machine_step = 0;
						}
						break;
						
					case 1:
						if(recv_data == 0xAA)// 状态二
						{
								machine_step = 2; 
								recv_cnt = 0; 
						}
						else
						{
								machine_step = 0;
						}
						break;
						
					case 2:// 开始和校验 异或校验
						sum_check += recv_data;
						xor_check ^= recv_data;
						recv_buf[recv_cnt] = recv_data;
						recv_cnt++;
						if(recv_cnt > 2) // 三帧数据接收完毕
						{
								machine_step = 3;
						}
						else
						{
								machine_step = 2;
						}
						break;	
						
					case 3:	
						if(sum_check == recv_data) // 将数据一次性接收过来
						{
								machine_step = 4;
						}
						else // 重新接收
						{
								machine_step = 0;
								sendByte('S');
						}
					break;

					case 4:	
						if(xor_check == recv_data) // 将数据一次性接收过来
						{
								recv_flag = 1; // 接收完一串数据,标志位置1
						}
						else
						{
								sendByte('X');
						}
						machine_step = 0;
						recv_cnt = 0; 
						
					break;					
					
					default:	
						break;	
				}
			
		}	
}	

为了进一步保障代码的可行性以及增加错误提示功能,代码中还增加了特征码或提示信息的形式(直接使用 sendByte 函数发送即可)来提示用户因为什么原因造成通信出错。(比如:帧头、帧尾、校验等等)

这样,发现视频中的代码有一个问题,就是

校验字清零应该放到 step 0 处无条件执行,因为有可能 执行不到 step 4 .

其他的代码没变。

测试代码,为了方便校验位的计算,有网址链接: http://www.metools.info/code/c128.html。需要注意,这里使用的 hex 模式,如果是按照文本格式发的字符数据,就使用 ASCII 模式。

已验证,代码运行正常。
51单片机串口_第51张图片

最终的代码已上传 CSDN 。

九、串口中断即时解析用户自定义通讯协议的编程实现——协议内带数据长度及接收应答处理

通信协议如下:
51单片机串口_第52张图片
然后将接收到的有效数据用 OLED 显示出来。

main.c

//	 
//  功能描述   : OLED 4接口演示例程(51系列)
//              说明: 
//              ----------------------------------------------------------------
//              GND    电源地
//              VCC  接5V或3.3v电源
//              SCL  P17(SCL)
//              SDA  P16(SDA)
//              RES  P15 注:SPI接口显示屏改成IIC接口时需要接RES引脚
//                           IIC接口显示屏用户请忽略
//              ----------------------------------------------------------------
//******************************************************************************/

#include "stc12c5a60s2.h"		 
#include "oled.h"
#include "bmp.h"
#include "time.h"
#include "uart.h"
#include "led.h"
#include "beep.h"
#include "display.h"

extern unsigned char recv_flag;

void main(void)
{	
	led_init();
	beep_init();
	Timer0_Init();
	UartInit();
	OLED_Init();//初始化OLED
	
	EA = 1;
	WindowShow();	
	while(1) 
	{		
		if(recv_flag)
		{
			recv_flag = 0;
			Rec_ResolutionShow();
		}
	}	 
}

uart.c

#include "uart.h"

unsigned char code recv_correct[]    = {0x55, 0xAA, 0x80, 0x00, 0x80, 0x80};
unsigned char code sum_check_error[] = {0x55, 0xAA, 0x81, 0x00, 0x81, 0x81}; 
unsigned char code xor_check_error[] = {0x55, 0xAA, 0x82, 0x00, 0x82, 0x82}; 

unsigned char recv_flag;

unsigned char recv_buf[MAX_REV_NUM]; // 接收缓冲区, MAX_REV_NUM 要大于接收的字符数

unsigned char recv_cnt = 0;
unsigned char recv_length = 0; // 保存接收字节的长度

unsigned char machine_step = 0;


void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
  
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}
	
/* 将接收数据缓冲区清零 */
void clear_recvBuffer(unsigned char *buf, unsigned char len)
{
	unsigned char i;
	for(i = 0; i < len; i++)
	{
		buf[i] = 0;
	}
}

void usart_isr() interrupt 4
{
		unsigned char i;
		unsigned char recv_data;  // 接收到的数据不再放在接收缓冲区中,而是直接进行处理。
	
		static unsigned char sum_check; // 用于进行 和 校验
		static unsigned char xor_check; // 用于进行异或校验
	
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
			
				recv_data = SBUF;
			
				switch(machine_step)			
				{
					case 0:									// 状态一
						
						sum_check = 0;
						xor_check = 0;
					
						if(recv_data == 0x55)
						{
								machine_step = 1; // 在状态二接收下一帧的数据
						}
						else
						{
								machine_step = 0;
						}
						break;
						
					case 1:
						if(recv_data == 0xAA)// 状态二
						{
								machine_step = 2; 
								recv_cnt = 0; 
						}
						else
						{
								machine_step = 0;
						}
						break;
						
					case 2:// 开始和校验 异或校验
						sum_check = recv_data;
						xor_check = recv_data;
						recv_buf[recv_cnt] = recv_data; // 接收的是数据类型 — 放入接收缓冲区
						recv_cnt++;
						machine_step = 3;
						break;	
						
					case 3:	
						sum_check += recv_data;
						xor_check ^= recv_data;
						recv_length = recv_data;        // 接收的是数据长度 — 不放入接收缓冲区
						machine_step = 4;
					break;
					
					case 4:	
						sum_check += recv_data;
						xor_check ^= recv_data;
						recv_buf[recv_cnt] = recv_data;
						
						if(recv_cnt == recv_length)
						{
								machine_step = 5;
						}
						else
						{
								machine_step = 4;           // 继续接收
						}
						recv_cnt++;  // 如果放到 if 前面去,if条件判断中就需要使用 >
					break;
						
					case 5:	
						if(sum_check == recv_data) // 和校验正确
						{
								machine_step = 6;
						}
						else // 重新接收
						{
								machine_step = 0;
								for(i = 0; i<6; i++)
								{
										sendByte(sum_check_error[i]);
								}
						}
					break;

					case 6:	
						if(xor_check == recv_data) // 异或校验正确
						{
								recv_flag = 1; // 接收完一串数据,标志位置1
								for(i = 0; i<6; i++)
								{
										sendByte(recv_correct[i]);
								}
						}
						else
						{
								for(i = 0; i<6; i++)
								{
										sendByte(xor_check_error[i]);
								}
						}
						machine_step = 0;
						recv_cnt = 0; 
					break;					
					
					default:	
						machine_step = 0;
						recv_cnt = 0; 
						break;	
				}
		}	
}	

uart.h

#ifndef _UART_H_
#define _UART_H_

#include "stc12c5a60s2.h"	

#define MAX_REV_NUM  10  // 最大接收数据量为 9 ,最后一位是 '\0'

// 函数声明
extern void UartInit(void);		//[email protected]

extern void sendString(unsigned char *dat);

extern void clear_recvBuffer(unsigned char *buf, unsigned char len);

#endif

display.c (使用的是 IIC 接口的 OLED 显示屏)

#include "display.h"

extern unsigned char recv_buf[MAX_REV_NUM];

unsigned char display_buf[MAX_REV_NUM*2];

void WindowShow(void)
{
		INVERSE_OLED_ShowChinese(0,0,7,16);
		INVERSE_OLED_ShowChinese(16,0,1,16);//景
		INVERSE_OLED_ShowChinese(32,0,2,16);//园
		INVERSE_OLED_ShowChinese(48,0,3,16);//电
		INVERSE_OLED_ShowChinese(64,0,4,16);//子
		INVERSE_OLED_ShowChinese(80,0,5,16);//科
		INVERSE_OLED_ShowChinese(96,0,6,16);//技
		INVERSE_OLED_ShowChinese(112,0,7,16);
}

void Rec_ResolutionShow(void)
{
		OLED_ShowString(0,3,"      ",16);
		switch(recv_buf[0])
		{
				case 1:
						display_buf[0] = (recv_buf[1] >> 4) + '0';
						display_buf[1] = (recv_buf[1] & 0x0F) + '0';
						display_buf[2] ='\0';
						
				break;
					
				case 2:
						display_buf[0] = (recv_buf[1] >> 4) + '0';
						display_buf[1] = (recv_buf[1] & 0x0F)+ '0';
						display_buf[2] = (recv_buf[2] >> 4)+ '0';
						display_buf[3] = (recv_buf[2] & 0x0F)+ '0';
						display_buf[4] ='\0';
				break;
				
				case 3:
						display_buf[0] = (recv_buf[1] >> 4)+ '0';
						display_buf[1] = (recv_buf[1] & 0x0F)+ '0';
						display_buf[2] = (recv_buf[2] >> 4)+ '0';
						display_buf[3] = (recv_buf[2] & 0x0F)+ '0';
						display_buf[4] = (recv_buf[3] >> 4)+ '0';
						display_buf[5] = (recv_buf[3] & 0x0F)+ '0';
						display_buf[6] ='\0';
				break;
				
				default:
						clear_recvBuffer(recv_buf, MAX_REV_NUM);
						clear_recvBuffer(display_buf, MAX_REV_NUM*2);
				break;
		}	
		OLED_ShowString(0,3,display_buf,16);
}

用不到定时器,如果要用定时器的话,可以将显示部分放到定时器中,在实验时这样操作,遇到了一点问题。

上述函数 Rec_ResolutionShow(void) 是包含数据包解析和OLED显示两个部分,如果把 Rec_ResolutionShow 直接放到 while 循环中或者放到 定时中断 中时,实验效果都可以。但是,如果将解析部分放到 while 循环中,而将显示放到 定时器中断中(1ms 中断一次)时,就会出现只能接收一次的情况,后面再接收就不行了。我认为是定时器中断的优先级高于接收中断高于 while 循环,从而导致定时中断显示和接收打架。从而出现问题。

此外,这个程序还存在问题,就是如果再发送时不按照协议约定的发,比如将这种特征代码发送出来,

51单片机串口_第53张图片
在本次范例中,82是类别,00是长度,而在程序中

case 3:	
	sum_check += recv_data;
	xor_check ^= recv_data;
	recv_length = recv_data;        // 接收的是数据长度 — 不放入接收缓冲区
	machine_step = 4;
break;

case 4:	
	sum_check += recv_data;
	xor_check ^= recv_data;
	recv_buf[recv_cnt] = recv_data;
	
	if(recv_cnt == recv_length)
	{
			machine_step = 5;
	}
	else
	{
			machine_step = 4;           // 继续接收
	}
	recv_cnt++;  // 如果放到 if 前面去,if条件判断中就需要使用 >
break;

由于 recv_length 是 0,而 recv_cnt 非 0,程序到后面会造成 recv_buf 溢出也不能往下执行,直到 recv_cnt 溢出变为 0 ,然后接着往下执行。所以就会造成问题,还有就是 recv_length 长度定义超过数据接收缓冲器的最大接收量,也会造成这样 recv_buf 溢出,造成问题。因此,结论就是,可以在上述发现问题的基础之上做一些修改进行约束(这里不再添加),该代码思路常规应用(知道对方大概会发过来约定好的协议数据)是没有问题的。

代码已上传至 CSDN 资料库。

十、串口超时接收用户自定义通讯协议的编程实现——协议内 CRC16 校验及接收应答处理

CRC校验一般指循环冗余校验码。 循环冗余校验码(CRC),简称循环码,是一种常用的、具有检错、纠错能力的校验码。在编程时,可以使用现有的CRC校验库或者算法实现来进行CRC校验。常见的编程语言和工具中都提供了CRC校验的函数库或者算法,可以方便地进行CRC校验的计算和验证

在单片机中,主要使用的是查表法(校验库)。因为查表法的算法比较简单,代码执行时间也较短。但有个缺点就是需要占用较大的 ROM,51 单片机有 4K 的 ROM,足够去使用。

算法链接: https://blog.csdn.net/weixin_41542513/article/details/94201518?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169018297716800197018920%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=169018297716800197018920&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-4-94201518-null-null.142v90chatsearch,239v3insert_chatgpt&utm_term=modbus%20rtu%E9%80%9A%E8%AE%AF%E5%8D%8F%E8%AE%AE%20crc16&spm=1018.2226.3001.4187

CRC校验工具链接:http://www.ip33.com/crc.html

51单片机串口_第54张图片
需要注意,这里的参数模型选择 CRC-16/MODBUS 。

校验计算的结果是高位在前,低位在后 (发送时要先发低位,再发高位)。所以要颠倒下。

这里将要实现的功能如下:

51单片机串口_第55张图片

正常时,单片机执行相应的功能并将接收的数据发送回来,如果错误,将返回相应的一串代码 。

视频中的代码存在一定的问题,是在收到错误数据,回传后,要清除下 recv_buf 和 recv_cnt 数据(每次返回前需要清除)。不然发送两次错误后,就发送不了了。

代码如下,

main.c(应该分离出 uart.c 和 uart.h)

#include <reg52.h>
#include "crc16_modbus.h"
#include "time.h"
#include "led.h"
#include "beep.h"

#define MAX_REV_NUM   10  // 最大接收数据量为 9 ,最后一位是 '\0'

#define LOCAL_ADRESS  0x01

unsigned char start_timer = 0;

unsigned char recv_flag = 0;

unsigned char recv_timer_cnt = 0; // 定时器启动时间计数

unsigned char recv_buf[MAX_REV_NUM]; // 接收缓冲区, MAX_REV_NUM 要大于接收的字符数

unsigned char recv_cnt = 0;

unsigned int led_data;
unsigned int led_cnt;

unsigned int beep_data;
unsigned int beep_cnt;

void UartInit(void)		//[email protected]
{
	SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)
									//									 0   1   0   1   0   0   0  0
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式

	TL1 = 0xFD;			//设置定时初始值
	TH1 = 0xFD;			//设置定时重载值
  
	ET1 = 0;			  //禁止定时器中断(不用定时器1的中断)
	TR1 = 1;			  //定时器1开始计时
	ES  = 1;        //打开串口中断
}

void sendByte(unsigned char dat)
{
		SBUF = dat; // 10位,异步串口通信     0      1000 1000      1 
									//                    起始位     数据位     停止位
		// 当发送到停止位时,会将 TI 置 1
		while(!TI); // TI == 0 时,会一直等在这
		TI = 0;     // 手动清 0
}

void sendString(unsigned char *dat)
{
		while(*dat != '\0') 
		{
				sendByte(*dat++);
		}
}

/* 将接收数据缓冲区清零 */
void clear_recvBuffer(unsigned char *buf)
{
	unsigned char i;
	for(i = 0; i < MAX_REV_NUM; i++)
	{
		buf[i] = 0;
	}
}

void uart_service(unsigned char *buf)
{
		unsigned char i;
		unsigned int crc;
		unsigned char crch,crcl;
	
		if(recv_flag)
		{
				recv_flag = 0;
				start_timer = 0; // 关定时器
				
				// 校验本机地址 - 是本机 - 处理 - 否则直接返回
				if(recv_buf[0] != LOCAL_ADRESS)
				{
						clear_recvBuffer(buf);
						recv_cnt = 0;
						return ;
				}
				
				// CRC校验 - 校验正确才处理 - 否则直接返回 - 并给出错误码
				crc  = crc16(recv_buf, recv_cnt - 2);
				crch = crc >> 8;
				crcl = crc & 0xFF;
				if((crch != recv_buf[recv_cnt - 2]) || (crcl != recv_buf[recv_cnt - 1]))
				{
						recv_buf[1] = recv_buf[1] | 0x80;  // 到这说明校验错误
						crc = crc16(recv_buf, recv_cnt - 2);
						recv_buf[4] = crc & 0xFF; // 低位(先发)
						recv_buf[5] = crc >> 8;   // 高位(后发)
					
						for(i = 0; i<recv_cnt; i++)
						{
								sendByte(recv_buf[i]);
						}	
						
						clear_recvBuffer(buf);
						recv_cnt = 0;
						return ;
				}
				
				switch(recv_buf[1]) // 到这说明校验正确
				{
						case 1:
							led_data = recv_buf[2];
							led_data = led_data << 8;
							led_data = led_data + recv_buf[3];
							led_cnt  = 0; // 目的是使 LED 点亮上述接收的数据时间
						break;
							
						case 2:
							beep_data = recv_buf[2];
							beep_data = beep_data << 8;
							beep_data = beep_data + recv_buf[3];
							beep_cnt  = beep_data;
						break;
						
						default:
						break;
				}
				
				for(i = 0; i<recv_cnt; i++)
				{
						sendByte(recv_buf[i]);
				}	
			
				clear_recvBuffer(buf);
				recv_cnt = 0;
		}
}

void main()
{
	led_init();
	beep_init();
	Timer0_Init();
	UartInit();
	EA = 1;
	while(1)
	{
		 uart_service(recv_buf);
	}
}

void usart_isr() interrupt 4
{
		if(RI)          // 接收到1帧数据
		{	
				RI = 0;     // 手动清 0
				start_timer = 1; // 1、接收第一帧数据的时候,打开软件定时器去计数

				if(recv_cnt < MAX_REV_NUM - 1)
				{
						recv_buf[recv_cnt++] = SBUF; // 2、接收数据到数据缓冲区,注意缓冲区的大小和范围
				}		
				else
				{
						recv_cnt = MAX_REV_NUM - 1;
				}
				recv_buf[recv_cnt] = '\0';
				recv_timer_cnt = 0;	// 3、当接收到下一个数据后,就将计数器清零,喂狗。
		}	
}	

time.c

#include "time.h"

extern unsigned char start_timer ;

extern unsigned char recv_flag;

extern unsigned char recv_timer_cnt ; // 定时器启动时间计数

extern unsigned int led_data;
extern unsigned int led_cnt;

extern unsigned int beep_data;
extern unsigned int beep_cnt;

void Timer0_Init(void)		//1毫秒@11.0592MHz
{

	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	
	TL0 = 0x66;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值

	TF0 = 0;					//清除TF0标志
	ET0 = 1;

	TR0 = 1;					//定时器0开始计时
}


void timer_isr() interrupt 1
{
		TR0 = 0;				//定时器0开始计时
	
		if(led_cnt < led_data)
		{
				led_cnt++;
				LED1 = 0;
		}
		else
		{
				LED1 = 1;
		}
		
		if(beep_cnt > 0)
		{
				beep_cnt--;
				BEEP = 0;
		}
		else
		{
				BEEP = 1;
		}
		
		if(start_timer == 1)
		{
			  recv_timer_cnt++;  // 1、累加定时时间计数器
			  if(recv_timer_cnt > MAX_REV_TIME) // 2、判断定时时间是否超过了设定的最大的阈值,超过则说明等待一段时间后没有新的数据到,我们判定一个数据包接收完毕 
				{
					recv_timer_cnt = 0; // 3、清除定时计数器,处理数据(在主循环中),清除buffer(放到数据处理之后)
					recv_flag = 1;
				}
		}
	
		TL0 = 0x66;				//设置定时初始值
		TH0 = 0xFC;				//设置定时初始值

		TR0 = 1;				//定时器0开始计时
}

time.h

#ifndef _TIME_H_
#define _TIME_H_

#include <reg52.h>
#include "beep.h"
#include "led.h"

#define MAX_REV_TIME 5 

// 函数的声明
void Timer0_Init(void);		//1毫秒@11.0592MHz

#endif

crc16_modbus.c(代码来自 csdn ,豪哥追求卓越)

#include "crc16_modbus.h"

/***********************CRC校验*************************/

// CRC 高位字节值表
unsigned char code auchCRCHi[260] = { 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
	0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
	0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
	0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
	0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 
	0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
	0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 
	0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
	0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
	0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
	0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 
	0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 
	0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 
	0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 
	0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 
} ; 

// CRC低位字节值表
unsigned char code  auchCRCLo[260] = { 
	0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 
	0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 
	0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 
	0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 
	0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 
	0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 
	0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 
	0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 
	0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 
	0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 
	0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 
	0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 
	0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 
	0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 
	0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 
	0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 
	0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 
	0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 
	0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 
	0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 
	0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 
	0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 
	0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 
	0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 
	0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 
	0x43, 0x83, 0x41, 0x81, 0x80, 0x40 
} ;

unsigned int crc16(unsigned char *puchMsg, unsigned int usDataLen) 
{ 
	 unsigned int  uIndex ;            // CRC循环中的索引 
	 unsigned char uchCRCHi = 0xFF ;   //*高CRC字节初始化 
	 unsigned char uchCRCLo = 0xFF ;   //*低CRC字节初始化  
	 
	 while (usDataLen--)               //传输消息缓冲区 
	 { 
		uIndex = uchCRCHi ^ *puchMsg++ ; // 计算CRC  
		uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ; 
		uchCRCLo = auchCRCLo[uIndex] ; 
	 } 
	 
	 return (uchCRCHi << 8 | uchCRCLo);
}

该代码可应用于实际项目,代码已上传 CSDN 。

测试指令(Hex模式发送):
51单片机串口_第56张图片
需要注意的是,代码中的 CRC 校验码的高低位的前后顺序。

你可能感兴趣的:(51单片机,嵌入式硬件,单片机)