51单片机串口

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

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
一般的应用层的协议中采用和校验或CRC校验,而奇偶校验还是解决基本通信中的帧格式中的校验。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述

后面只介绍模式1。

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

在这里插入图片描述
对于 SCON ,主要用到的就是 SM0、SM1、REN、TI 和 RI ,其他几位用的不多。

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

在这里插入图片描述

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

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

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

在这里插入图片描述

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

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

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

二、安装虚拟串口

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

安装好后,打开该软件,那如何使用这个软件呢?
在这里插入图片描述
点击添加端口,

在这里插入图片描述

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

在这里插入图片描述
与单片机相连接。
在这里插入图片描述
双击该元器件,将该器件的 COM 口设置为 COM2。

在这里插入图片描述
为什么要这样连接:
在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述
上面工具准备好之后,就可以编程代码来进行实现了。

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

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

在这里插入图片描述

#include <reg52.h>
#include "delay.h"void UartInit(void)		//4800bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI  RI  (REN是允许接收)//		0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!TI); // TI == 0 时,会一直等在这TI = 0;     // 手动清 0Delay_Xms(1000);}
}

将波特率修改为 11.0592 MHz,

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述
之后,应该就可以了。

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

在这里插入图片描述

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

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

在这里插入图片描述

以文本模式进行显示:

在这里插入图片描述
将其封装成函数,

发送一个字节:

在这里插入图片描述

发送一个字符串:

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

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

这里将波特率改为 9600 ,
在这里插入图片描述

代码如下:

#include <reg52.h>
#include "delay.h"void UartInit(void)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 <stdio.h>

调用时,可如下所示。

在这里插入图片描述

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

在这里插入图片描述
为了方便显示,可以加上回车换行符。

运行结果,如下:
在这里插入图片描述
在这里插入图片描述
有问题。这是格式化输出的问题。

参考手册

在这里插入图片描述

在这里插入图片描述

所以,修改代码,如下。

在这里插入图片描述

之后,输出正常。

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

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

12MHz,确实误差较大。

#include <reg52.h>unsigned char recv_data;void UartInit(void)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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;     // 手动清 0recv_data = SBUF; recv_data = recv_data + 1;SBUF = recv_data;}	if(TI)  {TI = 0;}
}	

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

在这里插入图片描述

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

在这里插入图片描述

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

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

完整代码如下:

#include <reg52.h>unsigned char recv_flag = 0;
unsigned char recv_data;void UartInit(void)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0recv_data = SBUF; recv_flag = 1 ;}	
}	

运行效果与上图一样。

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

在这里插入图片描述

运行效果如下:

在这里插入图片描述

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

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

在这里插入图片描述

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

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

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

#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)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0temp = 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;}}	
}	

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

在这里插入图片描述
我分析,应该是串口中断抢占 while 循环,主循环只来的及发送一个 01,就被第二个 0D 给终止了,所以说该程序是有瑕疵的。

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

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

在这里插入图片描述

5.1 编程思路

假设有两个数据包,第一个数据包有四个数据帧,第二个数据包有五个数据帧。
在这里插入图片描述

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

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

在这里插入图片描述
从而在第二帧数据与第一帧数据相差的时间不会超过一个数据帧的时间长度,也就是 1.042 ms 。

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

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

5.2 程序代码

main.c

#include <reg52.h>
#include "time.h"#define MAX_REV_NUM  10  // 最大接收数据量为 10unsigned 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)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0start_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,然后解析数据位,

在这里插入图片描述

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

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

在这里插入图片描述
程序源码如下:

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)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0start_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 循环中),但是这种方式并不适合于实时性要求高的项目。可以使用在中断中边接收边解析的方式。

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

在这里插入图片描述
状态机的编程思想(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)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0switch(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);		//9600bps@11.0592MHzextern void sendString(unsigned char *dat);extern void clear_recvBuffer(unsigned char *buf, unsigned char len);#endif

beep.c 和 led.c 省略。

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

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

在这里插入图片描述

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

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

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

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

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

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

