【STM32学习】——USART串口数据包&HEX/文本数据包&收发流程&串口收发HEX/文本数据包实操

文章目录

  • 前言
  • 一、数据包格式(江科大规定)
    • 1.HEX数据包
    • 2.文本数据包
    • 3.两者对比
  • 二、数据包收发流程
    • 1.HEX数据包接收(只演示固定包长)
    • 2.文本数据包接收(只演示可变包长)
  • 三、实操案例
    • 1.串口收发HEX数据包
    • 2.串口收发文本数据包(发直接用SendString,代码主要写接收)
  • 总结


声明:学习笔记根据b站江科大自化协stm32入门教程编辑,仅供学习交流使用!

前言

本次学习有两个实操代码,第一个是串口收发HEX数据包,第二个是串口收发文本数据包


一、数据包格式(江科大规定)

数据包的作用是把一个个单独的数据打包起来,方便我们进行多字节的数据通信。之前学习的串口代码,发送一个或接收一个字节都没问题。但在实际应用中需要把多个字节打包为一个整体进行发送。比如有一个陀螺仪传感器需要用串口发送数据到STM32,比如X轴一个字节、Y轴一个字节、Z轴一个字节总共3个数据需要连续不断地发送,当按照XYZXYZXYZ…进行连续发送时会出现一个问题,接收方不知道哪个对应X、哪个对应Y,哪个对应Z,因为接收方可能会从任意位置开始接收,会出现数据错位的现象,这时需要一种方式对数据进行分割为一个个数据包,这样接收方可以方便识别第1个为X、第2个位Y、第3个为Z。
分割打包的方法可以自己发挥设计,只要逻辑合理即可比如可以设计在XYZXYZ…数据流中,数据包第1个数据也就是X数据包,它的最高位置1,其余数据包最高位都置0,当接收到数据后判断下最高位,如果是1就是X数据,然后紧跟着的两个数据就是Y和Z,这种分割方法就是把每个数据的最高位当作标志位来进行分割,实际例子比如UTF8的编码方法和这个类似(不过它更高级些)。
本节的数据包分割方法并不是这种,这种方式破坏了原有数据使用起来比较复杂,串口数据包通常使用的是额外添加包头包尾的方式


1.HEX数据包

包头包尾方法如下江科大列举了2种数据包格式:
具体格式可根据需求自己规定,也可能是买了个模块别的开发者规定的
1、固定包长,含包头包尾。即每个数据包的长度都固定不变,数据包前面的是包头,后面的是包围。
在这里插入图片描述
这里规定了一批数据有4个字节,在这4个字节首尾加上包头包围,比如规定0xFF为包头,0xFE为包围(类似一个标志位作用)。
2、可变包长,含包头包尾。每个数据包的长度可以是不一样的。
在这里插入图片描述
研究问题:
①包头包尾和数据载荷重复的问题。比如规定0xFF为包头,0xFE为包围,那如果传输的数据本身就是就是FF或FE呢?这里确实会引起误判,解决方法有:第一种,限制载荷数据的范围,在发送的时候对数据进行限幅,比如XYZ3个数据变化范围只可以0-100。第二种,如果无法避免数据与包头包尾重复,就尽量使用固定长度的数据包(固定包长),这样由于载荷数据固定只要通过包头包尾对齐了数据,就可严格知道哪个应该是包头包尾哪个应该是数据(只在特定步长处if判断是否为包头包尾)。第三种,增加包头包尾的数量,并且让它尽量呈现出载荷数据出现不了的状态,比如使用FF、FE作为包头,FD、FC作为包尾。
②包头包尾并不是全部都需要,比如可以只要包头FF不要包尾,包头+4个字节,收够4个字节后置标志位,一个数据包接收完成,只不过这样载荷和包头重复的问题会更严重些。
③固定包长和可变包长的选择。对于HEX数据包,如果载荷会出现和包头包尾重复的情况,最好选择固定包长,无重复情况可选择可变包长。
④各种数据转换为字节流。这里的数据包都是一个字节一个字节组成的,如果想发送16位整型数据、32位整型数据、float、double、甚至结构体都没问题,因为它们内部其实都是由一个字节一个字节组成的。只需要用一个uint8_t的指针指向它,把它们当作一个字节数组发送就行(可见指针学习)


2.文本数据包

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


3.两者对比

HEX数据包优点是传输最直接、解析数据简单,比较适合一些模块发送原始数据,如一些使用串口通信的陀螺仪、温湿传感器;缺点是灵活性不足、载荷数据易于包头包尾重复。
文本数据包优点是直观易理解、灵活,比较适合一些文本指令进行人机交互的场合,如蓝牙模块常使用的AT指令、CNC和3D打印机常用的G代码都是文本数据包格式;缺点是解析效率低,比如发送一个100,HEX就直接是100一个字节,文本数据包要3个字节的字符‘1’、‘0’、‘0’,收到后还要把字符转换为数据才能得到100。


二、数据包收发流程

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

1.HEX数据包接收(只演示固定包长)

