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信号采集的完整链路画出来了。从模拟信号输入,到数字值输出,再到物理量换算,每一步都离不开。
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是嵌入式开发的基础功,看似简单,但细节不少。把这一章吃透,后面做传感器驱动、数据采集、远程监控,都会顺手很多。