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 决定可测的最大电压
⚠️ 重要提醒: ADC2 在 Wi-Fi 开启时会被占用。如果你同时用 Wi-Fi,请只用 ADC1 的通道。我有个项目因为这个排查了两天,最后发现是 ADC2 和 Wi-Fi 抢资源。

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 之间变化。

💡 我的习惯: 在 ADC 引脚和 GND 之间并联一个 0.1μF 的电容。它能滤掉高频噪声,让读数更稳定。这个做法在工业现场特别管用。

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 采集流程。从硬件信号到最终的应用数据,每一步都绕不开。

ADC 采集全流程 模拟信号输入 衰减器配置 ADC 采样 原始值 0~4095 软件滤波 电压换算 应用数据输出 关键说明: 1. 衰减器决定量程,11dB 覆盖 0~3.3V,最常用。 2. 软件滤波是必选项,均值滤波最简单,中值滤波抗尖峰。 3. 电压换算用实际参考电压,不要迷信理论值 3.3V。

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 的基础驱动就这些。你可以在面包板上搭个光敏电阻和电位器,跑一下上面的代码,看看串口打印的数值变化。嗯,动手试过,才算真正掌握。