一、前言
在资源受限的嵌入式设备(蓝牙 BLE、低功耗物联网模组、窄带串口通信)开发中,受硬件 MTU、缓存区大小限制,应用层长报文必须拆分为多个小片帧分次传输。如何高效缓存分片、自动重组完整报文、防止丢包内存泄漏,是底层通用刚需。
本文基于重构命名后的通用源码,讲解一套无业务依赖、纯底层通用的分包缓存重组模块设计,采用位图管理分片状态、动态内存申请释放 + 定时回收,适配蓝牙 GATT、UART、LoRa 等各类低速可靠通信场景。
二、模块整体设计目标
- 轻量化:仅用 4 字节位图管理最多 32 路分片,极致节省 RAM;
- 动态内存:按需申请整包缓存,避免静态数组冗余占用;
- 状态自治:通过消息 ID 区分多路并发报文,互不干扰;
- 防泄漏:定时超时自动回收内存,异常断连不堆内存;
- 高内聚低耦合:对外接口极简,上层只需关心发分片、取整包。
三、核心数据结构设计(重构后通用命名)
3.1 宏配置
// 最大支持并发缓存的报文任务数,可按需裁剪
#define PKG_NODE_MAX_NUM 8
3.2 分片节点结构体
// 分包重组内存节点结构体
typedef struct
{
uint8_t used; // 节点占用标记:1在用 0空闲
uint8_t msg_id; // 报文唯一标识(替代原cmdID,区分不同数据流)
uint8_t slice_total; // 当前报文总分片数
uint32_t slice_bitmap; // 分片接收位图:bit置1=已收到该分片
uint8_t* p_data; // 整包重组数据动态缓存指针
uint16_t data_len; // 重组后完整报文总长度
uint8_t timeout_cnt; // 超时倒计时(定时递减)
} pkg_node_t;
3.3 全局内存池
// 全局分包节点内存池,统一管理所有并发分片任务
static pkg_node_t PkgNodePool[PKG_NODE_MAX_NUM];
static const char *TAG = "pkg-mgr";
核心亮点:slice_bitmap 32 位位图,仅 4 字节即可标记 32 个分片的接收状态;收包完成判断仅需一次位与比对,运算效率极高,适合 MCU 低主频场景。
四、对外核心接口详解(重构命名版)
4.1 模块初始化:pkg_node_init
功能:系统上电初始化,清空内存池所有节点标记与野指针,防止开机脏数据。
s8_t pkg_node_init(void)
{
int idx_pool;
for(idx_pool = 0; idx_pool < PKG_NODE_MAX_NUM; idx_pool++)
{
PkgNodePool[idx_pool].used = 0;
PkgNodePool[idx_pool].p_data = NULL;
}
return 0;
}
调用时机:设备初始化、协议栈启动、通信链路复位时调用一次。
4.2 分片注册缓存(核心收包接口):pkg_slice_cache_add
功能:每收到一个分片帧调用,自动匹配已有报文、申请内存、写入分片、刷新位图,判断是否收齐整包。
- 入参:
msg_id报文 ID、slice_total总分片、slice_idx当前分片序号、分片数据指针、单片长度、自定义超时 - 返回:
- >0:所有分片收齐,返回完整报文长度
- 0:分片接收中,等待后续包
- <0:参数错误 / 内存申请失败 / 节点已满
业务流程:
- 根据
msg_id遍历内存池,找到已有报文节点直接编辑; - 无旧节点则申请空闲节点,动态申请整包所需内存;
- 刷新超时计数器,校验分片序号合法性;
- 位图置位当前分片,按偏移拷贝分片到整包缓存;
- 比对全量位图掩码,命中则标识收包完成。
4.3 获取重组完整报文:pkg_full_data_get
功能:上层检测到收包完成后,调用该接口读取完整报文,读取成功自动释放动态内存、回收节点。
- 返回:
- 0:读取成功,数据已拷贝到上层缓冲区
- 1:分片未收齐,继续等待
- -1:无该报文 ID 缓存
4.4 主动释放报文缓存:pkg_node_free_by_msgid
功能:异常场景手动干预,主动删除指定报文 ID 的缓存,立即释放内存,适配链路重连、指令取消场景。
4.5 定时超时回收:pkg_node_timeout_process
功能:由系统定时器周期调用(1s / 次),所有在用节点倒计时递减;倒计时归零则强制释放内存、清空节点,彻底解决丢包卡死导致的内存泄漏。
void pkg_node_timeout_process(void)
{
for(int idx = 0; idx < PKG_NODE_MAX_NUM; idx++)
{
if(PkgNodePool[idx].used == 1)
{
PkgNodePool[idx].timeout_cnt--;
if(PkgNodePool[idx].timeout_cnt == 0)
{
PkgNodePool[idx].used = 0;
vPortFree(PkgNodePool[idx].p_data);
}
}
}
}
五、典型业务调用流程(蓝牙 BLE/GATT 场景)
-
系统初始化
pkg_node_init(); // 上电初始化分包管理模块
-
底层 BLE 收到分片帧 解析帧内:
msg_id、总分片、当前分片序号、分片数据 → 调用:s16_t ret = pkg_slice_cache_add(msg_id, total, idx, data, len, 20);
if(ret > 0)
{
// 收齐整包,触发上层业务读取
uint8_t out_buf[256] = {0};
pkg_full_data_get(msg_id, out_buf, sizeof(out_buf));
// 业务处理完整out_buf报文(解析JSON、指令、透传数据)
} -
后台定时防泄漏定时器 1 秒触发一次:
pkg_node_timeout_process();
-
异常断连清理
pkg_node_free_by_msgid(msg_id);
六、模块优势总结
- 通用性极强:剥离原有 OAM 业务前缀,纯底层通用逻辑,蓝牙、串口、LoRa 均可直接移植;
- 资源极致节省:位图替代数组记录分片,4 字节搞定 32 分片状态;动态内存按需分配;
- 稳定性可靠:超时自动回收 + 手动释放双保险,杜绝长期运行内存泄漏;
- 接口极简清晰:初始化→存分片→取整包→定时兜底,上层无需关心内部缓存与位运算;
- 易于维护扩展:命名语义化(slice 分片、msg_id 报文 ID、bitmap 位图),新人接手可快速看懂逻辑。
七、适用场景拓展
- BLE 蓝牙 GATT 大透传报文分包重组;
- 物联网网关串口 AT 长指令分片接收;
- 窄带低功耗无线通信多帧合并;
- 嵌入式设备本地日志分片缓存上报。