4. 模拟信号采集:ADC原理、ESP32 ADC特性、电位器电压读取、光敏电阻实验
好,咱们进入第四章。前面几章我们一直在跟数字信号打交道——GPIO要么高要么低,I2C和SPI传的也是0和1的组合。但现实世界不是非黑即白的。温度、光照、音量,这些都是连续变化的物理量。怎么让ESP32理解这些“模拟”信号?答案就是ADC——模数转换器。
4.1 ADC基本原理:从连续到离散
ADC,全称Analog-to-Digital Converter,说白了就是把连续变化的电压,映射成一组离散的数字值。你想想看,ESP32的CPU只能处理0和1,它不认识2.5V或者1.8V。ADC就是那个“翻译官”。
我刚开始做嵌入式时,总觉得ADC很神秘。后来自己用分立元件搭过一个最简单的“闪速ADC”,才明白原理其实不复杂。常见的ADC工作原理有几种:
| 类型 | 速度 | 精度 | 典型应用 |
|---|---|---|---|
| 逐次逼近型(SAR) | 中速 | 中高 | MCU内置ADC(ESP32用的就是这种) |
| 积分型(双斜率) | 慢 | 高 | 万用表、精密测量 |
| 闪速型(Flash) | 极快 | 低 | 高速示波器、射频 |
| Σ-Δ型 | 慢 | 极高 | 音频、高精度传感器 |
关键参数就两个:分辨率和参考电压。分辨率决定你能把电压分成多少份。比如12位ADC,就是2^12=4096份。参考电压决定测量范围。ESP32的ADC是12位的,参考电压默认是0~3.3V。所以每个LSB(最低有效位)对应的电压是3.3V / 4096 ≈ 0.8mV。
核心公式:
测量电压 = (ADC读数 / 4096) × 参考电压
反过来:ADC读数 = (测量电压 / 参考电压) × 4096
4.2 ESP32 ADC特性:别踩这些坑
ESP32的ADC,嗯,这里要重点说一下。它跟STM32或者Arduino的ADC不太一样,有几个“脾气”你得摸清楚。我在项目里吃过亏,所以把这些坑提前告诉你。
4.2.1 非线性问题
ESP32的ADC在0~1V和2.5~3.3V这两个区间,线性度还不错。但在中间1V~2.5V这段,误差可能达到2%~3%。什么意思?你测2V,读出来可能是1.94V或者2.06V。对于精度要求不高的场景(比如判断光照强弱),这没问题。但如果你要做精密测量,就得做校准。
我曾经踩过的坑: 有个项目用ESP32读压力传感器,满量程5V,分压后进ADC。结果数据波动很大,查了两天才发现是ADC中间段非线性导致的。后来加了分段校准表才解决。
4.2.2 衰减器配置
ESP32的ADC默认只能测0~1.1V。你没看错,是1.1V。要测0~3.3V,必须开启衰减器。衰减器有4档:
| 衰减配置 | 测量范围 | 推荐场景 |
|---|---|---|
| ADC_ATTEN_DB_0 | 0 ~ 1.1V | 内部参考、低电压信号 |
| ADC_ATTEN_DB_2_5 | 0 ~ 1.5V | 很少用 |
| ADC_ATTEN_DB_6 | 0 ~ 2.2V | 部分传感器 |
| ADC_ATTEN_DB_11 | 0 ~ 3.3V | 最常用,电位器、光敏电阻都用这个 |
4.2.3 引脚选择
ESP32不是所有引脚都能做ADC。只有ADC1和ADC2通道的引脚才行。而且ADC2在Wi-Fi开启时会被占用,所以我的习惯是——只用ADC1的引脚。省心。
- ADC1通道: GPIO32~GPIO39
- ADC2通道: GPIO0、GPIO2、GPIO4、GPIO12~GPIO15、GPIO25~GPIO27
个人建议: 如果你同时用Wi-Fi和ADC,老老实实走ADC1。ADC2在Wi-Fi发包时读数会乱跳,我因为这个被测试同事骂过。
4.3 电位器电压读取:第一个ADC实验
咱们先拿电位器练手。电位器本质上就是一个可调分压器。拧旋钮,中间抽头的电压就在0~3.3V之间变化。电路连接很简单:
- 电位器一脚接3.3V
- 另一脚接GND
- 中间脚接GPIO34(ADC1_CH6)
代码怎么写?看下面:
#include <driver/adc.h>
#include <esp_adc_cal.h>
#define ADC_PIN ADC1_CHANNEL_6 // GPIO34
#define ADC_WIDTH ADC_WIDTH_BIT_12
#define ADC_ATTEN ADC_ATTEN_DB_11
void app_main(void)
{
// 1. 配置ADC
adc1_config_width(ADC_WIDTH);
adc1_config_channel_atten(ADC_PIN, ADC_ATTEN);
// 2. 读取原始值
int raw = adc1_get_raw(ADC_PIN);
printf("Raw ADC value: %d\n", raw);
// 3. 换算电压(粗略)
float voltage = raw * (3.3f / 4096.0f);
printf("Voltage: %.2f V\n", voltage);
}
拧电位器,串口打印的数值应该在0~4095之间变化,电压在0~3.3V之间。如果数值跳变厉害,检查一下接线,或者加个100nF电容滤波。
4.4 光敏电阻实验:感知环境光
光敏电阻,也叫光依赖电阻(LDR)。它的特性很简单:光越强,电阻越小;光越暗,电阻越大。但你不能直接把光敏电阻接到ESP32的ADC引脚上。为什么?因为ADC测的是电压,不是电阻。你需要一个分压电路。
4.4.1 电路设计
我常用的电路是这样的:光敏电阻和固定电阻(比如10kΩ)串联,中间抽头接ADC引脚。固定电阻接GND,光敏电阻接3.3V。这样光照强时,光敏电阻变小,抽头电压升高;光照弱时,光敏电阻变大,抽头电压降低。
电路参数:
- VCC = 3.3V
- R_fixed = 10kΩ(推荐,跟光敏电阻暗阻/亮阻匹配)
- V_adc = VCC × R_fixed / (R_ldr + R_fixed)
4.4.2 代码实现
跟电位器实验类似,但这次我们要把电压换算成光照强度。不过说实话,光敏电阻的阻值-光照曲线不是线性的,所以一般不做精确的勒克斯(lux)换算。我的做法是:直接拿ADC原始值做阈值判断。
#include <driver/adc.h>
#define LDR_ADC_CHANNEL ADC1_CHANNEL_7 // GPIO35
void app_main(void)
{
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(LDR_ADC_CHANNEL, ADC_ATTEN_DB_11);
while (1) {
int ldr_raw = adc1_get_raw(LDR_ADC_CHANNEL);
// 原始值越大,光照越强
if (ldr_raw > 3000) {
printf("☀️ 强光环境 (raw=%d)\n", ldr_raw);
} else if (ldr_raw > 1500) {
printf("🌤 正常光照 (raw=%d)\n", ldr_raw);
} else if (ldr_raw > 500) {
printf("🌥 较暗 (raw=%d)\n", ldr_raw);
} else {
printf("🌙 黑暗 (raw=%d)\n", ldr_raw);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
避坑指南: 光敏电阻的响应速度很慢,大概几十毫秒。所以采样间隔别太短,100ms以上比较稳。我曾经为了省时间设了10ms采样,结果数据抖得像心电图。
4.5 本章知识体系
下面这张图把本章的核心逻辑串起来了。从物理世界的模拟信号,到ESP32的ADC采样,再到软件层面的数据处理和应用,一条线走通。
4.6 小结与经验
这一章我们走通了模拟信号采集的完整链路。从ADC原理,到ESP32的硬件特性,再到电位器和光敏电阻两个实战实验。说白了,核心就三件事:配置衰减器、读原始值、换算物理量。
最后分享一个经验:ADC读数一定要做滤波。最简单的办法是连续读5次取平均。我见过太多人直接拿单次读数做判断,结果系统忽明忽暗、忽快忽慢。加个滑动平均滤波,效果立竿见影。
本章核心要点:
- ADC把连续电压映射成离散数字值,ESP32是12位、0~4095
- 衰减器必须配ADC_ATTEN_DB_11才能测0~3.3V
- ADC1和ADC2要分清,Wi-Fi开启时只用ADC1
- 电位器直接分压,光敏电阻需要固定电阻配合
- 读数滤波是必须的,别偷懒