STM32 串口收发文本数据包

单片机学习!


目录

前言

一、文本数据包格式

二、串口收发文本数据包代码

三、代码解析

 3.1 标志位清除

3.2 数据包接收

四、代码问题改进

总结


前言

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


一、文本数据包格式

        文本数据包的格式的定义如下图所示:可变包长,含包头包尾,其中包头为@,包尾为换行的两个符号\r和\n,中间的载荷字符数量不固定。

b16afb066c18430aa8d89c5115d051e4.png

二、串口收发文本数据包代码

        程序就只写接收的部分,因为发送不像HEX数组一样,方便一个个更改。这里发送就直接在主函数里SendString或者printf就行了。

总代码示例:

#include "stm32f10x.h"                  // Device header
#include <stdio.h>


char Serial_RxPacket[100];//接收缓存区
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);
	

    //配置USART1的接收中断
	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;
}


//函数实现一个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 == '@')
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if(RxState == 1)//接收数据
		{
			if(RxData == '\r')
			{
				RxState = 2;
			}
			else 
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if(RxState == 2)//等待第二个包尾
		{
			if(RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\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本文只分析新设计的接收文本数据包函数。


三、代码解析

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

char Serial_RxPacket[100];
uint8_t Serial_RxFlag;

        接收缓存用于接收字符,数量可以给多一点防止溢出,这要求单条指令最长不超过100个字符。

char Serial_RxPacket[100];

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

uint8_t Serial_RxFlag;

 3.1 标志位清除

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

uint8_t Serial_GetRxFlag(void)
{
    if(Serial_RxFlag == 1)
    {
        Serial_RxFlag = 0;
        return 1;
    }
    return 0;
}

3.2 数据包接收

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

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

代码示例:

//中断函数接收数据,执行状态机逻辑
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 == '@')
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if(RxState == 1)//接收数据
		{
			if(RxData == '\r')
			{
				RxState = 2;
			}
			else 
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if(RxState == 2)//等待第二个包尾
		{
			if(RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\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 。

 if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
    {
        ...;
    }

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

        uint8_t RxData = USART_ReceiveData(USART1);

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

1.等待包头

		if(RxState == 0)//等待包头
		{
			if(RxData == '@')
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}

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

2.接收数据

        因为载荷字符数量并不确定,所以每次接受之前必须先判断是不是包尾\r。

		else if(RxState == 1)//接收数据
		{
			if(RxData == '\r')
			{
				RxState = 2;
			}
			else 
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;//接收数据后位置编码就自增,指示接收下一个位置的数据。
			}
		}

        此代码逻辑是,每进一次接收状态数据就转存一次缓存数据,同时存的位置后移。当收到包尾\r就证明数据收完了,这时就可以转移到下一个状态。同时对pRxPacket清0,为下次接收准备,可以在状态0转移到状态1时提前清一个0.

3.等待第二个包尾

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

		else if(RxState == 2)//等待第二个包尾
		{
			if(RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\0';
				Serial_RxFlag = 1;
			}
		}

        接收到包尾之后还需要给字符数组的最后加一个字符串结束标志位\0,方便后续对字符串进行处理,不然这个字符数组没有结束标志位,就不知道这个字符串有多长了。

Serial_RxPacket[pRxPacket] = '\0';

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

   USART_ClearITPendingBit(USART1,USART_IT_RXNE);

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

四、代码问题改进

        当然这样还是存在一个问题,如果连续发送数据包,程序处理不及时,可能导致数据包错位。一般文本数据包是独立的,不存在连续,错位的话问题就比较大。所以程序可以稍作修改来解决这个问题,等每次处理完成之后,再开始接收下一个数据包。

        可以利用设计的获取标志位Serial_GetRxFlag函数,程序逻辑不使用原来读取Flag之后立刻清除的策略。在中断函数里,等待包头的时候再加一个条件。

			if(RxData == '@' && Serial_RxFlag == 0)

如果收到包头,并且Serial_RxFlag=0时,才执行接收,若Serial_RxFlag不等于0,就是发太快了,还没处理完呢,就跳过这个数据包。

        之前读取标志位之后立刻清零的函数Serial_GetRxFlag先删除。

        把Serial_RxFlag也申明为外部可调用,暂时不封装了。

extern uint8_t Serial_RxFlag;

        主函数里当Serial_RxFlag标志位为1时,就代表接收到数据包了,可以执行相应的操作。等操作完成之后,再把Serial_RxFlag标志位清0,在中断函数USART1_IRQHandler里只有Serial_RxFlag标志位为0了,才会继续接收下一个数据包。这样写数据和读数据就是严格分开的,不会同时进行,就可以避免数据包错位的现象了。但是这样发送数据包的频率就不能太快了,太快会丢弃部分数据包。

总代码示例:

Serial.c

#include "stm32f10x.h"                  // Device header
#include <stdio.h>

char Serial_RxPacket[100];//接收缓存用于接收字符
uint8_t Serial_RxFlag;//自定义的标志位,如果收到一个数据包,就置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);
	
//配置USART1的接收中断
	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;
}


//函数实现一个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;
	static uint8_t pRxPacket = 0;
	//判断标志位
	if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);
		if(RxState == 0)//等待包头
		{
			if(RxData == '@' && Serial_RxFlag == 0)
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if(RxState == 1)//接收数据
		{
			if(RxData == '\r')
			{
				RxState = 2;
			}
			else 
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if(RxState == 2)//等待第二个包尾
		{
			if(RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\0';
				Serial_RxFlag = 1;
			}
		}
		
				USART_ClearITPendingBit(USART1,USART_IT_RXNE); 
	}
}



 

Serial.h

#ifndef __SERIAL_H
#define __SERIAL_H

#include <stdio.h>

extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number,uint8_t Length);

#endif


总结

        以上就是今天要讲的内容,本文仅仅简单介绍了串口收发文本数据包程序设计的思路并详解了一些设计代码的细节,最后对于代码可能产生的问题做了改进。

作者:Echo_cy_

物联沃分享整理
物联沃-IOTWORD物联网 » STM32 串口收发文本数据包

发表回复