数据包理论
一、为什么需要数据包
一帧只能传 8 位
串口物理层一帧 = 起始位 + 8 数据位 + 停止位,一次只能传 1 个字节。可实际通信往往要传一组有含义的信息
打包
给数据加上固定的边界标记,让接收方"一看就知道这包从哪开始、到哪结束":
┌────┬─────────────┬────┐ │包头 │ 数据区 │包尾 │ └────┴─────────────┴────┘ 固定值 内容 固定值二、数据包的基本格式
一个完整的数据包通常包含:
部分 | 作用 | 说明 |
|---|---|---|
包头(Head) | 标记"一包从这里开始" | 1~2 个固定字节,如 |
数据区(Data) | 真正要传的内容 | 长度固定或可变 |
包尾(Tail) | 标记"这包到此结束" | 固定字节,如 |
校验(Check,可选) | 检测传输是否出错 | 和校验/CRC 等 |
发送与接收两端必须约定同一套格式(通信协议),否则无法互相理解。
三、两种数据包类型
1. HEX 数据包(十六进制/原始数据包)
包头 数据区 包尾 0xFF 0xFE │ 0x01 0x02 0x03 │ 0xFC特点 | 说明 |
|---|---|
边界 | 用不可见控制字节(0xFF 等)当包头包尾 |
数据 | 直接传二进制数值(原始数据) |
优点 | 传输高效、紧凑 |
缺点 | 不可读、需程序解析、数据区不能出现和包头相同的值(要转义) |
适用 | 程序间通信、传感器数据、指令下发 |
2. 文本数据包(ASCII/字符数据包)
包头 数据区 包尾 @ │ "123 456" │ \r\n特点 | 说明 |
|---|---|
边界 | 用可见字符( |
数据 | 以 ASCII 文本传输(如 “123” 是 3 个 ASCII 字符) |
优点 | 人眼可读、上位机解析简单、调试方便 |
缺点 | 效率低(数字要转字符串)、传输量大 |
适用 | 调试输出、人机交互、简单上位机协议 |
3. 对比表
对比项 | HEX 数据包 | 文本数据包 |
|---|---|---|
包头示例 |
|
|
包尾示例 |
|
|
数据形态 | 二进制原始值 | ASCII 字符 |
人眼可读性 | ✗ 不可读 | ✓ 可读 |
传输效率 | 高 | 低 |
解析难度 | 需处理转义 | 简单直观 |
常见场景 | 指令/传感器 | 调试/上位机 |
四、固定长度可变长度
数据区的长度有两种设计方式:
1. 固定长度数据包
数据个数固定,接收方数够指定个数即为一包,甚至不需要包尾。
包头(0xFF) 数据1 数据2 数据3(固定3个) ← 数到3个即收完2. 可变长度数据包
数据个数不定,必须靠包尾来判定一包结束。
包头(0xFF) 数据...(数量不定)... 包尾(0xFC) ← 看到包尾才收完长度固定与否决定了拆包逻辑——固定数个数,可变看包尾。
五、打包与拆包思想
1. 发送端:打包
按协议格式把数据"组装"再逐个发送:
填包头 → 填数据 → 填包尾 → 依次 Serial_SendByte 发出2. 接收端:拆包
接收是边收边判断的,用"状态机"描述:
┌─────────────────────────────────────────┐ ▼ │ 状态0:等包头 ──收到包头──► 状态1:收数据 ──收到包尾──► 状态2:处理数据 ▲ │ └────────────── 处理完回到状态0,等下一包 ◄──────────────┘状态 | 干什么 |
|---|---|
状态0(等包头) | 收到包头 → 转状态1;否则继续等 |
状态1(收数据) | 数据存入缓冲区;收到包尾 → 转状态2 |
状态2(处理) | 校验/解析缓冲区内容 → 回状态0 |
状态机的好处:不阻塞。每来一个字节喂给它,它自动流转,主程序不用死等整包。
3. 数据区出现包头值
HEX 包用0xFF当包头,若数据里恰好也有0xFF,接收方会误判为新包头:
转义:数据中的包头值改成两个字节的转义序列(如0xFF→0xFD 0x01)或用连续两个字节做包头(0xFF 0xFE)降低误判概率。
六、数据包可能遇到的问题
问题 | 现象 | 应对 |
|---|---|---|
错位 | 一包被拦腰截断 | 包头定位,重新同步 |
粘包 | 多包连成一片 | 包头+包尾界定边界 |
丢帧 | 中间丢字节 | 靠包头重新同步(状态机回到等包头) |
数据区含包头 | 误判为新包 | 双字节包头 / 转义 |
只要状态机设计得当,上述问题都能在"等包头"环节自动恢复同步。
七、接收方案总体设计
串口中断只负责"收字节存起来",拆包在专门函数里做(状态机):
串口中断:收到字节 → Serial_RxData → 置标志 主循环:把 Serial_RxData 喂给 Packet_Receive() 状态机拆包 收完一包 → PacketFlag 置位 → 主循环解析 RxPacket[]状态机三大要素:
要素 | 代码体现 |
|---|---|
当前状态 |
|
数据缓冲 |
|
完成标志 |
|
八、方案①:HEX 数据包收发
协议约定:包头1(0xFF) 包头2(0xFE) 数据1~3 包尾(0xFC)
/* ================= HEXPacket.c ================= */ #include "stm32f10x.h" // 包含 STM32 标准库头文件 #include "Serial.h" // 串口模块(见 9-3 笔记) /* ===== 协议定义 ===== */ #define HEAD1 0xFF // 包头1 #define HEAD2 0xFE // 包头2 #define TAIL 0xFC // 包尾 #define PACKET_LEN 3 // 固定数据个数(示例) uint8_t RxPacket[PACKET_LEN]; // 接收缓冲(本包数据) uint8_t RxPacketFlag = 0; // 收包完成标志 uint8_t RxState = 0; // 状态机当前状态 /** * @brief 发送 HEX 数据包 * @param Data 数据指针;Length 数据个数 */ void HEXPacket_Send(uint8_t *Data, uint8_t Length) { uint8_t i; Serial_SendByte(HEAD1); // 包头1 Serial_SendByte(HEAD2); // 包头2 for (i = 0; i < Length; i++) { Serial_SendByte(Data[i]); // 数据 } Serial_SendByte(TAIL); // 包尾 } /** * @brief 接收状态机,每收到一字节调用一次 * @param Byte 收到的字节 */ void HEXPacket_Receive(uint8_t Byte) { static uint8_t DataIndex = 0; switch (RxState) { case 0: // 等包头1 if (Byte == HEAD1) { RxState = 1; } break; case 1: // 等包头2 if (Byte == HEAD2) { RxState = 2; // 包头完整,开收数据 DataIndex = 0; } else if (Byte != HEAD1) { RxState = 0; } // 非包头,重新等 break; case 2: // 收数据 if (Byte == TAIL) // 包尾到了 { RxState = 3; } else if (DataIndex < PACKET_LEN) { RxPacket[DataIndex] = Byte; // 存入缓冲 DataIndex++; } break; case 3: // 收包完成,转主循环处理 RxPacketFlag = 1; RxState = 0; break; } } /** * @brief 查询收包标志 * @retval 1=收到完整一包 */ uint8_t HEXPacket_GetFlag(void) { if (RxPacketFlag == 1) { RxPacketFlag = 0; return 1; } return 0; }/* ================= HEXPacket.h ================= */ #ifndef __HEXPACKET_H // 防止头文件重复包含 #define __HEXPACKET_H #include "stm32f10x.h" extern uint8_t RxPacket[]; // 接收到的数据 extern uint8_t RxPacketFlag; // 收包标志 void HEXPacket_Send(uint8_t *Data, uint8_t Length); // 打包发送 void HEXPacket_Receive(uint8_t Byte); // 状态机收包 uint8_t HEXPacket_GetFlag(void); // 查收包标志 #endif九、方案②:文本数据包收发
协议约定:@ "数据(ASCII文本,不定长)" \r \n
与 HEX 包区别:
HEX 包 | 文本包 | |
|---|---|---|
包头 |
|
|
包尾 |
|
|
数据 | 二进制 | ASCII,收完要加 |
可读性 | 不可读 | 可直接 printf / 显示 |
/* ================= TextPacket.h ================= */ #ifndef __TEXTPACKET_H // 防止头文件重复包含 #define __TEXTPACKET_H #include "stm32f10x.h" extern uint8_t RxPacket[]; // 接收到的文本(含 '\0') extern uint8_t RxPacketFlag; // 收包标志 void TextPacket_Send(char *Data); // 打包发送 void TextPacket_Receive(uint8_t Byte); // 状态机收包 uint8_t TextPacket_GetFlag(void); // 查收包标志 #endif十、主函数:中断收字节 → 喂状态机
/* ================= main.c ================= */ #include "stm32f10x.h" // 包含 STM32 标准库头文件 #include "Delay.h" // 延时模块 #include "OLED.h" // OLED 模块 #include "Serial.h" // 串口模块(9-3,含中断接收) #include "HEXPacket.h" // HEX 包 #include "TextPacket.h" // 文本包 uint8_t TestData[] = {0x01, 0x02, 0x03}; // 测试数据 /** * @brief 主函数:串口中断收字节 → 主循环喂状态机拆包 */ int main(void) { OLED_Init(); Serial_Init(); // 串口 + 中断接收 while (1) { // 每 1 秒发一个 HEX 包 HEXPacket_Send(TestData, 3); Delay(1000000); // 中断收到字节 → 喂给两个状态机(分别拆 HEX/文本包) if (Serial_GetRxFlag() == 1) { HEXPacket_Receive(Serial_RxData); // HEX 包状态机 TextPacket_Receive(Serial_RxData); // 文本包状态机 } // 收到完整 HEX 包:显示内容 if (HEXPacket_GetFlag() == 1) { OLED_ShowHexNum(1, 1, RxPacket[0], 2); OLED_ShowHexNum(1, 5, RxPacket[1], 2); OLED_ShowHexNum(1, 9, RxPacket[2], 2); } // 收到完整文本包:当字符串显示 if (TextPacket_GetFlag() == 1) { OLED_ShowString(3, 1, (char *)RxPacket); } } }