跳到主要内容

示例:ADC 采集模拟信号

重要提示:关于开发板的兼容性

本教程的核心逻辑适用于所有 ESP32 开发板,但所有操作步骤均以 微雪 ESP32-S3-Zero 迷你开发板 为例进行讲解。如果您使用其他型号的开发板,请根据实际情况修改相应设置。

本教程将介绍如何使用乐鑫 ESP-IDF 框架,通过 ADC(模数转换器)外设的单次转换模式读取电位器的模拟电压值,并使用 ADC 校准驱动将原始数据转换为以毫伏(mV)为单位的实际电压。

1. ADC 外设

ADC(Analog-to-Digital Converter,模数转换器) 用于把连续变化的模拟电压转换为离散的数字值。

ESP32-S3 内置两组 12 位 SAR ADC:

  • ADC1:对应 GPIO1–GPIO10(共 10 个通道),推荐使用。
  • ADC2:对应 GPIO11–GPIO20,会与 Wi-Fi 共享硬件资源,启用 Wi-Fi 时无法可靠使用。如果应用涉及 Wi-Fi/蓝牙,请使用 ADC1。

12 位分辨率意味着原始结果在 0 ~ 4095 之间。ADC 输入电压范围由衰减系数 (attenuation) 决定(数据来源于 ESP 硬件设计指南 - ADC),以 ESP32-S3 为例:

衰减选项ESP32-S3 推荐输入范围(约)
ADC_ATTEN_DB_00 ~ 950 mV
ADC_ATTEN_DB_2_50 ~ 1250 mV
ADC_ATTEN_DB_60 ~ 1750 mV
ADC_ATTEN_DB_120 ~ 2900 mV

要测量 0~3.3V 范围的常规信号,应使用 ADC_ATTEN_DB_12。注意衰减不是"放大输入",而是"在 ADC 内部对参考电压做衰减"以扩大可测量范围。

输入电压最高 3.3V,为什么衰减 12dB 下只到 ~2.9V

这是 ADC 内部线性区的上限。超过这个值,读数会"饱和"在最大值(4095)附近,但电压数值不再准确。

1.1 ESP32 各芯片 ADC 性能与校准方案

ESP32 系列芯片的 SAR ADC 因架构和工艺差异,分辨率、电压范围、线性度和校准方案各不相同。下表汇总了常见型号的关键参数(数据来源:Comparing ADC Performance of Espressif SoCs):

芯片分辨率通道数测量范围 (mV)DNLINL校准方案
ESP3212-bit8+10150 – 2450±7±12Line Fitting
ESP32-S213-bit10+100 – 2500±7±12Line Fitting
ESP32-S312-bit10+100 – 2900±4±8Curve Fitting
ESP32-C212-bit50 – 2800+3, -1+8, -4Line Fitting
ESP32-C312-bit60 – 2500±7±12Curve Fitting
ESP32-C512-bit60 – 3300±5±5Curve Fitting
ESP32-C612-bit70 – 3300+12,-8±10Curve Fitting
ESP32-H212-bit50 – 3300+12,-8±10Curve Fitting
ESP32-P412-bit140 – 3300+3,-1+3,-5Curve Fitting

几个值得注意的事实:

  • 测量范围并非都是 0~3.3V。ESP32 起始电压偏到 150 mV,ESP32-S3 上限约 2.9V,只有新一代(C5/C6/H2/P4)能覆盖完整 0~3.3V。
  • DNL/INL 是 ADC 线性度指标,数字越小越好。ESP32-P4、C5 在这两项上表现最佳;ESP32 与 ESP32-S2/S3 相对较差,所以更依赖校准来纠正非线性。
  • 若应用对精度敏感(如电压表、电池电量计),ESP32-C2、C5、C6、H2、P4 是更稳妥的选择;ESP32/S3 适合一般场景(电位器读取、传感器趋势监测)。

Line Fitting 与 Curve Fitting

ESP-IDF 把出厂校准数据封装为两套校准方案,开发者通过统一的 adc_cali_raw_to_voltage() API 调用,底层算法对应用透明:

  • Line Fitting(线性拟合):把原始读数到电压的关系当作一条直线来纠正——求出偏移增益两个参数,再代入线性公式。简单、低开销,但两端电压区域的非线性误差无法消除。适用芯片:ESP32、ESP32-S2、ESP32-C2。
  • Curve Fitting(曲线拟合):用 多项式(高阶曲线) 对原始读数到电压的非线性关系建模,每个衰减选项有一组芯片出厂时调好的系数。能更好地纠正中高电压段的非线性弯曲。适用芯片:ESP32-S3、ESP32-C3、ESP32-C5、ESP32-C6、ESP32-H2、ESP32-P4。

