STM32笔记(6) ——————USART收发HEX数据包 &文本数据包

一、USART串口数据包格式

在实际应用中,我们常常需要连续发送或接收数据,有时需要对连续的数据进行分割和打包,我们才可以正确处理数据;使用数据包发送和接收数据还可以实现简单的人机交互设计。
打包和分割数据的方法可以自行设计(如把每个数据的最高位当作标志位来进行分割),串口的数据包采用添加包头和包尾的方式实现。

1、HEX数据包

1、固定包长,含包头包尾。即每个数据包的长度都固定不变,数据包前面的是包头,后面的是包围。

这里规定了一批数据有4个字节,在这4个字节首尾加上包头包围,比如规定0xFF为包头,0xFE为包围(类似一个标志位作用)
2、可变包长,含包头包尾。每个数据包的长度可以是不一样的。

HEX数据包适合发送最原始的数据,例如一些使用串口通信的陀螺仪、温湿度传感器等
1、如果载荷数据可能存在与包头包尾重复的情况,会产生误判,可以采用以下的方法解决:

  1. 规定有效载荷数据的范围(例如只发送0~100)
  2. 增加包头包尾的数量,尽量使其产生载荷数据中不会出现的格式
  3. 尽量采用固定包长发送数据包,在接收数据时,我们不关心有效数据是否和包头包尾重复,我们只关心应该是包头包尾的位置是否是包头包尾
      在实际使用时,如果载荷数据不会和包头包尾重复,可以二者留其一,例如只添加包头或者只添加包尾
    3、固定包长和可变包长的选择问题
    (1)对HEX来说,若载荷出现和包头包尾重复的情况,最好选择固定包长,避免接受错误
    (2)若不重复,可以选择可变包长
    4、各种数据转化为数据流的问题
    数据包都是一个字节一个字节组成的,若想发送16位整型数据、32位整型数据,float、double、甚至是结构体(其内部都是由一个字节一个字节组成的),只需要用一个uint8_t的指针指向它,把数据当作字节数组发送即可

2、接收HEX数据包

根据之前代码,每收到一个字节程序都会进一遍中断,在中断函数里我们可以拿到这一个字节,但拿到之后就要退出中断了,所以每拿到一个数据都是一个独立的过程。而对于数据包来说,它具有前后关联性,对于包头、数据、包尾这三种状态我们需要不同的处理逻辑,所以在程序中,我们需要设计一个能记住不同状态的机制,在不同的状态执行不同的操作,同时还要进行状态的合理转移,这种程序思维叫做“状态机”
要想设计好的“状态机”程序,画一个以下的状态转移图很有必要:

每收到一个字节,函数都会进入一次中断,在中断函数中,可以拿到一个字节,但拿到字节之后,就得退出中断,故每拿到一个数据,都是一个独立的过程,而对数据包来说,有数据、包头、包尾三种状态,根据状态不同处理也不同


执行流程
①最开始S=0。收到一个数据,进中断,根据S=0进入第一个状态的程序,判断数据是不是包头FF,如果是FF则代表收到包头,之后置S=1退出中断,结束。这样下次再进中断,根据S=1就可以进行接收数据的程序了。如果在第一个状态收到的不是FF,就说明数据包未对齐,这时应该等待数据包包头的出现,S仍是0,下次进中断仍是执行判断包头的逻辑,直到出现FF才可进入下一个状态。
②到接收数据的状态后,收到数据就把它存在数组中,再用一个变量记录接收了多少数据,没到4个就一直是这个接收数据状态,收够了就置S=2,进入下一个状态。
②最后等待包尾,判断数据是否为FE,是的话就置S=0回到最初状态,开始下一轮回。也有可能不是FE,比如数据于包头重复,导致前面包头位置判断错误,就可能导致包尾不是FE,这时就可进入重复等待包尾的状态,直到接收到真正包尾。


3、发送文本数据包

在HEX数据包里,数据都是以原始的字节数据本身呈现,在文本数据包里,每个字节经过了一层编码和译码,最终表现出来的就是文本格式。所以实际上每个文本字符背后都是一个字节的HEX数据:
在这里插入图片描述
由于数据译码成为字符形式,所以存在大量字符可作为包头包尾,可有效避免数据与包头包尾重复的问题。这里以@作为包头,\r和\n作为包尾,当我们接收到载荷数据之后得到就是一个字符串,在软件中再对字符串进行操作和判断,就可实现各种指令控制功能,且字符串数据包表达的意义很明显,可发送到串口助手在电脑显示打印,所以常以\n换行符作为包尾,这样打印是就可一行一行显示。


4、接收文本数据包

