# 基于 ESP32 的 BLE 长连接下,如何用连接事件间隙调度 ADC 采样而不丢包 ## 1. 问题背景与挑战 在嵌入式物联网设备中,BLE 长连接(如心率监测、环境传感)常需同时进行高频 ADC 采样。ESP32 的 BLE 协议栈运行在 CPU 核心上,每个连接事件(Connection Event)会占用 CPU 处理射频数据,若此时 ADC 中断触发,可能被延迟或丢失,导致采样数据不连续。此外,BLE 连接间隔(通常 7.5ms~4s)决定了射频活动的周期,若 ADC 采样频率与连接间隔冲突,丢包率会显著上升。 ## 2. 原理剖析:BLE 连接事件与 CPU 调度 ### 2.1 BLE 连接事件时序 BLE 连接中,主从设备在每个连接间隔的起始点进行数据交换,称为连接事件。事件持续时间取决于数据包长度和 PHY 速率,通常为 1~3ms。在事件期间,ESP32 的蓝牙控制器会占用 CPU(或专用协处理器,但协议栈仍会占用资源)。 ### 2.2 冲突根源 - **中断抢占**:ADC 采样通常使用定时器触发中断,若中断优先级低于蓝牙协议栈,则可能被延迟。 - **DMA 缓冲溢出**:若采样数据未及时搬运,DMA 缓冲会溢出,导致数据丢失。 - **任务调度延迟**:FreeRTOS 中,若 ADC 处理任务优先级低于蓝牙任务,则处理不及时。 ### 2.3 解决思路:时分复用 利用连接事件之间的空闲间隙(即连接间隔减去事件持续时间),将 ADC 采样调度到这些间隙中。具体做法: - 配置 ADC 采样频率为连接间隔的整数倍,使采样点落在事件间隙内。 - 使用 DMA 环形缓冲,即使 CPU 忙碌也能暂存数据。 - 通过 FreeRTOS 任务同步,在事件间隙处理数据。 ## 3. 硬件与软件准备 - 硬件:ESP32 开发板(如 ESP32-WROOM-32) - 软件:ESP-IDF v4.4+,或 Arduino-ESP32 核心(本文以 ESP-IDF 为例) - 外设:ADC1 通道(如 GPIO34) ## 4. 配置步骤 ### 4.1 初始化 ADC 与 DMA 使用 ADC 连续模式(Continuous Mode)配合 DMA,可自动将采样结果存入缓冲区,无需 CPU 干预。 ```c #include "esp_adc/adc_continuous.h" #define ADC_CHANNEL ADC_CHANNEL_6 // GPIO34 #define ADC_ATTEN ADC_ATTEN_DB_12 #define ADC_SAMPLE_RATE 1000 // 1kHz 采样率 adc_continuous_handle_t adc_handle; void adc_init(void) { adc_continuous_handle_cfg_t handle_cfg = { .max_store_buf_size = 1024, .conv_frame_size = 256, }; adc_continuous_new_handle(&handle_cfg, &adc_handle); adc_digi_pattern_config_t pattern = { .atten = ADC_ATTEN, .channel = ADC_CHANNEL, .unit = ADC_UNIT_1, .bit_width = ADC_BITWIDTH_12, }; adc_digi_config_t digi_cfg = { .pattern_num = 1, .adc_pattern = &pattern, .sample_freq_hz = ADC_SAMPLE_RATE, .conv_mode = ADC_CONV_SINGLE_UNIT_1, .format = ADC_DIGI_OUTPUT_FORMAT_TYPE1, }; adc_continuous_config(adc_handle, &digi_cfg); adc_continuous_start(adc_handle); } ``` ### 4.2 配置 BLE 连接参数 在 BLE 连接中,设置连接间隔为 30ms(即 30 个 1.25ms 单位),事件持续约 2ms,则间隙为 28ms,足够进行多次采样。 ```c // 在 BLE 连接回调中设置参数 uint16_t interval = 30; // 单位 1.25ms,实际 37.5ms uint16_t latency = 0; uint16_t timeout = 400; // 4s esp_ble_gap_update_conn_params(&conn_params); ``` ### 4.3 调度策略:定时器与任务同步 使用一个定时器,其周期等于连接间隔,在定时器回调中触发 ADC 数据读取(通过 DMA 缓冲),并处理数据。 ```c #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/timers.h" TimerHandle_t sample_timer; QueueHandle_t data_queue; void timer_callback(TimerHandle_t xTimer) { // 读取 DMA 缓冲中的数据 uint8_t result[256]; uint32_t len = 0; adc_continuous_read(adc_handle, result, sizeof(result), &len, 0); // 发送到处理任务 xQueueSend(data_queue, result, 0); } void adc_task(void *arg) { uint8_t data[256]; while (1) { if (xQueueReceive(data_queue, data, portMAX_DELAY)) { // 处理采样数据(如滤波、存储) } } } void app_main() { adc_init(); data_queue = xQueueCreate(10, 256); sample_timer = xTimerCreate("sample", pdMS_TO_TICKS(30), pdTRUE, NULL, timer_callback); xTimerStart(sample_timer, 0); xTaskCreate(adc_task, "adc_task", 4096, NULL, 10, NULL); // 初始化 BLE 并连接 } ``` ### 4.4 优化:动态调整采样窗口 若连接间隔变化,可动态调整定时器周期。通过 BLE 连接事件回调获取当前连接参数,并更新定时器。 ```c void ble_conn_update_callback(esp_ble_gap_cb_param_t *param) { if (param->update_conn_params.status == ESP_GAP_CONN_UPDATE_SUCCESS) { uint16_t new_interval = param->update_conn_params.conn_int; xTimerChangePeriod(sample_timer, pdMS_TO_TICKS(new_interval * 1.25), 0); } } ``` ## 5. 完整代码示例 以下为整合后的最小示例(省略 BLE 初始化细节,重点展示调度逻辑): ```c #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/timers.h" #include "freertos/queue.h" #include "esp_adc/adc_continuous.h" #include "esp_ble_gap.h" // ADC 配置 #define ADC_CHANNEL ADC_CHANNEL_6 #define ADC_ATTEN ADC_ATTEN_DB_12 #define ADC_SAMPLE_RATE 1000 adc_continuous_handle_t adc_handle; TimerHandle_t sample_timer; QueueHandle_t data_queue; // ADC 初始化(同上) void adc_init(void) { /* ... */ } // 定时器回调:读取 ADC 数据 void timer_callback(TimerHandle_t xTimer) { uint8_t result[256]; uint32_t len = 0; if (adc_continuous_read(adc_handle, result, sizeof(result), &len, 0) == ESP_OK) { xQueueSend(data_queue, result, 0); } } // 数据处理任务 void adc_task(void *arg) { uint8_t data[256]; while (1) { if (xQueueReceive(data_queue, data, portMAX_DELAY)) { // 解析数据,例如打印平均值 uint32_t sum = 0; for (int i = 0; i < 256; i += 2) { sum += (data[i] & 0xFF) | (data[i+1] << 8); } printf("ADC avg: %lu\n", sum / 128); } } } // BLE 连接参数更新回调 void ble_gap_cb(esp_ble_gap_cb_event_t event, esp_ble_gap_cb_param_t *param) { if (event == ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT) { if (param->update_conn_params.status == ESP_GAP_CONN_UPDATE_SUCCESS) { uint16_t interval = param->update_conn_params.conn_int; xTimerChangePeriod(sample_timer, pdMS_TO_TICKS(interval * 1.25), 0); } } } void app_main(void) { adc_init(); data_queue = xQueueCreate(10, 256); sample_timer = xTimerCreate("sample", pdMS_TO_TICKS(30), pdTRUE, NULL, timer_callback); xTimerStart(sample_timer, 0); xTaskCreate(adc_task, "adc_task", 4096, NULL, 10, NULL); // 初始化 BLE(省略) // 设置连接参数:间隔 30*1.25=37.5ms esp_ble_gap_set_conn_params(&conn_params); } ``` ## 6. 注意事项 - **采样率与连接间隔匹配**:采样率必须小于连接间隔的倒数,否则无法保证每个采样点都在间隙内。例如,连接间隔 30ms,采样率最高约 33Hz,但实际需留余量,建议采样率 ≤ 20Hz。 - **DMA 缓冲大小**:根据采样率和连接间隔计算缓冲大小,避免溢出。若采样率 1kHz,连接间隔 30ms,则每次事件间隙需存储 30 个样本,缓冲至少 60 字节(12位数据占2字节)。 - **任务优先级**:ADC 处理任务优先级应高于 BLE 协议栈任务(通常为 20),但低于定时器服务(优先级 1)。建议设为 10~15。 - **功耗考虑**:频繁唤醒 CPU 会增加功耗,可结合 BLE 的 latency 参数减少事件次数,但需权衡采样实时性。 - **调试技巧**:使用逻辑分析仪抓取 GPIO 翻转信号,验证采样时序是否落在连接事件间隙。 ## 7. 总结 通过理解 BLE 连接事件与 CPU 调度的关系,利用连接间隔的间隙进行 ADC 采样,配合 DMA 缓冲和 FreeRTOS 任务同步,可以显著降低丢包率。本文提供的策略不仅适用于 ESP32,也可推广到其他支持 BLE 和 DMA 的 MCU。实际项目中,需根据具体连接参数和采样需求动态调整,确保系统稳定可靠。