3. 模拟信号采集:ESP32 ADC原理、电压测量、光敏电阻实战
从数字世界跨入模拟世界,是嵌入式开发中非常有意思的一步。ESP32 虽然有强大的数字处理能力,但它也得「感知」真实世界的电压、温度、光照强度——这些连续变化的物理量。ADC(模数转换器)就是那座桥梁。
我个人习惯把 ADC 比作「电压翻译官」。它把模拟电压翻译成数字,CPU 才能读懂。今天我们就来彻底搞懂 ESP32 的 ADC,并用光敏电阻做个实战。
3.1 ADC 基本原理:从电压到数字
ADC 全称 Analog-to-Digital Converter。它的工作流程可以概括为三步:采样 → 量化 → 编码。
- 采样: 每隔一段时间「拍一张快照」,记录当前电压值。
- 量化: 把连续的电压范围分成若干等级。比如 0~3.3V 分成 4096 级。
- 编码: 把等级编号转换成二进制数字,CPU 直接读取。
分辨率决定了 ADC 的「精细程度」。ESP32 的 ADC 默认是 12 位,也就是 2¹² = 4096 个等级。如果参考电压是 3.3V,那么每个 LSB(最低有效位)代表:
LSB = 3.3V / 4096 ≈ 0.806 mV
也就是说,ADC 读数每变化 1,对应电压变化约 0.8 毫伏。嗯,这个精度对于大多数传感器来说已经够用了。
3.2 ESP32 ADC 硬件特性
ESP32 内部有两个 ADC 模块:ADC1 和 ADC2。它们各有各的脾气。
| 特性 | ADC1 | ADC2 |
|---|---|---|
| 通道数 | 8 个(GPIO32~39) | 10 个(GPIO0,2,4,12~15,25~27) |
| WiFi 兼容性 | 可用(WiFi 不影响) | 不可用(WiFi 占用时无法使用) |
| 分辨率 | 12 位(可配置为 9~12 位) | 12 位(可配置为 9~12 位) |
| 衰减设置 | 0dB / 2.5dB / 6dB / 11dB | 0dB / 2.5dB / 6dB / 11dB |
| 推荐用途 | 传感器采集(首选) | 非 WiFi 场景 |
3.3 衰减设置与电压测量范围
ESP32 的 ADC 参考电压(Vref)内部约为 1.1V。如果不设置衰减,输入电压不能超过 1.1V。这显然不够用——我们常用的传感器输出往往是 0~3.3V 甚至更高。
衰减(Attenuation)就是用来解决这个问题的。它相当于在 ADC 输入前加了一个可调的分压器。
| 衰减设置 | 可用输入范围 | 典型用途 |
|---|---|---|
| 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,覆盖 0~3.3V 全范围。除非你明确知道信号电压很低,否则就用这个,省心。
3.4 电压测量实战:读取并换算
理论讲完了,直接上代码。下面是一个完整的电压测量示例,读取 GPIO34(ADC1_CH6)上的电压。
#include <driver/adc.h>
#include <esp_adc_cal.h>
#define ADC_CHANNEL ADC1_CHANNEL_6 // GPIO34
#define ADC_WIDTH ADC_WIDTH_BIT_12
#define ADC_ATTEN ADC_ATTEN_DB_11
static esp_adc_cal_characteristics_t adc_chars;
void adc_init(void) {
// 1. 配置 ADC 宽度和衰减
adc1_config_width(ADC_WIDTH);
adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
// 2. 校准特性(获取 Vref)
esp_adc_cal_characterize(
ADC_UNIT_1, ADC_ATTEN, ADC_WIDTH,
1100, &adc_chars // 1100 = 默认 Vref (mV)
);
}
uint32_t read_voltage_mv(void) {
uint32_t adc_reading = 0;
// 多次采样取平均,减少噪声
for (int i = 0; i < 64; i++) {
adc_reading += adc1_get_raw(ADC_CHANNEL);
}
adc_reading /= 64;
// 将原始值转换为电压(mV)
uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_reading, &adc_chars);
return voltage;
}
void app_main(void) {
adc_init();
while (1) {
uint32_t mv = read_voltage_mv();
printf("ADC Raw: %d, Voltage: %d mV (%.3f V)\n",
adc_reading, mv, mv / 1000.0);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
- 使用
esp_adc_cal_raw_to_voltage()自动完成换算,不需要手动计算。 - 64 次采样取平均,能有效抑制随机噪声。
- 校准函数
esp_adc_cal_characterize()会读取芯片内部的 eFuse Vref 值,提高精度。
3.5 光敏电阻实战:光照强度采集
光敏电阻(LDR)的阻值随光照强度变化——光照越强,阻值越低。我们需要把它和固定电阻组成分压电路,让 ESP32 读取变化的电压。
电路连接
3.3V ──┬── R_fixed (10kΩ) ──┬── GND
│ │
└── LDR ─────────────┘
│
GPIO34 (ADC输入)
这个电路是 上拉分压 结构。LDR 和固定电阻串联,中间点接 ADC。光照变化 → LDR 阻值变化 → 分压点电压变化 → ADC 读数变化。
代码实现
#include <driver/adc.h>
#include <esp_adc_cal.h>
#define LDR_ADC_CH ADC1_CHANNEL_6 // GPIO34
#define SAMPLE_COUNT 32
static esp_adc_cal_characteristics_t adc_chars;
void ldr_init(void) {
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(LDR_ADC_CH, ADC_ATTEN_DB_11);
esp_adc_cal_characterize(
ADC_UNIT_1, ADC_ATTEN_DB_11,
ADC_WIDTH_BIT_12, 1100, &adc_chars);
}
// 返回光照等级:0(最暗)~ 100(最亮)
int ldr_read_light_level(void) {
uint32_t sum = 0;
for (int i = 0; i < SAMPLE_COUNT; i++) {
sum += adc1_get_raw(LDR_ADC_CH);
}
uint32_t raw = sum / SAMPLE_COUNT;
// 将 ADC 原始值映射到 0~100
// 经验值:暗处约 500,亮处约 3500
int level = (raw - 500) * 100 / (3500 - 500);
if (level < 0) level = 0;
if (level > 100) level = 100;
return level;
}
void app_main(void) {
ldr_init();
while (1) {
int light = ldr_read_light_level();
printf("Light Level: %d/100\n", light);
if (light < 20) {
printf("→ 环境很暗,开灯!\n");
} else if (light > 80) {
printf("→ 环境很亮,关灯!\n");
} else {
printf("→ 环境适中\n");
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
3.6 软件滤波:让数据更稳定
ADC 原始数据往往带有噪声。你想想看,电源纹波、电磁干扰、甚至手指靠近都会让读数跳动。不加滤波的话,你的设备会「神经质」一样频繁触发。
我常用的三种滤波方法:
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 均值滤波 | 多次采样取平均 | 简单,抑制随机噪声 | 对突发干扰敏感 |
| 中值滤波 | 排序后取中间值 | 抗脉冲干扰强 | 计算量大,有延迟 |
| 一阶低通滤波 | y[n] = α·x[n] + (1-α)·y[n-1] | 平滑,响应可调 | 有相位滞后 |
3.7 迟滞比较:防止临界抖动
光敏电阻在临界光照下,ADC 读数会在阈值附近来回跳动。如果不做处理,你的设备会频繁开关,既耗电又影响寿命。
迟滞比较的思路很简单:开启和关闭使用不同的阈值。
#define THRESHOLD_ON 30 // 低于此值开灯
#define THRESHOLD_OFF 50 // 高于此值关灯
static bool light_on = false;
void control_light(int light_level) {
if (!light_on && light_level < THRESHOLD_ON) {
light_on = true;
printf("→ 开灯\n");
}
else if (light_on && light_level > THRESHOLD_OFF) {
light_on = false;
printf("→ 关灯\n");
}
// 在 30~50 之间保持当前状态,不动作
}
这样就有了 20 个单位的「死区」。只要噪声不超过这个范围,输出就不会抖动。
3.8 本章小结
- ADC 把模拟电压转换成数字量,ESP32 是 12 位分辨率。
- ADC1 和 WiFi 兼容,ADC2 不兼容——记住选 ADC1。
- 衰减设置
ADC_ATTEN_DB_11覆盖 0~3.3V,日常够用。 - 光敏电阻 + 固定电阻组成分压电路,ADC 读取电压变化。
- 软件滤波(均值/中值/低通)和迟滞比较是实战必备技巧。
掌握了这些,你已经可以采集大多数模拟传感器了。温度、湿度、气压、声音——原理都一样,只是前端电路和换算公式不同。动手试试看,把光敏电阻换成电位器,你就能做一个简易的电压表了。