STM32学习:深入理解USART串口数据包传输

文章目录

  • 前言
  • 一、数据包格式(江科大规定)
  • 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数据包接收(只演示固定包长)

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

    对于上面的一个固定包长的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.文本数据包接收(只演示可变包长)


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


    三、实操案例

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

    1.串口收发HEX数据包


    代码是在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,代码主要写接收)

    //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精讲

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32学习:深入理解USART串口数据包传输

    发表评论