在这里插入图片描述
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)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0recv_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 模式。

已验证,代码运行正常。
在这里插入图片描述

最终的代码已上传 CSDN 。

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

通信协议如下:
在这里插入图片描述
然后将接收到的有效数据用 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();//初始化OLEDEA = 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)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0recv_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; // 接收完一串数据,标志位置1for(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);		//9600bps@11.0592MHzextern 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 循环,从而导致定时中断显示和接收打架。从而出现问题。

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

在这里插入图片描述
在本次范例中,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

在这里插入图片描述
需要注意,这里的参数模型选择 CRC-16/MODBUS 。

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

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

在这里插入图片描述

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

视频中的代码存在一定的问题,是在收到错误数据,回传后,要清除下 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  0x01unsigned 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)		//9600bps@11.0592MHz
{SCON = 0x50;		//8位数据,可变波特率 SM0 SM1 SM2 REN TR8 RB8 TI RI  (REN是允许接收)//									 0   1   0   1   0   0   0  0TMOD &= 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 置 1while(!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;     // 手动清 0start_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模式发送):
在这里插入图片描述
需要注意的是,代码中的 CRC 校验码的高低位的前后顺序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/6610.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Eclipse整合tomcat时要注意的几点

Eclipse整合tomcat时要注意的几点 1、安装目录及jdk 2、参数配置 注意&#xff1a;Arguments的配置&#xff0c;日志输出文件目录及java内存大小设置等&#xff0c;如下&#xff1a; -Dcatalina.base"E:\apache-tomcat-7.0.52" -Dcatalina.home"E:\apache-tomc…

无符号数和有符号数的“bug”

1. 起因 在实现kmp算法时&#xff0c;出现了诡异的现象&#xff0c;看下面的代码&#xff1a; int KMP (const char *s, const char *t) {int lenS strlen (s);int lenT strlen (t);int next[lenT];get_next (next, t);int i 0;int j 0;while (i < lenS && j …

程序化广告还有未来么?——程序化领域变化的底层逻辑和反思

三、近几年程序化广告领域的变化底层逻辑是什么呢&#xff1f; 当前国内程序化生态的状态&#xff0c;更像是希腊的古典时代&#xff1a;古希腊时代的城邦高度繁荣的时期。很多人可能对古希腊城邦没有概念&#xff0c;我们解释一下&#xff1a; 所谓城邦就是城市国家&#xff0…

涵子来信——自己的电脑——谈谈想法

大家好&#xff1a; 上一次谈论了苹果的那些事&#xff0c;今天我们来聊聊电脑。 我的第一台电脑现在成了这样子&#xff1a; 很多人以为是我自己拆了电脑做研究&#xff0c;其实是我的第一台电脑&#xff0c;真的坏了。 2021年&#xff0c;我有了属于我自己的第一台电脑&am…

链表 --- C语言实现

本篇文章来详细介绍一下数据结构中的链表。 目录 1.链表的概念及结构 2.链表的分类 3.单链表的实现 4.链表的面试题 5.双向链表的实现 6.顺序表和链表的区别 1.链表的概念及结构 概念&#xff1a;链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素…

【HTML5】拖放详解及实现案例

文章目录 效果预览代码实现 效果预览 代码实现 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>一颗不甘坠落的流星</title><style>#div1,#div2 {float: left;width: 100px;height: 27px;margin: 10px;paddin…

关于 Qt在windows使用mingw32编译器时从Qt5.9切换至Qt5.12出现“C2001:常量中有换行符“不修改编码 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/131901444 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…

Git简介与工作原理:了解Git的基本概念、版本控制系统和分布式版本控制的工作原理

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

day33哈希表

1.哈希表 常见的哈希表分为三类&#xff0c;数组&#xff0c;set&#xff0c;map&#xff0c;C语言的话是不是只能用数组和 2.例题 题目一&#xff1a; 分析&#xff1a;题目就是判断两个字符串出现的次数是否相同&#xff1b; 1&#xff09;哈希表26个小写字母次数初始化为0&…

RB-tree(红黑树)详解

RB-tree(红黑树) 红黑树的规则如下&#xff1a; 1.每个节点不是红色就是黑色 2.根节点为黑色 3.如果节点为红色&#xff0c;那么它的子节点必须为黑色 4.任何一个节点到NULL&#xff08;树的尾端&#xff09;的任何路径所包含的黑节点个数相同 简而言之就是每个路径的黑色节点数…

模拟量输出FC S_RTI(信捷C语言源代码)

模拟量输出FC SCL源代码请查看下面博客: PLC模拟量输出 模拟量转换FC S_RTI_博途模拟量转换指令_RXXW_Dor的博客-CSDN博客1、本文主要展示西门子博途模拟量输出转换的几种方法, 方法1:先展示下自编FC:计算公式如下:intput intput Real ISH Real //工程量上限 ISL Real //工…

【数据挖掘】将NLP技术引入到股市分析

一、说明 在交易中实施的机器学习模型通常根据历史股票价格和其他定量数据进行训练&#xff0c;以预测未来的股票价格。但是&#xff0c;自然语言处理&#xff08;NLP&#xff09;使我们能够分析财务文档&#xff0c;例如10-k表格&#xff0c;以预测股票走势。 二、对自然语言处…

【OpenCV】常见问题及解决办法

文章目录 0 前言1 中文乱码问题2 非法路径问题 0 前言 本篇博客主要是总结OpenCV使用过程中遇到的一些问题&#xff0c;以及对应的解决办法&#xff0c;这里重点是关注OpenCV&#xff0c;既有基于C的&#xff0c;也有基于Python的&#xff0c;比较全面&#xff0c;而且也会随着…

RocketMQ教程-安装和配置

Linux系统安装配置 64位操作系统&#xff0c;推荐 Linux/Unix/macOS 64位 JDK 1.8 Maven3.0 yum 安装jdk8 yum 安装maven 1.下载安装Apache RocketMQ RocketMQ 的安装包分为两种&#xff0c;二进制包和源码包。 点击这里 下载 Apache RocketMQ 5.1.3的源码包。你也可以从这…

Windows11的VS201x编译OpenCV+Contrib+CUDA

(1) CUDA下载&#xff0c;注意要和cudnn版本号相关。 我安装的是cuda11.0,注意VS2015不能编译CUDA11&#xff0c;所以用VS2015的话需要下载CUDA 10。因为更高的版本目前还没有cudnn。 (2) 下载和安装VS2015。 (3) 下载和解压CMake。 CMake地址&#xff1a; Releases Kitw…

Android dp to pix resources.getDimension(R.dimen.xxx) ,kotlin

Android dp to pix resources.getDimension(R.dimen.xxx) ,kotlin <?xml version"1.0" encoding"utf-8"?> <resources><dimen name"my_size_dp">20dp</dimen><dimen name"my_size_px">20px</dime…

数据仓库表设计理论

数据仓库表设计理论 数仓顾名思义是数据仓库&#xff0c;其数据来源大多来自于业务数据(例如:关系型数据库)&#xff0c;当设计数仓中表类型时(拉链表、增量表、全量表、流水表、切片表)时&#xff0c;应先观察业务数据的特点再设计数仓表结构 首先业务数据是会不断增长的-即…

练习——动态内存分配的笔试题

今天我们分享几道经典的笔试题&#xff0c;做完直接变成陈泽 第一题 ~~ --------------------------------------------------------------------------------------------------~~ void GetMemory(char* p) {p (char*)malloc(100); } void Test(void) {char* str NULL;Get…

阿里云容器镜像仓库(ACR)的创建和使用

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

使用spark进行hbase的bulkload

使用spark进行hbase的bulkload 一、 背景 HBase 是一个面向列&#xff0c;schemaless&#xff0c;高吞吐&#xff0c;高可靠可水平扩展的 NoSQL 数据库&#xff0c;用户可以通过 HBase client 提供的 put get 等 api 实现在数据的实时读写。在过去的几年里&#xff0c;HBase …