4. 模拟信号采集:ADC原理、ESP32 ADC特性、电位器电压读取、光敏电阻实验

好,咱们进入第四章。前面几章我们一直在跟数字信号打交道——GPIO要么高要么低,I2C和SPI传的也是0和1的组合。但现实世界不是非黑即白的。温度、光照、音量,这些都是连续变化的物理量。怎么让ESP32理解这些“模拟”信号?答案就是ADC——模数转换器。

4.1 ADC基本原理:从连续到离散

ADC,全称Analog-to-Digital Converter,说白了就是把连续变化的电压,映射成一组离散的数字值。你想想看,ESP32的CPU只能处理0和1,它不认识2.5V或者1.8V。ADC就是那个“翻译官”。

我刚开始做嵌入式时,总觉得ADC很神秘。后来自己用分立元件搭过一个最简单的“闪速ADC”,才明白原理其实不复杂。常见的ADC工作原理有几种:

类型 速度 精度 典型应用
逐次逼近型(SAR) 中速 中高 MCU内置ADC(ESP32用的就是这种)
积分型(双斜率) 慢 高 万用表、精密测量
闪速型(Flash) 极快 低 高速示波器、射频
Σ-Δ型 慢 极高 音频、高精度传感器

关键参数就两个:分辨率和参考电压。分辨率决定你能把电压分成多少份。比如12位ADC,就是2^12=4096份。参考电压决定测量范围。ESP32的ADC是12位的,参考电压默认是0~3.3V。所以每个LSB(最低有效位)对应的电压是3.3V / 4096 ≈ 0.8mV。

核心公式:

测量电压 = (ADC读数 / 4096) × 参考电压

反过来:ADC读数 = (测量电压 / 参考电压) × 4096

4.2 ESP32 ADC特性:别踩这些坑

ESP32的ADC,嗯,这里要重点说一下。它跟STM32或者Arduino的ADC不太一样,有几个“脾气”你得摸清楚。我在项目里吃过亏,所以把这些坑提前告诉你。

4.2.1 非线性问题

ESP32的ADC在0~1V和2.5~3.3V这两个区间,线性度还不错。但在中间1V~2.5V这段,误差可能达到2%~3%。什么意思?你测2V,读出来可能是1.94V或者2.06V。对于精度要求不高的场景(比如判断光照强弱),这没问题。但如果你要做精密测量,就得做校准。

我曾经踩过的坑: 有个项目用ESP32读压力传感器,满量程5V,分压后进ADC。结果数据波动很大,查了两天才发现是ADC中间段非线性导致的。后来加了分段校准表才解决。

4.2.2 衰减器配置

ESP32的ADC默认只能测0~1.1V。你没看错,是1.1V。要测0~3.3V,必须开启衰减器。衰减器有4档:

衰减配置 测量范围 推荐场景
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.2.3 引脚选择

ESP32不是所有引脚都能做ADC。只有ADC1和ADC2通道的引脚才行。而且ADC2在Wi-Fi开启时会被占用,所以我的习惯是——只用ADC1的引脚。省心。

  • ADC1通道: GPIO32~GPIO39
  • ADC2通道: GPIO0、GPIO2、GPIO4、GPIO12~GPIO15、GPIO25~GPIO27

个人建议: 如果你同时用Wi-Fi和ADC,老老实实走ADC1。ADC2在Wi-Fi发包时读数会乱跳,我因为这个被测试同事骂过。

4.3 电位器电压读取:第一个ADC实验

咱们先拿电位器练手。电位器本质上就是一个可调分压器。拧旋钮,中间抽头的电压就在0~3.3V之间变化。电路连接很简单:

  • 电位器一脚接3.3V
  • 另一脚接GND
  • 中间脚接GPIO34(ADC1_CH6)

代码怎么写?看下面:

#include <driver/adc.h>
#include <esp_adc_cal.h>

#define ADC_PIN    ADC1_CHANNEL_6  // GPIO34
#define ADC_WIDTH  ADC_WIDTH_BIT_12
#define ADC_ATTEN  ADC_ATTEN_DB_11

