【江协STM32】9-4/5 USART串口数据包、串口收发HEX数据包&串口收发文本数据包

1. 数据包

把一个个单独的数据打包,方便进行多字节的数据通信。

例如陀螺仪传感器,需要用串口发送数据到STM32。对于陀螺仪的数据,假设X、Y、Z轴各为一个字节,共计3个数据需要连续不断地发送。如果像XYZXYZ...连续发送时,接收方无法区分X、Y和Z轴的数据,因为接收方可能会从任意位置开始接收,所以会出现数据错位的现象。因此,需要将数据进行分割,把XYZ一批数据分开,分成一个个数据包。

数据包分割方法有多种,串口数据包通常使用的是额外添加包头包尾的方式。

1.1 HEX数据包

包头包尾和数据载荷重复------这里定义FF为包头,FE为包尾,如果传输的数据本身就是FF和FE,有如下几种解决方法:

  1. 限制载荷数据的范围,避免和包头包尾重复;
  2. 如果无法避免载荷数据和包头包尾重复,尽量使用固定长度的数据包;
  3. 增加包头包尾的数量,并且使之尽量呈现出载荷数据出现不了的状态,比如使用FF、FE作为包头,FD、FC作为包尾

包头包尾并不是全部都需要的,比如可以只要一个包头,把包尾删掉。这样数据包的格式就是,一个包头FF加4个数据。当检测到FF开始接收,收够4个字节后,置标志位,一个数据包接收完成。不过这种情况下载荷和包头重复的问题会更加严重。

固定包长和可变包长的选择------对应HEX数据包来说,如果载荷会出现和包头包尾重复的情况,最好选择固定包长,这样可以避免接收错误。

1.2 文本数据包

在HEX数据包中,数据均以原始字节数据呈现。在文本数据包中,每个字节经过了一层编码和译码,最终表现出来的就是文本格式。

1.3 固定包长HEX数据包的接收

根据之前的代码,我们知道,每收到一个字节,程序都会进一遍中断,在中断函数中获取到这一字节,但获取之后就需要退出中断,所以,每获取到一个数据都是一个独立的过程。而对于数据包来说,很明显它具有前后关联性,包头之后是数据,数据之后是包尾。对于包头、数据和包尾这3种状态,需要有不同的处理逻辑,所以需要在程序中设计一个能记住不同状态的机制,在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序设计思维就叫做"状态机"。

这里使用状态机的方法来接收一个数据包。对于上面这样一个固定包长HEX数据包来说,可以定义三个状态"等待包头、接收数据和等待包尾",每个状态需要用一个变量标志,可以标志三个状态依次为"S=0、S=1和S=2"。执行的流程是

  1. 最开始S=0,收到一个数据后进入中断,根据S=0进入第一个状态的程序,判断数据是否为包头FF。如果是FF,则代表收到包头,之后置S=1,退出中断,结束。 这样下次再进入中断,根据S=1,就可以进行接收数据的程序。如果在第一个状态收到的不是FF,就证明数据包没有对齐,需要等待数据包包头的出现,这时状态仍然为0,下次进入中断还是判断包头的逻辑,直到出现FF才能转到下一个状态。
  2. 收到FF进入接收数据状态S=1。此时再收到数据就直接保存在数组中,另外再使用一个变量,用来记录接收数据的个数。如果未收够4个数据,就一直是接收状态,如果收够了,就置S=2,下次中断时即可进入下一个状态。
  3. 最后一个状态为等待包尾,判断数据是否是FE。正常情况下应该为FE,这样就可以置S=0,回到最初的状态,开始下一个轮回。也有可能这个数据不是FE,比如数据和包头重复,导致包头位置判断错误,此时就可以进入重复等待包尾的状态,直到接收到真正的包尾。

1.4 可变包长文本数据包的接收

同样也是利用状态机,定义3个状态。

  1. 等待包头S=0。判断收到的是否为规定的@符号。
  2. 接收数据等待包尾S=1。因为是可变包长,所以此状态需要兼具等待包尾的功能。收到一个数据,判断是否为\r。如果不是,则正常接收,如果是,则不接收,同时跳到下一个状态。
  3. 等待包尾S=2。因为数据包有两个包尾\r、\n,所以需要第三个状态等待包尾\n。如果只有一个包尾,那么出现包尾之后就可以直接回到初始状态了,只需两个状态即可,因为接收数据和等待包尾需要在一个状态内同时进行。

2. 串口收发HEX数据包

2.1 接线图

按键按一下,发送一次数据包。同时在OLED上显示发送的数据包和接收的数据包。

2.2 代码

在"9-2 串口发送+接收"的程序上修改。

HEX数据包格式:固定包长,含包头包尾,其中包头为FF,包尾为FE,载荷数据固定4字节。

Serial.c

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include <stdio.h>

