ESP32-C3 入门笔记08:多帧数据解析
BLE 每帧协议格式:
问题:
ESP32设备通过BLE接收微信小程序端发送的数据,而且是多帧数据。
数据解析函数初始代码逻辑如下:
// 数据解析函数
void esp_ble_parse_data(uint8_t *frame)
{
if (frame[0] != FRAME_HEADER || frame[53] != FRAME_TAIL) // 判断帧头/帧尾
{
ESP_LOGE(ESP_BLE_TAG, "Invalid frame header or tail");
return;
}
esp_ble_data_frame_t *parsed_frame = (esp_ble_data_frame_t *)frame;
// uint8_t computed_checksum = esp_ble_calculate_checksum(frame + 1, 52); // 校验和计算 1-54 0-53 0-52 数据长度 54 /
// if (computed_checksum != parsed_frame->checksum)
// {
// ESP_LOGE(ESP_BLE_TAG, "Checksum mismatch: expected 0x%02X, got 0x%02X", computed_checksum, parsed_frame->checksum);
// return;
// }
ESP_LOGI(ESP_BLE_TAG, "Valid frame received");
ESP_LOGI(ESP_BLE_TAG, "Mode: 0x%02X, Data Length: %d, Interval: 0x%02X", parsed_frame->mode, parsed_frame->data_length, parsed_frame->interval);
// 根据模式设置模式变量
switch (parsed_frame->mode)
{
case 0x01:
t_show_mode = true;
clear_erase_key(esp_nvs_keys[0]);
clear_erase_key(esp_nvs_keys[2]);
save_show_mode(esp_nvs_keys[0], t_show_mode); // 保存转灯的显示模式
save_img_data(esp_nvs_keys[2], tsl_static_img, parsed_frame->data, sizeof(parsed_frame->data));
ESP_LOGI(ESP_BLE_TAG, "转灯静图模式, t_show_mode = %d", t_show_mode);
break;
case 0x02:
t_show_mode = false;
clear_erase_key(esp_nvs_keys[0]);
clear_erase_key(esp_nvs_keys[3]);
save_show_mode(esp_nvs_keys[0], t_show_mode); // 保存转灯的显示模式
save_img_data(esp_nvs_keys[3], tsl_dynamic_img, parsed_frame->data, sizeof(parsed_frame->data));
ESP_LOGI(ESP_BLE_TAG, "转灯动态图模式, t_show_mode = %d", t_show_mode);
break;
case 0x03:
d_show_mode = true;
clear_erase_key(esp_nvs_keys[1]);
clear_erase_key(esp_nvs_keys[4]);
save_show_mode(esp_nvs_keys[1], d_show_mode); // 保存日行灯的显示模式
save_img_data(esp_nvs_keys[4], drl_static_img, parsed_frame->data, sizeof(parsed_frame->data));
ESP_LOGI(ESP_BLE_TAG, "日行灯静图模式, d_show_mode = %d", d_show_mode);
xEventGroupSetBits(light_event_group, EVENT_RUNNING_LIGHT); // 触发日行灯显示刷新
break;
case 0x04:
// d_show_mode = false;
// clear_erase_key(esp_nvs_keys[1]);
// save_show_mode(esp_nvs_keys[1], d_show_mode); // 保存日行灯的显示模式
// // clear_erase_key(esp_nvs_keys[5]);
// // save_img_data(esp_nvs_keys[5], drl_static_img, parsed_frame->data, sizeof(parsed_frame->data));
// ESP_LOGI(ESP_BLE_TAG, "日行灯动态图模式, d_show_mode = %d", d_show_mode);
// xEventGroupSetBits(light_event_group, EVENT_RUNNING_LIGHT); // 触发日行灯显示刷新
/*********************************************/
// uint8_t total_frames = (parsed_frame->data_length >> 4) & 0x0F; // 高位表示总包数 最大6
// uint8_t frame_index = parsed_frame->data_length & 0x0F; // 低位表示包序号 最小1
// // 计算延时时间(单位:毫秒)
// uint8_t drl_dynamic_img_delay_time = parsed_frame->interval * 100;
// (void)drl_dynamic_img_delay_time; // 暂时忽略未使用警告
// // 检查总包数和当前帧序号的有效性
// if (total_frames > 6 || frame_index < 1 || frame_index > total_frames)
// {
// ESP_LOGE(ESP_BLE_TAG, "Invalid frame index (%d) or total frames (%d)", frame_index, total_frames);
// return;
// }
// // 分配缓冲区
// uint8_t *drl_dynamic_img_buffer = (uint8_t *)malloc(48 * total_frames);
// if (!drl_dynamic_img_buffer)
// {
// ESP_LOGE(ESP_BLE_TAG, "Failed to allocate memory for dynamic image buffer");
// return;
// }
// // 拷贝数据
// memcpy(drl_dynamic_img_buffer + (frame_index - 1) * 48, parsed_frame->data, 48);
// // 检查是否接收到所有帧
// if (frame_index == total_frames)
// {
// // drl_dynamic_img_delay = drl_dynamic_img_delay_time;
// d_show_mode = false;
// clear_erase_key(esp_nvs_keys[1]);
// clear_erase_key(esp_nvs_keys[5]);
// save_show_mode(esp_nvs_keys[1], d_show_mode); // 保存日行灯的显示模式
// save_img_data(esp_nvs_keys[5], drl_dynamic_img, drl_dynamic_img_buffer, 48 * total_frames);
// ESP_LOGI(ESP_BLE_TAG, "日行灯动态图模式, d_show_mode = %d", d_show_mode);
// xEventGroupSetBits(light_event_group, EVENT_RUNNING_LIGHT); // 触发日行灯显示刷新
// }
// free(drl_dynamic_img_buffer);
// break;
/*********************************************/
break;
default:
ESP_LOGE(ESP_BLE_TAG, "Unknown mode: 0x%02X", parsed_frame->mode);
break;
}
}
关键代码解释:
memcpy(drl_dynamic_img_buffer + (frame_index - 1) * 48, parsed_frame->data, 48);
这段代码的作用是将当前帧的数据复制到动态图片缓冲区(drl_dynamic_img_buffer
)的正确位置。以下是对每个部分的详细解析:
代码的整体功能
memcpy(drl_dynamic_img_buffer + (frame_index - 1) * 48, parsed_frame->data, 48);
功能
- 目的:将
parsed_frame->data
中的 48 字节数据复制到drl_dynamic_img_buffer
的指定位置。 - 场景:
drl_dynamic_img_buffer
是一个存储多帧动态图片数据的缓冲区,而parsed_frame->data
是当前帧的数据。
代码分解与解释
1. memcpy
函数
memcpy
是 C 标准库中的内存拷贝函数,用于从源地址复制指定字节数的数据到目标地址。
- 原型:
void *memcpy(void *dest, const void *src, size_t n);
dest
:目标内存地址(复制到这里)。src
:源内存地址(从这里复制)。n
:需要复制的字节数。
2. drl_dynamic_img_buffer + (frame_index - 1) * 48
目标地址:
drl_dynamic_img_buffer
是整个动态图片缓冲区的起始地址。(frame_index - 1)
是帧的索引,从 0 开始计算偏移量。- 每帧数据占用 48 字节,因此偏移量需要乘以
48
。
计算结果:当前帧的数据存储到 drl_dynamic_img_buffer
偏移量为 (frame_index - 1) * 48
的位置。
3. parsed_frame->data
源地址:
parsed_frame->data
是当前帧的数据指针,指向 48 字节的数据块。- 这些数据是从帧结构体中提取的,通常由某种协议或外部数据包解析得到。
4. 48
复制的字节数:
- 每帧固定大小为 48 字节,所以
memcpy
只复制 48 个字节的数据。
数据流示例
假设条件
drl_dynamic_img_buffer
大小为288
字节(支持存储 6 帧,每帧 48 字节)。- 当前处理第
frame_index = 2
帧。 parsed_frame->data
包含 48 字节的内容,例如:[0x01, 0x02, ..., 0x30]
。
计算偏移量
目标地址:
drl_dynamic_img_buffer + (frame_index - 1) * 48
= drl_dynamic_img_buffer + (2 - 1) * 48
= drl_dynamic_img_buffer + 48
数据会被存储到缓冲区的第 48 字节位置(从索引 48 到 95)。
缓冲区内容更新
初始缓冲区(假设第 1 帧已存储 0xFF
):
[0xFF, 0xFF, ..., 0xFF] (48 字节,帧 1)
[未初始化] (48 字节,帧 2)
[未初始化] (192 字节,其余帧)
复制后缓冲区内容:
[0xFF, 0xFF, ..., 0xFF] (48 字节,帧 1)
[0x01, 0x02, ..., 0x30] (48 字节,帧 2)
[未初始化] (192 字节,其余帧)
使用场景与意义
- 多帧动态图片存储:动态图片可能由多个帧组成,每帧数据分包发送到设备。
drl_dynamic_img_buffer
用于依次存储这些帧数据,最终形成完整的动态图。 - 数据顺序管理:通过
(frame_index - 1)
确保每帧数据存储在正确的位置,避免数据覆盖或顺序错误。 - 通用性强:通过计算偏移量和字节数,代码可支持任意数量的帧和大小。
总结
memcpy(drl_dynamic_img_buffer + (frame_index - 1) * 48, parsed_frame->data, 48);
作用:将当前帧的 48 字节数据复制到动态图片缓冲区中,存储在对应的帧位置。
核心逻辑:
- 根据
frame_index
计算目标存储位置。 - 将当前帧的数据完整复制到缓冲区。
- 确保多帧数据按顺序存储在统一的缓冲区中,为后续处理或显示提供基础。
如果有更多细节需要补充,请随时告诉我!
代码逻辑分析与改进建议
代码功能分析
功能
-
解析数据包:
total_frames
:表示数据包总数(最大 6)。frame_index
:当前帧的序号(最小 1)。
-
检查数据有效性:
- 验证
total_frames
和frame_index
的范围。
- 验证
-
动态分配缓冲区:
- 为总帧数据分配内存,大小为
48 * total_frames
。
- 为总帧数据分配内存,大小为
-
存储帧数据:
- 将当前帧数据存储到动态缓冲区的正确位置。
-
接收完成时处理:
- 如果接收完成(即
frame_index == total_frames
),保存数据并更新显示模式。
- 如果接收完成(即
-
释放资源:
- 释放动态分配的缓冲区内存,避免内存泄漏。
关键问题与改进
问题 1:每次函数调用都会重新分配缓冲区
- 当前代码中,缓冲区
drl_dynamic_img_buffer
每次调用函数都会重新分配,即使是处理同一个动态图片的数据包。 - 这种做法导致内存分配重复且覆盖之前已接收的数据。
改进方法
- 使用 静态缓冲区 或 全局缓冲区,使所有帧数据共享一个缓冲区。
- 示例:
static uint8_t *drl_dynamic_img_buffer = NULL; // 静态缓冲区
static uint8_t total_frames_allocated = 0; // 记录已分配的帧数
if (drl_dynamic_img_buffer == NULL) {
drl_dynamic_img_buffer = (uint8_t *)malloc(48 * total_frames);
if (!drl_dynamic_img_buffer) {
ESP_LOGE(ESP_BLE_TAG, "Failed to allocate memory for dynamic image buffer");
return;
}
total_frames_allocated = total_frames; // 记录分配的总帧数
} else if (total_frames != total_frames_allocated) {
ESP_LOGE(ESP_BLE_TAG, "Inconsistent total_frames allocation");
free(drl_dynamic_img_buffer);
return;
}
问题 2:未处理数据包丢失情况
- 当前逻辑假定所有数据包按顺序到达,未处理数据包丢失或乱序的问题。
- 如果数据包丢失,最后一帧接收完成时可能会保存不完整的动态图。
改进方法
- 增加接收状态标记,如使用一个布尔数组
frame_received[]
记录每帧接收状态。 - 示例:
static bool frame_received[6] = {false}; // 最大支持 6 帧
// 标记当前帧为已接收
frame_received[frame_index - 1] = true;
// 检查所有帧是否接收完成
bool all_received = true;
for (int i = 0; i < total_frames; i++) {
if (!frame_received[i]) {
all_received = false;
break;
}
}
if (all_received) {
// 处理接收完成逻辑
}
问题 3:缓冲区释放时机不当
- 缓冲区
drl_dynamic_img_buffer
在函数结束时总是释放,但在最后一帧时需要保留完整数据。
改进方法
- 仅在数据接收完成并保存后释放缓冲区。
- 示例:
if (frame_index == total_frames) {
// 保存数据
save_img_data(esp_nvs_keys[5], drl_dynamic_img, drl_dynamic_img_buffer, 48 * total_frames);
// 释放缓冲区
free(drl_dynamic_img_buffer);
drl_dynamic_img_buffer = NULL; // 避免悬空指针
}
改进后的完整代码
以下是改进后的代码示例:
static uint8_t *drl_dynamic_img_buffer = NULL;
static uint8_t total_frames_allocated = 0;
static bool frame_received[6] = {false}; // 最大支持 6 帧
void process_dynamic_img(parsed_frame_t *parsed_frame) {
uint8_t total_frames = (parsed_frame->data_length >> 4) & 0x0F; // 高位表示总包数 最大6
uint8_t frame_index = parsed_frame->data_length & 0x0F; // 低位表示包序号 最小1
// 检查总包数和当前帧序号的有效性
if (total_frames > 6 || frame_index < 1 || frame_index > total_frames) {
ESP_LOGE(ESP_BLE_TAG, "Invalid frame index (%d) or total frames (%d)", frame_index, total_frames);
return;
}
// 初始化缓冲区
if (drl_dynamic_img_buffer == NULL) {
drl_dynamic_img_buffer = (uint8_t *)malloc(48 * total_frames);
if (!drl_dynamic_img_buffer) {
ESP_LOGE(ESP_BLE_TAG, "Failed to allocate memory for dynamic image buffer");
return;
}
total_frames_allocated = total_frames;
memset(frame_received, 0, sizeof(frame_received));// 初始化为 0
} else if (total_frames != total_frames_allocated) {
ESP_LOGE(ESP_BLE_TAG, "Inconsistent total_frames allocation");
free(drl_dynamic_img_buffer);
drl_dynamic_img_buffer = NULL;
return;
}
// 拷贝当前帧数据
memcpy(drl_dynamic_img_buffer + (frame_index - 1) * 48, parsed_frame->data, 48);
frame_received[frame_index - 1] = true;
// 检查所有帧是否接收完成
bool all_received = true;
for (int i = 0; i < total_frames; i++) {
if (!frame_received[i]) {
all_received = false;
break;
}
}
if (all_received) {
// 保存数据
clear_erase_key(esp_nvs_keys[1]);
clear_erase_key(esp_nvs_keys[5]);
save_show_mode(esp_nvs_keys[1], false);
save_img_data(esp_nvs_keys[5], drl_dynamic_img, drl_dynamic_img_buffer, 48 * total_frames);
ESP_LOGI(ESP_BLE_TAG, "日行灯动态图模式, d_show_mode = false");
xEventGroupSetBits(light_event_group, EVENT_RUNNING_LIGHT); // 触发显示刷新
// 释放缓冲区
free(drl_dynamic_img_buffer);
drl_dynamic_img_buffer = NULL;
}
}
主要改进点
- 静态缓冲区:避免每次函数调用重复分配内存。
- 接收状态检查:确保所有帧完整接收后再处理。
- 释放时机优化:仅在数据完全保存后释放缓冲区。
- 错误处理:增加分配失败和状态不一致的错误检查。
通过这些改进,代码的内存管理更高效,逻辑更可靠。