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 | 决定最大输入电压 |
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,后来做了两点校准才解决。
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 知识体系总览
下面这张图,把本章的核心知识点串在了一起。你可以把它当作一个快速索引:
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,打开串口监视器,你就能看到两路模拟信号实时变化。用手遮挡光敏电阻,旋转电位器,观察数值的对应关系。这就是模拟输入最直观的体验。