4. 模拟信号采集:ADC原理、ESP32的ADC通道、电压读取、模拟值映射

各位同学,欢迎来到第四章。从这一章开始,我们正式进入「感知世界」的环节。前面几章我们搞定了数字通信,但现实世界里的温度、光照、声音、压力……这些可都是连续变化的模拟量。怎么让ESP32听懂这些模拟信号?答案就是——ADC。

4.1 ADC是什么?为什么需要它?

ADC,全称 Analog-to-Digital Converter,模数转换器。说白了,就是把连续变化的电压值,变成一串离散的数字。

你想想看,ESP32的CPU只认识0和1,它没法直接理解「3.3V」或者「1.8V」这种模拟电压。但传感器输出的偏偏就是电压——温度越高,输出电压越高;光照越强,输出电压越高。所以中间必须有个翻译官,把电压翻译成数字,CPU才能处理。

核心概念:ADC的分辨率决定了它能区分多小的电压变化。ESP32的ADC是12位的,意味着它能输出0~4095之间的整数。3.3V参考电压下,每一步代表约0.8mV的变化。

我在做第一个物联网项目时,选了个10位的ADC模块,结果发现温度变化0.5度,读数才跳了2个LSB,根本没法用。后来换成12位内置ADC,精度一下就上来了。所以,分辨率这东西,真不是参数党在吹牛。

4.2 ESP32的ADC通道与硬件限制

ESP32内部集成了两个SAR ADC模块:ADC1和ADC2。每个模块有多个通道,分别对应不同的GPIO引脚。

ADC模块 可用通道 对应GPIO 注意事项
ADC1 CH0 ~ CH7 GPIO36, 39, 34, 35, 32, 33, 25, 26 Wi-Fi开启时不受影响,推荐使用
ADC2 CH0 ~ CH9 GPIO4, 0, 2, 15, 13, 12, 14, 27, 16, 17 Wi-Fi开启时会被占用,慎用

重要警告:ADC2的通道在Wi-Fi启用后会被Wi-Fi驱动抢占,导致读取失败或返回错误值。如果你同时要用Wi-Fi和ADC,请务必使用ADC1的通道。我曾经在这个坑里爬了整整一个下午,最后发现是GPIO4在搞鬼。

另外,ESP32的ADC还有个「非线性」的问题。在0~100mV和3.1V~3.3V这两个区间,读数偏差比较明显。我个人的习惯是,尽量让传感器输出电压落在0.2V~3.0V之间,避开两头。

4.3 电压读取:从硬件连接到代码实现

先看硬件连接。假设我们用一个电位器(可变电阻)来模拟传感器输出,中间抽头接ADC输入引脚。

// 最简单的接线:
// 电位器两端:一端接3.3V,一端接GND
// 电位器中间抽头:接GPIO34 (ADC1_CH6)

接下来是代码。ESP-IDF和Arduino框架都提供了ADC读取接口。我个人更推荐用Arduino框架做原型验证,代码简洁,调试方便。

// Arduino框架下的ADC读取示例
#define ADC_PIN 34  // GPIO34, ADC1_CH6

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);  // 设置分辨率,默认就是12位
  analogSetAttenuation(ADC_11db);  // 设置衰减,后面会讲
}

void loop() {
  int rawValue = analogRead(ADC_PIN);
  float voltage = rawValue * (3.3 / 4095.0);
  
  Serial.print("原始值: ");
  Serial.print(rawValue);
  Serial.print(" | 电压: ");
  Serial.print(voltage, 3);
  Serial.println(" V");
  
  delay(500);
}

这里有个关键函数——analogSetAttenuation()。它决定了ADC的输入电压范围。ESP32提供了三种衰减选项:

衰减设置 可测电压范围 适用场景
ADC_0db 0 ~ 1.1V 低电压传感器(如光敏电阻分压后)
ADC_2_5db 0 ~ 1.5V 中间范围
ADC_6db 0 ~ 2.2V 常用范围
ADC_11db 0 ~ 3.3V 满量程,最常用

