示例: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_0 | 0 ~ 950 mV |
ADC_ATTEN_DB_2_5 | 0 ~ 1250 mV |
ADC_ATTEN_DB_6 | 0 ~ 1750 mV |
ADC_ATTEN_DB_12 | 0 ~ 2900 mV |
要测量 0~3.3V 范围的常规信号,应使用 ADC_ATTEN_DB_12。注意衰减不是"放大输入",而是"在 ADC 内部对参考电压做衰减"以扩大可测量范围。
这是 ADC 内部线性区的上限。超过这个值,读数会"饱和"在最大值(4095)附近,但电压数值不再准确。
1.1 ESP32 各芯片 ADC 性能与校准方案
ESP32 系列芯片的 SAR ADC 因架构和工艺差异,分辨率、电压范围、线性度和校准方案各不相同。下表汇总了常见型号的关键参数(数据来源:Comparing ADC Performance of Espressif SoCs):
| 芯片 | 分辨率 | 通道数 | 测量范围 (mV) | DNL | INL | 校准方案 |
|---|---|---|---|---|---|---|
| ESP32 | 12-bit | 8+10 | 150 – 2450 | ±7 | ±12 | Line Fitting |
| ESP32-S2 | 13-bit | 10+10 | 0 – 2500 | ±7 | ±12 | Line Fitting |
| ESP32-S3 | 12-bit | 10+10 | 0 – 2900 | ±4 | ±8 | Curve Fitting |
| ESP32-C2 | 12-bit | 5 | 0 – 2800 | +3, -1 | +8, -4 | Line Fitting |
| ESP32-C3 | 12-bit | 6 | 0 – 2500 | ±7 | ±12 | Curve Fitting |
| ESP32-C5 | 12-bit | 6 | 0 – 3300 | ±5 | ±5 | Curve Fitting |
| ESP32-C6 | 12-bit | 7 | 0 – 3300 | +12,-8 | ±10 | Curve Fitting |
| ESP32-H2 | 12-bit | 5 | 0 – 3300 | +12,-8 | ±10 | Curve Fitting |
| ESP32-P4 | 12-bit | 14 | 0 – 3300 | +3,-1 | +3,-5 | Curve 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 通用步骤(单次模式 + 校准)
-
包含头文件
-
创建 ADC 单元句柄:用
adc_oneshot_unit_init_cfg_t指定 ADC1/ADC2,调用adc_oneshot_new_unit()。 -
配置通道:用
adc_oneshot_chan_cfg_t指定位宽和衰减,调用adc_oneshot_config_channel()。 -
创建校准方案(推荐):根据芯片支持情况调用
adc_cali_create_scheme_curve_fitting()(ESP32-S3 等)或adc_cali_create_scheme_line_fitting()(ESP32、ESP32-S2、ESP32-C2)。两套方案的对比详见 1.1 节。乐鑫模组出厂时已经烧录了校准用的 eFuse 位,开发者无需额外操作。 -
读取与转换:
adc_oneshot_read()取原始值;adc_cali_raw_to_voltage()将原始值换算成 mV。 -
释放资源(如果不再使用):
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 电路
需要使用的器件有:
- 电位器(10 kΩ 线性)* 1
- 面包板 * 1
- 导线
- ESP32 开发板(微雪 ESP32-S3-Zero 迷你开发板)
接线方式:电位器两端分别接 3.3V 和 GND,中间抽头接 GPIO7。
ESP32-S3-Zero 引脚图


GPIO7 在 ESP32-S3 上对应 ADC1 的 6 号通道(ADC_CHANNEL_6)。ADC1 通道与 GPIO 的完整对应关系参考:ESP 硬件设计指南 - ADC
2.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 构建并烧录
-
配置烧录选项
首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第2节 运行示例 - 1.3 配置项目。
-
点击
一键自动依次执行构建、烧录和监视。
-
烧录完成后,旋转电位器,串口监视器中输出的电压值会从约 0 mV 平滑变化到约 2900 mV:
I (266) main_task: Calling app_main()I (266) example: ADC calibration: Curve Fitting enabledI (276) example: raw = 0, voltage = 0 mVI (476) example: raw = 294, voltage = 260 mVI (676) example: raw = 1770, voltage = 1490 mVI (876) example: raw = 2315, voltage = 1943 mVI (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_SUPPORTEDadc_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_SUPPORTEDadc_cali_line_fitting_config_t cali_cfg = { ... };if (adc_cali_create_scheme_line_fitting(&cali_cfg, &cali_handle) == ESP_OK) {calibrated = true;}#endifADC 原始读数只是一个 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 输入引脚与 GND 之间并联一个 100 nF 的陶瓷电容作为旁路电容,可有效滤除高频噪声。详见 ADC 校准驱动 - 减少噪声。
- 软件多重采样:在循环里连续读 N 次(例如 16 次)取平均,可显著平滑读数。
对采样率要求较高的场景,可改用连续采样模式(详见 ADC 连续转换模式驱动)。