数据包的发送过程很简单,如HEX数据包发送,先定义一个数组,填充数据,然后用上一节写过USART_SendArray函数;文本数据包同理,写一个字符串,调用上一节写的USART_SendString函数。之所以简单是因为发送过程完全自主可控,想发什么就发什么,上一节串口也可体会到发送比接收简单多了。

执行流程:
可变包长,接受数据的状态(S=1)在进行数据接收的逻辑时,还要兼具等待包尾的功能:收到一个数据判断是否为\r,如果不是\r则正常接收数据;如果是\r则不接收数据,同时跳到下一个状态(S=2),等待包尾\n。因为这里设置了两个包尾\r、\n,所以需要第三个状态(S=2),如果只有一个包尾\r,那么在S=1状态中逻辑判断出现包尾\r,后就可直接回到初始状态。


5、HEX数据包和文本数据包对比

(1)在hex数据包中,数据都是以原始的字节数据本身呈现的
(2)在文本数据包中,每个字节就经过一层编码和译码,最终表现出文本格式(文本背后还是一个字节的HEX数据)
(3)hex数据包:传输直接、解析数据简单,适合一些模块发送原始的数据,比如一些使用串口通信的陀螺仪、温湿度传感器,但是灵活性不足、载荷容易和包头包尾重复
(4)文本数据包:数据直观易理解、灵活,适合一些输入指令进行人机交互,如蓝牙模块常使用的AT指令、CNC和3D打印机,但解析效率低.
(5)发送100,hex直接发送一个字节100,而文本发送三个字节’1’,‘0’.‘0’,收到之后还要把字符转换程数据,才能得到100。


二、串口收发HEX数据包&串口收发文本数据包

1、串口发送HEX数据包

HEX固定包长
1)Serial.c

将uint8_t Serial_GetRxData(void)替换成void Serial_SendPacket(void)

uint8_t Serial_TxPacket[4];

void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}

(2)Serial.h

extern uint8_t Serial_TxPacket[];

(3)main.c中添加

	Serial_TxPacket[0] = 0x01;
	Serial_TxPacket[1] = 0x02;
	Serial_TxPacket[2] = 0x03;
	Serial_TxPacket[3] = 0x04;
	Serial_SendPacket();

现象: 将程序下载到开发板中,按下复位键,在串口调试助手中显示FF 01 02 03 04 FE

2、串口收发HEX数据包

最终现象:
(1)主函数显示发送数据
将单片机接收到的数据(即串口调试助手发送的数据)显示在OLED显示屏上

if (Serial_GetRxFlag() == 1)//接收到了数据包
		{
		    //接收到的数据放在第四行
			OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2);
			OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);
			OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);
			OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);
		}

(2)主函数添加key
添加了key按键,将数据发送出去的数据显示在OLED,按一次健,发送数据都+1

KeyNum = Key_GetNum();
if (KeyNum == 1)
{
	Serial_TxPacket[0] ++;
	Serial_TxPacket[1] ++;
	Serial_TxPacket[2] ++;
	Serial_TxPacket[3] ++;
			
	Serial_SendPacket();
			
	OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2);
	OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);
	OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);
	OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);
}

完整代码:
1.Serial.c

#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>
//1、开启时钟,打开USART和GPIO时钟都打开
//2、GPIO初始化,把TX配置成复用输出,RX配置为输入
//3、配置USART,使用一个结构体即可配置所有相关参数
//4、如果只需要发送功能,直接开启USART初始化就结束了
//(5、如果需要接收功能,可能需要配置中断,在开启USART之前加上ITConfig和NVIC的代码就行)
//初始化完成后,如果需要发送数据调用一个发送函数就行,接收数据同理,如果要获取发送和接收标志位也是调用一个函数


//先定义两个缓存区的数组,4个字节(只存储发送或接收的载荷数据)
//在头文件里声明外部可调用,使它们可在main.c里使用赋值
uint8_t Serial_RxPacket[4];
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxFlag;//多

void Serial_Init (void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//USART1为APB2总线上的外设
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP ;//复用推挽输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//供串口外设TX脚使用
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);

	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate= 9600;//Init函数内部会自动计算需要的分频系数写入BRR寄存器
	USART_InitStructure.USART_HardwareFlowControl= USART_HardwareFlowControl_None;//无流控
	USART_InitStructure.USART_Mode= USART_Mode_Tx | USART_Mode_Rx;//接收+发送
	USART_InitStructure.USART_Parity= USART_Parity_No;//Odd奇、Even偶、No无校验
	USART_InitStructure.USART_StopBits= USART_StopBits_1;//停止位长度
	USART_InitStructure.USART_WordLength= USART_WordLength_8b;//字长8位
	USART_Init(USART1,&USART_InitStructure);
	
	//对于串口接收可使用查询和中断两种方法,如果使用查询到此初始化就结束
	//查询流程:在主函数里不断判断RXNE标志位,置1说明收到数据了,
	          //再调用USART_ReceiveData读取DR(与上面的Serial_SendByte类似)
	//下面我们程序中实现下中断的方法:
	//写入中断的代码
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启RXNE标志位到中断的输出
	
	//以下是配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//先分组 一般为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);//等待置1
	//该标志位不需要手动清零,下一次SendData这个标志位会自动清零
}

