4. ADC 模拟量采集:从原理到实战

ADC,全称模数转换器。说白了,就是把连续变化的电压,变成一串离散的数字。我们生活的世界是模拟的——温度、光照、声音,都是连续量。但 ESP32 是数字芯片,它只认 0 和 1。ADC 就是连接这两个世界的桥梁。

我个人习惯把 ADC 比作一把「电压尺子」。尺子上的刻度越密,量出来的值就越准。这把尺子的总长度,就是 ADC 的参考电压;刻度的数量,就是 ADC 的分辨率。

4.1 ADC 工作原理:一把数字尺子

ADC 的核心思想很简单:比较。它内部有一个比较器,不断把输入电压和一个内部生成的参考电压做对比。最常见的逐次逼近型(SAR)ADC,工作方式有点像「猜数字」——先猜中间值,再根据反馈往高或往低调整,直到逼近真实值。

为什么我要强调这个?因为理解了这个过程,你才能明白采样率、分辨率这些参数到底在说什么。

参数 含义 对实际的影响
分辨率 ADC 能区分的最小电压变化 12位 ADC 可区分 4096 个等级,精度约 0.8mV(3.3V 参考)
采样率 每秒能完成的转换次数 ESP32 最高约 200ksps,采集音频够用,采集射频信号不够
参考电压 ADC 量程的上限 ESP32 内部参考约 1.1V,外部可接至 3.3V
量化误差 数字值与真实值的偏差 分辨率越低,误差越大;12位下误差约 ±0.5 LSB
我的经验: 很多初学者只看分辨率,忽略了参考电压的稳定性。我在一个项目中用过 ESP32 的内部参考,结果电池电压一波动,采集值就飘。后来换成外部基准电压源,问题才解决。记住,ADC 的精度上限,由参考电压的稳定性决定。

4.2 ESP32 ADC 特性:没那么简单

ESP32 的 ADC 有点特殊。它有两个 ADC 模块——ADC1 和 ADC2。ADC1 有 8 个通道,ADC2 有 10 个通道。但 ADC2 会被 Wi-Fi 占用,所以做传感器采集时,我建议你只用 ADC1。

为什么会这样?因为 ESP32 的 Wi-Fi 模块和 ADC2 共用了一些硬件资源。一旦 Wi-Fi 开启,ADC2 的读数就会乱跳。我曾经踩过这个坑,调试了整整一个下午才找到原因。

另外,ESP32 的 ADC 还有一个「非线性」的问题。在 0~1.1V 范围内还算线性,但超过 1.1V 后,曲线就开始弯曲。乐鑫官方提供了衰减选项(attenuation),可以扩展量程到 3.3V,但代价是精度下降。

衰减设置 量程范围 适用场景
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 常用,兼容 3.3V 传感器
注意: ESP32 的 ADC 输入电压不能超过 3.3V!超过会烧坏引脚。如果你要测量更高电压,必须使用分压电阻。我曾经见过有人直接把 5V 接到 ADC 引脚,结果芯片直接冒烟。

4.3 模拟电压读取:动手写代码

好了,理论说够了,我们直接上手。下面这段代码,是我在多个项目中反复使用的 ADC 读取模板。它做了两件事:配置 ADC 参数,然后循环读取电压值。

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

// 定义 ADC 通道和衰减
#define ADC_CHANNEL ADC1_CHANNEL_0  // GPIO36
#define ADC_ATTEN   ADC_ATTEN_DB_11 // 0~3.3V

// 校准结构体
static esp_adc_cal_characteristics_t adc_chars;

void adc_init(void) {
    // 1. 配置 ADC 宽度(分辨率)
    adc1_config_width(ADC_WIDTH_BIT_12);  // 12位分辨率

    // 2. 配置衰减
    adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);

    // 3. 校准 ADC(获取 Vref 值)
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN, 
                             ADC_WIDTH_BIT_12, 1100, &adc_chars);
}