根据之前代码,每收到一个字节程序都会进一遍中断,在中断函数里我们可以拿到这一个字节,但拿到之后就要退出中断了,所以每拿到一个数据都是一个独立的过程。而对于数据包来说,它具有前后关联性,对于包头、数据、包尾这三种状态我们需要不同的处理逻辑,所以在程序中,我们需要设计一个能记住不同状态的机制,在不同的状态执行不同的操作,同时还要进行状态的合理转移,这种程序思维叫做“状态机”。
要想设计好的“状态机”程序,画一个以下的状态转移图很有必要:
【STM32学习】——USART串口数据包&HEX/文本数据包&收发流程&串口收发HEX/文本数据包实操_第1张图片
对于上面的一个固定包长的HEX数据包,我们定义三个状态:等待包头、接收数据、等待包尾,每个状态都需要一个变量来标志一下,比如上面依次用S=0、S=1、S=2(有点类似置标志位,只不过标志位只有0/1,而状态机是多标志位状态的一种方式)。


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


状态机”是一种普遍编程思路,一般最好配合状态转移图思路更加清晰。比如做个菜单,按什么键切换什么菜单,执行什么程序;还有一些芯片内部逻辑,芯片什么时候进入待机状态什么时候进入工作状态,都会用到状态机。


2.文本数据包接收(只演示可变包长)

【STM32学习】——USART串口数据包&HEX/文本数据包&收发流程&串口收发HEX/文本数据包实操_第2张图片
流程类似,只不过这里因为演示的是可变包长接受数据的状态(S=1)在进行数据接收的逻辑时,还要兼具等待包尾的功能:收到一个数据判断是否为\r,如果不是\r则正常接收数据;如果是\r则不接收数据,同时跳到下一个状态(S=2),等待包尾\n。因为这里设置了两个包尾\r、\n,所以需要第三个状态(S=2),如果只有一个包尾\r,那么在S=1状态中逻辑判断出现包尾\r,后就可直接回到初始状态。


三、实操案例

接线图与上次串口的基本相同,只不过HEX数据包接线图里PB1口接了个按键,用于控制;收发文本的接线图,在PA1口接了LED用于指示。按键和LED的附加功能可自己实现,下面的代码只写核心部分

1.串口收发HEX数据包

【STM32学习】——USART串口数据包&HEX/文本数据包&收发流程&串口收发HEX/文本数据包实操_第3张图片
代码是在9-2串口发送+接收(只可一个字节)基础上改进而成:

//Serial.c
//在9-2串口发送+接收(只可一个字节)基础上改进


#include "stm32f10x.h"   
//先定义两个缓存区的数组,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);
	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;
	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;
	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]);
	}
}

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

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(接收)
	//这个静态变量类似于全局变量,函数进入后只会初始化一次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);
	}
}
//Serial.h
#ifndef __SERIAL_H
#define __SERIAL_H

extern uint8_t Serial_RxPacket[];//数组声明时数量可以不要
extern uint8_t Serial_TxPacket[];

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
uint8_t Serial_GetRxFlag(void);
void Serial_SendPacket(void);
#endif
//main.c
#include "stm32f10x.h"   
#include "Delay.h"   
#include "OLED.h"
#include "Serial.h"

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

2.串口收发文本数据包(发直接用SendString,代码主要写接收)

【STM32学习】——USART串口数据包&HEX/文本数据包&收发流程&串口收发HEX/文本数据包实操_第4张图片

//Serial.c
#include "stm32f10x.h"  

char Serial_RxPacket[100];//数量100给大点,防止溢出,这要求单条指令不可超过100个字符

uint8_t Serial_RxFlag;

void Serial_Init(void){
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	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;
	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;
	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]);
	}
	
}

uint32_t Serial_Pow(uint32_t X,uint32_t Y){
	uint32_t Result = 1;
	while(Y--){
		Result *= X;
	}
	return Result;
}
void Serial_SendNumer(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+0x30);
	}
	
}


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_GetFlagStatus(USART1,USART_IT_RXNE)==SET){
		uint8_t RxData = USART_ReceiveData(USART1);
		if (RxState == 0){
			if (RxData == '@'){
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if (RxState == 1){
			if (RxData == '\r'){//因为可变包长,接受前先判断包尾
				RxState = 2;
			}
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket++;
		}
		else if (RxState == 2){
			if (RxData == '\n'){
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\0';
				//接收到之后还要在字符数组的最后加上结束标志位'\0',方便后续对字符串进行处理
				Serial_RxFlag = 1;
			}
		}
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}
//main.c
#include "stm32f10x.h"   
#include "Delay.h"   
#include "OLED.h"
#include "Serial.h"

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);
		}
		
	}
}


总结

做任何事情,都要有一股坚忍不拔的毅力,只要坚持,挺过风雨,终会看见彩虹;只要坚持,走过黑暗,总会拥抱黎明;只要坚持,战胜失败,总能赢得成功!
今天的学习分享到此就结束了,我们下次再见!!
在这里插入图片描述往期精彩:
STM32定时器输入捕获(IC)
STM32定时器输出比较(PWM波)
STM32定时中断
STM32外部中断
STM32GPIO精讲

你可能感兴趣的:(STM32学习,stm32,学习,单片机)