经验之谈:如果你不确定传感器输出范围,直接用ADC_11db最保险。代价是分辨率会稍微降低——因为同样的12位要覆盖更大的电压范围。但绝大多数物联网应用,这个精度完全够用。

4.4 模拟值映射:把电压变成有意义的物理量

读到了电压,事情才完成一半。用户想知道的是温度、光照强度、水位高度,而不是一个冰冷的电压值。这就需要「映射」——把电压值换算成物理量。

举个例子,假设你用的是TMP36温度传感器,它的输出特性是:

  • 0°C时输出0.5V
  • 每升高1°C,电压增加10mV

那么换算公式就是:

float temperature = (voltage - 0.5) * 100.0;

再比如,一个光敏电阻配合10kΩ上拉电阻,光照越强电压越低。你需要先标定两个点:

  • 黑暗环境下的电压值(比如2.8V)
  • 强光环境下的电压值(比如0.3V)

然后用线性插值把电压映射到0~100%的光照强度:

float lightPercent = map(voltage, 0.3, 2.8, 100.0, 0.0);
// 注意:电压越低光照越强,所以映射是反的

Arduino的map()函数很好用,但它只处理整数。我习惯自己写一个浮点版本的映射函数:

float floatMap(float x, float in_min, float in_max, float out_min, float out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

关键提醒:映射的前提是传感器输出与物理量之间呈线性关系。如果传感器是非线性的(比如热敏电阻NTC),你需要查表或者用更复杂的公式(如Steinhart-Hart方程)。千万别硬套线性映射,否则数据会错得离谱。

4.5 本章知识体系总览

下面这张图,把ADC信号采集的完整链路画出来了。从模拟信号输入,到数字值输出,再到物理量换算,每一步都离不开。

ADC信号采集流程 ADC信号采集完整流程 模拟信号 传感器输出电压 ADC采样 12位分辨率 0~4095 原始数字值 rawValue 电压换算 raw × (3.3 / 4095) 物理量映射 温度/光照/压力等 ⚠️ 常见问题 • ADC2与Wi-Fi冲突 • 非线性区间(0~100mV) • 衰减设置影响量程 图4-1 ADC信号采集流程:从模拟信号到物理量的完整链路

4.6 避坑指南与实用技巧

做ADC采集,有几个坑我几乎每次项目都会遇到,这里集中说一下:

  • 电源噪声:ESP32的3.3V引脚如果同时给传感器供电,Wi-Fi发射时会产生几十毫伏的波动。我建议传感器单独用LDO供电,或者至少加一个10μF+0.1μF的去耦电容。
  • 引脚悬空:ADC引脚不接任何东西时,读数会随机跳动。这不是ESP32坏了,是浮空引脚在感应环境噪声。解决办法是接一个100kΩ的下拉电阻到GND。
  • 多次采样取平均:单次采样受噪声影响很大。我习惯连续读10次,去掉最大最小值,再取平均。代码就几行,效果立竿见影。
// 多次采样取平均,去除毛刺
int readADC_Average(int pin, int samples) {
  long sum = 0;
  int minVal = 4095, maxVal = 0;
  
  for (int i = 0; i < samples; i++) {
    int val = analogRead(pin);
    sum += val;
    if (val < minVal) minVal = val;
    if (val > maxVal) maxVal = val;
    delay(2);  // 采样间隔
  }
  
  sum = sum - minVal - maxVal;  // 去掉极值
  return sum / (samples - 2);
}

我的习惯:在产品代码里,我会把ADC读取、电压换算、物理量映射封装成一个独立的类。这样换传感器时只需要改映射函数,其他代码不用动。模块化设计,后期维护省心很多。

好了,这一章的内容就到这里。ADC是嵌入式开发的基础功,看似简单,但细节不少。把这一章吃透,后面做传感器驱动、数据采集、远程监控,都会顺手很多。