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.rhkb.cn/news/49268.html

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

相关文章

初学者入门:认识STM32单片机

本教程含有较多专业词汇&#xff0c;大部分时候&#xff0c;不完全理解并不影响继续往下阅读&#xff0c;大家只需要了解大致的概念即可。当然&#xff0c;也鼓励大家多查百度和多问chatgpt&#xff0c;让自己学会的更多。 什么是单片机&#xff1f; 单片机&#xff0c;就是把…

chatgpt赋能python:Python单片机:从入门到实践

Python单片机&#xff1a;从入门到实践 近年来&#xff0c;Python在嵌入式领域越来越受到开发者的青睐。Python具有易学易用的特点&#xff0c;方便开发者快速实现单片机的开发。本文将介绍Python单片机的基础知识以及实践应用。 Python单片机的基础知识 Python单片机用的是…

chatgpt赋能python:Python烧录单片机:快速的开发工具

Python烧录单片机&#xff1a;快速的开发工具 简介 Python是一种高级的编程语言&#xff0c;被广泛应用于各种领域&#xff0c;包括机器学习、数据分析和物联网等领域。Python的易用性和简洁性已经成为其成功的关键因素之一。Python也能在烧录单片机时提供极大的方便性和灵活…

推荐给程序员的书:七月图书推荐

七月&#xff0c;图灵原创书相继出炉&#xff0c;并在网店的排行榜上荣登前三甲&#xff0c;图灵原创书的作者皆是各社区的领军人物&#xff0c;有着相当深厚的技术功底&#xff0c;这是图灵原创书在《结网》后的一个跨越。 本月推荐&#xff0c;是本版书与外版书相结合&#x…

这五本 Python 急速入门必读的书,送给正在学习 Python 的你!

书籍是人类进步的阶梯&#xff0c;这句话从古至今都是适用的。为什么会这么说呢&#xff1f;书籍&#xff0c;它记录了人们实践的经验&#xff0c;这些经验有助于我们快速的学习&#xff0c;对于编程学习来说也不例外&#xff0c;今天就给大家带来了以下的书籍干货&#xff0c;…

人际沟通必看的书推荐

人际沟通与口才训练方面的书籍我推荐你看两本&#xff08;并且看这两本就完全足够了&#xff09;&#xff0c;一本是《沟通与说服必读12篇》&#xff0c;另一本是《演讲与口才必读12篇》&#xff0c;注意这两本书都仅能从12READS官网购买&#xff0c;避免广告&#xff0c;地址请…

学会演讲必看的五本书籍推荐

有哪些演讲必看的书值得推荐&#xff1f;今天小编为大家精选了以下这五本学会演讲必看的经典书籍&#xff0c;提升口才与演讲能力必读哦。首推榜首的《演讲与口才必读12篇》&#xff0c;虽然比其他的书要贵&#xff0c;但是真的有东西。 演讲必看的书推荐之一&#xff1a;《演…

程序猿必看10本好书推荐

版权声明&#xff1a;本文为 ABC实验室 原创文章&#xff0c;版权所有&#xff0c; 侵权必究&#xff01; 引言 2022年注定是一个不平凡的一年&#xff0c;当下新冠病毒肆虐全球、股市熔断、经济停顿&#xff0c;各行各业都遭受着沉重的打击。作为IT业也难幸免&#xff0c;同…

程序员阅读书籍推荐

文章目录 1、《程序员修炼之道》2、《Effective C#》3、《黑客与画家》4、《编程之美》5、《软技能&#xff1a;代码之外的生存指南》6、《数学之美》7、《增长黑客》8、《富爸爸财务自由之路》9、《编写可读代码的艺术》10、《代码大全》第二版11、《点石成金&#xff1a;访客…

程序员必看的书籍推荐

程序员必看的书籍推荐&#xff1a; 推荐1&#xff1a;Python 网络数据采集 作者&#xff1a;Ryan Mitchell 译者&#xff1a;陶俊杰&#xff0c;陈小莉 原书4.6星好评&#xff0c;一本书搞定数据采集 涵盖数据抓取、数据挖掘和数据分析 提供详细代码示例&#xff0c;快速解决实…

