ESP32-S3 双核下 ADC 连续采样与 Wi-Fi 任务共存时采样抖动来源与缓解方法

在 ESP32-S3 上做音频采集、电能质量分析或振动监测时,很多开发者会遇到一个棘手问题:单独跑 ADC 连续采样时数据很稳,一旦开启 Wi-Fi,采样间隔就开始“呼吸”——时而准时,时而延迟几十甚至几百微秒。这种抖动(jitter)会直接破坏 FFT 相位、导致音频爆音或计量误差。本文从原理到代码,帮你定位并缓解这个问题。

一、抖动从哪里来?

ESP32-S3 是双核 Xtensa LX7,ADC 模块通过 SAR ADC 和 数字控制器(RTC/ DIG ADC) 工作。连续采样通常有两种方式:

  • 软件轮询/定时器触发:CPU 读 adc1_get_raw(),受任务调度影响大。
  • DMA 连续采样:ADC 按固定时钟写入内存,CPU 只处理数据,抖动最小。

当 Wi-Fi 开启后,抖动主要来自四个层面:

  1. Wi-Fi 任务抢占:Wi-Fi 协议栈运行在 CPU0(PRO_CPU),优先级高于普通应用任务,会打断 ADC 读取任务。
  2. 中断延迟:Wi-Fi 中断、蓝牙中断、定时器中断同时触发时,ADC 相关中断被推迟。
  3. Cache 与总线争用:Wi-Fi 收发数据经过 Cache 和 PSRAM,与 ADC DMA 争抢内部总线。
  4. 电源噪声:Wi-Fi 射频突发使电源纹波增大,影响 ADC 参考电压,表现为采样值抖动。

关键结论:纯软件轮询在 Wi-Fi 下几乎无法消除抖动;要稳定,必须用 DMA + 双核隔离 + 合理中断优先级。

二、配置步骤:让 ADC 采样与 Wi-Fi 和平共处

1. 使用 ADC DMA 连续采样模式

ESP-IDF 提供 adc_continuous 驱动,底层用 DMA,采样时钟由硬件产生,不受 CPU 调度影响。

#include "esp_adc/adc_continuous.h"

#define ADC_UNIT        ADC_UNIT_1
#define ADC_CHANNEL     ADC_CHANNEL_0
#define SAMPLE_RATE_HZ  16000
#define BUF_SIZE        1024

static adc_continuous_handle_t adc_handle;

void adc_dma_init(void)
{
    adc_continuous_handle_cfg_t handle_cfg = {
        .max_store_buf_size = BUF_SIZE * 4,
        .conv_frame_size    = BUF_SIZE,
    };
    ESP_ERROR_CHECK(adc_continuous_new_handle(&handle_cfg, &adc_handle));

    adc_digi_pattern_config_t pattern = {
        .atten           = ADC_ATTEN_DB_12,
        .channel         = ADC_CHANNEL,
        .unit            = ADC_UNIT,
        .bit_width       = ADC_BITWIDTH_12,
    };
    adc_continuous_config_t dig_cfg = {
        .sample_freq_hz = SAMPLE_RATE_HZ,
        .conv_mode      = ADC_CONV_SINGLE_UNIT_1,
        .format         = ADC_DIGI_OUTPUT_FORMAT_TYPE2,
        .pattern_num    = 1,
        .adc_pattern    = &pattern,
    };
    ESP_ERROR_CHECK(adc_continuous_config(adc_handle, &dig_cfg));
    ESP_ERROR_CHECK(adc_continuous_start(adc_handle));
}

2. 双核任务绑定:ADC 处理放 CPU1

Wi-Fi 默认在 CPU0,把 ADC 数据搬运和处理任务固定到 CPU1,避免被 Wi-Fi 任务抢占。

void adc_reader_task(void *arg)
{
    uint8_t buf[BUF_SIZE];
    uint32_t ret_len;
    while (1) {
        esp_err_t ret = adc_continuous_read(adc_handle, buf, BUF_SIZE,
                                            &ret_len, 100);
        if (ret == ESP_OK) {
            // 这里做滤波、FFT 或打包,尽量短小
            process_adc_data(buf, ret_len);
        }
    }
}

