4. ADC 模拟量采集:片上 ADC 驱动,光敏电阻与电位器读数
ADC,全称模数转换器。说白了,就是把连续变化的电压,变成一串离散的数字。ESP32 内部集成了两个 12 位的 SAR ADC,精度不算顶级,但做传感器读取、电位器旋钮、电池电压监测,完全够用。
我最早用 ESP32 做 ADC 时,踩过一个坑——读出来的数值跳得厉害,以为是硬件坏了。后来才发现,是没做软件滤波。嗯,这节课咱们就把 ADC 的底细摸清楚。
4.1 ESP32 的 ADC 硬件特性
ESP32 的 ADC 模块有几个关键参数,我习惯先列个表,心里有数再写代码。
| 参数 | 值 | 说明 |
|---|---|---|
| 分辨率 | 12 位 | 0~4095 的数字范围 |
| ADC 数量 | 2 个(ADC1、ADC2) | ADC1 有 8 个通道,ADC2 有 10 个 |
| 输入电压范围 | 0 ~ 3.3V | 超过会烧引脚,切记 |
| 采样速率 | 最高约 200 ksps | 实际受 Wi-Fi 影响会降速 |
| 衰减器(Attenuation) | 0dB / 2.5dB / 6dB / 11dB | 决定可测的最大电压 |
4.2 衰减器(Attenuation)怎么选?
衰减器决定了 ADC 能测量的最大电压。ESP32 的 ADC 默认只能测 0~1.1V,超过这个范围,必须配置衰减器。
我个人常用的配置是 ADC_ATTEN_DB_11,它能覆盖 0~3.3V 的满量程。你想想看,如果只配了 6dB,最大只能测到 2.2V,接个 3.3V 的光敏电阻分压电路,读数就会饱和在 4095,完全失去意义。
| 衰减配置 | 可测电压范围 | 适用场景 |
|---|---|---|
| ADC_ATTEN_DB_0 | 0 ~ 1.1V | 低电压传感器 |
| ADC_ATTEN_DB_2_5 | 0 ~ 1.5V | 窄范围信号 |
| ADC_ATTEN_DB_6 | 0 ~ 2.2V | 中等电压 |
| ADC_ATTEN_DB_11 | 0 ~ 3.3V | 通用满量程 |
4.3 硬件连接:光敏电阻与电位器
咱们这节课做两个实验:光敏电阻测光照,电位器测角度。电路都非常简单。
光敏电阻电路
光敏电阻和 10kΩ 电阻串联,组成分压电路。光敏电阻一端接 3.3V,另一端接 ADC 引脚和 10kΩ 电阻到 GND。光照越强,光敏电阻阻值越小,ADC 引脚电压越高。
电位器电路
电位器三个引脚:两端分别接 3.3V 和 GND,中间抽头接 ADC 引脚。旋转电位器,抽头电压在 0~3.3V 之间变化。
4.4 代码实现:单次读取
先写一个最简单的单次读取函数。我用的是 ESP-IDF 的驱动库,Arduino 用户也可以参考逻辑。
#include "esp_adc/adc_oneshot.h"
adc_oneshot_unit_handle_t adc_handle;
void adc_init(void)
{
// 1. 创建 ADC 单元
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_new_unit(&init_cfg, &adc_handle);
// 2. 配置通道
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_12,
};
adc_oneshot_config_channel(adc_handle, ADC_CHANNEL_0, &chan_cfg);
}
int adc_read(void)
{
int raw_value;
adc_oneshot_read(adc_handle, ADC_CHANNEL_0, &raw_value);
return raw_value;
}
这段代码做了三件事:创建 ADC 单元、配置通道衰减和位宽、读取原始值。原始值范围 0~4095,对应 0~3.3V。
4.5 软件滤波:让读数不再乱跳
直接读出来的数值,波动很大。为什么?因为环境噪声、电源纹波、甚至手指靠近引脚都会影响。我做过测试,同一光照下,原始读数能跳 ±50 个码值。
解决办法是软件滤波。我推荐两种方法:
- 均值滤波: 连续采样 N 次,取平均值。N 一般取 10~50。
- 中值滤波: 连续采样 N 次,排序后取中间值。对尖峰脉冲噪声特别有效。
int adc_read_filtered(int samples)
{
int sum = 0;
for (int i = 0; i < samples; i++) {
sum += adc_read();
vTaskDelay(pdMS_TO_TICKS(2)); // 每次采样间隔 2ms
}
return sum / samples;
}
我曾经在一个光照传感器项目里,用了 20 次均值滤波,读数稳定度从 ±5% 降到了 ±0.5%。效果立竿见影。
4.6 电压换算:从原始值到真实电压
原始值 2048 对应多少伏?公式很简单:
电压 = (原始值 / 4095.0) × 3.3
但注意,这个 3.3V 是理论值。实际 ESP32 的参考电压会有偏差,不同芯片之间可能差 0.1V 左右。如果你需要高精度,建议用万用表测一下板子的 3.3V 实际值,代入公式。
float adc_to_voltage(int raw_value)
{
return (raw_value / 4095.0f) * 3.3f;
}
4.7 知识体系:ADC 采集全流程
下面这张图,是我自己画的一个 ADC 采集流程。从硬件信号到最终的应用数据,每一步都绕不开。
4.8 完整示例:光敏电阻 + 电位器双通道
最后,给一个完整的双通道读取示例。ADC1 的通道 0 接光敏电阻,通道 3 接电位器。每 500ms 打印一次电压值。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_adc/adc_oneshot.h"
adc_oneshot_unit_handle_t adc1_handle;
void app_main(void)
{
// 初始化 ADC1
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_new_unit(&init_cfg, &adc1_handle);
// 配置通道 0(光敏电阻)
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_12,
};
adc_oneshot_config_channel(adc1_handle, ADC_CHANNEL_0, &chan_cfg);
// 配置通道 3(电位器)
adc_oneshot_config_channel(adc1_handle, ADC_CHANNEL_3, &chan_cfg);
int raw0, raw3;
float volt0, volt3;
while (1) {
// 读取光敏电阻
adc_oneshot_read(adc1_handle, ADC_CHANNEL_0, &raw0);
volt0 = (raw0 / 4095.0f) * 3.3f;
// 读取电位器
adc_oneshot_read(adc1_handle, ADC_CHANNEL_3, &raw3);
volt3 = (raw3 / 4095.0f) * 3.3f;
printf("光敏: %d (%.2fV) | 电位器: %d (%.2fV)\n",
raw0, volt0, raw3, volt3);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
- ADC 初始化分两步:创建单元 → 配置通道。
- 衰减器选 11dB,覆盖 0~3.3V 满量程。
- 软件滤波是稳定读数的关键,别偷懒。
- Wi-Fi 开启时只用 ADC1,别碰 ADC2。
好了,ADC 的基础驱动就这些。你可以在面包板上搭个光敏电阻和电位器,跑一下上面的代码,看看串口打印的数值变化。嗯,动手试过,才算真正掌握。