3、模拟信号采集:ADC原理,ESP32的12位ADC特性,光敏电阻与电位计数据读取,电压换算公式
好,咱们进入第三讲。这一讲要聊的是模拟信号采集。说白了,就是让ESP32能“听”懂自然界里连续变化的物理量,比如光线强弱、旋钮转了多少度。
数字芯片只能认0和1,但现实世界是连续的。怎么把连续信号变成数字?这就得靠ADC——模数转换器。我个人习惯把ADC比作一把“数字尺子”,它把电压范围分成很多小格,然后告诉你当前电压在第几格。
3.1 ADC基本原理:从模拟到数字的那一步
ADC的原理,其实没那么玄乎。它内部有个比较器,不断跟参考电压做对比。最常见的逐次逼近型ADC,就像你猜一个数字,先猜中间值,再根据反馈往高或往低调整,一步步逼近真实值。
关键参数有几个:
- 分辨率:比如12位,就是2^12=4096个刻度。分辨率越高,尺子上的格子越细。
- 参考电压:ADC能测量的最大电压。ESP32默认是0~3.3V。
- 采样率:每秒能转换多少次。ESP32的ADC采样率最高能到几MHz,但实际用的时候别跑满,容易不稳定。
核心公式:
数字值 = (模拟电压 / 参考电压) × 最大分辨率
反过来:模拟电压 = (数字值 / 最大分辨率) × 参考电压
举个例子,如果读到ADC值是2048,参考电压3.3V,分辨率12位(4096),那实际电压就是 (2048/4096)×3.3 = 1.65V。嗯,正好一半。
3.2 ESP32的12位ADC特性:别被坑了
ESP32的ADC是12位的,理论上能输出0~4095。但我要提醒你,实际用起来有几个坑,我踩过不止一次。
第一个坑:非线性
ESP32的ADC在低电压段(0~0.1V)和高电压段(3.2~3.3V)表现不太好。中间段(0.2V~3.0V)相对线性。我在项目中遇到过,用ADC测电池电压,结果低电量时读数飘得厉害,后来加了分压电阻把测量范围压缩到0.5V~2.8V才解决。
第二个坑:衰减模式
ESP32的ADC有0dB、2.5dB、6dB、11dB四种衰减模式。默认是0dB,测量范围0~1.1V。如果你要测3.3V,必须设置11dB衰减。代码里这样写:
#include <driver/adc.h>
// 配置ADC1通道0,11dB衰减,测量范围0~3.3V
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);
第三个坑:噪声
ESP32的ADC噪声比较大,尤其是WiFi开启时。我建议你:
- 多次采样取平均,比如读10次求平均
- 硬件上加个100nF电容滤波
- 避开WiFi工作时段(如果可能的话)
我的经验:ESP32的ADC不适合做高精度测量。如果你需要12位以上的精度,建议外挂ADS1115这类独立ADC芯片。ESP32的ADC更适合做“有没有变化”这种定性判断,比如光线变暗了、旋钮转到了某个位置。
3.3 光敏电阻数据读取:感知光线强弱
光敏电阻,说白了就是电阻值随光照变化。光线越强,电阻越小。但光敏电阻不能直接接ADC,得搭个分压电路。
电路连接:
- 光敏电阻一端接3.3V
- 另一端串联一个10kΩ固定电阻到GND
- 光敏电阻和固定电阻的中间点接ADC引脚
为什么用10kΩ?我试过不同阻值,10kΩ在常见光照条件下,分压点电压变化最明显。你想想看,如果固定电阻太大或太小,电压变化范围都会被压缩。
代码示例:
#include <driver/adc.h>
#define LIGHT_SENSOR_PIN ADC1_CHANNEL_0 // GPIO36
void setup() {
Serial.begin(115200);
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(LIGHT_SENSOR_PIN, ADC_ATTEN_DB_11);
}
void loop() {
uint32_t sum = 0;
for(int i = 0; i < 10; i++) {
sum += adc1_get_raw(LIGHT_SENSOR_PIN);
delay(10);
}
int adc_value = sum / 10; // 取平均
// 换算成电压
float voltage = (adc_value / 4095.0) * 3.3;
// 根据电压反推光敏电阻值(分压公式)
float r_light = (3.3 / voltage - 1) * 10000; // 单位欧姆
Serial.printf("ADC: %d, Voltage: %.2fV, R_light: %.0f ohm\n",
adc_value, voltage, r_light);
delay(500);
}
这里有个细节:我取了10次平均。为什么是10次?不是5次也不是20次?嗯,这是我试出来的。10次能在噪声抑制和响应速度之间取得平衡。如果你对实时性要求高,可以减到5次;如果环境噪声大,可以加到20次。
3.4 电位计数据读取:旋钮的“数字翻译”
电位计,就是可调电阻。三个引脚:两端接电源和地,中间抽头接ADC。旋转旋钮时,中间抽头的电压在0~3.3V之间变化。
电路连接:
- 电位计一脚接3.3V
- 另一脚接GND
- 中间脚接ADC引脚
代码跟光敏电阻类似,但电位计的输出是线性的,不需要反推电阻值。直接读ADC值,换算成电压就行。
#include <driver/adc.h>
#define POT_PIN ADC1_CHANNEL_3 // GPIO39
void setup() {
Serial.begin(115200);
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(POT_PIN, ADC_ATTEN_DB_11);
}
void loop() {
int raw = adc1_get_raw(POT_PIN);
float voltage = (raw / 4095.0) * 3.3;
// 换算成百分比(0~100%)
float percent = (raw / 4095.0) * 100.0;
Serial.printf("Raw: %d, Voltage: %.2fV, Position: %.1f%%\n",
raw, voltage, percent);
delay(200);
}
注意:电位计旋转时,接触点可能会有抖动,导致ADC读数跳变。我曾经在做一个音量控制项目时,旋钮一转,音量忽大忽小,用户体验极差。解决办法是加一个简单的滑动平均滤波,或者用硬件消抖电容(0.1μF并联在中间脚和地之间)。
3.5 电压换算公式:从数字回到物理世界
ADC读到的数字,最终要换算成有物理意义的量。核心公式就两个:
| 换算目标 | 公式 | 说明 |
|---|---|---|
| ADC值 → 电压 | V = (ADC_value / 4095) × V_ref | V_ref通常是3.3V |
| 电压 → 传感器物理量 | 取决于传感器特性 | 光敏电阻用分压公式反推 |
| 电压 → 百分比 | % = (ADC_value / 4095) × 100 | 电位计常用 |
举个例子,假设你用一个NTC热敏电阻测温度。NTC的电阻值跟温度不是线性关系,得查表或用Steinhart-Hart方程。我一般偷懒,用查表法加线性插值,精度够用。
// 电压换算通用函数
float adcToVoltage(int adc_value, float vref = 3.3) {
return (adc_value / 4095.0) * vref;
}
// 分压电路反推传感器电阻
float voltageToResistance(float voltage, float fixed_r, float vref = 3.3) {
if(voltage <= 0) return 999999; // 避免除零
return (vref / voltage - 1) * fixed_r;
}
避坑指南:我曾经在换算时直接用整数除法,结果所有电压都算成0。记住,4095.0要写成浮点数,否则C++会做整数除法,结果永远是0。这种低级错误,我犯过一次就再也不会忘了。
好了,这一讲的内容就这些。ADC是连接数字世界和模拟世界的桥梁,掌握了它,你就能让ESP32感知光线、位置、温度、压力……几乎所有物理量。下一讲,我们会把这些传感器数据用起来,开始做真正的智能场景联动。