STM32 串口收发HEX数据包

单片机学习!


目录

前言

一、HEX数据包格式

二、串口收发HEX数据包代码

三、代码解析

[3.1 数据包发送](#3.1 数据包发送)

[3.2 标志位清除](#3.2 标志位清除)

[3.3 数据包接收](#3.3 数据包接收)

总结


前言

本文介绍了串口收发HEX数据包程序设计的思路并详解代码作用。


一、HEX数据包格式

收发HEX数据包中HEX数据包的格式的定义如下图所示:固定包长,含包头包尾,其中包头为FF,载荷数据固定4字节,包尾为FE。

二、串口收发HEX数据包代码

总代码示例:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include <stdio.h>


uint8_t Serial_TxPacket[4];//发送缓存区
uint8_t Serial_RxPacket[4];//接收缓存区
uint8_t Serial_RxFlag;//标志位


void Serial_Init(void)
{
	//第一步开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIO的时钟

	
	//第二步初始化GPIO引脚
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_AF_PP;//引脚模式
	GPIO_InitStruct.GPIO_Pin= GPIO_Pin_9;//引脚选择Pin_9
	GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);//初始化GPIOA

	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_IPU;//引脚模式
	GPIO_InitStruct.GPIO_Pin= GPIO_Pin_10;//引脚选择Pin_10
	GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);//初始化GPIOA
	
	
	//第三步初始化USART
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//串口模式
	USART_InitStructure.USART_Parity = USART_Parity_No;//校验位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位
	USART_InitStructure.USART_WordLength =USART_WordLength_8b; //字长
	USART_Init(USART1,&USART_InitStructure);
	
    //配置中断
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//中断通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =1; 
	NVIC_InitStructure.NVIC_IRQChannelSubPriority =1;
	NVIC_Init(&NVIC_InitStructure);
	
	USART_Cmd(USART1,ENABLE);
	
}


//发送数据的函数
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); 
}

//发送一个数组的函数
void Serial_SendArray(uint8_t *Array,uint16_t Length) 
{
	uint16_t i;
	for(i = 0 ; i < Length ; i++)
	{
		Serial_SendByte(Array[i]);
	}
}

//发送字符串
void Serial_SendString(char *String)
{
	uint8_t i;
	for(i = 0;String[i] != '\0';i++)
	{
		Serial_SendByte(String[i]);
	}
}

//这个函数的返回值是X的Y次方
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{
	uint32_t Result = 1;
	while(Y--)
	{
		Result *= X;
	}
	return Result;
}


//函数可以将发送的数字显示为字符串的形式
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{
	uint8_t i;
	for(i = 0;i < Length;i++)
	{
		Serial_SendByte(Number / Serial_Pow(10,Length - i - 1) %10 + '0');
	}
}

//printf函数重定向到串口
int fputc(int ch,FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

//调用这个函数,TxPacket数组的4个数据就会自动加上包头包尾发送出去
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);//发送包头0xFF
	Serial_SendArray(Serial_TxPacket,4);//参数给Serial_TxPacket,长度4,这样就可以依次把4个载荷数据发出去了。
	Serial_SendByte(0xFE);//发送包尾0xFE
}

//函数实现一个Serial_RxData变量读后自动清除标志位Serial_RxFlag的功能
uint8_t Serial_GetRxFlag(void)
{
	if(Serial_RxFlag == 1)
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}


//中断接收,执行状态机逻辑函数
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;//作为状态变量S
	static uint8_t pRxPacket = 0;//这个静态变量用于指示接收到数据包中哪一个数据了,最开始默认为0
	//先判断标志位
	if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)//如果RXNE确实置1了,就进入if
	{
		//首先获取一下RxData
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if(RxState == 0)//等待包头
		{
			if(RxData == 0xFF)
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if(RxState == 1)//接收数据
		{
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket ++;
			if(pRxPacket >= 4)
			{
				RxState = 2;
			}
		}
		else if(RxState == 2)//等待包尾
		{
			if(RxData == 0xFE)
			{
				RxState = 0;
				Serial_RxFlag = 1;
			}
		}
		
			USART_ClearITPendingBit(USART1,USART_IT_RXNE); 
	}
}

串口配置部分和数据发送、接收的代码详解可以看前两篇博文:

STM32 USART串口发送_串口发送代码-CSDN博客https://blog.csdn.net/Echo_cy_/article/details/142794600?spm=1001.2014.3001.5501