校准对精度的提升非常显著。以 ESP32-S3 为例,未校准时原始读数在高电压段(>2750 mV)有明显非线性偏移;启用 Curve Fitting 后,全量程误差被压缩到约 -30 ~ 0 mV 之间(参考乐鑫博客实测数据)。

备注

所有乐鑫官方模组在出厂时都已经烧录了校准所需的 eFuse 位,开发者无需自行校准。直接调用对应 adc_cali_create_scheme_xxx_fitting() 即可。

提示

ESP-IDF 的代码中常见 #if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED ... #elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED ... 这种条件编译写法。这是官方推荐的可移植模板——同一份代码搬到不同芯片上都能自动选用正确的方案,无需修改。本示例也采用了这种写法。

1.2 单次模式和连续模式

ESP-IDF 把 ADC 驱动拆成两套:

  • 单次转换模式(oneshot):CPU 主动发起一次转换,读到一个数。适合按需采样(如周期性读电位器)。开销小、上手快,本示例使用此模式。
  • 连续转换模式(continuous / DMA):硬件按固定采样率持续把结果写入内存,CPU 通过回调取数据。适合高速、连续采样(音频、振动分析)。配置较复杂,本文不展开,具体可参考 ADC 连续转换模式驱动

1.3 通用步骤(单次模式 + 校准)

  1. 包含头文件

  2. 创建 ADC 单元句柄:用 adc_oneshot_unit_init_cfg_t 指定 ADC1/ADC2,调用 adc_oneshot_new_unit()

  3. 配置通道:用 adc_oneshot_chan_cfg_t 指定位宽和衰减,调用 adc_oneshot_config_channel()

  4. 创建校准方案(推荐):根据芯片支持情况调用 adc_cali_create_scheme_curve_fitting()(ESP32-S3 等)或 adc_cali_create_scheme_line_fitting()(ESP32、ESP32-S2、ESP32-C2)。两套方案的对比详见 1.1 节。乐鑫模组出厂时已经烧录了校准用的 eFuse 位,开发者无需额外操作。

  5. 读取与转换adc_oneshot_read() 取原始值;adc_cali_raw_to_voltage() 将原始值换算成 mV。

  6. 释放资源(如果不再使用):adc_cali_delete_scheme_curve_fitting() / adc_cali_delete_scheme_line_fitting() 删除校准句柄,adc_oneshot_del_unit() 删除 ADC 单元句柄。

2. 示例项目

本示例使用电位器作为可调电压源,接到 ESP32-S3-Zero 的 GPIO7(ADC1_CH6),周期性读取并打印 ADC 原始值与校准后的电压。

2.1 电路

需要使用的器件有:

接线方式:电位器两端分别接 3.3VGND,中间抽头接 GPIO7

ESP32-S3-Zero 引脚图

ESP32-S3-Zero-Pinout

接线图
备注

GPIO7 在 ESP32-S3 上对应 ADC1 的 6 号通道(ADC_CHANNEL_6)。ADC1 通道与 GPIO 的完整对应关系参考:ESP 硬件设计指南 - ADC

2.2 创建项目

  1. 创建一个项目。如果不清楚如何操作,请参考 从模板创建项目

  2. 查看 ADC 单次转换模式 API 参考ADC 校准驱动程序。根据文档中的指引完成以下步骤。

    首先在 main.c 中包含头文件:

    #include "esp_adc/adc_oneshot.h"
    #include "esp_adc/adc_cali.h"
    #include "esp_adc/adc_cali_scheme.h"

    然后在 main/CMakeLists.txt 中声明 esp_adc 组件:

    idf_component_register(SRCS "main.c"
    INCLUDE_DIRS "."
    REQUIRES esp_adc)

2.3 示例代码

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"

static const char *TAG = "example";

#define ADC_UNIT ADC_UNIT_1 // 使用 ADC1
#define ADC_CHANNEL ADC_CHANNEL_6 // GPIO7 = ADC1_CH6
#define ADC_ATTEN ADC_ATTEN_DB_12 // 0 ~ ~2900 mV
#define ADC_BITWIDTH ADC_BITWIDTH_DEFAULT // 12 位

