4、ADC模拟输入:电位器读取、光敏电阻、电池电压检测、ADC校准技巧

ADC,模数转换器,说白了就是把电压值变成数字。

ESP32 有两个 12 位的 ADC,精度是 4096 级。也就是 0~3.3V 对应 0~4095。听起来挺简单,对吧?但实际用起来,坑不少。我最早用 ESP32 做电池电压检测时,就吃过亏——读出来的电压值飘得厉害,后来才发现是参考电压没校准。

4.1 电位器读取:最简单的模拟输入

电位器,就是可调电阻。三个引脚,中间那个是滑动端,输出分压值。

接线很简单:

  • 一脚接 3.3V
  • 一脚接 GND
  • 中间脚接 ADC 引脚(比如 GPIO34)

代码也很直接:

#include <driver/adc.h>

void setup() {
  Serial.begin(115200);
  adc1_config_width(ADC_WIDTH_BIT_12);  // 12位精度
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11); // 衰减11dB,量程0~3.3V
}

void loop() {
  int val = adc1_get_raw(ADC1_CHANNEL_6);
  float voltage = val * (3.3 / 4095.0);
  Serial.printf("ADC值: %d, 电压: %.2fV\n", val, voltage);
  delay(500);
}

嗯,这里要注意:ESP32 的 ADC 引脚有编号限制。GPIO34~39 只能做输入,不能输出。我刚开始就犯过这个错,把 GPIO32 当 ADC 用,结果死活读不到数据。

小技巧: 电位器读取时,建议加一个 0.1uF 的电容到 GND,能有效滤除噪声。我在项目中试过,不加电容时 ADC 值跳动 ±20,加了之后稳定在 ±3 以内。

4.2 光敏电阻:环境光检测

光敏电阻,阻值随光照变化。暗的时候阻值大,亮的时候阻值小。

用法很简单:光敏电阻和固定电阻(比如 10kΩ)串联,中间点接 ADC 引脚。这就构成了一个分压电路。

电路连接:

  • 3.3V → 光敏电阻 → ADC 引脚 → 10kΩ 电阻 → GND

代码示例:

#define LIGHT_SENSOR_PIN 34

void setup() {
  Serial.begin(115200);
  pinMode(LIGHT_SENSOR_PIN, INPUT);
}

void loop() {
  int lightValue = analogRead(LIGHT_SENSOR_PIN);
  // 注意:analogRead() 默认是 12 位,但内部实现有差异
  float voltage = lightValue * (3.3 / 4095.0);
  
  // 根据电压反推光敏电阻阻值
  float r_light = (3.3 / voltage - 1) * 10000; // 10kΩ 固定电阻
  
  Serial.printf("光照ADC: %d, 电压: %.2fV, 光敏阻值: %.0fΩ\n", 
                lightValue, voltage, r_light);
  delay(1000);
}

我个人习惯用 analogRead() 而不是 adc1_get_raw(),因为前者更简单。但要注意,analogRead() 内部其实调用了 ADC 驱动,精度和 adc1_get_raw() 是一样的。

避坑指南: 光敏电阻的响应速度很慢,大概几十毫秒。所以不要用太高的采样频率,否则读到的都是噪声。我曾经在项目中用 10ms 间隔采样,结果数据乱跳,后来改成 100ms 就稳定了。

4.3 电池电压检测:分压电路与低功耗设计

电池电压检测,是智能家居安防摄像头的标配功能。你得知道电池还剩多少电,才能决定要不要充电或者关机。

但 ESP32 的 ADC 输入范围是 0~3.3V,而锂电池满电是 4.2V。所以必须用分压电路把电压降下来。

典型分压电路:

  • 电池正极 → 100kΩ 电阻 → ADC 引脚 → 100kΩ 电阻 → GND

这样分压比是 1:2,4.2V 变成 2.1V,在 ADC 范围内。

代码实现:

#define BATTERY_PIN 35

float readBatteryVoltage() {
  int raw = analogRead(BATTERY_PIN);
  float voltage = raw * (3.3 / 4095.0);
  // 分压比 1:2,所以实际电压要乘以 2
  float batteryVoltage = voltage * 2.0;
  return batteryVoltage;
}

void setup() {
  Serial.begin(115200);
  // 设置衰减,确保量程准确
  analogSetAttenuation(ADC_11db);
}

void loop() {
  float vbat = readBatteryVoltage();
  Serial.printf("电池电压: %.2fV\n", vbat);
  
  if (vbat < 3.3) {
    Serial.println("电量低,建议充电!");
  } else if (vbat < 3.0) {
    Serial.println("电量严重不足,即将关机!");
    // 这里可以执行关机操作
  }
  delay(5000); // 5秒检测一次,省电
}
重要提醒: 分压电阻的取值要平衡功耗和精度。电阻太大(比如 1MΩ),ADC 输入阻抗不够,读数会偏小。电阻太小(比如 10kΩ),电池耗电快。我一般用 100kΩ+100kΩ,功耗约 20μA,可以接受。

