4、ADC采样与电压测量:ESP32 ADC特性(12位分辨率、衰减器设置)、分压电路设计、电压采集代码实现

各位同学,咱们今天来聊聊ADC采样和电压测量。说实话,这个知识点在智能家居项目里太常用了——你要监测电池电压、读取传感器输出、或者检测电源状态,都离不开它。ESP32的ADC模块我用了好几年,踩过不少坑,今天把这些经验都掏给你们。

4.1 ESP32 ADC的硬件特性

ESP32内置了两个12位的逐次逼近型ADC,分别叫ADC1和ADC2。12位分辨率意味着什么?说白了,就是把0~3.3V的电压范围,分成2^12=4096个台阶。每个台阶对应的电压是3.3V/4096≈0.8mV。嗯,这个精度对于大多数家用传感器来说,完全够用了。

但有个坑我必须先说——ADC2在Wi-Fi开启时会被占用。我在项目中遇到过,明明ADC2的引脚接好了传感器,一开Wi-Fi读数全乱套。后来查手册才发现,Wi-Fi驱动会抢占ADC2的控制权。所以我的习惯是:能用ADC1就别用ADC2。ADC1的8个通道(GPIO32~GPIO39)是独立工作的,不受Wi-Fi影响。

⚠️ 重要提醒: ESP32的ADC输入电压绝对不能超过3.3V!超过会烧坏芯片。如果你要测量更高的电压,必须用分压电路。

4.2 衰减器设置(Attenuation)

ESP32的ADC有个很实用的功能——衰减器。默认情况下(不设置衰减),ADC的测量范围是0~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 最常用,推荐使用

我个人习惯直接开ADC_ATTEN_DB_11,省事。但要注意,衰减越大,精度会稍微下降。如果你需要测量非常微弱的信号(比如光敏电阻),可以考虑用DB_0档位,配合外部运放放大信号。

💡 小技巧: 衰减器设置后,ESP32的ADC读数并不是完全线性的。我建议你在代码里做一次简单的校准——用万用表测一个已知电压,然后对比ADC读数,算出修正系数。

4.3 分压电路设计

好,现在问题来了:你要测量一个12V的电池电压,但ESP32只能承受3.3V,怎么办?答案就是分压电路。

分压电路说白了就是两个电阻串联。输入电压加在串联电阻两端,从中间抽头取电压。公式很简单:

Vout = Vin × R2 / (R1 + R2)

举个例子,我要测12V电池,想让Vout在3.3V以内。取R1=10kΩ,R2=3.3kΩ,那么:

Vout_max = 12V × 3.3 / (10 + 3.3) ≈ 2.98V

嗯,留了点余量,安全。实际项目中我还会加一个5.1V的齐纳二极管做钳位保护——万一电阻焊错了,也不至于烧ADC引脚。

🔧 避坑指南: 我曾经在分压电路上吃过亏——用了1kΩ和330Ω的电阻,结果电阻发热严重,读数还飘。后来才意识到,分压电阻的阻值不能太小,否则功耗太大。建议总阻值在10kΩ~100kΩ之间,既省电又稳定。

另外,如果你要测量交流电(比如220V市电),千万别直接分压!必须用电压互感器隔离。这个后面讲功率测量时会详细说。

4.4 电压采集代码实现

理论讲完了,咱们直接上代码。这是我在智能家居项目中反复使用的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
#define ADC_WIDTH ADC_WIDTH_BIT_12

// 校准用参数
static esp_adc_cal_characteristics_t adc_chars;

void adc_init(void) {
    // 1. 配置ADC宽度和衰减
    adc1_config_width(ADC_WIDTH);
    adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
    
    // 2. 初始化校准特性
    esp_adc_cal_characterize(
        ADC_UNIT_1, 
        ADC_ATTEN, 
        ADC_WIDTH, 
        0,  // 默认Vref,实际可用efuse值
        &adc_chars
    );
    
    printf("ADC初始化完成\n");
}

uint32_t read_adc_voltage(void) {
    // 读取原始ADC值(0~4095)
    int raw = adc1_get_raw(ADC_CHANNEL);
    
    // 转换为毫伏电压
    uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
    
    return voltage_mv;
}

void app_main(void) {
    adc_init();
    
    while(1) {
        uint32_t mv = read_adc_voltage();
        // 如果是分压后的电压,需要还原真实电压
        // 假设分压比是10k:3.3k,还原系数 = (10+3.3)/3.3 ≈ 4.03
        float real_voltage = mv * 4.03 / 1000.0;
        
        printf("ADC原始值: %d, 测量电压: %dmV, 真实电压: %.2fV\n", 
               adc1_get_raw(ADC_CHANNEL), mv, real_voltage);
        
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

这段代码有几个关键点:

  • 使用校准函数:esp_adc_cal_raw_to_voltage() 会根据芯片内部的efuse值做校准,比手动计算准得多。我对比过,校准后误差能控制在±2%以内。
  • 分压还原:代码里我写死了4.03的系数,实际项目中建议用宏定义或者从配置文件中读取,方便调试。
  • 采样间隔:我设了1秒读一次,对于电压监测足够了。如果你要测快速变化的信号,可以缩短到10ms,但记得做软件滤波。
💡 滤波小技巧: 我习惯用滑动平均滤波——连续采10次,去掉最大最小值,再取平均。这样能有效抑制噪声,又不增加太多延迟。代码就几行,你们可以自己加上。

4.5 实际项目中的注意事项

最后再唠叨几点我在项目中踩过的坑:

  1. 参考电压不稳定:ESP32的ADC参考电压是芯片内部的,会随温度漂移。如果你需要高精度测量,建议用外部基准电压源(比如TL431)。
  2. 引脚阻抗问题:ADC引脚的输入阻抗不高,如果信号源内阻太大(比如超过10kΩ),读数会偏低。我一般会在ADC引脚前加一个电压跟随器(运放)。
  3. 多通道切换:如果你要用多个ADC通道,切换后要等一小段时间(约10μs)再读数,让采样电容稳定下来。
  4. 数字电源噪声:ESP32的ADC对数字噪声很敏感。我习惯在ADC引脚附近加一个0.1μF的滤波电容,效果立竿见影。

好了,ADC采样这部分就讲到这里。下节课咱们会把这些知识用到实际项目中——做一个电池电压监测模块。你们先把代码跑通,有问题随时问我。