static adc_oneshot_unit_handle_t adc_handle;
static adc_cali_handle_t cali_handle = NULL;
static bool calibrated = false;

static void adc_init(void)
{
// 1. 创建 ADC 单元
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = ADC_UNIT,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&unit_cfg, &adc_handle));

// 2. 配置通道
adc_oneshot_chan_cfg_t chan_cfg = {
.bitwidth = ADC_BITWIDTH,
.atten = ADC_ATTEN,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, ADC_CHANNEL, &chan_cfg));

// 3. 创建校准方案
// ESP32-S3 走 Curve Fitting 分支;同一份代码搬到只支持 Line Fitting
// 的芯片(如 ESP32)时,自动走 #elif 分支。
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
adc_cali_curve_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT,
.chan = ADC_CHANNEL,
.atten = ADC_ATTEN,
.bitwidth = ADC_BITWIDTH,
};
if (adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali_handle) == ESP_OK) {
calibrated = true;
ESP_LOGI(TAG, "ADC calibration: Curve Fitting enabled");
}
#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
adc_cali_line_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT,
.atten = ADC_ATTEN,
.bitwidth = ADC_BITWIDTH,
};
if (adc_cali_create_scheme_line_fitting(&cali_cfg, &cali_handle) == ESP_OK) {
calibrated = true;
ESP_LOGI(TAG, "ADC calibration: Line Fitting enabled");
}
#endif

if (!calibrated) {
ESP_LOGW(TAG, "ADC calibration not available, raw values only");
}
}

void app_main(void)
{
adc_init();

while (1) {
int raw = 0;
ESP_ERROR_CHECK(adc_oneshot_read(adc_handle, ADC_CHANNEL, &raw));

if (calibrated) {
int voltage_mv = 0;
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(cali_handle, raw, &voltage_mv));
ESP_LOGI(TAG, "raw = %4d, voltage = %4d mV", raw, voltage_mv);
} else {
ESP_LOGI(TAG, "raw = %4d", raw);
}

vTaskDelay(pdMS_TO_TICKS(500));
}
}

2.4 构建并烧录

  1. 配置烧录选项

    首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第2节 运行示例 - 1.3 配置项目

    VS Code 工具栏

  2. 点击 VS Code 一键构建烧录监视图标 一键自动依次执行构建、烧录和监视。

  3. 烧录完成后,旋转电位器,串口监视器中输出的电压值会从约 0 mV 平滑变化到约 2900 mV:

    I (266) main_task: Calling app_main()
    I (266) example: ADC calibration: Curve Fitting enabled
    I (276) example: raw = 0, voltage = 0 mV
    I (476) example: raw = 294, voltage = 260 mV
    I (676) example: raw = 1770, voltage = 1490 mV
    I (876) example: raw = 2315, voltage = 1943 mV
    I (976) example: raw = 4095, voltage = 3146 mV
    ...

2.5 代码解析

1. 包含头文件

#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
  • esp_adc/adc_oneshot.h:ADC 单次转换模式驱动。提供 adc_oneshot_new_unit()adc_oneshot_config_channel()adc_oneshot_read() 等核心 API。
  • esp_adc/adc_cali.h:ADC 校准 API 的通用接口,主要提供 adc_cali_raw_to_voltage()
  • esp_adc/adc_cali_scheme.h:具体校准方案的创建/销毁函数(如 adc_cali_create_scheme_curve_fitting())和 ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED 这类宏。
备注

这三个头文件都属于 esp_adc 组件,因此 CMakeLists.txt 中只需声明 REQUIRES esp_adc 一次。

2. 定义全局常量

#define ADC_UNIT ADC_UNIT_1
#define ADC_CHANNEL ADC_CHANNEL_6
#define ADC_ATTEN ADC_ATTEN_DB_12
#define ADC_BITWIDTH ADC_BITWIDTH_DEFAULT
  • ADC_UNIT_1:使用 ADC1。如果以后启用 Wi-Fi,ADC2 会被占用,所以入门示例统一用 ADC1。
  • ADC_CHANNEL_6:对应 GPIO7。ADC 通道号是逻辑编号,不是 GPIO 编号,二者需要查表对照。也可以调用 adc_oneshot_io_to_channel() 在运行时做转换。
  • ADC_ATTEN_DB_12:选择衰减,决定可测电压范围。0~3V 范围的常规信号选 12 dB(ESP32-S3 实测上限约 2.9V,详见 1.1 节)。
  • ADC_BITWIDTH_DEFAULT:让驱动使用芯片支持的默认位宽。ESP32-S3 上是 12 位(即 ADC_BITWIDTH_12)。

