3、ADC模拟量采集:ESP32 ADC特性、电压读取、光敏电阻与电位器实验
ADC,全称模数转换器。说白了,就是把连续的模拟电压,变成单片机能认的离散数字。
ESP32 的 ADC 跟 Arduino 那种 10 位分辨率不太一样。它有两个 ADC 模块,ADC1 和 ADC2,都是 12 位的。12 位意味着什么?2 的 12 次方,4096 个量化等级。输入电压范围默认是 0 ~ 3.3V,对应的数字量就是 0 ~ 4095。
嗯,这里要注意。ADC2 在 Wi-Fi 开启时会被占用。我早期做项目时踩过这个坑——Wi-Fi 一开,ADC2 的读数全乱套了。所以我的习惯是:只要项目里用了 Wi-Fi,模拟量采集一律走 ADC1。
3.1 ESP32 ADC 硬件特性速览
先看一张表,把关键参数理清楚。
| 参数 | 说明 |
|---|---|
| 分辨率 | 12 位(0 ~ 4095),可通过函数配置为 9~12 位 |
| 输入电压范围 | 默认 0 ~ 3.3V,可衰减至 0 ~ 1.1V(11dB 衰减) |
| ADC1 可用通道 | GPIO32 ~ GPIO39(共 8 个) |
| ADC2 可用通道 | GPIO0、2、4、12 ~ 15、25 ~ 27(共 10 个) |
| 采样速率 | 理论最高 200 ksps,实际建议 1 kHz 以内 |
| 非线性误差 | 约 ±2 LSB,首尾两端非线性较明显 |
为什么我要强调首尾两端非线性?因为 ESP32 的 ADC 在接近 0V 和接近 3.3V 时,读数会飘。我曾经用万用表量着 3.28V,ADC 读数却已经到 4095 了。所以实际项目中,我一般只使用 0.1V ~ 3.2V 这个区间,两头各留一点余量。
3.2 核心 API 与配置流程
ESP-IDF 里操作 ADC,主要用 adc1_config_width() 和 adc1_config_channel_atten() 这两个函数。配置流程其实就三步:
- 设置分辨率:调用
adc1_config_width(ADC_WIDTH_BIT_12),告诉硬件我要 12 位精度。 - 配置衰减:调用
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11),设置通道和衰减系数。 - 读取数值:循环里调用
adc1_get_raw(ADC1_CHANNEL_6),拿到原始数字量。
衰减系数这块,我多说两句。ESP32 提供了四种衰减档位:
| 衰减设置 | 可测电压范围 | 典型场景 |
|---|---|---|
| 0 dB | 0 ~ 1.1V | 高精度小信号 |
| 2.5 dB | 0 ~ 1.5V | 传感器输出 1.2V 左右 |
| 6 dB | 0 ~ 2.2V | 中间档位,不常用 |
| 11 dB | 0 ~ 3.3V | 最常用,覆盖全量程 |
我个人习惯,只要不确定输入电压范围,一律用 11 dB 衰减。虽然分辨率会稍微损失一点,但至少不会烧引脚。
3.3 实战:读取电位器电压
电位器是最简单的模拟量输入设备。三个引脚:两端接 3.3V 和 GND,中间抽头接 GPIO34(ADC1_CHANNEL6)。
来看代码:
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/adc.h>
void app_main(void)
{
// 1. 配置 ADC1 为 12 位分辨率
adc1_config_width(ADC_WIDTH_BIT_12);
// 2. 配置通道 6(GPIO34),11dB 衰减,量程 0~3.3V
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
while (1) {
// 3. 读取原始值
int raw = adc1_get_raw(ADC1_CHANNEL_6);
// 4. 换算为电压(mV)
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, ADC_WIDTH_BIT_12, ADC_ATTEN_DB_11);
printf("Raw: %4d, Voltage: %3d mV\n", raw, voltage);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
这里有个细节。早期 ESP-IDF 版本里,esp_adc_cal_raw_to_voltage() 这个函数需要先调用 esp_adc_cal_characterize() 做校准。但从 v4.4 开始,内部已经集成了查表逻辑,直接传参就能用。如果你用的是旧版本,记得加上校准步骤。
3.4 光敏电阻实验:感知环境光强
光敏电阻,也叫光敏电阻器(LDR)。它的阻值随光照强度变化——光照越强,阻值越低。
实际电路里,光敏电阻不能直接接 ADC。需要搭一个分压电路:光敏电阻和固定电阻(比如 10kΩ)串联,中间抽头接 ADC 引脚。
电路连接很简单:
- 3.3V → 光敏电阻一端
- 光敏电阻另一端 → GPIO35(ADC1_CHANNEL7)→ 10kΩ 电阻 → GND
代码跟电位器实验几乎一样,只是换了个通道:
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/adc.h>
void app_main(void)
{
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_DB_11);
while (1) {
int raw = adc1_get_raw(ADC1_CHANNEL_7);
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, ADC_WIDTH_BIT_12, ADC_ATTEN_DB_11);
// 粗略换算:电压越高,光照越强
float light_percent = (float)voltage / 3300.0 * 100.0;
printf("Raw: %4d, Voltage: %3d mV, Light: %.1f%%\n", raw, voltage, light_percent);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
为什么电压越高光照越强?因为光敏电阻受光照后阻值下降,分到的电压就低了。但我们的分压电路里,光敏电阻在上端,固定电阻在下端。光敏电阻阻值下降,意味着它分到的电压减少,固定电阻分到的电压增加——ADC 读到的就是固定电阻的电压。所以电压高,代表光照强。
3.5 数据滤波:让读数更可靠
ADC 原始数据里,噪声是不可避免的。电源纹波、电磁干扰、甚至手指靠近引脚都会影响读数。我常用的滤波方法有两种:
- 均值滤波:连续采样 N 次,取平均值。N 一般取 5~20,效果立竿见影。
- 中值滤波:连续采样 N 次,排序后取中间值。对脉冲噪声特别有效。
来看一个简单的均值滤波实现:
#define SAMPLE_COUNT 10
int adc_read_filtered(adc1_channel_t channel)
{
int sum = 0;
for (int i = 0; i < SAMPLE_COUNT; i++) {
sum += adc1_get_raw(channel);
}
return sum / SAMPLE_COUNT;
}
就这么几行代码,能把噪声从 ±15 LSB 压到 ±3 LSB 以内。性价比极高。
3.6 本章知识结构图
下面这张图,把本章的核心知识点串起来了。从硬件特性到 API 配置,再到两个实战案例和滤波方法,一目了然。
3.7 几个容易踩的坑
最后,我把这些年做 ADC 采集踩过的坑列出来,你遇到了可以直接对照排查。
- 引脚悬空读数飘忽:ADC 引脚不接任何东西时,读数会在 0~4095 之间乱跳。这不是硬件坏了,是正常现象。接上传感器就好了。
- 分压电阻选太大:光敏电阻实验里,如果分压电阻选 100kΩ,ADC 输入阻抗不够,读数会偏低。我一般用 10kΩ 或 4.7kΩ。
- Wi-Fi 干扰 ADC2:这个前面说过了。只要开了 Wi-Fi,ADC2 的通道就别用了。别问我怎么知道的——问就是烧过一版 PCB。
- 电压换算公式别搞错:
电压 = (raw / 4095.0) * 3.3这个公式只适用于线性度好的区间。如果用了 11dB 衰减,建议用esp_adc_cal_raw_to_voltage()查表换算,更准。
本章核心要点:
- ESP32 ADC 是 12 位,0~4095 对应 0~3.3V(11dB 衰减)
- ADC1 和 ADC2 要分清,Wi-Fi 项目只用 ADC1
- 电位器和光敏电阻都是分压电路,代码结构几乎一样
- 数据滤波是 ADC 采集的标配,均值滤波最简单实用