// 创建任务时绑定到 CPU1
xTaskCreatePinnedToCore(adc_reader_task, "adc_reader", 4096,
                        NULL, 10, NULL, 1);

3. 中断优先级与 Wi-Fi 配置

  • 将 ADC DMA 完成中断优先级设为中等(如 2),不要高于 Wi-Fi 中断(通常 1~3),避免破坏 Wi-Fi 实时性。
  • 在 menuconfig 中关闭不必要的 Wi-Fi 省电模式:Component config → Wi-Fi → WiFi sleep → None,减少射频突发引起的电源波动。
  • 若使用 PSRAM,尽量把 ADC DMA 缓冲区放在内部 SRAM,减少总线争用。
// 将 DMA 缓冲区放到内部 RAM
static uint8_t adc_dma_buf[BUF_SIZE] __attribute__((aligned(4)));

三、完整最小示例(ADC DMA + Wi-Fi 共存)

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_adc/adc_continuous.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

#define BUF_SIZE 1024
static adc_continuous_handle_t adc_handle;

static void adc_task(void *arg)
{
    uint8_t buf[BUF_SIZE];
    uint32_t out_len;
    while (1) {
        if (adc_continuous_read(adc_handle, buf, BUF_SIZE, &out_len, 100) == ESP_OK) {
            // 仅做轻量处理,避免长时间占用 CPU1
            volatile uint32_t sum = 0;
            for (int i = 0; i < out_len; i++) sum += buf[i];
        }
    }
}

void app_main(void)
{
    // 1. NVS + Wi-Fi 初始化
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_start();

    // 2. ADC DMA 初始化
    adc_continuous_handle_cfg_t hcfg = {
        .max_store_buf_size = BUF_SIZE * 4,
        .conv_frame_size    = BUF_SIZE,
    };
    adc_continuous_new_handle(&hcfg, &adc_handle);
    adc_digi_pattern_config_t pat = {
        .atten = ADC_ATTEN_DB_12, .channel = ADC_CHANNEL_0,
        .unit = ADC_UNIT_1, .bit_width = ADC_BITWIDTH_12,
    };
    adc_continuous_config_t ccfg = {
        .sample_freq_hz = 16000, .conv_mode = ADC_CONV_SINGLE_UNIT_1,
        .format = ADC_DIGI_OUTPUT_FORMAT_TYPE2,
        .pattern_num = 1, .adc_pattern = &pat,
    };
    adc_continuous_config(adc_handle, &ccfg);
    adc_continuous_start(adc_handle);

    // 3. 采样任务绑定 CPU1
    xTaskCreatePinnedToCore(adc_task, "adc", 4096, NULL, 10, NULL, 1);
}

四、注意事项与实测建议

  • 不要用 adc1_get_raw() 做高速连续采样:它依赖软件触发,Wi-Fi 下抖动可达数百微秒。
  • 采样率不要超过 ADC 实际能力:ESP32-S3 SAR ADC 在 12 位下最高约 83.3 kSPS,但多通道轮询会下降,建议单通道 ≤ 40 kSPS。
  • Wi-Fi 省电模式影响巨大:WIFI_PS_MIN_MODEM 或 WIFI_PS_MAX_MODEM 会引起周期性射频唤醒,建议关闭或改用 WIFI_PS_NONE。
  • 电源去耦:在 ADC 参考引脚和电源脚加 10 µF + 0.1 µF,Wi-Fi 突发时纹波可降低 30% 以上。
  • 用 GPIO 翻转测抖动:在 ADC DMA 回调里翻转一个 GPIO,用示波器观察周期稳定性,比打印日志更可靠。
  • 若仍不够稳:可外接专用 ADC(如 ADS131M08)通过 SPI/I2S 采样,ESP32-S3 只做数据搬运,彻底隔离 Wi-Fi 干扰。

通过 DMA 硬件采样、双核任务隔离、合理中断优先级和电源优化,ESP32-S3 在 Wi-Fi 全速收发时,ADC 采样抖动可从数百微秒降到几微秒量级,满足大多数音频和计量应用。