声明:学习笔记根据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的编码方法和这个类似(不过它更高级些)。
本节的数据包分割方法并不是这种,这种方式破坏了原有数据使用起来比较复杂,串口数据包通常使用的是额外添加包头包尾的方式:
包头包尾方法如下江科大列举了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的指针指向它,把它们当作一个字节数组发送就行(可见指针学习)
在HEX数据包里,数据都是以原始的字节数据本身呈现,在文本数据包里,每个字节经过了一层编码和译码,最终表现出来的就是文本格式。所以实际上每个文本字符背后都是一个字节的HEX数据:
1、固定包长,含包头包尾
2、可变包长,含包头包尾
由于数据译码成为字符形式,所以存在大量字符可作为包头包尾,可有效避免数据与包头包尾重复的问题。这里以@作为包头,\r和\n作为包尾,当我们接收到载荷数据之后得到就是一个字符串,在软件中再对字符串进行操作和判断,就可实现各种指令控制功能,且字符串数据包表达的意义很明显,可发送到串口助手在电脑显示打印,所以常以\n换行符作为包尾,这样打印是就可一行一行显示。
HEX数据包优点是传输最直接、解析数据简单,比较适合一些模块发送原始数据,如一些使用串口通信的陀螺仪、温湿传感器;缺点是灵活性不足、载荷数据易于包头包尾重复。
文本数据包优点是直观易理解、灵活,比较适合一些文本指令进行人机交互的场合,如蓝牙模块常使用的AT指令、CNC和3D打印机常用的G代码都是文本数据包格式;缺点是解析效率低,比如发送一个100,HEX就直接是100一个字节,文本数据包要3个字节的字符‘1’、‘0’、‘0’,收到后还要把字符转换为数据才能得到100。
数据包的发送过程很简单,如HEX数据包发送,先定义一个数组,填充数据,然后用上一节写过的USART_SendArray函数;文本数据包同理,写一个字符串,调用上一节写的USART_SendString函数。之所以简单是因为发送过程完全自主可控,想发什么就发什么,上一节串口也可体会到发送比接收简单多了。
下面重点介绍下接收(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,这时就可进入重复等待包尾的状态,直到接收到真正包尾。
“状态机”是一种普遍编程思路,一般最好配合状态转移图思路更加清晰。比如做个菜单,按什么键切换什么菜单,执行什么程序;还有一些芯片内部逻辑,芯片什么时候进入待机状态什么时候进入工作状态,都会用到状态机。
流程类似,只不过这里因为演示的是可变包长,接受数据的状态(S=1)在进行数据接收的逻辑时,还要兼具等待包尾的功能:收到一个数据判断是否为\r,如果不是\r则正常接收数据;如果是\r则不接收数据,同时跳到下一个状态(S=2),等待包尾\n。因为这里设置了两个包尾\r、\n,所以需要第三个状态(S=2),如果只有一个包尾\r,那么在S=1状态中逻辑判断出现包尾\r,后就可直接回到初始状态。
接线图与上次串口的基本相同,只不过HEX数据包接线图里PB1口接了个按键,用于控制;收发文本的接线图,在PA1口接了LED用于指示。按键和LED的附加功能可自己实现,下面的代码只写核心部分
代码是在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轴数据,温湿度数据等,它们相邻数据包之间的数据具有连续性即使混在一起也没关系
}
}
}
//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精讲
…