uint32_t adc_read_mv(void) {
    // 读取原始值
    int raw = adc1_get_raw(ADC_CHANNEL);

    // 转换为毫伏
    uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);

    return voltage;
}

void app_main(void) {
    adc_init();

    while (1) {
        uint32_t mv = adc_read_mv();
        printf("ADC Raw: %d, Voltage: %d mV\n", 
               adc1_get_raw(ADC_CHANNEL), mv);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

这段代码里,我特别想强调校准那一步。很多教程会忽略 esp_adc_cal_characterize,直接用原始值换算。但 ESP32 的 ADC 出厂时每个芯片的 Vref 都不一样,不校准的话误差可能达到 10% 以上。

核心要点: 永远使用校准函数!esp_adc_cal_raw_to_voltage 会自动补偿芯片间的差异,把原始值转换成真实的毫伏数。这是我在几十块 ESP32 上验证过的经验。

4.4 光敏电阻实验:感知光线

理论学完了,代码也写了,我们来做个实际的东西。光敏电阻(LDR)是最简单的模拟传感器——光照越强,电阻越小。配合一个固定电阻组成分压电路,就能把电阻变化变成电压变化。

电路连接很简单:

  • 光敏电阻一端接 3.3V
  • 另一端串联一个 10kΩ 电阻到 GND
  • 光敏电阻和 10kΩ 的中间点,接到 ESP32 的 GPIO36(ADC1_CH0)

为什么选 10kΩ?我试过 1kΩ、4.7kΩ、10kΩ、47kΩ。10kΩ 在普通室内光照下,分压点电压在 1.5V~2.5V 之间,正好落在 ADC 量程的中段,线性度最好。你想想看,如果选太小,暗光下电压太低;选太大,强光下电压饱和。10kΩ 是个折中。

// 光敏电阻读取示例
void light_sensor_task(void *pvParameters) {
    adc_init();

    while (1) {
        uint32_t mv = adc_read_mv();

        // 将电压映射到光照等级(0~100%)
        // 3.3V 对应最亮,0V 对应最暗
        float light_percent = (mv / 3300.0) * 100.0;

        printf("Light: %.1f%% (Voltage: %d mV)\n", 
               light_percent, mv);

        // 根据光照做决策
        if (light_percent < 20.0) {
            printf("-> 太暗了,开灯!\n");
        } else if (light_percent > 80.0) {
            printf("-> 太亮了,拉窗帘!\n");
        }

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

这段代码里,我把电压直接映射成百分比。虽然不精确,但做智能家居的阈值判断足够了。如果你需要精确的勒克斯(lux)值,那就得查光敏电阻的数据手册,做一条电压-光照的校准曲线。

避坑指南: 我曾经在光敏电阻实验中犯过一个低级错误——把 ADC 引脚悬空了。悬空时 ADC 会读到随机值,程序以为光照在剧烈变化,疯狂触发开关。后来我加了一个 100nF 的电容到 GND,滤掉了高频噪声,读数才稳定下来。

4.5 知识体系总览

下面这张图,是我自己总结的 ADC 知识体系。每次做新项目前,我都会对照着检查一遍,确保没有遗漏。

ADC 模拟量采集知识体系 ADC 工作原理 ESP32 ADC 硬件特性 ADC1 vs ADC2 选择 衰减与量程配置 非线性与校准 软件驱动实现 初始化与配置 原始值读取 电压校准与转换 实战:光敏电阻 分压电路设计 电压→光照映射 阈值判断与滤波 输出:稳定的模拟量采集值

这张图把 ADC 知识分成了三个维度:硬件特性、软件实现、实战应用。你不需要一次性全记住,但每次遇到 ADC 相关的问题,都可以回来对照一下,看看自己卡在了哪个环节。

嗯,这一章的内容就到这里。ADC 是嵌入式开发的基础功,也是传感器驱动的核心。把这一章吃透了,后面学温度传感器、湿度传感器、气体传感器,都会轻松很多。