一、问题背景与现象

在ESP32双核(Core0/Core1)项目中,常见架构是:一个任务(生产者)以固定频率读取ADC,通过FreeRTOS队列发送给另一个任务(消费者)处理。当消费者处理速度慢于生产者,或队列长度设置不合理时,会出现队列满(缓存溢出),表现为:

  • xQueueSend 返回 errQUEUE_FULL,数据丢失;
  • 系统偶发卡顿、看门狗复位;
  • 串口打印采样值不连续、跳变。

这类问题在多核环境下更难定位,因为生产者和消费者可能运行在不同核心,调度时序不确定。

二、FreeRTOS队列原理速览

队列是FreeRTOS内核对象,本质是一块环形缓冲区,支持任务间、中断与任务间通信。关键特性:

  • 固定长度:创建时指定 uxQueueLength 和 uxItemSize;
  • 拷贝语义:发送时数据被拷贝进队列,接收时拷贝出;
  • 阻塞机制:发送/接收可设置超时,队列满时发送任务可阻塞等待;
  • 多核安全:队列操作由内核临界区保护,可在双核间安全使用。

队列满的根本原因:入队速率 > 出队速率 + 队列容量缓冲。

三、溢出定位方法

3.1 监控队列水位

使用 uxQueueMessagesWaiting() 获取当前队列消息数,结合队列长度计算水位百分比。在生产者任务中周期性打印:

UBaseType_t waiting = uxQueueMessagesWaiting(adc_queue);
UBaseType_t len = uxQueueGetQueueLength(adc_queue); // 自定义封装或使用配置宏
ESP_LOGI(TAG, "Queue level: %d/%d", waiting, len);

若水位持续接近满,说明消费能力不足。

3.2 统计发送失败次数

在 xQueueSend 返回值处累加失败计数,并记录时间戳,观察失败频率与ADC采样率的关系:

if (xQueueSend(adc_queue, &sample, 0) != pdPASS) {
    send_fail_cnt++;
    if (send_fail_cnt % 100 == 0) {
        ESP_LOGW(TAG, "Queue full! fail=%lu", send_fail_cnt);
    }
}

3.3 检查任务优先级与核心绑定

双核下需确认:

  • 消费者任务优先级是否不低于生产者;
  • 是否使用 xTaskCreatePinnedToCore 将两者绑定到不同核心,避免同核抢占导致消费延迟;
  • 消费者任务中是否有阻塞操作(如 vTaskDelay、长耗时计算)拖慢出队。

3.4 栈溢出排查

队列溢出有时伴随任务栈溢出。使用 uxTaskGetStackHighWaterMark() 检查剩余栈空间,若接近0则需增大栈。

四、配置步骤与完整代码示例

4.1 队列创建

#define ADC_QUEUE_LEN   32
#define ADC_SAMPLE_SIZE sizeof(adc_sample_t)

typedef struct {
    uint32_t timestamp;
    uint16_t value;
} adc_sample_t;

QueueHandle_t adc_queue;

void app_main(void)
{
    adc_queue = xQueueCreate(ADC_QUEUE_LEN, ADC_SAMPLE_SIZE);
    configASSERT(adc_queue);

    xTaskCreatePinnedToCore(adc_producer_task, "adc_prod", 4096, NULL, 5, NULL, 0);
    xTaskCreatePinnedToCore(adc_consumer_task, "adc_cons", 4096, NULL, 6, NULL, 1);
}

4.2 生产者任务(ADC采样)

void adc_producer_task(void *arg)
{
    adc_sample_t sample;
    uint32_t fail_cnt = 0;

    while (1) {
        sample.timestamp = esp_timer_get_time();
        sample.value = adc1_get_raw(ADC1_CHANNEL_0);

        if (xQueueSend(adc_queue, &sample, 0) != pdPASS) {
            fail_cnt++;
            if (fail_cnt % 50 == 0) {
                ESP_LOGW(TAG, "Send fail: %lu, queue=%d/%d",
                         fail_cnt,
                         uxQueueMessagesWaiting(adc_queue),
                         ADC_QUEUE_LEN);
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10)); // 100Hz采样
    }
}

4.3 消费者任务(数据处理)

void adc_consumer_task(void *arg)
{
    adc_sample_t sample;
    while (1) {
        if (xQueueReceive(adc_queue, &sample, portMAX_DELAY) == pdPASS) {
            // 模拟耗时处理,实际应优化
            process_sample(&sample);
        }
    }
}

4.4 溢出定位增强版

在消费者中增加水位打印,在生产者中增加超时发送策略:

// 生产者改用带超时发送,避免硬丢数据
if (xQueueSend(adc_queue, &sample, pdMS_TO_TICKS(5)) != pdPASS) {
    // 超时仍失败,记录并丢弃
    ESP_LOGE(TAG, "Queue full after 5ms, drop sample");
}

五、注意事项

  • 队列长度不是越大越好:过大会增加内存占用和延迟,建议按“最大突发量 × 2”估算;
  • 避免在中断中调用阻塞式发送:中断中应使用 xQueueSendFromISR,且超时为0;
  • 双核绑定需谨慎:将生产者和消费者绑定到不同核心可减少干扰,但需确保核心间通信开销可接受;
  • 消费者处理时间必须小于采样周期:否则队列必然溢出,需优化算法或降低采样率;
  • 使用 configASSERT 检查队列创建结果,避免空指针操作;
  • 监控任务栈水位,双核下栈溢出可能表现为队列异常,需综合排查。

通过以上方法,可系统性地定位并解决ESP32双核下ADC数据队列溢出问题,提升系统稳定性。