STM32学习笔记16:USART串口数据包

文章详细介绍了STM32F103RC开发中如何使用标准外设库处理HEX和文本数据包,包括固定包长和可变包长的设计,以及在USART通信中使用中断服务函数进行包头、包尾和数据的正确接收。还讨论了解决包头包尾重复、数据错位等问题的方法。
摘要由CSDN通过智能技术生成

芯片型号:STM32F103RC

软件开发包:标准外设库

一、基本介绍

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

数据包的任务就是把属于同一批的数据进行打包和分割,方便接收方进行识别。

数据包有多种分割方式,比如在数据的高位添加标志位、额外添加包头包尾等,串口数据包通常使用的是额外添加包头包尾的方式。以下列举了两种数据包格式,第一种是固定包长,含包头包尾,也就是每个数据包的长度都固定不变,数据包前面是包头,后面是包尾;第二种是可变包长,含包头包尾,也就是每个数据包的长度可以是不一样的,前面是包头,后面是包尾。

1.1 HEX数据包
  • 固定包长,含包头包尾

固定包长,含包头包尾

  • 可变包长,含包头包尾

可变包长,含包头包尾

数据包的格式可以是用户根据需求自己规定的,也可以是买一个模块,别的开发者规定的。比如这里,固定包长规定一批数据有4 个字节,在这 4 个字节之前加一个包头,之后加一个包尾,这里是定义 0xFF 为包头,0xFE 为包尾。当接收到 0xFF 时就知道一个数据包来了,接着再接收的 4 个字节就当做数据包的第 1、2、3、4 个数据,存在一个数组里,最后跟一个包尾,当接收到 0xFE 之后,就可以置一个标志位,表示收到了一个数据包。然后新的数据包过来,再重复之前的过程,这样就可以在一个连续不断的数据流中分割出需要的数据包了。

有了思路之后,还有几个问题需要解决。

(1)第一个问题就是包头包尾和数据载荷重复的问题,这里定义 0xFF 为包头,0xFE 为包尾,如果传输的数据本身就是 0xFF 和 0xFE 怎么办呢?

解决方法:

  1. 限制载荷数据的范围,如果可以的话,可以在发送的时候,对数据进行限幅;
  2. 如果无法避免载荷数据和包头包尾重复,就尽可能使用固定长度的数据包,这样由于载荷数据是固定的,只要通过包头包尾对齐了数据,就可以知道哪个数据应该是包头包尾,哪个数据是载荷数据;
  3. 增加包头包尾的数量,并且让它尽量呈现出载荷数据出现不了的状态。

(2)第二个问题是包头包尾并不是全部都需要的,比如可以只要一个包头,把包尾删掉,当检测到包头,开始接收,收够 4 个字节后,置标志位,一个数据包接收完成。不过这样的话,载荷和包头重复的问题会更严重一些。

(3)第三个问题是固定包长和可变包长的选择问题。对应 HEX 数据包来说,如果载荷会出现和包头包尾重复的情况,那就最好选择固定包长;如果载荷不会和包头包尾重复,那就可以可以选择可变包长。

(4)第四个问题是各种数据转换为字节流的问题。这里数据包都是一个字节一个字节组成的,如果想发送 16 位的整型数据、32 位的整型数据、浮点型、甚至是结构体,其实都没问题。因为它们内部其实都是由一个字节一个字节组成的,只需要用一个 uint8_t 的指针指向它,把它们当作一个字节数组发送就行了。

1.2 文本数据包
  • 固定包长,含包头包尾

固定包长,含包头包尾

  • 可变包长,含包头包尾

可变包长,含包头包尾

HEX 数据包和文本数据包分别对应 HEX 和文本这两种模式。在 HEX 数据包中,数据都是以原始的字节数据本身呈现的,而在文本数据包里面,每个字节就经过了一层编码和译码,最终表现出来的就是文本格式,但实际上,每个文本字符背后,其实都还是一个字节的 HEX 数据。

