STM32外设学习-串口数据包笔记-(数据包的了解)
我们上次的博客完成了单字节的数据发送,然后我们来了解一下数据包。
一.数据包解释
首先,要明确一个概念:“HEX数据包”并不是一个官方的协议,而是一种常见的实践。它指的是我们将要发送的数据,以其原始的二进制字节形式(通常用十六进制,即HEX表示)进行传输,而不是转换成ASCII字符。例如,发送一个0x41,在接收端看到的就是一个字节0x41,而不是字符'A'和'1'两个ASCII码。
一个完整的、可靠的HEX数据包通常包含以下几个部分:
| 组成部分 | 示例 (HEX) | 说明 |
|---|---|---|
| 包头 | AA FF | 固定值,用于标识一个数据包的开始,接收方靠它来同步。 |
| 包长 | 00 08 | 指示数据包有效数据的长度,方便接收方解析。 |
| 有效数据 | 01 02 03 04 | 实际需要传输的信息。 |
| 校验和 | 0A | 用于检查数据在传输过程中是否出错,常见的有和校验、CRC校验等。 |
| 包尾 | 55 EE | (可选) 固定值,进一步辅助确认数据包结束。 |
1.发送流程(以状态机思路为例)
整个发送过程可以看作一个状态机,下面是其核心步骤:
1. 初始化配置
在发送任何数据之前,必须先对STM32的串口外设进行初始化。
-
使能时钟:开启USART和对应GPIO口的时钟。
-
GPIO配置:将指定的TX引脚配置为复用推挽输出模式,RX引脚配置为浮空输入或上拉/下拉输入模式。
-
USART参数配置:
-
波特率(例如 115200)
-
数据位(通常为8位)
-
停止位(通常为1位)
-
奇偶校验位(通常无)
-
硬件流控制(通常无)
-
-
使能USART:最后启动USART模块。
2. 构建数据包
在应用程序中,你需要根据你的协议来构造一个完整的数据包。
3. 发送数据包
构建好数据包后,就可以通过串口将其发送出去。STM32 HAL库提供了几种发送方式:
方法一:阻塞式发送 (HAL_UART_Transmit)
-
流程:CPU会一直等待,直到整个数据包发送完毕才会继续执行后续代码。
-
优点:代码简单,顺序执行,不易出错。
-
缺点:在发送期间CPU被挂起,无法处理其他任务,效率低
方法二:中断发送 (HAL_UART_Transmit_IT)
-
流程:
-
调用函数启动发送。
-
CPU立即返回,继续执行主程序。
-
USART在后台一个字节一个字节地发送。
-
当发送完指定数量的字节后,会触发发送完成中断。
-
在中断回调函数
HAL_UART_TxCpltCallback中,你可以进行下一步操作,例如释放缓冲区或标记发送完成。
-
-
优点:不阻塞CPU,效率高。
-
缺点:编程稍复杂,需要注意缓冲区的生命周期和中断的嵌套。
方法三:DMA发送 (HAL_UART_Transmit_DMA)
-
流程:
-
配置DMA通道,将其与USART的发送数据寄存器连接。
-
调用函数启动DMA发送。
-
CPU完全不用管,DMA控制器会自动将内存中的数据搬运到USART的发送寄存器。
-
当整个数据包发送完毕后,DMA会触发传输完成中断。
-
-
优点:CPU占用率最低,效率最高,尤其适合发送大量数据或高频数据。
-
缺点:配置最为复杂,需要了解DMA控制器。
4.总结与选择
| 发送方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 阻塞式 | 简单应用、单任务、调试阶段 | 简单可靠 | 效率低,阻塞CPU |
| 中断式 | 中等复杂度的多任务应用 | 效率较高,不阻塞主循环 | 编程稍复杂,有中断开销 |
| DMA式 | 高速数据流、实时性要求高的系统 | 效率最高,极大解放CPU | 配置复杂,需要额外DMA资源 |
核心流程再梳理:
-
初始化:配置好硬件。
-
打包:按照协议将数据、长度、校验等组合成一个字节数组。
-
发送:根据应用需求选择阻塞、中断或DMA方式将字节数组送出。
-
善后:在发送完成(特别是中断/DMA方式)后,进行必要的处理,如释放内存、设置标志位等。
二.数据包收发思路及流程
1.HEX数据包格式
(1)简介

