4、ADC模拟输入:模拟信号读取、电压转换计算、光敏电阻与电位器实验、ADC衰减配置

ADC,全称模数转换器。说白了,就是把连续变化的电压,变成一串离散的数字。ESP32 内部集成了两个 12 位的 SAR ADC,精度不算顶级,但做传感器读取、旋钮检测、电池电压监测,完全够用。

我刚开始用 ESP32 做 ADC 时,踩过一个坑:读出来的数值跳得厉害,以为是硬件坏了。后来才发现,是衰减配置没设对,输入电压范围跟 ADC 参考电压不匹配。嗯,这节课我们就把它彻底讲透。

4.1 ESP32 ADC 硬件基础

ESP32 的 ADC 模块有几个关键参数,你得先记住:

参数 值 说明
分辨率 12 位 输出范围 0 ~ 4095
参考电压 约 1.1V(内部) 不衰减时最大输入 1.1V
ADC1 通道 GPIO32 ~ GPIO39 共 8 个通道
ADC2 通道 GPIO0, 2, 4, 12 ~ 15, 25 ~ 27 Wi-Fi 开启时不可用
衰减配置 0dB / 2.5dB / 6dB / 11dB 决定最大输入电压
重要提醒:ADC2 在 Wi-Fi 使能时会被占用。如果你同时用 Wi-Fi 和 ADC,请务必使用 ADC1 的通道。我见过有人调试半天读不到数据,最后发现是用了 ADC2 的 GPIO25,Wi-Fi 一开,ADC 就罢工了。

4.2 衰减配置与电压范围

ESP32 内部 ADC 的参考电压大约是 1.1V。如果不做衰减,你只能测量 0 ~ 1.1V 的信号。这在实际项目中基本不够用。所以 ESP32 提供了 4 档衰减配置,用来扩展输入范围。

衰减档位 宏定义 有效输入范围 典型应用
0 dB ADC_ATTEN_DB_0 0 ~ 1.1V 内部参考、精密小信号
2.5 dB ADC_ATTEN_DB_2_5 0 ~ 1.5V 部分传感器输出
6 dB ADC_ATTEN_DB_6 0 ~ 2.2V 锂电池电压监测
11 dB ADC_ATTEN_DB_11 0 ~ 3.3V 通用模拟输入、电位器

我个人习惯:只要不确定输入电压范围,直接上 11dB 衰减。虽然精度会稍微损失一点,但至少不会烧引脚。你想想看,3.3V 直接灌进 0dB 的 ADC 引脚,轻则读数全满,重则烧坏 GPIO。

4.3 电压转换计算

ADC 读出来的是 0 ~ 4095 的原始值。要换算成实际电压,公式很简单:

电压 = (ADC原始值 / 4095.0) × 最大输入电压

最大输入电压取决于你配置的衰减档位。以 11dB 为例,最大输入约 3.3V:

float voltage = (adc_value / 4095.0) * 3.3;

但这里有个细节——ESP32 的 ADC 并不是完美的线性器件。尤其是靠近 0V 和靠近满量程的区域,误差会偏大。我在做电池电压监测项目时,发现低电量区域读数偏差超过 50mV,后来做了两点校准才解决。

经验之谈:如果项目对精度要求高,建议用万用表实测几个点的电压,然后做线性校准。比如测 0.5V、1.5V、2.5V 三个点,拟合出一条校准曲线。ESP32 的 ADC 出厂前做过基本校准,但每颗芯片的个体差异仍然存在。

4.4 光敏电阻实验

光敏电阻,也叫光依赖电阻(LDR)。它的阻值随光照强度变化:光照越强,阻值越低。配合一个固定电阻组成分压电路,就能把光信号变成电压信号。

电路连接很简单:

  • 3.3V → 固定电阻(10kΩ)→ 光敏电阻 → GND
  • 光敏电阻与固定电阻的连接点 → ESP32 GPIO34(ADC1_CH6)

代码实现:

#include <driver/adc.h>

void setup() {
  Serial.begin(115200);
  
  // 配置 ADC1 通道 6,11dB 衰减
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
}

void loop() {
  int raw = adc1_get_raw(ADC1_CHANNEL_6);
  float voltage = raw * 3.3 / 4095.0;
  
  Serial.print("原始值: ");
  Serial.print(raw);
  Serial.print("  电压: ");
  Serial.print(voltage);
  Serial.println(" V");
  
  delay(500);
}

用手遮住光敏电阻,你会发现电压值下降;用手电筒照射,电压值上升。这就是光信号到电信号的转换过程。

4.5 电位器实验

电位器本质上是一个可调分压器。三根引脚:两端接电源和地,中间抽头输出可调电压。