//  为了收发数据包,先定义两个缓存区的数组
uint8_t Serial_TxPacket[4];//   4个数据只存储发送或接收的载荷数据
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;//    收到一个数据包,置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;//   字长。不需要校验,所以选8位即可
    USART_Init(USART1, &USART_InitStructure);
    
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//    开启RXNE标志位到NVIC的输出
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitSturcture;
    NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn;//    中断通道
    NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitSturcture);
    
    USART_Cmd(USART1, ENABLE);
}

//  发送字节
void Serial_SendByte(uint8_t Byte)
{
    USART_SendData(USART1, Byte);// 调用此库函数,Byte变量就写入TDR了,写完后需要等待TDR数据转移至移位寄存器。如果数据在TDR内再写入数据,会产生数据覆盖
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//  发送数据寄存器(TDR)空标志位
    //  这里标志位置1后不需要手动清零,当下一次再SendData时,此标志位会自动清零
}

//  发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)// 指向待发送数组的首地址。由于数组无法判断是否结束,所以需要传递一个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++)//   循环条件用结束标志位来判断。这里数字0对应空字符,是字符串的结束标志位。如果不等于0,就是还没结束,进行循环
                                   //   这里数据0也可以写成字符形式,就是'\0',这就是空字符的转义字符表示形式for(i = 0; String[i] != '\0'; i++),和直接写0最终效果是一样的
    {
        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_SendNumber(uint32_t Number, uint8_t Length)
{
    //  需要把Number的个位、十位、百位等以十进制拆分开,然后转换成字符数字对应的数据,依次发送
    uint8_t i;
    for(i = 0; i < Length; i++)
    {
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');//    最终要以字符的形式发送,所以最后要加上字符的偏移,根据ASCII码表,字符0对应的数据是0x30,也可以以字符的形式写'0'
    }
}

int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}

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

//  实现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
                               //   这个静态变量类似于全局变量,函数进入只会初始化一次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;//  转移状态RxState=1
                pRxPacket = 0;//    清零
            }
        }
        else if(RxState == 1)// 接收数据
        {
            Serial_RxPacket[pRxPacket] = RxData;
            pRxPacket++;
            if(pRxPacket >= 4)
            {
                RxState = 2;//  转移状态RxState=2
            }
        }
        else if(RxState == 2)// 等待包尾
        {
            if(RxData == 0xFE)
            {
                RxState = 0;//  回到最初的状态,同时,代表一个数据包收到了
                Serial_RxFlag = 1;//    置接收标志位1
            }
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);//   清除标志位
    }
}

Serial.h

cpp 复制代码
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>

extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];

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

void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);
    
#endif

main.c

cpp 复制代码
#include "stm32f10x.h"                  // Device 
#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] = 0x01;
    Serial_TxPacket[1] = 0x02;
    Serial_TxPacket[2] = 0x03;
    Serial_TxPacket[3] = 0x04;
    
	while(1)
	{
        //  按键按下后,数组数据加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)
        {
            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);
        }
	}
}

其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具)、 Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)、 Key.h(【江协STM32】3-4 按键控制LED&光敏传感器控制蜂鸣器,第1.3节)

3. 串口收发文本数据包

3.1 接线图

3.2 代码

在上节程序的基础上进行修改。

文本数据包格式:可变包长,含包头包尾,以@符号为包头,以\r\n两个符号为包尾,载荷字符数量不固定。

Serial.c

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include <stdio.h>

//  为了接收数据包,先定义缓存区的数组
char Serial_RxPacket[100];//    设置单条指令最长不能超过100个字符
uint8_t Serial_RxFlag;//    收到一个数据包,置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;//   字长。不需要校验,所以选8位即可
    USART_Init(USART1, &USART_InitStructure);
    
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//    开启RXNE标志位到NVIC的输出
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitSturcture;
    NVIC_InitSturcture.NVIC_IRQChannel = USART1_IRQn;//    中断通道
    NVIC_InitSturcture.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitSturcture.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitSturcture.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitSturcture);
    
    USART_Cmd(USART1, ENABLE);
}

//  发送字节
void Serial_SendByte(uint8_t Byte)
{
    USART_SendData(USART1, Byte);// 调用此库函数,Byte变量就写入TDR了,写完后需要等待TDR数据转移至移位寄存器。如果数据在TDR内再写入数据,会产生数据覆盖
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//  发送数据寄存器(TDR)空标志位
    //  这里标志位置1后不需要手动清零,当下一次再SendData时,此标志位会自动清零
}

