3、模拟信号采集:ADC原理,ESP32的12位ADC特性,光敏电阻与电位计数据读取,电压换算公式

好,咱们进入第三讲。这一讲要聊的是模拟信号采集。说白了,就是让ESP32能“听”懂自然界里连续变化的物理量,比如光线强弱、旋钮转了多少度。

数字芯片只能认0和1,但现实世界是连续的。怎么把连续信号变成数字?这就得靠ADC——模数转换器。我个人习惯把ADC比作一把“数字尺子”,它把电压范围分成很多小格,然后告诉你当前电压在第几格。

3.1 ADC基本原理:从模拟到数字的那一步

ADC的原理,其实没那么玄乎。它内部有个比较器,不断跟参考电压做对比。最常见的逐次逼近型ADC,就像你猜一个数字,先猜中间值,再根据反馈往高或往低调整,一步步逼近真实值。

关键参数有几个:

  • 分辨率:比如12位,就是2^12=4096个刻度。分辨率越高,尺子上的格子越细。
  • 参考电压:ADC能测量的最大电压。ESP32默认是0~3.3V。
  • 采样率:每秒能转换多少次。ESP32的ADC采样率最高能到几MHz,但实际用的时候别跑满,容易不稳定。

核心公式:

数字值 = (模拟电压 / 参考电压) × 最大分辨率

反过来:模拟电压 = (数字值 / 最大分辨率) × 参考电压

举个例子,如果读到ADC值是2048,参考电压3.3V,分辨率12位(4096),那实际电压就是 (2048/4096)×3.3 = 1.65V。嗯,正好一半。

3.2 ESP32的12位ADC特性:别被坑了

ESP32的ADC是12位的,理论上能输出0~4095。但我要提醒你,实际用起来有几个坑,我踩过不止一次。

第一个坑:非线性

ESP32的ADC在低电压段(0~0.1V)和高电压段(3.2~3.3V)表现不太好。中间段(0.2V~3.0V)相对线性。我在项目中遇到过,用ADC测电池电压,结果低电量时读数飘得厉害,后来加了分压电阻把测量范围压缩到0.5V~2.8V才解决。

第二个坑:衰减模式

ESP32的ADC有0dB、2.5dB、6dB、11dB四种衰减模式。默认是0dB,测量范围0~1.1V。如果你要测3.3V,必须设置11dB衰减。代码里这样写:

#include <driver/adc.h>

// 配置ADC1通道0,11dB衰减,测量范围0~3.3V
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);

第三个坑:噪声

ESP32的ADC噪声比较大,尤其是WiFi开启时。我建议你:

  • 多次采样取平均,比如读10次求平均
  • 硬件上加个100nF电容滤波
  • 避开WiFi工作时段(如果可能的话)

我的经验:ESP32的ADC不适合做高精度测量。如果你需要12位以上的精度,建议外挂ADS1115这类独立ADC芯片。ESP32的ADC更适合做“有没有变化”这种定性判断,比如光线变暗了、旋钮转到了某个位置。

3.3 光敏电阻数据读取:感知光线强弱

光敏电阻,说白了就是电阻值随光照变化。光线越强,电阻越小。但光敏电阻不能直接接ADC,得搭个分压电路。

电路连接:

  • 光敏电阻一端接3.3V
  • 另一端串联一个10kΩ固定电阻到GND
  • 光敏电阻和固定电阻的中间点接ADC引脚

为什么用10kΩ?我试过不同阻值,10kΩ在常见光照条件下,分压点电压变化最明显。你想想看,如果固定电阻太大或太小,电压变化范围都会被压缩。

代码示例:

#include <driver/adc.h>

#define LIGHT_SENSOR_PIN ADC1_CHANNEL_0  // GPIO36

void setup() {
    Serial.begin(115200);
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(LIGHT_SENSOR_PIN, ADC_ATTEN_DB_11);
}

void loop() {
    uint32_t sum = 0;
    for(int i = 0; i < 10; i++) {
        sum += adc1_get_raw(LIGHT_SENSOR_PIN);
        delay(10);
    }
    int adc_value = sum / 10;  // 取平均
    
    // 换算成电压
    float voltage = (adc_value / 4095.0) * 3.3;
    
    // 根据电压反推光敏电阻值(分压公式)
    float r_light = (3.3 / voltage - 1) * 10000;  // 单位欧姆
    
    Serial.printf("ADC: %d, Voltage: %.2fV, R_light: %.0f ohm\n", 
                  adc_value, voltage, r_light);
    delay(500);
}

这里有个细节:我取了10次平均。为什么是10次?不是5次也不是20次?嗯,这是我试出来的。10次能在噪声抑制和响应速度之间取得平衡。如果你对实时性要求高,可以减到5次;如果环境噪声大,可以加到20次。

3.4 电位计数据读取:旋钮的“数字翻译”

电位计,就是可调电阻。三个引脚:两端接电源和地,中间抽头接ADC。旋转旋钮时,中间抽头的电压在0~3.3V之间变化。

电路连接:

  • 电位计一脚接3.3V
  • 另一脚接GND
  • 中间脚接ADC引脚

代码跟光敏电阻类似,但电位计的输出是线性的,不需要反推电阻值。直接读ADC值,换算成电压就行。

#include <driver/adc.h>

#define POT_PIN ADC1_CHANNEL_3  // GPIO39

void setup() {
    Serial.begin(115200);
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(POT_PIN, ADC_ATTEN_DB_11);
}

void loop() {
    int raw = adc1_get_raw(POT_PIN);
    float voltage = (raw / 4095.0) * 3.3;
    
    // 换算成百分比(0~100%)
    float percent = (raw / 4095.0) * 100.0;
    
    Serial.printf("Raw: %d, Voltage: %.2fV, Position: %.1f%%\n", 
                  raw, voltage, percent);
    delay(200);
}

注意:电位计旋转时,接触点可能会有抖动,导致ADC读数跳变。我曾经在做一个音量控制项目时,旋钮一转,音量忽大忽小,用户体验极差。解决办法是加一个简单的滑动平均滤波,或者用硬件消抖电容(0.1μF并联在中间脚和地之间)。

3.5 电压换算公式:从数字回到物理世界

ADC读到的数字,最终要换算成有物理意义的量。核心公式就两个:

换算目标 公式 说明
ADC值 → 电压 V = (ADC_value / 4095) × V_ref V_ref通常是3.3V
电压 → 传感器物理量 取决于传感器特性 光敏电阻用分压公式反推
电压 → 百分比 % = (ADC_value / 4095) × 100 电位计常用

举个例子,假设你用一个NTC热敏电阻测温度。NTC的电阻值跟温度不是线性关系,得查表或用Steinhart-Hart方程。我一般偷懒,用查表法加线性插值,精度够用。

// 电压换算通用函数
float adcToVoltage(int adc_value, float vref = 3.3) {
    return (adc_value / 4095.0) * vref;
}

// 分压电路反推传感器电阻
float voltageToResistance(float voltage, float fixed_r, float vref = 3.3) {
    if(voltage <= 0) return 999999;  // 避免除零
    return (vref / voltage - 1) * fixed_r;
}

避坑指南:我曾经在换算时直接用整数除法,结果所有电压都算成0。记住,4095.0要写成浮点数,否则C++会做整数除法,结果永远是0。这种低级错误,我犯过一次就再也不会忘了。

好了,这一讲的内容就这些。ADC是连接数字世界和模拟世界的桥梁,掌握了它,你就能让ESP32感知光线、位置、温度、压力……几乎所有物理量。下一讲,我们会把这些传感器数据用起来,开始做真正的智能场景联动。