ESP32-S3 ADC 我是这样真正用顺的:从 SAR ADC 原理到一次完整的电压采集实战

在这里插入图片描述

ESP32-S3 集成了 两个 12 位 SAR ADC,共支持 20 个模拟通道输入,在实际项目中非常适合用来采集:

  • 电位器
  • 传感器模拟输出
  • 电压检测点

但在真正上手之前,有一个非常容易被忽略的问题。

ESP32-S3 的 ADC 并不是“随便一个 GPIO 都能用”


一、ESP32-S3 ADC 硬件资源说明

ESP32-S3 内部的 SAR ADC 管脚,并不是独立存在的,而是通过 IO MUX 与多种外设功能复用,包括:

  • GPIO1 ~ GPIO20
  • RTC_GPIO1 ~ RTC_GPIO20
  • 触摸传感器接口
  • UART 接口
  • SPI 接口
  • USB_D- / USB_D+
    在这里插入图片描述在这里插入图片描述
    因此这里有一个非常关键的结论:

并不是所有 IO 都支持 ADC 功能,只有特定 GPIO 才能作为 ADC 输入。

在画原理图或选引脚之前,一定要先对照 ADC 通道与 GPIO 的映射关系,否则软件写得再对,也永远读不到数据。


二、ESP32-S3 中使用 ADC 的基本流程

在 ESP-IDF 中,ADC 的使用并不是“一行 API 就结束”,而是一个相对规范的流程,整体可以分为五步:

  1. 创建 ADC 单元实例
  2. 配置 ADC 通道
  3. 读取 ADC 原始数据
  4. 创建校准方案
  5. 将原始值转换为实际电压

下面按顺序来看。


三、创建 ADC 单元实例

ESP-IDF 使用 oneshot(单次采样)模式 来完成常规 ADC 读取。

创建 ADC 单元的核心 API 是:

esp_err_t adc_oneshot_new_unit(
    const adc_oneshot_unit_init_cfg_t *init_config,
    adc_oneshot_unit_handle_t *out_handle
);

参数说明

*adc_oneshot_unit_init_cfg_t init_config
用于配置 ADC 单次采样单元的初始化参数。

*adc_oneshot_unit_handle_t out_handle
输出参数,返回 ADC 单元句柄,后续所有 ADC 操作都基于该句柄完成。


adc_oneshot_unit_init_cfg_t 结构体说明

typedef struct {
    adc_unit_t unit_id;
    adc_oneshot_clk_src_t clk_src;
    adc_ulp_mode_t ulp_mode;
} adc_oneshot_unit_init_cfg_t;

字段含义如下:

  • unit_id
    选择 ADC 单元

    • ADC_UNIT_1
    • ADC_UNIT_2
  • clk_src
    ADC 时钟源,设为 0 使用默认时钟

  • ulp_mode
    是否支持超低功耗模式下的 ADC 使用


使用示例

adc_oneshot_unit_init_cfg_t init_config1 = {
    .unit_id = ADC_UNIT_1,
    .ulp_mode = ADC_ULP_MODE_DISABLE,
};

adc_oneshot_unit_handle_t adc1_handle;
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));

四、配置 ADC 通道参数

创建 ADC 单元后,还需要为具体的 ADC 通道 配置采样参数。

核心 API:

esp_err_t adc_oneshot_config_channel(
    adc_oneshot_unit_handle_t handle,
    const adc_channel_t *channel,
    const adc_oneshot_chan_cfg_t *chan_cfg
);

参数说明

  • handle
    ADC 单元句柄

  • channel
    ADC 通道编号(如 ADC_CHANNEL_0)

  • chan_cfg
    ADC 通道配置参数


adc_oneshot_chan_cfg_t 结构体

typedef struct {
    adc_atten_t atten;
    adc_bitwidth_t bitwidth;
} adc_oneshot_chan_cfg_t;
atten(衰减)

用于扩展 ADC 可测量的输入电压范围:

  • ADC_ATTEN_DB_0 :无衰减
  • ADC_ATTEN_DB_2_5 :2.5 dB
  • ADC_ATTEN_DB_6 :6 dB
  • ADC_ATTEN_DB_12 :12 dB(最常用)
bitwidth(位宽)

决定 ADC 精度:

  • ADC_BITWIDTH_9
  • ADC_BITWIDTH_10
  • ADC_BITWIDTH_11
  • ADC_BITWIDTH_12(默认)

使用示例

adc_oneshot_chan_cfg_t config = {
    .atten = ADC_ATTEN_DB_12,
    .bitwidth = ADC_BITWIDTH_DEFAULT,
};

ESP_ERROR_CHECK(adc_oneshot_config_channel(
    adc1_handle,
    ADC_CHANNEL_0,
    &config
));

五、读取 ADC 转换结果(原始值)

ADC 配置完成后,即可读取原始采样结果:

esp_err_t adc_oneshot_read(
    adc_oneshot_unit_handle_t handle,
    const adc_channel_t *channel,
    int32_t *out_raw
);

使用示例

static int adc_ch2_raw;

adc_oneshot_read(adc1_handle, ADC_CHANNEL_0, &adc_ch2_raw);