//  发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)// 指向待发送数组的首地址。由于数组无法判断是否结束,所以需要传递一个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++)//   循环条件用结束标志位来判断。这里数字0对应空字符,是字符串的结束标志位。如果不等于0,就是还没结束,进行循环
                                   //   这里数据0也可以写成字符形式,就是'\0',这就是空字符的转义字符表示形式for(i = 0; String[i] != '\0'; i++),和直接写0最终效果是一样的
    {
        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_SendNumber(uint32_t Number, uint8_t Length)
{
    //  需要把Number的个位、十位、百位等以十进制拆分开,然后转换成字符数字对应的数据,依次发送
    uint8_t i;
    for(i = 0; i < Length; i++)
    {
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');//    最终要以字符的形式发送,所以最后要加上字符的偏移,根据ASCII码表,字符0对应的数据是0x30,也可以以字符的形式写'0'
    }
}

int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}

//  中断函数。用状态机执行接收逻辑
void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;//   标志当前状态的变量S
                               //   这个静态变量类似于全局变量,函数进入只会初始化一次0,在函数退出后数据仍然有效
    static uint8_t pRxPacket = 0;// 指示接收到第几个数据
    if(USART_GetFlagStatus(USART1, USART_IT_RXNE) == SET)// RXNE:当RDR移位寄存器中的数据被转移到USART_DR寄存器中,该位被硬件置位。
    {
        uint8_t RxData = USART_ReceiveData(USART1);
        
        if(RxState == 0)//  等待包头
        {
            if(RxData == '@' && Serial_RxFlag == 0)//    收到包头。等待每次处理完成之后再开始接收下一个数据包,Serial_RxFlag==0才执行接收,否则就是发送太快,还没处理完成
            {
                RxState = 1;//  转移状态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';//    在字符数组最后加字符串结束标志位\0,方便对字符串进行处理。否则使用ShowString时由于没有结束标志位,无法判断字符串长度
                Serial_RxFlag = 1;//    置接收标志位1
            }
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);//   清除标志位
    }
}

Serial.h

cpp 复制代码
#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);
uint32_t Serial_Pow(uint32_t X, uint32_t Y);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
    
#endif

main.c

cpp 复制代码
#include "stm32f10x.h"                  // Device 
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include <string.h> //  判断字符串时使用

int main(void)
{
	OLED_Init();
    LED_Init();
    Serial_Init();
    
    OLED_ShowString(1, 1, "TxPacket");
    OLED_ShowString(3, 1, "RxPacket");
    
	while(1)
	{
        if(Serial_RxFlag == 1)//    代表接收到数据包
        {
            OLED_ShowString(4, 1, "                ");//    擦除第4行
            OLED_ShowString(4, 1, Serial_RxPacket);
            
            if(strcmp(Serial_RxPacket, "LED_ON") == 0)// 判断两个字符串是否相等。相等则返回0
            {
                LED1_ON();
                Serial_SendString("LED_ON_OK\r\n");
                OLED_ShowString(2, 1, "                ");//    擦除第4行
                OLED_ShowString(2, 1, "LED_ON_OK");
            }
            else if(strcmp(Serial_RxPacket, "LED_OFF") == 0)// 判断两个字符串是否相等。相等则返回0
            {
                LED1_OFF();
                Serial_SendString("LED_ON_OFF\r\n");
                OLED_ShowString(2, 1, "                ");//    擦除第4行
                OLED_ShowString(2, 1, "LED_OFF_OK");
            }
            else
            {
                Serial_SendString("ERROR_COMMAND\r\n");
                OLED_ShowString(2, 1, "                ");//    擦除第4行
                OLED_ShowString(2, 1, "ERROR_COMMAND");
            }
            Serial_RxFlag = 0;//    清零
        }
    }
}

其他引用的头文件和c代码可在此处查阅:OLED.h(【江协STM32】4 OLED调试工具,第5节)、Delay.h(【江协STM32】3-2 LED闪烁&LED流水灯&蜂鸣器,第1.3节)、LED.h(【江协STM32】3-4 按键控制LED&光敏传感器控制蜂鸣器,第1.3节)

相关推荐
赖small强1 小时前
【Linux 网络基础】HTTPS 技术文档
linux·网络·https·tls
雲烟2 小时前
嵌入式设备EMC安规检测参考
网络·单片机·嵌入式硬件
泽虞2 小时前
《STM32单片机开发》p7
笔记·stm32·单片机·嵌入式硬件
Yue丶越2 小时前
【C语言】数据在内存中的存储
c语言·开发语言·网络
田甲3 小时前
【STM32】 数码管驱动
stm32·单片机·嵌入式硬件
Altair12313 小时前
nginx的https的搭建
运维·网络·nginx·云计算
李宥小哥3 小时前
Redis10-原理-网络模型
开发语言·网络·php
Umi·3 小时前
iptables的源地址伪装
运维·服务器·网络
up向上up3 小时前
基于51单片机垃圾箱自动分类加料机快递物流分拣器系统设计
单片机·嵌入式硬件·51单片机
在路上看风景3 小时前
6.4 LANS
网络