void Serial_SendArray(uint8_t* Array,uint16_t Length)
{
	//第一个参数为uint8_t*类型指向数组首地址,传递数组需要用到指针
	//第二个参数由于数组无法判断是否结束,用Length
	uint16_t i;
	for(i=0;i<Length;i++){
		Serial_SendByte(Array[i]);
	}
}

void Serial_SendString(char* String)//uint8_t也可以,由于字符串自带结束标志位0,不用Length了
{
	uint8_t i;
    for(i=0;String[i] != '\0';i++)//可以把字符串当作一个数组
	{
		Serial_SendByte(String[i]);
	}
	
}

uint32_t Serial_Pow(uint32_t X,uint32_t Y)//次方函数X**Y   这个在math.h这个库里用POW()函数可以直接得出
{
	uint32_t Result = 1;
	while(Y--)
	{
		Result *= X;
	}
	return Result;
}

void Serial_SendNumer(uint32_t Number,uint8_t Length)
{
	//首先要把需要发送的Number的个位、十位、百位、千位等每一位以十进制拆分开
	//然后转换为字符数字对应的数据,依次发送出去
	//以十进制拆分:比如一个数字为1234,取千位1:1234/10**3=1.234;1.234%10=1
	uint8_t i;
	for(i=0;i<Length;i++)
	{
		Serial_SendByte(Number/Serial_Pow(10,Length-i-1)%10+0x30);
		//之所以是Length-i-1是因为从高位开始取的(如1234依次取千位、百位、十位、个位依次发送)
		//之所以+0x30是因为要用字符(文本)显示,ASCII码表里字符0对应0x30。其实也可写成+'0'
	}	
}

int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);//改发送给串口
	return ch;
}

//用来接收后面的可变参数列表
void Serial_Printf(char *format, ...)
{
	char String[100];
	va_list arg;
	va_start(arg, format);
	vsprintf(String, format, arg);
	va_end(arg);
	Serial_SendString(String);
}

/****************************************************/
//函数功能:发送。调用后,TxPacket数组里的4个数据,就会自动加上包头包尾发送出去
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket,4);
	Serial_SendByte(0xFE);
}

//意思是返回这个标志位,0就返回0,1就返回1,但给这个1复位一下,使得查一次就能反应这一次
uint8_t Serial_GetRxFlag(void)//读后自动清除的功能 用于判断是否收到了数据包
{
	if (Serial_RxFlag == 1)
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}
 
 
/*uint8_t Serial_GetRxData(void)//封装接收数据函数
{
	return Serial_RxData;
}*/
 
/*void USART1_IRQHandler(void){//RXNE标志位一但置1,就会向NVIC申请中断,之后就会在中断函数里接收数据
							//其实就是在中断里面对数据进行了一次转存,最终还要扫描查询RxFlag来接收数据
							//放在这里转运一个字节意义看着不大,但是为下节多字节数据包接收作铺垫
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)//判断标志位
	{
		//在中断中对数据进行转存
		Serial_RxData = USART_ReceiveData(USART1);//先读取模块的变量里
		Serial_RxFlag = 1;
		//如果读取DR,就自动清除,如果没有读取DR,就要手动清除
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除标志位
	}
}*/

/****************************************************/

void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;//状态变量S(接收)
	//这个静态变量类似于全局变量,函数进入后只会初始化一次0,函数退出后数据仍然有效
	//与全局变量不同的是,静态变量只能在本函数使用
	//因为不用静态,退出中断函数,变量就无了,下次进中断又是重新计数
	static uint8_t pRxPacket = 0;//指示接收到哪一个字节(载荷数据)
	
	if (USART_GetFlagStatus(USART1,USART_IT_RXNE)==SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);//取出接收到的字节(一次一个)
		if (RxState == 0)
		{
			if (RxData == 0xFF)//检测到包头
			{
				RxState = 1;//进入下一个状态
				pRxPacket = 0;//在进入S=1前,提前清0
			}
		}
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket] = RxData;//传给接收数组
			pRxPacket++;
			if (pRxPacket >=4)//接收够4个字节的载荷数据
			{
				RxState = 2;//进入下一个状态
			}
		}
		else if (RxState == 2)
		{
			if (RxData == 0xFE)//检测到包尾
			{
				RxState = 0;//S清0开始下一轮回
				Serial_RxFlag = 1;//一个数据包接收完毕,置一个标志位
			}
		}
		//别用3个if,防止上一个if执行一半时出现多分枝同时成立,执行故障。比如if(RxState=0)执行到置S为1时...
		//用else if可保证每次进来只能选择一个分支执行,也可用switch实现
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}