ESP_LOGI(TAG,
         "ADC%d Channel[%d] Raw Data: %d",
         ADC_UNIT_1 + 1,
         ADC_CHANNEL_0,
         adc_ch2_raw);

此时读到的是 ADC 原始码值,并不是真正的电压。


六、创建 ADC 校准方案

如果希望 ADC 数据具有工程意义,就必须使用校准

ESP-IDF 提供的 ADC 校准机制,基于芯片内部 eFuse 中的出厂参数,用户无需额外烧录

创建校准方案的 API:

esp_err_t adc_cali_create_scheme_curve_fitting(
    const adc_cali_curve_fitting_config_t *config,
    adc_cali_handle_t *ret_handle
);

adc_cali_curve_fitting_config_t 示例

adc_cali_curve_fitting_config_t cali_config = {
    .unit_id = ADC_UNIT_1,
    .atten = ADC_ATTEN_DB_12,
    .chan = ADC_CHANNEL_2,
    .bitwidth = ADC_BITWIDTH_DEFAULT,
};

ESP_ERROR_CHECK(adc_cali_create_scheme_curve_fitting(
    &cali_config,
    &handle
));

注意:
校准参数必须与 ADC 实际配置完全一致,否则转换结果会不准确。


七、转换为实际电压值

完成校准后,即可将 ADC 原始值转换为真实电压(单位 mV):

esp_err_t adc_cali_raw_to_voltage(
    adc_cali_handle_t handle,
    int raw,
    int *out_voltage
);

使用示例

int voltage;
adc_cali_raw_to_voltage(cali_handle, adc_ch2_raw, &voltage);

八、完整实战:采集烟雾传感器

硬件说明

从原理图可知,底板的 烟雾传感器的模拟输出口接在 GPIO1 上。

在这里插入图片描述

GPIO1 对应:

  • ADC1
  • ADC_CHANNEL_0

在这里插入图片描述


完整示例代码

"ADC%d Channel[%d] Cali Voltage: %d mV", EXAMPLE_ADC1_UNIT + 1, EXAMPLE_ADC1_CHAN0, voltage_ch0

// 初始化ADC校准
static bool example_adc_calibration_init(adc_unit_t unit, adc_channel_t channel, adc_atten_t atten, adc_cali_handle_t *out_handle)
{
    esp_err_t ret = ESP_FAIL;
    bool calibrated = false;
    adc_cali_curve_fitting_config_t cali_config = {
        .unit_id = unit,          // ADC单元编号
        .chan = channel,          // ADC通道编号
        .atten = atten,           // 衰减值
        .bitwidth = ADC_BITWIDTH_DEFAULT, // 位宽
    };
    // 创建曲线拟合校准方案
    ret = adc_cali_create_scheme_curve_fitting(&cali_config, out_handle);
    if (ret == ESP_OK)
    {
        calibrated = true;
    }
    return calibrated;
}

void app_main(void)
{
    //-------------ADC1 Init---------------//
    adc_oneshot_unit_handle_t adc1_handle;
    adc_oneshot_unit_init_cfg_t init_config1 = {
        .unit_id = EXAMPLE_ADC1_UNIT, // 设置ADC单元为ADC1
        .ulp_mode = ADC_ULP_MODE_DISABLE, // 禁用ULP模式
    };
    // 初始化ADC1单元
    ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));

    //-------------ADC1 Config---------------//
    adc_oneshot_chan_cfg_t config = {
        .atten = EXAMPLE_ADC_ATTEN, // 设置衰减值为12dB
        .bitwidth = ADC_BITWIDTH_DEFAULT, // 设置位宽为默认值
    };
    // 配置ADC1通道0
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, EXAMPLE_ADC1_CHAN0, &config));

    //-------------ADC1 Calibration Init---------------//
    adc_cali_handle_t adc1_cali_chan0_handle = NULL;
    // 初始化ADC1通道0的校准
    bool do_calibration1_chan0 = example_adc_calibration_init(EXAMPLE_ADC1_UNIT, EXAMPLE_ADC1_CHAN0, EXAMPLE_ADC_ATTEN, &adc1_cali_chan0_handle);

    while (1)
    {
        // 读取ADC1通道0的原始数据
        ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, EXAMPLE_ADC1_CHAN0, &adc_raw_ch0));
        ESP_LOGI(TAG, "ADC%d Channel[%d] Raw Data: %d", EXAMPLE_ADC1_UNIT + 1, EXAMPLE_ADC1_CHAN0, adc_raw_ch0);
        // 如果通道0进行了校准,将原始数据转换为电压值
        if (do_calibration1_chan0)
        {
            ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_chan0_handle, adc_raw_ch0, &voltage_ch0));
            ESP_LOGI(TAG, "ADC%d Channel[%d] Cali Voltage: %d mV", EXAMPLE_ADC1_UNIT + 1, EXAMPLE_ADC1_CHAN0, voltage_ch0);
        }
        // 延迟1秒
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

九、最终效果

烧录程序后,可以看到:

  • ADC 原始值实时变化
  • 电压值同步变化

[图片]


更多推荐