void app_main(void)
{
    // 1. 配置ADC
    adc1_config_width(ADC_WIDTH);
    adc1_config_channel_atten(ADC_PIN, ADC_ATTEN);

    // 2. 读取原始值
    int raw = adc1_get_raw(ADC_PIN);
    printf("Raw ADC value: %d\n", raw);

    // 3. 换算电压(粗略)
    float voltage = raw * (3.3f / 4096.0f);
    printf("Voltage: %.2f V\n", voltage);
}

拧电位器,串口打印的数值应该在0~4095之间变化,电压在0~3.3V之间。如果数值跳变厉害,检查一下接线,或者加个100nF电容滤波。

4.4 光敏电阻实验:感知环境光

光敏电阻,也叫光依赖电阻(LDR)。它的特性很简单:光越强,电阻越小;光越暗,电阻越大。但你不能直接把光敏电阻接到ESP32的ADC引脚上。为什么?因为ADC测的是电压,不是电阻。你需要一个分压电路。

4.4.1 电路设计

我常用的电路是这样的:光敏电阻和固定电阻(比如10kΩ)串联,中间抽头接ADC引脚。固定电阻接GND,光敏电阻接3.3V。这样光照强时,光敏电阻变小,抽头电压升高;光照弱时,光敏电阻变大,抽头电压降低。

电路参数:

  • VCC = 3.3V
  • R_fixed = 10kΩ(推荐,跟光敏电阻暗阻/亮阻匹配)
  • V_adc = VCC × R_fixed / (R_ldr + R_fixed)

4.4.2 代码实现

跟电位器实验类似,但这次我们要把电压换算成光照强度。不过说实话,光敏电阻的阻值-光照曲线不是线性的,所以一般不做精确的勒克斯(lux)换算。我的做法是:直接拿ADC原始值做阈值判断。

#include <driver/adc.h>

#define LDR_ADC_CHANNEL ADC1_CHANNEL_7  // GPIO35

void app_main(void)
{
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(LDR_ADC_CHANNEL, ADC_ATTEN_DB_11);

    while (1) {
        int ldr_raw = adc1_get_raw(LDR_ADC_CHANNEL);
        
        // 原始值越大,光照越强
        if (ldr_raw > 3000) {
            printf("☀️ 强光环境 (raw=%d)\n", ldr_raw);
        } else if (ldr_raw > 1500) {
            printf("🌤 正常光照 (raw=%d)\n", ldr_raw);
        } else if (ldr_raw > 500) {
            printf("🌥 较暗 (raw=%d)\n", ldr_raw);
        } else {
            printf("🌙 黑暗 (raw=%d)\n", ldr_raw);
        }

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

避坑指南: 光敏电阻的响应速度很慢,大概几十毫秒。所以采样间隔别太短,100ms以上比较稳。我曾经为了省时间设了10ms采样,结果数据抖得像心电图。

4.5 本章知识体系

下面这张图把本章的核心逻辑串起来了。从物理世界的模拟信号,到ESP32的ADC采样,再到软件层面的数据处理和应用,一条线走通。

模拟信号采集知识体系 物理信号源 电位器 / 光敏电阻 分压电路 电压信号 0~3.3V ESP32 ADC 12位 / 衰减器配置 驱动配置 adc1_config_width 数据读取 adc1_get_raw 数值换算 电压 / 光照等级 应用场景 智能灯光控制 / 环境监测 / 旋钮输入 ⚠️ 注意:ADC2在Wi-Fi开启时不可用,建议全部使用ADC1通道(GPIO32~GPIO39) 衰减器必须配置为ADC_ATTEN_DB_11才能测量0~3.3V范围

4.6 小结与经验

这一章我们走通了模拟信号采集的完整链路。从ADC原理,到ESP32的硬件特性,再到电位器和光敏电阻两个实战实验。说白了,核心就三件事:配置衰减器、读原始值、换算物理量。

最后分享一个经验:ADC读数一定要做滤波。最简单的办法是连续读5次取平均。我见过太多人直接拿单次读数做判断,结果系统忽明忽暗、忽快忽慢。加个滑动平均滤波,效果立竿见影。

本章核心要点:

  • ADC把连续电压映射成离散数字值,ESP32是12位、0~4095
  • 衰减器必须配ADC_ATTEN_DB_11才能测0~3.3V
  • ADC1和ADC2要分清,Wi-Fi开启时只用ADC1
  • 电位器直接分压,光敏电阻需要固定电阻配合
  • 读数滤波是必须的,别偷懒