4、ADC模拟量采集:从原理到实战
各位同学,欢迎来到第四章。前面几章我们都在跟数字信号打交道——GPIO要么高要么低,简单粗暴。但从这一章开始,我们要进入模拟量的世界了。
说白了,现实世界里的物理量,比如光照强度、温度、声音大小,它们都是连续变化的,不是非0即1。ESP32要读懂这些信号,就得靠ADC——模数转换器。
本章核心目标:理解ADC工作原理,掌握ESP32的ADC特性,最终用光敏电阻做一个光照强度采集器,并通过串口把数据打印出来。
4.1 ADC原理:连续世界到离散数字的桥梁
ADC的全称是Analog-to-Digital Converter。它的任务很简单:把一个连续的电压值,映射成一个离散的数字值。
举个例子。假设你的ADC是10位分辨率,那么它能输出的数字范围是0~1023(2^10=1024个台阶)。如果参考电压是3.3V,那么输入0V对应数字0,输入3.3V对应数字1023。中间的值按比例线性映射。
关键公式:
数字值 = (输入电压 / 参考电压) × (2^分辨率 - 1)
反过来,从数字值算电压:
电压 = (数字值 / (2^分辨率 - 1)) × 参考电压
我在项目中遇到过一个问题:有个同事直接用这个公式算电压,结果发现读数总是偏大。后来排查发现,他用的参考电压是3.3V,但实际板子上的VREF只有3.2V。嗯,这里要注意——参考电压一定要以实际测量为准。
| 分辨率(位) | 台阶数 | 3.3V下的最小分辨率 | 典型应用 |
|---|---|---|---|
| 8 | 256 | 12.89 mV | 低精度传感器 |
| 10 | 1024 | 3.22 mV | Arduino经典配置 |
| 12 | 4096 | 0.81 mV | ESP32默认配置 |
| 16 | 65536 | 0.05 mV | 高精度工业采集 |
你想想看,分辨率越高,台阶越密,能分辨的电压变化就越小。但也不是越高越好——分辨率高了,噪声也会更明显,转换速度也会变慢。
4.2 ESP32的ADC特性:你需要知道的坑
ESP32内置了两个ADC模块:ADC1和ADC2。每个模块有多个通道,但ADC2有个大坑——它跟Wi-Fi共用硬件。如果你同时开了Wi-Fi,ADC2的读数会变得乱七八糟。
重要警告:使用ESP32的ADC时,优先选择ADC1的通道(GPIO32~GPIO39)。ADC2的通道(GPIO0、GPIO2、GPIO4、GPIO12~GPIO15、GPIO25~GPIO27)在Wi-Fi开启时不可靠。我曾经因为没注意这个,调试了整整一个下午,最后发现是Wi-Fi在捣乱。
ESP32的ADC默认是12位分辨率,也就是0~4095。但它的线性度并不是完美的——尤其是在电压接近0V和接近3.3V的两端,非线性比较明显。我个人的习惯是,尽量让信号落在0.2V~3.1V之间,避开两端。
ESP32 ADC通道映射表:
| ADC模块 | 可用通道 | 对应GPIO | Wi-Fi影响 |
|---|---|---|---|
| ADC1 | CH0 ~ CH7 | GPIO36, 37, 38, 39, 32, 33, 34, 35 | 无影响 |
| ADC2 | CH0 ~ CH9 | GPIO4, 0, 2, 15, 13, 12, 14, 27, 25, 26 | Wi-Fi开启时不可用 |
4.3 电压读取:动手写代码
好,理论说完了,咱们直接上手。下面这段代码,用ADC1的通道6(GPIO34)读取模拟电压,然后通过串口打印出来。
#include <driver/adc.h>
#include <esp_adc_cal.h>
// 定义ADC参数
#define ADC_CHANNEL ADC1_CHANNEL_6 // GPIO34
#define ADC_WIDTH ADC_WIDTH_BIT_12 // 12位分辨率
#define ADC_ATTEN ADC_ATTEN_DB_11 // 衰减11dB,量程0~3.3V
void setup() {
Serial.begin(115200);
// 配置ADC
adc1_config_width(ADC_WIDTH);
adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
Serial.println("ADC初始化完成,开始读取电压...");
}
void loop() {
// 读取原始值
int raw_value = adc1_get_raw(ADC_CHANNEL);
// 计算电压(mV)
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw_value, &adc_chars);
// 打印结果
Serial.print("原始值: ");
Serial.print(raw_value);
Serial.print(" 电压: ");
Serial.print(voltage);
Serial.println(" mV");
delay(500);
}
个人经验:衰减(Attenuation)参数很关键。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。我一般直接用ADC_ATTEN_DB_11,省事。但如果你只需要测小信号,用低衰减能获得更好的精度。
4.4 光敏电阻实验:把光照变成电压
光敏电阻,也叫光依赖电阻(LDR)。它的特性很简单:光照越强,电阻越小;光照越弱,电阻越大。
但ESP32不能直接测电阻,它只能测电压。所以我们需要一个分压电路,把电阻变化转换成电压变化。
电路连接:
- 光敏电阻一端接3.3V
- 光敏电阻另一端接一个10kΩ固定电阻,再接地
- 光敏电阻和固定电阻的中间节点,接到ESP32的GPIO34(ADC输入)
这个电路就是一个典型的分压器。光照变化时,光敏电阻的阻值变化,中间节点的电压也跟着变化。ESP32读取这个电压,就能反推出光照强度。
分压公式:Vout = 3.3V × (R_fixed / (R_ldr + R_fixed))
当光照强时,R_ldr变小,Vout变大;光照弱时,R_ldr变大,Vout变小。
下面这个SVG图,把整个系统的数据流画清楚了:
4.5 完整实验代码与校准
下面是一个完整的光敏电阻实验代码。它读取ADC值,计算电压,并估算光照强度百分比。
#include <driver/adc.h>
#include <esp_adc_cal.h>
#define LDR_ADC_CHANNEL ADC1_CHANNEL_6 // GPIO34
#define LDR_PIN 34
// 校准用ADC特性参数
static esp_adc_cal_characteristics_t adc_chars;
void setup() {
Serial.begin(115200);
// 配置ADC
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(LDR_ADC_CHANNEL, ADC_ATTEN_DB_11);
// 校准ADC(使用VREF默认值1100mV)
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars);
Serial.println("光敏电阻实验开始");
Serial.println("原始值, 电压(mV), 光照百分比(%)");
}
void loop() {
// 读取原始ADC值
int raw = adc1_get_raw(LDR_ADC_CHANNEL);
// 转换为电压(mV)
uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
// 计算光照百分比(假设3.3V对应100%光照)
float light_percent = (voltage_mv / 3300.0) * 100.0;
// 限制范围
if (light_percent > 100.0) light_percent = 100.0;
if (light_percent < 0.0) light_percent = 0.0;
// 打印数据(CSV格式,方便串口绘图)
Serial.print(raw);
Serial.print(", ");
Serial.print(voltage_mv);
Serial.print(", ");
Serial.println(light_percent, 1);
delay(200);
}
避坑指南:我曾经直接用原始ADC值除以4095再乘以3.3V来计算电压,结果发现跟万用表测出来的总是差几十毫伏。后来才意识到,ESP32的内部参考电压并不是精确的3.3V,而是1.1V(经过内部倍压)。正确的做法是用esp_adc_cal_raw_to_voltage()函数做校准。这个函数会考虑芯片的个体差异,精度好很多。
4.6 实验验证与调试技巧
代码烧录进去之后,你可以做几个简单的测试:
- 用手遮住光敏电阻——串口打印的电压值应该下降,光照百分比接近0%
- 用手电筒照射光敏电阻——电压值上升,光照百分比接近100%
- 用万用表测量GPIO34的电压——跟串口打印的电压值对比,误差应该在±50mV以内
如果发现读数跳动很大,别慌。这是正常现象——ADC本身有噪声。我一般会在软件里做一下简单的滤波:连续读10次,去掉最大最小值,然后取平均。效果立竿见影。
// 简单的中位平均滤波
int readLDRFiltered() {
const int SAMPLES = 10;
int values[SAMPLES];
// 采集10次
for (int i = 0; i < SAMPLES; i++) {
values[i] = adc1_get_raw(LDR_ADC_CHANNEL);
delay(5); // 稍微延时,让采样点分散
}
// 冒泡排序
for (int i = 0; i < SAMPLES - 1; i++) {
for (int j = 0; j < SAMPLES - i - 1; j++) {
if (values[j] > values[j + 1]) {
int temp = values[j];
values[j] = values[j + 1];
values[j + 1] = temp;
}
}
}
// 去掉最大最小,取平均
int sum = 0;
for (int i = 1; i < SAMPLES - 1; i++) {
sum += values[i];
}
return sum / (SAMPLES - 2);
}
注意:ADC采样后不要立即进行下一次采样。ESP32的ADC内部有采样保持电容,需要一点时间放电。我习惯在两次采样之间加至少5ms的延时。如果你用连续采样模式,记得检查数据手册里的采样周期参数。
好了,这一章的内容就到这里。ADC是嵌入式开发里非常基础但又容易踩坑的部分。掌握了原理和ESP32的特性,后面做各种模拟传感器(温度、湿度、气体、声音)都会轻松很多。动手试试吧,把光敏电阻接上,看看你房间的光照变化曲线是什么样的。