4. ADC 模拟量采集:从原理到实战
ADC,全称模数转换器。说白了,就是把连续变化的电压,变成一串离散的数字。我们生活的世界是模拟的——温度、光照、声音,都是连续量。但 ESP32 是数字芯片,它只认 0 和 1。ADC 就是连接这两个世界的桥梁。
我个人习惯把 ADC 比作一把「电压尺子」。尺子上的刻度越密,量出来的值就越准。这把尺子的总长度,就是 ADC 的参考电压;刻度的数量,就是 ADC 的分辨率。
4.1 ADC 工作原理:一把数字尺子
ADC 的核心思想很简单:比较。它内部有一个比较器,不断把输入电压和一个内部生成的参考电压做对比。最常见的逐次逼近型(SAR)ADC,工作方式有点像「猜数字」——先猜中间值,再根据反馈往高或往低调整,直到逼近真实值。
为什么我要强调这个?因为理解了这个过程,你才能明白采样率、分辨率这些参数到底在说什么。
| 参数 | 含义 | 对实际的影响 |
|---|---|---|
| 分辨率 | ADC 能区分的最小电压变化 | 12位 ADC 可区分 4096 个等级,精度约 0.8mV(3.3V 参考) |
| 采样率 | 每秒能完成的转换次数 | ESP32 最高约 200ksps,采集音频够用,采集射频信号不够 |
| 参考电压 | ADC 量程的上限 | ESP32 内部参考约 1.1V,外部可接至 3.3V |
| 量化误差 | 数字值与真实值的偏差 | 分辨率越低,误差越大;12位下误差约 ±0.5 LSB |
4.2 ESP32 ADC 特性:没那么简单
ESP32 的 ADC 有点特殊。它有两个 ADC 模块——ADC1 和 ADC2。ADC1 有 8 个通道,ADC2 有 10 个通道。但 ADC2 会被 Wi-Fi 占用,所以做传感器采集时,我建议你只用 ADC1。
为什么会这样?因为 ESP32 的 Wi-Fi 模块和 ADC2 共用了一些硬件资源。一旦 Wi-Fi 开启,ADC2 的读数就会乱跳。我曾经踩过这个坑,调试了整整一个下午才找到原因。
另外,ESP32 的 ADC 还有一个「非线性」的问题。在 0~1.1V 范围内还算线性,但超过 1.1V 后,曲线就开始弯曲。乐鑫官方提供了衰减选项(attenuation),可以扩展量程到 3.3V,但代价是精度下降。
| 衰减设置 | 量程范围 | 适用场景 |
|---|---|---|
| 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 | 常用,兼容 3.3V 传感器 |
4.3 模拟电压读取:动手写代码
好了,理论说够了,我们直接上手。下面这段代码,是我在多个项目中反复使用的 ADC 读取模板。它做了两件事:配置 ADC 参数,然后循环读取电压值。
#include <driver/adc.h>
#include <esp_adc_cal.h>
// 定义 ADC 通道和衰减
#define ADC_CHANNEL ADC1_CHANNEL_0 // GPIO36
#define ADC_ATTEN ADC_ATTEN_DB_11 // 0~3.3V
// 校准结构体
static esp_adc_cal_characteristics_t adc_chars;
void adc_init(void) {
// 1. 配置 ADC 宽度(分辨率)
adc1_config_width(ADC_WIDTH_BIT_12); // 12位分辨率
// 2. 配置衰减
adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
// 3. 校准 ADC(获取 Vref 值)
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN,
ADC_WIDTH_BIT_12, 1100, &adc_chars);
}
uint32_t adc_read_mv(void) {
// 读取原始值
int raw = adc1_get_raw(ADC_CHANNEL);
// 转换为毫伏
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
return voltage;
}
void app_main(void) {
adc_init();
while (1) {
uint32_t mv = adc_read_mv();
printf("ADC Raw: %d, Voltage: %d mV\n",
adc1_get_raw(ADC_CHANNEL), mv);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
这段代码里,我特别想强调校准那一步。很多教程会忽略 esp_adc_cal_characterize,直接用原始值换算。但 ESP32 的 ADC 出厂时每个芯片的 Vref 都不一样,不校准的话误差可能达到 10% 以上。
esp_adc_cal_raw_to_voltage 会自动补偿芯片间的差异,把原始值转换成真实的毫伏数。这是我在几十块 ESP32 上验证过的经验。
4.4 光敏电阻实验:感知光线
理论学完了,代码也写了,我们来做个实际的东西。光敏电阻(LDR)是最简单的模拟传感器——光照越强,电阻越小。配合一个固定电阻组成分压电路,就能把电阻变化变成电压变化。
电路连接很简单:
- 光敏电阻一端接 3.3V
- 另一端串联一个 10kΩ 电阻到 GND
- 光敏电阻和 10kΩ 的中间点,接到 ESP32 的 GPIO36(ADC1_CH0)
为什么选 10kΩ?我试过 1kΩ、4.7kΩ、10kΩ、47kΩ。10kΩ 在普通室内光照下,分压点电压在 1.5V~2.5V 之间,正好落在 ADC 量程的中段,线性度最好。你想想看,如果选太小,暗光下电压太低;选太大,强光下电压饱和。10kΩ 是个折中。
// 光敏电阻读取示例
void light_sensor_task(void *pvParameters) {
adc_init();
while (1) {
uint32_t mv = adc_read_mv();
// 将电压映射到光照等级(0~100%)
// 3.3V 对应最亮,0V 对应最暗
float light_percent = (mv / 3300.0) * 100.0;
printf("Light: %.1f%% (Voltage: %d mV)\n",
light_percent, mv);
// 根据光照做决策
if (light_percent < 20.0) {
printf("-> 太暗了,开灯!\n");
} else if (light_percent > 80.0) {
printf("-> 太亮了,拉窗帘!\n");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
这段代码里,我把电压直接映射成百分比。虽然不精确,但做智能家居的阈值判断足够了。如果你需要精确的勒克斯(lux)值,那就得查光敏电阻的数据手册,做一条电压-光照的校准曲线。
4.5 知识体系总览
下面这张图,是我自己总结的 ADC 知识体系。每次做新项目前,我都会对照着检查一遍,确保没有遗漏。
这张图把 ADC 知识分成了三个维度:硬件特性、软件实现、实战应用。你不需要一次性全记住,但每次遇到 ADC 相关的问题,都可以回来对照一下,看看自己卡在了哪个环节。
嗯,这一章的内容就到这里。ADC 是嵌入式开发的基础功,也是传感器驱动的核心。把这一章吃透了,后面学温度传感器、湿度传感器、气体传感器,都会轻松很多。