这里,文本数据包同样有固定包长和可变包长这两种模式,由于数据译码成了字符形式,这就会存在大量的字符可以作为包头包尾,可以有效避免载荷和包头包尾重复的问题。

当接收到载荷数据之后,得到的就是一个字符串,在程序中再对字符串进行操作和判断,就可以实现各种指令控制的功能了。

1.3 HEX数据包接收

HEX数据包接收

对于上面这样一个固定包长 HEX 数据包来说,可以定义 3 个状态。第一个状态是等待包头,第二个状态是接收数据,第三个状态是等待包尾,每个状态需要用一个变量来标志一下,比如这里用变量 s 来标志,三个状态依次为 s=0、s=1、s=2。

执行流程:

最开始 s=0,收到一个数据进中断,根据 s=0 进入第一个状态的程序,判断数据是不是包头 0xFF ,如果是 0xFF ,则表示收到包头,之后置 s=1,退出中断,这样下次再进中断,根据 s=1 就可以进行接收数据的程序了;在第一个状态,如果收到的不是 0xFF,就证明数据包没有对齐,应该等待数据包包头的出现,这样状态仍然是 0,下次进中断就还是判断包头的逻辑。

确认收到包头之后,再收到数据,就可以直接存到数组中,另外再用一个变量记录收了多少个数据,如果没收够 4 个数据,就一直是接收状态;如果收够了,就置 s=2,下次进中断时,就可以进入下一个状态了。

最后一个状态就是等待包尾了,判断数据是不是 0xFE,正常情况应该是 0xFE,这样就可以置 s=0,回到最初的状态,开始下一轮。当然也有可能这个数据不是 0xFE,比如数据和包头重复,导致包头位置判断错了,那这个包尾就有可能不是 0xFE。这时就可以进入重复等待包尾的状态,直到接收到真正的包尾。这样加入包尾的判断,更能预防因数据和包头重复造成的错误。

1.4 文本数据包接收

文本数据包接收

同样也是定义 3 个状态,第一个状态是等待包头,判断收到的是不是规定的 @ 符号。第二个状态是接收数据,同时还应该兼具等待包尾的功能,因为这是可变包长,接收数据的时候,也要时刻监视,是不是收到包尾了,一旦收到包尾了就结束。那这里,这个状态的逻辑就应该是收到一个数据,判断是不是 ‘\r’,如果不是,则正常接收;如果是,则不接收,同时跳到下一个状态,等待包尾 ‘\n’。因为这里数据包有两个包尾 ‘\r’、 ‘\n’,所以需要第三个状态。如果只有一个包尾,那在出现包尾之后了,就可以直接回到初始状态了,只需要两个状态就行了,因为接收数据和等待包尾需要在一个状态里同时进行。由于串口的包头包尾不会出现在数据中,所以基本不会出现数据错位的现象。

二、举例

2.1 HEX数据包接收,固定包长,含包头包尾

前提:将 USART1 设置为发送和接收模式,并使能 USART1 的接收中断。

uint8_t Serial_TxPacket[4]; // 定义一个长度为4的发送数据包数组
uint8_t Serial_RxPacket[4]; // 定义一个长度为4的接收数据包数组
uint8_t Serial_RxFlag; 		// 定义一个接收标志位

// 发送数据包函数
void Serial_SendPacket(void)
{
    Serial_SendByte(0xFF); 					// 发送包头0xFF
    Serial_SendArray(Serial_TxPacket, 4); 	// 发送数据包中的4个字节
    Serial_SendByte(0xFE); 					// 发送包尾0xFE
}

// 获取接收标志位函数
uint8_t Serial_GetRxFlag(void)
{
    if (Serial_RxFlag == 1) // 如果接收标志位为1
    {
        Serial_RxFlag = 0; 	// 将接收标志位清零
        return 1; 			// 返回1表示有新的数据包接收
    }
    return 0; 				// 返回0表示没有新的数据包接收
}