首先数据包的作用是把一个个单独的数据打包起来。方便我们进行多字节的数据通讯,上次我们学习了如何发送一个字节,实际应用中要把一个字节打包为一个整体进行发送。比如说我们有一个陀螺仪传感器需要串口发送数据到STM32。
陀螺仪的数据,比如X轴一个字节,Y轴一个字节,Z轴一个字节。总共三个数据需要连续的发送比如XYZXYZXYZ,就会出现一个问题,比如说接收方并不知道哪个对应X,哪个对应Y,哪个对应Z。因为接收方会从任意位置接收所以会出现数据错位的现象。
这时候我们就需要研究一种方式把XYZ的数据进行分割,分成一个个数据包这样在接受时候就知道了。数据包第一个数据是X,第二个数据Y,第三个数据Z,这就是数据包的作用,把属于同一批的数据进行打包和分割。方便接收包进行识别
我们串口数据包通常就是添加包头包尾的模式,比如图片就列举了两种数据包的格式。
(2)固定包长,含包头包尾

固定包长,含包头包尾。也就是每个数据包的长度都固定不变,数据包前面是包头,后面是包尾。
(3)可变包长,含包头包尾

每歌数据包长度可以是不一样的。他们的数据包格式,可以是用户根据需求,自己规定,也可以是买个模块别的开发者规定的。
(4)思路
如图就是定义0xFF为包头,在四个字节之后加一个0xFE为包尾,那么当我接收到0xFF我就知道,一个数据包来了之后我在接收到的4个字节就当作数据包的第1,2,3,4个数据,存在一个数组里。最后跟一个包尾,当我收到0xFE之后,就可以置一个标志位,告诉程序,我收到了一个数据包。然后新的数据包过来,再重复之前的过程。这样就可以在一个连续不断的数据流中,分割出我们想要的数据包了。
这就是通过添加包头包尾实现数据分割打包的思路了。
2.问题研究
(1)包头包尾和数据载荷重复
这里定义FF为包头,FE为包尾,如果我传输的数据本身就是FF和FE怎么办呢。
第一钟
我们可以限制载荷数据的范围,如果可以的话我们可以在数据发送的时候,对数据进行限幅比如:X,Y,Z三个数据,变化范围都可以是0~100,那就好办了,我们可以在载荷中只发送0~100的数据,这样就不会和包头包尾重复了
第二种
如果我们无法避免载荷数据和包头包尾重复,那么我们就尽量使用固定格式的数据包,这样由于载荷数据是固定的,只要我们通过包头包尾对齐了数据,我们就可以严格知道,哪个数据是应该是载荷数据,哪个数据应该是包头包尾。在接收载荷数据的时候,我们并不会判断它是否是包头包尾,而在接收包头包尾的时候,我们会判断它是不是确实是包头包尾呢,用于数据对齐。
第三种
增加包头包尾的数量,并且让他尽量呈现出载荷数据出现不了的状态,比如我们使用FF,FE作为包头,FD,FC作为包尾。这样也可以避免载荷数据和包头包尾重复的情况发生。
(2)包头包尾并不是全部需要
比如我们可以只要一个包头,把包尾删掉,这样数据包的格式就是一个包头FF加上4个数据,这样子也是可以的,这样检测到FF,开始接收,收够四个字节后,置标志位,一个数据包接收完成这样也可以。不过这样子载荷和包头都是FF,那你肯定不知道哪个是包头了,而加上FE作为包尾,无论数据怎么变化,都是可以分辨出包头包尾的,
(3)固定包长和可变包长的问题
对于HEX数据包来说,如果你的设备出现包头和包尾重复的情况,那就最好选择固定包长,这样可以避免数据错误,如果你又会重复又选择可变包长,那数据很容易乱套了,如果载荷和数据不会重复,那么就可以选择可变包长
(4)各种数据转换为数据流问题
这里数据包都是由一个一个字节组成的,如果你想发送16位整型数据,32位整型数据,float,double或结构体,都没有问题,因为他们内部都是由一个字节一个字节组成的,只需要用一个uint8_t的指针,指向它,把他们当作一个字节数组一起发送就行了
3.文本数据包
在hex数据包钟,数据都是以原始的字节数据本身呈现的,而在文本数据包里面,每个字节就经过了一系列的编码和译码。