STM32 USART串口接收_stm32 uart发送数据-CSDN博客https://blog.csdn.net/Echo_cy_/article/details/143817933?spm=1001.2014.3001.5501本文只分析新设计的收发HEX数据包函数。


三、代码解析

程序最前面为了收发数据包,先定义两个缓存区的数组和一个标志位。

cpp 复制代码
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;

发送缓存区Serial_TxPacket,数据个数为4个:

cpp 复制代码
uint8_t Serial_TxPacket[4];

接收缓存区Serial_RxPacket,数据个数为4个,这4个数据只存储发送或接收的载荷数据,包头包尾就不存了:

cpp 复制代码
uint8_t Serial_RxPacket[4];

自定义的标志位,如果收到一个数据包,就置Serial_RxFlag为1:

cpp 复制代码
uint8_t Serial_RxFlag;

3.1 数据包发送

调用Serial_SendPacket这个函数,Serial_TxPacket数组的4个数据就会自动加上包头包尾发送出去:

cpp 复制代码
void Serial_SendPacket(void)
{
    Serial_SendByte(0xFF);
    Serial_SendArray(Serial_TxPacket,4);
    Serial_SendByte(0xFE);
}

发送包头0xFF:

cpp 复制代码
    Serial_SendByte(0xFF);

Serial_SendArray函数,参数给Serial_TxPacket,长度4,这样就可以依次把4个载荷数据发出去了。

cpp 复制代码
    Serial_SendArray(Serial_TxPacket,4);

发送包尾0xFE:

cpp 复制代码
    Serial_SendByte(0xFE);

3.2 标志位清除

Serial_GetRxFlag函数实现一个Serial_RxData变量读后自动清除标志位Serial_RxFlag的功能。

cpp 复制代码
uint8_t Serial_GetRxFlag(void)
{
    if(Serial_RxFlag == 1)
    {
        Serial_RxFlag = 0;
        return 1;
    }
    return 0;
}

3.3 数据包接收

在中断函数USART1_IRQHandler里需要用状态机来执行接收逻辑,接收数据包,然后把载荷数据存在Serial_RxPacket数组里。

根据状态转移图首先要定义一个标志当前状态的变量S,在中断函数里面定义一个静态变量。

代码示例:

cpp 复制代码
//中断接收,执行状态机逻辑函数
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;//作为状态变量S
	static uint8_t pRxPacket = 0;//这个静态变量用于指示接收到数据包中哪一个数据了,最开始默认为0
	//先判断标志位
	if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)//如果RXNE确实置1了,就进入if
	{
		//首先获取一下RxData
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if(RxState == 0)//等待包头
		{
			if(RxData == 0xFF)
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if(RxState == 1)//接收数据
		{
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket ++;
			if(pRxPacket >= 4)
			{
				RxState = 2;
			}
		}
		else if(RxState == 2)//等待包尾
		{
			if(RxData == 0xFE)
			{
				RxState = 0;
				Serial_RxFlag = 1;
			}
		}
		
			USART_ClearITPendingBit(USART1,USART_IT_RXNE); 
	}
}

注意要用else if,如果只用三个并列的if可能会在状态转移的时候出现问题。比如在状态0,需要转移到状态1,就置RxState=1,结果就会造成下面状态1的条件就立马满足了,这样会出现连续两个if都同时成立的情况,就不符合执行逻辑了。所以这里要使用else if,保证每次进状态机代码之后只能选择执行其中一个状态的代码。或者用switch case语句也可以保证只有一个条件满足。写好状态选择的部分,就可以依次写每个状态执行的操作逻辑和状态转移条件了。


重要变量:

USART1_IRQHandler中断函数是把数据进行了一次转存,最终还是要扫描查询Serial_RxFlag来接收数据。

RxState这个静态变量类似于全局变量,函数进入只会初始化一次为0,在函数退出后,数据仍然有效。与全局变量不同的是,静态变量只能在本函数使用。这里就用RxState当做状态变量S,根据状态转换图,三个状态S分别为0、1、2,所以在if语句里根据RxState的不同,需要进入不同的处理程序。

pRxPacket这个静态变量用于指示接收到数据包中哪一个数据了,最开始默认为0.


中断函数先判断标志位,如果RXNE确实置1了,就进入if 。

cpp 复制代码
 if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
    {
        ...;
    }

在if代码框里首先获取一下RxData。

cpp 复制代码
        uint8_t RxData = USART_ReceiveData(USART1);

接着就是三个状态的条件判断和相应状态下的程序逻辑。

1.等待包头

