芯片型号:STM32F103RC
软件开发包:标准外设库
一、基本介绍
数据包的作用是把一个个单独的数据打包起来,方便进行多字节的数据通信。
数据包的任务就是把属于同一批的数据进行打包和分割,方便接收方进行识别。
数据包有多种分割方式,比如在数据的高位添加标志位、额外添加包头包尾等,串口数据包通常使用的是额外添加包头包尾的方式。以下列举了两种数据包格式,第一种是固定包长,含包头包尾,也就是每个数据包的长度都固定不变,数据包前面是包头,后面是包尾;第二种是可变包长,含包头包尾,也就是每个数据包的长度可以是不一样的,前面是包头,后面是包尾。
1.1 HEX数据包
- 固定包长,含包头包尾
- 可变包长,含包头包尾
数据包的格式可以是用户根据需求自己规定的,也可以是买一个模块,别的开发者规定的。比如这里,固定包长规定一批数据有4 个字节,在这 4 个字节之前加一个包头,之后加一个包尾,这里是定义 0xFF 为包头,0xFE 为包尾。当接收到 0xFF 时就知道一个数据包来了,接着再接收的 4 个字节就当做数据包的第 1、2、3、4 个数据,存在一个数组里,最后跟一个包尾,当接收到 0xFE 之后,就可以置一个标志位,表示收到了一个数据包。然后新的数据包过来,再重复之前的过程,这样就可以在一个连续不断的数据流中分割出需要的数据包了。
有了思路之后,还有几个问题需要解决。
(1)第一个问题就是包头包尾和数据载荷重复的问题,这里定义 0xFF 为包头,0xFE 为包尾,如果传输的数据本身就是 0xFF 和 0xFE 怎么办呢?
解决方法:
- 限制载荷数据的范围,如果可以的话,可以在发送的时候,对数据进行限幅;
- 如果无法避免载荷数据和包头包尾重复,就尽可能使用固定长度的数据包,这样由于载荷数据是固定的,只要通过包头包尾对齐了数据,就可以知道哪个数据应该是包头包尾,哪个数据是载荷数据;
- 增加包头包尾的数量,并且让它尽量呈现出载荷数据出现不了的状态。
(2)第二个问题是包头包尾并不是全部都需要的,比如可以只要一个包头,把包尾删掉,当检测到包头,开始接收,收够 4 个字节后,置标志位,一个数据包接收完成。不过这样的话,载荷和包头重复的问题会更严重一些。
(3)第三个问题是固定包长和可变包长的选择问题。对应 HEX 数据包来说,如果载荷会出现和包头包尾重复的情况,那就最好选择固定包长;如果载荷不会和包头包尾重复,那就可以可以选择可变包长。
(4)第四个问题是各种数据转换为字节流的问题。这里数据包都是一个字节一个字节组成的,如果想发送 16 位的整型数据、32 位的整型数据、浮点型、甚至是结构体,其实都没问题。因为它们内部其实都是由一个字节一个字节组成的,只需要用一个 uint8_t 的指针指向它,把它们当作一个字节数组发送就行了。
1.2 文本数据包
- 固定包长,含包头包尾
- 可变包长,含包头包尾
HEX 数据包和文本数据包分别对应 HEX 和文本这两种模式。在 HEX 数据包中,数据都是以原始的字节数据本身呈现的,而在文本数据包里面,每个字节就经过了一层编码和译码,最终表现出来的就是文本格式,但实际上,每个文本字符背后,其实都还是一个字节的 HEX 数据。
这里,文本数据包同样有固定包长和可变包长这两种模式,由于数据译码成了字符形式,这就会存在大量的字符可以作为包头包尾,可以有效避免载荷和包头包尾重复的问题。
当接收到载荷数据之后,得到的就是一个字符串,在程序中再对字符串进行操作和判断,就可以实现各种指令控制的功能了。
1.3 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开发板》