ESP32蓝牙SPP发送CSV文件:缓冲区溢出与回调使用问题
ESP32蓝牙SPP传输CSV文件的缓冲区溢出问题解决
问题背景
开发ESP32应用时,需通过蓝牙串行端口协议(SPP)传输CSV文件,传输由收到"SEND_CSV"命令触发。当前通过循环调用esp_spp_write()发送文件时遇到缓冲区溢出问题,希望利用ESP_SPP_WRITE_EVT回调确保数据包有序发送,避免溢出并保证文件完整传输。
现有代码
SPP数据接收回调处理
case ESP_SPP_DATA_IND_EVT: ESP_LOGI(SPP_TAG, "Data received"); if (param->data_ind.len == 8 && strncmp((char*)param->data_ind.data, "SEND_CSV", 8) == 0) { send_csv_via_bluetooth(param->data_ind.handle); } break;
CSV发送函数
#define MAX_CHUNK_SIZE 127 static void send_csv_via_bluetooth(uint32_t handle) { FILE *file = fopen(FILE_PATH, "r"); if (file == NULL) { ESP_LOGE(SPP_TAG, "Failed to open the CSV file"); return; } ESP_LOGI(SPP_TAG, "Checking size..."); // Get the file size fseek(file, 0, SEEK_END); size_t file_size = ftell(file); fseek(file, 0, SEEK_SET); char size_string[20]; snprintf(size_string, sizeof(size_string), "%zu\n\r", file_size); // Send the file size using Bluetooth esp_spp_write(handle, strlen(size_string), (uint8_t *)size_string); char buffer[MAX_CHUNK_SIZE]; size_t bytes_read; while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) { esp_err_t err = esp_spp_write(handle, bytes_read, (uint8_t *)buffer); if (err != ESP_OK) { ESP_LOGE(SPP_TAG, "Send data error %s", esp_err_to_name(err)); break; } vTaskDelay(10 / portTICK_PERIOD_MS); ESP_LOGI(SPP_TAG, "CSV file sent successfully."); fclose(file); }
核心问题分析
直接循环调用esp_spp_write()会忽略SPP发送缓冲区的容量限制,当缓冲区满时继续写入就会导致溢出错误。ESP_SPP_WRITE_EVT是ESP32 SPP栈在当前数据包发送完成后触发的回调,必须利用这个事件来驱动下一包数据的发送,实现"发送-等待确认-发送下一包"的有序传输。
解决方案实现
1. 定义传输状态结构体
用于保存传输过程中的关键状态,同时通过互斥锁保证线程安全:
#define MAX_CHUNK_SIZE 127 typedef struct { FILE *csv_file; // 打开的CSV文件句柄 uint32_t spp_handle; // SPP连接句柄 bool is_transmitting; // 是否正在传输中 char buffer[MAX_CHUNK_SIZE]; // 数据缓冲区 } SppCsvTransferState_t; static SppCsvTransferState_t s_transfer_state = {0}; static SemaphoreHandle_t s_transfer_mutex = NULL;
2. 初始化互斥锁
在应用初始化阶段创建互斥锁,保护传输状态的访问:
void app_main() { // ... 其他初始化代码 ... s_transfer_mutex = xSemaphoreCreateMutex(); }
3. 修改CSV发送触发函数
不再循环发送,而是初始化传输状态并发送第一包数据:
static void send_csv_via_bluetooth(uint32_t handle) { // 检查是否正在传输,避免重复触发 if (xSemaphoreTake(s_transfer_mutex, portMAX_DELAY) != pdTRUE) { return; } if (s_transfer_state.is_transmitting) { xSemaphoreGive(s_transfer_mutex); ESP_LOGW(SPP_TAG, "CSV transfer already in progress"); return; } // 打开CSV文件 FILE *file = fopen(FILE_PATH, "r"); if (file == NULL) { ESP_LOGE(SPP_TAG, "Failed to open the CSV file"); xSemaphoreGive(s_transfer_mutex); return; } // 发送文件大小 fseek(file, 0, SEEK_END); size_t file_size = ftell(file); fseek(file, 0, SEEK_SET); char size_string[20]; snprintf(size_string, sizeof(size_string), "%zu\n\r", file_size); esp_spp_write(handle, strlen(size_string), (uint8_t *)size_string); // 初始化传输状态 s_transfer_state.csv_file = file; s_transfer_state.spp_handle = handle; s_transfer_state.is_transmitting = true; // 读取第一包数据并发送 size_t bytes_read = fread(s_transfer_state.buffer, 1, MAX_CHUNK_SIZE, file); if (bytes_read > 0) { esp_spp_write(handle, bytes_read, (uint8_t *)s_transfer_state.buffer); } else { // 文件为空,直接结束 fclose(file); s_transfer_state.is_transmitting = false; ESP_LOGI(SPP_TAG, "CSV file is empty"); } xSemaphoreGive(s_transfer_mutex); }
4. 在SPP回调中处理发送完成事件
当ESP_SPP_WRITE_EVT触发时,继续发送剩余数据:
// 在SPP回调函数中添加以下case case ESP_SPP_WRITE_EVT: if (xSemaphoreTake(s_transfer_mutex, portMAX_DELAY) != pdTRUE) { break; } if (!s_transfer_state.is_transmitting || s_transfer_state.csv_file == NULL) { xSemaphoreGive(s_transfer_mutex); break; } // 读取下一包数据 size_t bytes_read = fread(s_transfer_state.buffer, 1, MAX_CHUNK_SIZE, s_transfer_state.csv_file); if (bytes_read > 0) { // 发送下一包 esp_spp_write(s_transfer_state.spp_handle, bytes_read, (uint8_t *)s_transfer_state.buffer); } else { // 传输完成,清理资源 fclose(s_transfer_state.csv_file); s_transfer_state.csv_file = NULL; s_transfer_state.is_transmitting = false; ESP_LOGI(SPP_TAG, "CSV file sent successfully"); } xSemaphoreGive(s_transfer_mutex); break;
关键注意事项
- 缓冲区大小调整:
MAX_CHUNK_SIZE建议设置为SPP栈的MTU值(默认一般为127或255),可通过esp_spp_get_mtu()获取当前连接的MTU动态调整。 - 错误处理:如果
esp_spp_write()返回错误,需在回调中终止传输并及时清理文件句柄。 - 线程安全:所有访问传输状态的操作必须通过互斥锁保护,避免多线程同时修改状态导致崩溃。
- 重复触发防护:触发传输前检查是否正在传输,避免同一文件被重复发送。
内容的提问来源于stack exchange,提问作者Sérgio Venturi
相关产品推荐
相关产品推荐