// USART1中断服务函数,用于接收数据包
void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0; 	// 定义一个接收状态变量,用于记录当前接收状态
    static uint8_t pRxPacket = 0; 	// 定义一个指针变量,用于记录当前接收到的数据包的位置

    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)	// 如果USART1接收寄存器非空中断标志位为1
    {   
        uint8_t RxData = USART_ReceiveData(USART1); 	// 读取接收到的数据

        if (RxState == 0) 			// 如果当前状态为0
        {
            if (RxData == 0xFF) 	// 如果接收到包头
            {
                RxState = 1;	 	// 将接收状态设置为1
                pRxPacket = 0; 		// 将指针变量置为0
            }
        }
        else if (RxState == 1) 					// 如果当前状态为1
        {
            Serial_RxPacket[pRxPacket] = RxData; // 将接收到的数据存入接收数据包数组中
            pRxPacket++; 						// 指针变量加1
            if (pRxPacket >= 4) 				// 如果指针变量大于等于4
            {
                RxState = 2; 					// 将接收状态设置为2
            }
        }
        else if (RxState == 2) 		// 如果当前状态为2
        {
            if (RxData == 0xFE) 	// 如果接收到包尾
            {
                RxState = 0; 		// 将接收状态设置为0
                Serial_RxFlag = 1; 	// 将接收标志位设置为1
            }
        }

        USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除USART1接收寄存器非空中断标志位
    }
}
2.2 文本数据包接收,可变包长,含包头包尾

前提:将 USART1 设置为发送和接收模式,并使能 USART1 的接收中断。

发送文本数据包和发送字符串没有什么区别,重点在接收文本数据包。

char Serial_RxPacket[100]; 	// 定义一个长度为100的接收数据包数组
uint8_t Serial_RxFlag; 		// 定义一个接收标志位

void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0; 	// 定义一个接收状态变量,用于记录当前接收状态
    static uint8_t pRxPacket = 0; 	// 定义一个指针变量,用于记录当前接收到的数据包的位置

    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) // 如果USART1接收寄存器非空中断标志位为1
    {   
        uint8_t RxData = USART_ReceiveData(USART1); 	// 读取接收到的数据

        if (RxState == 0) 								// 如果当前状态为0
        {
            if (RxData == '@' && Serial_RxFlag == 0) 	// 如果接收到包头且接收标志位为0
            {
                RxState = 1; 	// 将接收状态设置为1
                pRxPacket = 0; 	// 将指针变量置为0
            }
        }
        else if (RxState == 1) 	// 如果当前状态为1
        {
            if (RxData == '\r') // 如果接收到的数据为'\r'
            {
                RxState = 2; 	// 将接收状态设置为2
            }
            else
            {
                Serial_RxPacket[pRxPacket] = RxData; 	// 将接收到的数据存入接收数据包数组中
                pRxPacket++; 							// 指针变量加1
            }
        }
        else if (RxState == 2) 	// 如果当前状态为2
        {
            if (RxData == '\n') // 如果接收到的数据为'\n'
            {
                RxState = 0; 						// 将接收状态设置为0
                Serial_RxPacket[pRxPacket] = '\0'; 	// 在接收数据包数组末尾添加字符串结束符
                Serial_RxFlag = 1; 					// 将接收标志位设置为1
            }
        }

        USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除USART1接收寄存器非空中断标志位
    }
}

如果连续发送数据包,程序处理不及时,可能导致数据包错位。在这里,文本数据包,每个数据包是独立的,不存在连续,如果错位了,问题就比较大,所以在程序这里,相较于 HEX 数据包接收,可以修改一下,等每次处理完成之后,再开始接收下一个数据包。具体操作就是在上面代码的第 16 行,等待包头的时候,再加一个条件,即如果数据等于包头,且 Serial_RxFlag == 0。同时,在主函数中,每次接收到数据包,并且操作完成之后,将 Serial_RxFlag = 0,大致框架如下:

if (Serial_RxFlag == 1)
{
	// 执行操作
    
	Serial_RxFlag = 0;
}

参考视频源于B站up主: 野火科技、江协科技
参考文档:《STM32库开发实战指南——基于野火MINI开发板》

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值