接线方式:

  • 电位器左端 → 3.3V
  • 电位器右端 → GND
  • 电位器中端 → GPIO35(ADC1_CH7)

代码跟光敏电阻几乎一样,只是换了个通道:

void setup() {
  Serial.begin(115200);
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_DB_11);
}

void loop() {
  int raw = adc1_get_raw(ADC1_CHANNEL_7);
  float voltage = raw * 3.3 / 4095.0;
  
  Serial.printf("电位器: %d  (%.2fV)\n", raw, voltage);
  delay(200);
}

旋转电位器旋钮,读数会从 0 平滑变化到 4095。这个实验虽然简单,但它是所有模拟输入的基础。我当年做智能家居面板时,就是用电位器模拟用户旋钮操作,配合 PWM 控制灯光亮度。

4.6 ADC 知识体系总览

下面这张图,把本章的核心知识点串在了一起。你可以把它当作一个快速索引:

ESP32 ADC 知识体系 ESP32 ADC 模块 硬件基础 12位分辨率 ADC1(8通道) / ADC2(10通道) Wi-Fi占用ADC2 衰减配置 0dB → 0~1.1V 2.5dB → 0~1.5V 6dB → 0~2.2V 11dB → 0~3.3V 电压转换 V = raw / 4095 × Vmax 非线性误差需校准 两点/三点校准法 光敏电阻实验 电位器实验 传感器扩展 核心原则 ① 优先使用ADC1通道 ② 不确定电压时用11dB衰减 ③ 高精度场景必须做校准 ④ 分压电路注意阻抗匹配

4.7 避坑指南与实用技巧

做 ADC 项目这么多年,我总结了几条经验,分享给你:

  • 输入阻抗问题:ESP32 的 ADC 输入阻抗不高,大约几十 kΩ。如果信号源内阻太大(比如某些高阻传感器),读数会偏小。解决办法是在 ADC 引脚前加一个电压跟随器(运放)。
  • 噪声抑制:ADC 对电源噪声很敏感。我建议在 ADC 引脚对地并联一个 0.1μF 电容,能有效滤除高频干扰。
  • 多次采样取平均:单次采样抖动大,可以连续采 10 次取平均。代码里加个循环就行,不费事。
  • 不要超过 3.3V:即使配置了 11dB 衰减,输入电压也绝对不能超过 3.3V。超过会损坏 GPIO。

我曾经在一个工业项目中,用 ESP32 读取 0~10V 的传感器信号。直接接上去肯定烧引脚。后来加了一个电阻分压网络,把 10V 降到 3.3V 以内,再用 11dB 衰减读取。分压电阻选 10kΩ 和 3.3kΩ,分压比约 3.3/13.3 ≈ 0.248。代码里再乘以这个系数的倒数,就还原出真实电压了。

4.8 完整实验代码参考

下面是一个综合示例,同时读取光敏电阻和电位器,并做了多次采样平均:

#include <driver/adc.h>

#define LIGHT_SENSOR_CHANNEL ADC1_CHANNEL_6  // GPIO34
#define POT_CHANNEL         ADC1_CHANNEL_7  // GPIO35
#define SAMPLE_COUNT        10

void setup() {
  Serial.begin(115200);
  
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(LIGHT_SENSOR_CHANNEL, ADC_ATTEN_DB_11);
  adc1_config_channel_atten(POT_CHANNEL, ADC_ATTEN_DB_11);
}

int read_adc_average(adc1_channel_t channel, int samples) {
  long sum = 0;
  for (int i = 0; i < samples; i++) {
    sum += adc1_get_raw(channel);
    delay(1);
  }
  return sum / samples;
}

void loop() {
  int light_raw = read_adc_average(LIGHT_SENSOR_CHANNEL, SAMPLE_COUNT);
  int pot_raw   = read_adc_average(POT_CHANNEL, SAMPLE_COUNT);
  
  float light_voltage = light_raw * 3.3 / 4095.0;
  float pot_voltage   = pot_raw * 3.3 / 4095.0;
  
  Serial.print("光照: ");
  Serial.print(light_raw);
  Serial.print(" (");
  Serial.print(light_voltage);
  Serial.print("V)  ");
  
  Serial.print("电位器: ");
  Serial.print(pot_raw);
  Serial.print(" (");
  Serial.print(pot_voltage);
  Serial.println("V)");
  
  delay(300);
}

把代码烧录进 ESP32,打开串口监视器,你就能看到两路模拟信号实时变化。用手遮挡光敏电阻,旋转电位器,观察数值的对应关系。这就是模拟输入最直观的体验。