最终显示的是文本数据,其实都是一个字节的hex数据。
这里同样给出了固定包长和可变包长两种模式。由于数据译码作为了字符的格式,那么就存在了大量的字符用来作为包头包尾。可以有效避免载荷和包头包尾重复的问题。
如图就是以@作为包头,以\r\n也就是换行,这两个字符作为包尾。在载荷数据中间,可以出现除了包头和包尾的任意字符,所以文本数据包基本不用担心载荷和包头包尾重复的问题。
当我们接收到载荷数据之后,得到的就是一个字符串,在软件中在对字符串进行操作和判断。姐可以使用各种指令控制功能了。而且字符串数据包表达的意义很明显,可以把字符串数据包直接打印到串口上,什么指令,什么数据一眼就看明白,所以文本数据包一般会以换行作为包尾,这样在打印的时候就可以一行一行打印了。
4.对比
HEX数据包优点是传输最直接,解析数据非常简单,比较适合一些模块,发送原始的数据,比如一些使用串口通讯的陀螺仪,温湿度传感器,缺点就是灵活性不足,载荷容易和包头包尾重复。
文本数据包优点是,数据直接易理解,非常灵活,比较适合一些输入指令进行人机交互的场景,比如蓝牙模块常用的AT指令CNC和3D打印机常用的G代码,都是文本数据包的格式。缺点就是,解析效率低,比如发送一个数100,HEX数据包就是一个字节100,文本数据包就得是三个字节的字符‘1’,‘0’,‘0’。收到之后还要把字符转换为数据才能得到100。
所以说我们要根据选择场景,来设计和选择数据包格式。
5.数据包收发流程
(1)数据包的发送
数据包的发送非常简单,就定义一个数组,填充数据,用上一次博客的sendarray已发送就完事了。文本数据包也很简单,写一个字符串然后调用sendstring就可以发送了。
(2)数据包的接收

接收就比较繁琐了,首先我们来看固定包长HEX的接受。
首先根据上一个博客的代码我们知道,每接受一个字节,程序就进入一个中断,在中断函数里我们可以拿到这个字节,拿到之后我们就需要退出中断。所以每拿到一个数据都是一个独立的过程,而对于数据包来说,很明显它具有前后关联性,包头之后是数组,数据之后是包尾。
对于包头,数据,包尾这三种状态,我们都需要有不同的处理逻辑,所以在程序钟我们要设置一个,能记住不同状态的机制,在不同状态进行不同的操作,同时还要进行状态的合理转移,这种程序的思维就叫做状态机。
(3)状态机
在这里我们就使用状态机的方法来接受一个数据包。要想设计一个好的状态机程序,画一个好的状态图是有必要的。


对于这样一个固定包长HEX数据来说,我们可以定义三个状态,第一个状态是等待包头,第二个状态是接受数据,第三个状态是等待包尾。每个状态需要用一个变量来标志一下。比如这里用变量S进行标志,三个状态依次为S=0,S=1,S=2。这一点用来置标志位,只不过标志位只有0和1.而状态机是多标志位的一种方式。
执行流程就是收到一个数据,进入中断,根据S=0,进入第一个状态的程序判断数是否为包头FF。如果是FF则代表收到包头,之后置S=1,退出中断结束,这样下次在进入中断根据S=1,就可以进入接受数据的程序了。
如果在第一个状态收到的不是FF,就证明数据包没有对齐,我们应该等待数据包包头的出现,这是状态仍然是0,下次进入中断就还是判断包头的逻辑直到出现FF,才能转到下一个状态。
那之后出现了FF我们就可以转移到接收数据的状态了这时在接受数据,我们就直接把它存在数组钟,另外在用一个变量,记录收了多少个数据,如果没有收够四个数据那么就是一只是接受状态,如果收购了就置S=2,下次进入中断时候,就可以进入下一个状态了。
那么最后的状态就是等待包尾了判断数据是不是FE,正常情况应该是FE,这样就可以置S=0回到最初的状态,开始下一个轮回,当然这个数据也有可能不是F0比如和包头重复,导致包头位置判断错误了,那这个包尾位置就有可能不是FE,这时就可以进入重复等待包尾的状态,直到收到真正的包尾,这样加入包尾的判断,更能预防因为数据和包头重复造成的错误,这就是使用状态机接收数据包的思路。
(4)使用步骤
就是先根据项目要求定义状态,画几个园,然后考虑好各个状态,会在什么情况下进行转移,如何转移,画好线和转移条件,最后根据这个图来编程,这样思维就会非常清晰了。

文本数据包同理。
只是在中间加入一个等待包尾的功能,因为是可变包长,我们接受数据的时候也要时刻监视,是不是接受到包尾了,一旦收到包尾就结束,这个状态的逻辑就是收到一个数据,判断是不是\r 如果不是的话,就正常接受,如果是的话,就不接受进入下一个状态,等待包尾\n。因为数据包有两个包尾\r\n所以还需要第三个状态,如果只有一个包尾,那么在包尾出现后,就可以直接回到初始状态了,只需要两个状态就行,因为接受数据和等待包尾,是在一个状态里进行,由于串口的包头包尾不会出现在数据中,所以基本不会出现数据错位的现象。
这就是使用状态机接受文本数据的方法。