Jeff Atwood倾情推荐——程序员必读之书

英文版&#xff1a;《Code Complete 2》中文版&#xff1a;《代码大全&#xff08;第二版&#xff09;》作者&#xff1a;Steve McConnell译者&#xff1a;金戈 汤凌 陈硕 张菲出版社&#xff1a;电子工业出版社出版日期&#xff1a;2007 年8月Jeff Atwood的推荐&#xff1a…

Linux内核必读五本书籍(强烈推荐)

《深入理解Linux内核》 推荐等级&#xff1a;5颗星 为了透彻理解Linux的工作机理&#xff0c;以及为何它在各种系统上能顺畅运行&#xff0c;你需要深入到内核的心脏。cPu与外部世界的所有交互活动都是由内核处理的&#xff0c;哪些程序会分享处理器的时间&#xff0c;以什么样…

新手程序员成长之路的五本必读书籍(附资源下载)

全文共3351字&#xff0c;预计学习时长7分钟 图片来自Pixabay&#xff0c;IvanPais 书籍可以清晰而有条理地陈诉观点&#xff0c;纸张上的笔墨也会给人一种不慌不忙的感觉。不过&#xff0c;科技类书籍存在一些严重的问题&#xff1a;它们几乎很快就过时了。由于缺乏交互性&…

程序员必读的十四本经典书籍

1、《代码大全》 史蒂夫迈克康奈尔 “优秀的编程实践的百科全书&#xff0c;《代码大全》注重个人技术&#xff0c;其中所有东西加起来&#xff0c; 就是我们本能所说的“编写整洁的代码”。这本书有50页在谈论代码布局。” —— Joel Spolsky Steve McConnell的原作《代码大全…

五本计算机必读书籍总结

一、计算机组成原理 思维导图&#xff1a; 1、计算机系统概述 主要讲授信息的数字化表示、存储程序与冯诺依曼体制&#xff1b;计算机的诞生和发展&#xff1b;计算机系统的层次结构和硬件系统组织&#xff1b;计算机的主要性能指标。 2、数据的表示、运算与校验 主要讲授数值…

程序员必读书籍及导读指南

最近在网上看了一个非常好的帖子《程序员一生必读的书》&#xff08;我的腾讯微博上有分享该贴子链接&#xff0c;有兴趣就点击进去看看吧&#xff09;&#xff0c;该贴的第一个张图片是一个雷达图&#xff0c; 这张图是由ThoughtWorks&#xff08;全球软件设计与定制领域的领袖…

强烈推荐10本程序员必读的书

经常有读者私下问我&#xff0c;能否推荐几本书&#xff0c;以便空闲的时间读一读。于是我跑去自己的书架上筛选了 10 本我最喜欢的书&#xff0c;你可以挑选感兴趣的来读一读。 01、《代码整洁之道》 我可以这么肯定地说&#xff1a;《代码整洁之道》值得所有的程序员读一读…

open AI API使用经验

open AI API 文章目录 open AI API引言概念TokenspromptsModels 使用流程1.登录open AI 账号获得API keys2.接入环境3.API用例&#xff08;1&#xff09;Completion&#xff08;2&#xff09;ChatCompletion&#xff08;3&#xff09;Images&#xff08;4&#xff09;Edit 引言…

卖AI数字人代理是小风口吗?

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 2023年第一个小风口是&#xff1a;以ChatGPT为代表的人工智能AI。第二个创业小风口则是&#xff1a;数字人直播带货。注意我说的是数字人代理。今天的卢松松的文章就扯一扯数字人直播。 最近龚文…

马车拉的再好,也该摸摸方向盘了!近500家美国企业用ChatGPT取代员工

Datawhale分享 最新&#xff1a;GPT影响&#xff0c;来源&#xff1a;量子位 自从ChatGPT掀起浪潮&#xff0c;不少人都在担心AI快要抢人类饭碗了。 据就业服务平台Resume Builder调查统计&#xff0c;在1000多家受访美国企业中&#xff0c;用ChatGPT取代部分员工的&#xff0…