3. ADC 初始化(adc_init

这一步完成三件事:创建 ADC 单元、配置通道、(尝试)创建校准方案。

  • 创建 ADC 单元

    adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = ADC_UNIT };
    ESP_ERROR_CHECK(adc_oneshot_new_unit(&unit_cfg, &adc_handle));

    得到一个 adc_oneshot_unit_handle_t 句柄,后续所有针对 ADC1 的操作都通过它进行。一个 ADC 单元下的所有通道共享这一个句柄。

  • 配置通道

    adc_oneshot_chan_cfg_t chan_cfg = {
    .bitwidth = ADC_BITWIDTH,
    .atten = ADC_ATTEN,
    };
    ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, ADC_CHANNEL, &chan_cfg));

    指定通道的位宽和衰减。如果一个项目要采集多个通道(例如两个电位器),就在同一个 adc_handle 上多次调用 adc_oneshot_config_channel(),每个通道分别配置即可。

  • 创建校准方案

    #if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
    adc_cali_curve_fitting_config_t cali_cfg = { ... };
    if (adc_cali_create_scheme_curve_fitting(&cali_cfg, &cali_handle) == ESP_OK) {
    calibrated = true;
    }
    #elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
    adc_cali_line_fitting_config_t cali_cfg = { ... };
    if (adc_cali_create_scheme_line_fitting(&cali_cfg, &cali_handle) == ESP_OK) {
    calibrated = true;
    }
    #endif

    ADC 原始读数只是一个 0~4095 的整数,要把它换算成准确的毫伏数,需要芯片在出厂时烧录的校准参数。前文 1.1 节 提到 ESP32-S3 支持 Curve Fitting、部分老芯片支持 Line Fitting——代码用 #if / #elif 编译期宏选择对应分支,同一份代码搬到不同芯片上都能自动走正确方案。如果两个分支都不支持(极少数情况)或校准句柄创建失败,calibrated 保持 false,程序退化为只输出原始值,不影响运行。

    备注

    注意两套方案的配置结构体不同:Curve Fitting 用 adc_cali_curve_fitting_config_t(多一个 chan 字段),Line Fitting 用 adc_cali_line_fitting_config_t。但释放接口、adc_cali_raw_to_voltage() 这些后续 API 是统一的,业务代码无需感知用了哪套方案。

4. 主循环:读取并换算

int raw = 0;
ESP_ERROR_CHECK(adc_oneshot_read(adc_handle, ADC_CHANNEL, &raw));

if (calibrated) {
int voltage_mv = 0;
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(cali_handle, raw, &voltage_mv));
...
}
  • adc_oneshot_read():发起一次转换,把结果写到 raw不要在 ISR 中调用——这个函数内部使用互斥锁,只能在普通任务上下文中用。
  • adc_cali_raw_to_voltage():用校准句柄把原始值换算成 mV。返回值是经过曲线拟合后的电压,比直接做线性换算(raw * Vmax / 4095)准确得多。
  • vTaskDelay(pdMS_TO_TICKS(500)):每 500 ms 采样一次。ADC 本身的转换速度远高于此,这里只是控制串口输出节奏。
便捷函数:adc_oneshot_get_calibrated_result()

ESP-IDF 提供了一个便捷函数 adc_oneshot_get_calibrated_result(),内部一次完成"读原始值 + 校准换算"两步:

int voltage_mv = 0;
ESP_ERROR_CHECK(adc_oneshot_get_calibrated_result(adc_handle, cali_handle, ADC_CHANNEL, &voltage_mv));

本示例特意拆成两步是为了让你看清"原始读数"和"校准换算"是两件独立的事。理解之后,实际项目中可以直接用这个便捷函数简化代码。

降低 ADC 噪声

如果观察到读数抖动严重,可结合以下两种方法:

  • 硬件滤波(官方推荐):在 ADC 输入引脚与 GND 之间并联一个 100 nF 的陶瓷电容作为旁路电容,可有效滤除高频噪声。详见 ADC 校准驱动 - 减少噪声
  • 软件多重采样:在循环里连续读 N 次(例如 16 次)取平均,可显著平滑读数。

对采样率要求较高的场景,可改用连续采样模式(详见 ADC 连续转换模式驱动)。

3. 参考链接