ESP32 BLE 长连接下利用连接事件间隙调度 ADC 采样:不丢包的实战指南
👁 2 阅读 · 2026-08-27 · 嵌入式
在 ESP32 上实现 BLE 长连接时,ADC 采样常因射频活动而中断,导致数据丢失或时序错乱。本文深入剖析 BLE 连接事件与 CPU 调度的冲突根源,提出基于连接间隔(Connection Interval)的时分复用调度策略,通过配置 ADC 采样窗口与 DMA 缓冲,结合 FreeRTOS 任务优先级设计,实现无丢包采样。文章包含原理讲解、完整代码示例及关键注意事项,助你构建稳定可靠的嵌入式采集系统。
# 基于 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。实际项目中,需根据具体连接参数和采样需求动态调整,确保系统稳定可靠。