4. 模拟信号采集:ADC原理、ESP32的ADC通道、电压读取、模拟值映射
好,咱们进入第四章。这一章要聊的是怎么让ESP32「感知」真实世界的模拟量——比如温度、光照、电位器位置。说白了,就是ADC(模数转换器)的使用。
我刚开始做嵌入式那会儿,总觉得数字信号才是王道,模拟信号又麻烦又不准。后来做了几个传感器项目才明白:真实世界就是模拟的,你绕不开它。ESP32内置了两个ADC模块,用好了,很多场景都能搞定。
4.1 ADC基本原理:从连续到离散
ADC的全称是Analog-to-Digital Converter,模数转换器。它的任务很简单:把连续变化的电压值,变成一串离散的数字。
你想想看,真实世界的电压是连续的——0V到3.3V之间有无穷多个值。但单片机只能处理0和1组成的二进制数。ADC就是中间的翻译官。
核心指标有两个:
- 分辨率(Resolution):用多少位二进制数来表示模拟量。ESP32的ADC是12位,也就是0~4095。
- 参考电压(Vref):ADC能测量的最大电压。ESP32内部参考电压通常是1.1V,但通过衰减器可以测量到3.3V。
关键公式:
数字值 = (模拟电压 / 参考电压) × (2^分辨率 - 1)
举个例子:如果Vref=3.3V,分辨率12位,测得模拟电压1.65V,那么数字值 = (1.65 / 3.3) × 4095 = 2047.5 ≈ 2048。
我在项目中遇到过一个问题:ADC读数跳得厉害,后来发现是参考电压不稳定。嗯,这个后面会细说。
4.2 ESP32的ADC通道与引脚
ESP32有两个ADC模块:ADC1和ADC2。ADC1有8个通道,ADC2有10个通道。但要注意,ADC2的通道和Wi-Fi功能共用硬件资源——如果你同时启用了Wi-Fi,ADC2的读数会受影响。
我个人习惯:只要能用ADC1,绝不用ADC2。省心。
| ADC模块 | 通道数 | 可用引脚 | 注意事项 |
|---|---|---|---|
| ADC1 | 8 | GPIO32~GPIO39 | Wi-Fi开启时不受影响,推荐使用 |
| ADC2 | 10 | GPIO0、GPIO2、GPIO4、GPIO12~GPIO15、GPIO25~GPIO27 | Wi-Fi开启时读数不稳定,慎用 |
注意:GPIO34~GPIO39是纯输入引脚,没有内部上拉电阻。另外,GPIO12在启动时会影响Flash电压,建议避开。
我曾经踩过一个坑:用GPIO12做ADC输入,结果板子死活启动不了。查了半天才发现是GPIO12的电压影响了Flash供电。从那以后,我选ADC引脚都会先查一遍ESP32的引脚功能表。
4.3 电压读取:从硬件连接到代码实现
好,咱们直接上手。先看硬件连接,再看代码。
4.3.1 硬件连接
最简单的场景:用一个电位器(可调电阻)分压,中间抽头接到ESP32的ADC引脚。
- 电位器两端:一端接3.3V,一端接GND
- 电位器中间抽头:接GPIO34(ADC1_CH6)
这样转动电位器,GPIO34上的电压就在0~3.3V之间变化。
4.3.2 代码实现:读取原始值
ESP-IDF提供了标准的ADC驱动接口。下面是一个最简单的读取示例:
#include <driver/adc.h>
void app_main(void)
{
// 1. 配置ADC1的通道和衰减
adc1_config_width(ADC_WIDTH_BIT_12); // 12位分辨率
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11); // 衰减11dB,量程0~3.3V
while (1) {
// 2. 读取原始值
int raw_value = adc1_get_raw(ADC1_CHANNEL_6);
printf("ADC原始值: %d\n", raw_value);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
这段代码做了两件事:配置ADC参数,然后循环读取。注意ADC_ATTEN_DB_11这个参数——它告诉ADC使用11dB衰减,这样输入范围就从0~1.1V扩展到0~3.3V。
小技巧:ESP32的ADC在低电压段(0~0.1V)和非线性段(接近3.3V)精度较差。我一般把测量范围控制在0.2V~3.0V之间,读数更可靠。
4.3.3 电压换算:从原始值到实际电压
原始值0~4095对应0~3.3V。换算公式很简单:
float voltage = (float)raw_value * 3.3 / 4095.0;
printf("实际电压: %.2f V\n", voltage);
但这里有个坑:ESP32的ADC线性度并不完美。尤其是靠近0V和3.3V两端,误差可能达到几十毫伏。如果你需要高精度测量,建议做两点校准。
我做过一个项目,要求电压测量误差小于10mV。最后没办法,只能用外部基准电压源做校准,把校准系数写进Flash。嗯,那是另一个故事了。
4.4 模拟值映射:把电压变成有意义的数据
拿到电压值之后,通常还要映射成实际物理量。比如:
- 电位器角度:0~3.3V → 0°~270°
- 光敏电阻:电压变化 → 光照强度(lux)
- NTC热敏电阻:电压变化 → 温度(℃)
映射的本质就是线性变换:实际值 = (电压 - 电压最小值) / (电压最大值 - 电压最小值) × (量程最大值 - 量程最小值) + 量程最小值
举个例子,把0~3.3V映射到0~100的百分比:
float percentage = (voltage / 3.3) * 100.0;
printf("百分比: %.1f%%\n", percentage);
如果是非线性传感器(比如NTC热敏电阻),就需要查表或者用Steinhart-Hart方程计算。这个后面讲传感器的时候再展开。
4.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:从模拟信号输入,到ADC转换,再到数值映射,最后输出有意义的数据。
4.6 避坑指南与实用建议
做ADC采集,有几个坑是新手必踩的。我帮你列出来:
- 引脚选择:避开GPIO12,它影响Flash启动电压。GPIO2在烧录时会被拉低,也不建议用。
- Wi-Fi干扰:ADC2在Wi-Fi开启时读数会跳。我习惯只用ADC1,省得排查问题。
- 参考电压:ESP32的内部参考电压有±10%的误差。如果你需要精确测量,用外部基准电压源。
- 滤波处理:单次读取噪声大。我一般做10次采样取平均,或者用滑动窗口滤波。
滤波示例:
int read_adc_filtered(int channel, int samples)
{
uint32_t sum = 0;
for (int i = 0; i < samples; i++) {
sum += adc1_get_raw(channel);
}
return sum / samples;
}
10次采样取平均,噪声能降低60%以上。