2.main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
uint8_t KeyNum;
int main(void)
{
	OLED_Init();
	Key_Init();
	Serial_Init();
	
	OLED_ShowString(1, 1, "TxPacket");
	OLED_ShowString(3, 1, "RxPacket");
	
	Serial_TxPacket[0]=0x11;//赋值要发送的
	Serial_TxPacket[1]=0x22;
	Serial_TxPacket[2]=0x33;
	Serial_TxPacket[3]=0x44;
	
	while(1)
	{
		KeyNum = Key_GetNum();
		if (KeyNum == 1)
		{
			Serial_TxPacket[0] ++;
			Serial_TxPacket[1] ++;
			Serial_TxPacket[2] ++;
			Serial_TxPacket[3] ++;
			
			Serial_SendPacket();
			
			OLED_ShowHexNum(2,1,Serial_TxPacket[0],2);
			OLED_ShowHexNum(2,4,Serial_TxPacket[1],2);
			OLED_ShowHexNum(2,7,Serial_TxPacket[2],2);
			OLED_ShowHexNum(2,10,Serial_TxPacket[3],2);
		}
		if (Serial_GetRxFlag() == 1)//这里写Serial_RxFlag=1是为了告诉主程序,收到了一帧完整的数据包
		{//如果接收到外部数据包,则显示
			OLED_ShowHexNum(4,1,Serial_RxPacket[0],2);
			OLED_ShowHexNum(4,4,Serial_RxPacket[1],2);
			OLED_ShowHexNum(4,7,Serial_RxPacket[2],2);
			OLED_ShowHexNum(4,10,Serial_RxPacket[3],2);
			//程序问题:Serial_R10xPacket是一个同时被写入又同时被读出的数组,
			//在中断函数里依次把接收的字节写入它,在main.c里由依次读出它显示,
			//这会导致数据包之间会混在一起,比如读出速度太慢,读到一半数组就刷新了
			//解决方法:在接收部分加入判断,在数据包读取完成后再写入下一轮
			//很多情况不需要考虑此问题,这种HEX数据包多用于传输各种传感器的每个独立数据:
			//比如陀螺仪的X、Y、Z轴数据,温湿度数据等,它们相邻数据包之间的数据具有连续性即使混在一起也没关系
		}
		
	}

}

3、串口发送文本数据包

在上一个程序Serial.c修改

/*
	中断函数
*/
void USART1_IRQHandler(void)
{
	static uint8_t pRxData = 0;
	static uint8_t RxState = 0;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)  // 接收寄存器不空
	{		
		uint8_t RxData = USART_ReceiveData(USART1);  // 接收数据
		
		if(RxState == 0)  // 等待包头
		{
			if(RxData == '@' && RxFlag==0)  // 收到包头  
			{
				RxState = 1;  // 转换状态
				pRxData = 0;
			}
		}
		else if(RxState == 1)  // 接收数据打包
		{
			if(RxData == '\r')  // 收到完整数据
			{
				RxState = 2;
			}
			else
			{
				RxPacket[pRxData++] = RxData;
			}
		}
		else if(RxState==2) // 等待包尾
		{
			if(RxData=='\n')
			{
				RxState = 0;
				RxPacket[pRxData] = '\0';
				//接收到之后还要在字符数组的最后加上结束标志位'\0',方便后续对字符串进行处理
				RxFlag = 1;  // 接收完成标志位
			}
		}
		// 清除中断标志
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

把读取标志位然后立即清0的函数删掉,在主函数中,判断Serial_RxFlag ==1,表示接收到数据包,等操作完成之后,再清0,在中断函数中,只有Flag=0了,才会继续接收到下一个数据包。这样数据的读写就是严格分开,不会混淆,但是这样发送数据包的频率不能太快,否则会被丢弃。或者定义一个指令缓存区,把接受好的字符串放在这个指令缓存区进行排队

main.c

int main(void){
	OLED_Init();
	OLED_ShowString(1,1,"RxPacket");
	
	Serial_Init();
	
	while(1){
		if (Serial_GetRxFlag() == 1){
			OLED_ShowString(2,1,"              ");//清屏
			OLED_ShowString(2,1,Serial_RxPacket);
		}
		
	}
}
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值