cpp 复制代码
		if(RxState == 0)//等待包头
		{
			if(RxData == 0xFF)
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}

如果RxData 收到包头,那就可以转移状态RxState=1;如果没有收到0xFF就不转移状态。

2.接收数据

接收数据这里要依次接收4个数据,存在Serial_RxPacket数组里,所以还需要一个变量来记一下接受了几个,相当于接收数据的位置编码,可以在定义一个静态变量pRxPacket在外面,最开始默认为0.

cpp 复制代码
		else if(RxState == 1)//接收数据
		{
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket ++;//接收数据后位置编码就自增,指示接收下一个位置的数据。
			if(pRxPacket >= 4)//4个载荷数据已经收完了,这时就可以转移到下一个状态了。同时对pRxPacket清0,为下次接收准备,可以在状态0转移到状态1时提前清一个0.
			{
				RxState = 2;
			}
		}

此代码逻辑是,每进一次接收数据状态就转存一次缓存数据,同时存的位置后移也就是静态变量pRxPacket++。当4个载荷数据已经收完了,这时就可以转移到下一个状态。同时对pRxPacket清0,为下次接收准备,可以在状态0转移到状态1时提前清一个0.

3.等待包尾

如果收到包尾,那就可以回到最初的状态RxState=0,同时为了表示一个数据包接收到了,可以置一个接收标志位Serial_RxFlag=1;如果没有收到0xFE就时还没收到包尾,也不做处理,仍然在这个状态等待包尾。

cpp 复制代码
else if(RxState == 2)//等待包尾
		{
			if(RxData == 0xFE)
			{
				RxState = 0;
				Serial_RxFlag = 1;
			}
		}

这里调用USART_ClearITPendingBit函数,直接清除一下标志位:

cpp 复制代码
   USART_ClearITPendingBit(USART1,USART_IT_RXNE);

以上中断接收和变量的封装就完成了!

这个程序还隐藏有一个问题需要注意。Serial_RxPacket数组是一个同时被写入又同时被读出的数组,在中断函数里会依次写入,在主函数里又会依次读出。这会造成一个问题,就是数据包之间可能会混在一起。比如读出的过程太慢了,前两个数据刚读出来,等了一会儿才继续往后读取,那这时后面的数据就可能会刷新为下一个数据包的数据,也就是读出的数据可能一部分属于上一个数据包,另一部分属于下一个数据包。

问题的解决方法可以是在接收部分加入判断,就是在每个数据包读取处理完毕后,再接收下一个数据包。

当然这个问题的发生也分情况。如果是HEX数据包,多是用于传输各种传感器的每个独立数据,比如陀螺仪的X、Y、Z轴数据,温湿度数据等等,这种相邻数据包之间的数据具有连续性。这样即使相邻数据包混在一起了,也没关系。


总结

以上就是今天要讲的内容,本文仅仅简单介绍了串口收发HEX数据包程序设计的思路并详解了一些设计代码的细节。

相关推荐
猿~~~19 分钟前
STM32的HAL库开发---多通道ADC采集(DMA读取)实验
stm32·单片机·嵌入式硬件
Freak嵌入式1 小时前
开源一款I2C电机驱动扩展板-FreakStudio多米诺系列
嵌入式硬件·嵌入式·智能硬件·开源硬件·micropython·电机驱动·电子模块
kongba0072 小时前
Cursor提示词模板,开发GD32,C语言开发GD32 ARM单片机编程规范提示词 大厂风格代码规范
c语言·arm开发·单片机
LaoZhangGong1233 小时前
STM32的“Unique device ID“能否修改?
c语言·经验分享·stm32·单片机·嵌入式硬件
1101 11014 小时前
STM32-心知天气项目
stm32·单片机·嵌入式硬件
Ronin-Lotus5 小时前
嵌入式硬件篇---数字电子技术中的触发器
嵌入式硬件·fpga开发·触发器·数字电子技术·上位机知识
Ronin-Lotus5 小时前
嵌入式硬件篇---数字电子技术中的时序逻辑
单片机·嵌入式硬件·蓝桥杯·时序分析·数字电子技术
sinat_3607048215 小时前
STM32 看门狗
stm32·单片机·嵌入式硬件
亿道电子Emdoor15 小时前
【ARM】MDK如何生成指定大小的bin文件,并指定空区域的填充数据
arm开发·stm32·单片机
mftang15 小时前
STM32 CAN过滤器配置和应用方法介绍
stm32·单片机·嵌入式硬件