低功耗设计方面,我建议:

  • 不要连续采样,用定时唤醒
  • 采样前先让 ADC 稳定,加 10ms 延时
  • 用深度睡眠模式,只在需要时唤醒

4.4 ADC校准技巧:让数据更准确

ESP32 的 ADC 有个通病——非线性。尤其是靠近 0V 和 3.3V 两端,误差很大。我测过一批 ESP32,同样的输入电压,不同芯片读出来的值能差 5%~10%。

所以,校准是必须的。

方法一:软件校准(两点校准)

找两个已知电压点,比如 0V 和 3.3V,测量实际 ADC 值,然后做线性修正。

// 校准参数,需要实际测量
#define CAL_0V_RAW   0     // 0V 时的 ADC 值(理论是 0)
#define CAL_3V3_RAW  3950  // 3.3V 时的 ADC 值(实际测量值)

float calibratedRead(int pin) {
  int raw = analogRead(pin);
  // 线性校准公式
  float voltage = (raw - CAL_0V_RAW) * (3.3 / (CAL_3V3_RAW - CAL_0V_RAW));
  return voltage;
}

方法二:使用 ESP32 内置 eFuse 校准

ESP32 出厂时,会在 eFuse 中存储两个校准点。我们可以读取这些值,自动校准。

#include <esp_adc_cal.h>

void setup() {
  Serial.begin(115200);
  
  // 配置 ADC
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
  
  // 读取 eFuse 校准值
  esp_adc_cal_characteristics_t adc_chars;
  esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                           ADC_WIDTH_BIT_12, 1100, &adc_chars);
  
  // 采样
  uint32_t raw = adc1_get_raw(ADC1_CHANNEL_6);
  uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
  
  Serial.printf("校准后电压: %dmV\n", voltage);
}
我的经验: 使用 eFuse 校准后,精度可以从 ±5% 提升到 ±1% 以内。但要注意,eFuse 校准只对特定衰减设置有效。如果你改了衰减,校准值就不准了。我一般固定使用 ADC_ATTEN_DB_11,这样校准一次,终身受益。

方法三:多次采样取平均

这是最简单的降噪方法。ADC 噪声通常服从高斯分布,取平均能有效抑制。

int stableAnalogRead(int pin, int samples = 16) {
  long sum = 0;
  for (int i = 0; i < samples; i++) {
    sum += analogRead(pin);
    delayMicroseconds(10); // 采样间隔
  }
  return sum / samples;
}

我个人习惯取 16 次平均,既保证稳定,又不影响响应速度。

4.5 综合示例:智能安防摄像头的电池管理

最后,我把这些知识点整合成一个完整的电池管理模块。这是我在一个安防摄像头项目中实际用过的代码。

class BatteryManager {
private:
  int adcPin;
  float r1, r2; // 分压电阻值
  float calFactor; // 校准系数
  
public:
  BatteryManager(int pin, float res1, float res2) {
    adcPin = pin;
    r1 = res1;
    r2 = res2;
    calFactor = 1.0; // 默认不校准
    pinMode(adcPin, INPUT);
  }
  
  void calibrate(float knownVoltage) {
    float measured = readRawVoltage();
    calFactor = knownVoltage / measured;
  }
  
  float readRawVoltage() {
    int raw = 0;
    for (int i = 0; i < 16; i++) {
      raw += analogRead(adcPin);
      delayMicroseconds(10);
    }
    raw /= 16;
    float voltage = raw * (3.3 / 4095.0);
    return voltage * (r1 + r2) / r2; // 补偿分压
  }
  
  float getBatteryVoltage() {
    return readRawVoltage() * calFactor;
  }
  
  int getBatteryPercent() {
    float v = getBatteryVoltage();
    // 锂电池电压与电量关系(近似)
    if (v >= 4.2) return 100;
    if (v >= 4.0) return 80;
    if (v >= 3.8) return 60;
    if (v >= 3.6) return 40;
    if (v >= 3.4) return 20;
    if (v >= 3.2) return 5;
    return 0;
  }
};

// 使用示例
BatteryManager battery(35, 100000, 100000); // GPIO35, 100k+100k分压

void setup() {
  Serial.begin(115200);
  battery.calibrate(4.2); // 用万用表测实际电压,校准
}

void loop() {
  float v = battery.getBatteryVoltage();
  int pct = battery.getBatteryPercent();
  Serial.printf("电池: %.2fV, %d%%\n", v, pct);
  delay(10000);
}
总结一下: ADC 看似简单,但要做好并不容易。电位器读取是入门,光敏电阻是应用,电池检测是实战,校准是进阶。每一步都有坑,但踩过坑之后,你就真正掌握了。

嗯,这一章就到这里。下一章我们会讲 PWM 输出,控制 LED 亮度、舵机角度,还有电机调速。到时候见。