3、ESP32模拟信号采集:ADC模拟输入、电压读取、光敏电阻与电位器实战

好,咱们今天来聊聊ESP32的ADC。说白了,就是让ESP32能“听懂”模拟世界的语言。

数字世界只有0和1,但现实世界是连续的电压、温度、光照。ADC(模数转换器)就是连接这两个世界的桥梁。ESP32内部集成了两个12位的ADC,精度还不错,能分辨出4096个不同的电压等级。

我个人习惯把ADC比作一把尺子。这把尺子量程是0到3.3V,刻度有4096格。你给它一个电压,它告诉你对应的数字是多少。反过来,我们也能通过数字反推电压值。

3.1 ESP32的ADC硬件特性

先说说ESP32的ADC有哪些特点,免得你踩坑。

  • 分辨率:12位,也就是0-4095的数值范围
  • 输入电压范围:0 ~ 3.3V(注意!不能超过3.3V,否则会烧引脚)
  • 可用通道:ADC1有8个通道(GPIO32-GPIO39),ADC2有10个通道
  • 注意:ADC2的通道在WiFi开启时会被占用,所以我建议你优先使用ADC1的引脚
⚠️ 重要提醒:ESP32的ADC在3.3V附近存在非线性问题。也就是说,当输入电压接近3.3V时,读数会不太准。我在项目中遇到过这个问题,当时用万用表量出来是3.2V,ESP32读出来却是3.0V。解决办法是做个简单的软件校准,或者避开高电压区间。

3.2 基础代码:读取ADC值

先来个最简单的例子,读取电位器(可变电阻)的电压值。

// 定义ADC引脚
const int adcPin = 34;  // GPIO34,属于ADC1

void setup() {
  Serial.begin(115200);
  // 不需要额外配置,analogRead直接可用
}

void loop() {
  int adcValue = analogRead(adcPin);
  float voltage = adcValue * (3.3 / 4095.0);
  
  Serial.print("ADC值: ");
  Serial.print(adcValue);
  Serial.print("  电压: ");
  Serial.print(voltage);
  Serial.println(" V");
  
  delay(500);
}

这段代码很简单,但有个细节我想提一下。计算电压时,我用了 3.3 / 4095.0 而不是 3.3 / 4096。为什么?因为ADC的读数范围是0到4095,总共4096个值。但实际测量中,0对应0V,4095对应3.3V,所以分母用4095更准确。嗯,这个小细节当年让我困惑了好一阵子。

3.3 实战一:光敏电阻测光照

光敏电阻(LDR)的阻值会随光照强度变化。光线越强,阻值越小。我们把它和一个固定电阻串联,中间抽头接到ESP32的ADC引脚,就能读出光照强度了。

电路连接很简单:

  • 3.3V → 10kΩ电阻 → A点 → 光敏电阻 → GND
  • A点 → GPIO34

代码实现:

const int ldrPin = 34;

void setup() {
  Serial.begin(115200);
}

void loop() {
  int ldrValue = analogRead(ldrPin);
  
  // 将ADC值映射到0-100的百分比
  // 注意:这个映射不是线性的,只是做个演示
  int lightPercent = map(ldrValue, 0, 4095, 0, 100);
  
  Serial.print("光照强度: ");
  Serial.print(lightPercent);
  Serial.println("%");
  
  // 简单的判断
  if (lightPercent < 20) {
    Serial.println("环境很暗,建议开灯");
  } else if (lightPercent < 60) {
    Serial.println("环境正常");
  } else {
    Serial.println("环境很亮");
  }
  
  delay(1000);
}
💡 经验之谈:光敏电阻的响应不是线性的。你想想看,在暗处稍微有点光,阻值变化很大;但在亮处再增加光照,变化就不明显了。所以实际项目中,我一般会用查表法或者拟合曲线来校准。曾经有个智能路灯项目,就是因为没做校准,导致黄昏时分灯光忽明忽暗,被客户投诉了好几次。

3.4 实战二:电位器控制LED亮度

这个实验很经典,用电位器控制LED的亮度。ADC读取电位器的电压,然后通过PWM控制LED。

电路连接:

  • 电位器中间脚 → GPIO34
  • 电位器另外两脚 → 3.3V 和 GND
  • LED正极(长脚) → GPIO2(通过220Ω电阻)
  • LED负极(短脚) → GND
const int potPin = 34;
const int ledPin = 2;

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  int potValue = analogRead(potPin);
  
  // 将ADC值(0-4095)映射到PWM值(0-255)
  int pwmValue = map(potValue, 0, 4095, 0, 255);
  
  analogWrite(ledPin, pwmValue);
  
  Serial.print("电位器值: ");
  Serial.print(potValue);
  Serial.print("  PWM值: ");
  Serial.println(pwmValue);
  
  delay(50);  // 快速响应
}

这里有个小技巧。我习惯把 delay(50) 而不是 delay(500)。为什么?因为电位器是手动调节的,响应要快才自然。你转一下旋钮,灯要立刻跟着变,不能有半秒延迟,否则体验很差。

3.5 ADC精度与校准

说实话,ESP32的ADC精度不算顶尖。官方文档也承认,在3.3V附近有±6%的误差。这在实际项目中可能是个问题。

我分享一个简单的校准方法:

  1. 用万用表测量一个稳定的电压(比如1.65V,即3.3V的一半)
  2. 用ESP32读取这个电压的ADC值
  3. 计算实际电压和测量电压的比例关系
  4. 把这个比例写入代码中
// 校准系数,需要根据实际测量调整
float calibrationFactor = 1.05;  // 假设实际值比测量值大5%

void loop() {
  int adcValue = analogRead(adcPin);
  float rawVoltage = adcValue * (3.3 / 4095.0);
  float calibratedVoltage = rawVoltage * calibrationFactor;
  
  Serial.print("校准前: ");
  Serial.print(rawVoltage);
  Serial.print("V  校准后: ");
  Serial.print(calibratedVoltage);
  Serial.println("V");
  
  delay(1000);
}
📊 典型ADC读数误差表
实际电压(V) 理论ADC值 典型实测值 误差
0.0 0 0-5 较小
1.65 2048 2000-2100 ±2%
3.0 3724 3500-3700 ±5%
3.3 4095 3900-4095 ±6%

3.6 避坑指南

做ADC项目时,有几个坑我踩过,分享给你:

  • 引脚电压不能超过3.3V:哪怕一瞬间超过,都可能烧坏引脚。如果需要测量更高电压,必须用分压电阻
  • ADC2与WiFi冲突:如果你同时用WiFi和ADC,记得只用ADC1的引脚(GPIO32-GPIO39)
  • 电源噪声影响:ESP32的ADC对电源噪声很敏感。我建议在ADC引脚附近加一个0.1μF的电容到GND,能有效滤除高频噪声
  • 多次采样取平均:单次采样可能不稳定,我习惯连续读10次取平均,能提高稳定性
// 多次采样取平均的示例
int readAverage(int pin, int samples) {
  long sum = 0;
  for (int i = 0; i < samples; i++) {
    sum += analogRead(pin);
    delay(1);  // 短暂延时,让ADC稳定
  }
  return sum / samples;
}

好了,关于ESP32的ADC模拟输入,核心内容就这些。从硬件特性到基础代码,从光敏电阻到电位器控制,再到精度校准和避坑技巧,你应该能上手实战了。记住,实践出真知,拿起你的ESP32和几个元件,动手试试吧。