4. 模拟信号采集:ADC原理、ESP32的ADC特性、光敏电阻与电位器读数实验

从这一章开始,我们正式接触「模拟世界」。前面几章我们玩的都是数字信号——要么高电平,要么低电平,非0即1。但现实世界不是这样的。温度是连续的,光照是渐变的,声音是波动的。要让ESP32理解这些连续变化的物理量,就得靠ADC——模数转换器。

说白了,ADC就是把连续的电压值,翻译成芯片能处理的离散数字。我刚开始做嵌入式那会儿,总觉得ADC很简单,不就是读个电压嘛。后来踩了不少坑才明白,ADC的水其实挺深的。

4.1 ADC工作原理:从模拟到数字的桥梁

ADC的核心思想,用一句话概括就是:用有限个数字阶梯,去逼近一条连续的模拟曲线。

常见的ADC架构有好几种,比如逐次逼近型(SAR)、积分型、流水线型。ESP32内部用的是逐次逼近型ADC(SAR ADC)。这种架构的优点是功耗低、速度快,非常适合物联网设备。

它的工作流程像一场「猜数字」游戏:

  1. 采样保持电路先「拍一张快照」,把当前时刻的电压值固定下来。
  2. 内部DAC从中间值开始,输出一个猜测电压。
  3. 比较器对比猜测电压和输入电压:猜高了就降,猜低了就升。
  4. 逐位逼近,直到精度达到ADC的位数上限。

举个例子,一个10位的SAR ADC,需要猜10次,每次确定一个比特位。猜完10次,一个完整的数字值就出来了。

关键参数速查表

参数含义典型值(ESP32)
分辨率数字输出的位数,决定阶梯数量12位(0~4095)
参考电压ADC能测量的最大电压0~3.3V(内部参考)
采样率每秒能完成的转换次数最高约200ksps
量化误差模拟值与数字值之间的固有误差±0.5 LSB

分辨率12位意味着什么?3.3V被分成4096份,每一份大约是0.8mV。也就是说,ESP32能分辨出0.8mV的电压变化。这个精度对于大多数传感器来说,已经够用了。

4.2 ESP32的ADC特性:别被数据手册骗了

ESP32的ADC模块有两个独立的SAR ADC:ADC1和ADC2。它们各有8个通道,但ADC2的部分通道会被Wi-Fi占用。我个人习惯只用ADC1,省心。

这里有个重要的坑,我必须提前告诉你:ESP32的ADC线性度并不完美。尤其是在电压接近0V或接近3.3V的两端,非线性失真比较明显。我曾经在一个光照监测项目里,发现ADC读数在低照度区间偏差很大,排查了半天才发现是ADC本身的非线性问题。

⚠️ 重要警告:ESP32 ADC的电压测量范围

ESP32的ADC标称输入范围是0~3.3V。但实际测试表明,当输入电压低于0.1V或高于3.1V时,ADC的线性度会显著下降。建议将有效测量范围控制在0.15V~3.1V之间,以保证精度。

另外,绝对不要给ADC引脚输入超过3.3V的电压,否则会烧毁芯片。

还有一个特性很多人不知道:ESP32的ADC在不同衰减(Attenuation)设置下,测量范围不同。衰减本质上是一个前置分压器,用来扩展测量范围。

衰减设置理论测量范围推荐使用场景
ADC_ATTEN_DB_00~1.1V高精度小信号
ADC_ATTEN_DB_2_50~1.5V中等信号
ADC_ATTEN_DB_60~2.2V常用范围
ADC_ATTEN_DB_110~3.3V全量程(推荐)

我一般默认用ADC_ATTEN_DB_11,覆盖0~3.3V全范围。虽然精度会稍微牺牲一点,但胜在通用。

4.3 硬件准备:光敏电阻与电位器

这一节的实验,我们需要两个最基本的模拟传感器:

  • 光敏电阻(LDR):阻值随光照强度变化。暗的时候阻值大(MΩ级),亮的时候阻值小(kΩ级)。
  • 电位器(Potentiometer):手动旋转改变分压比,输出0~3.3V可调电压。

接线方式很简单:

  • 光敏电阻与一个10kΩ固定电阻串联,组成分压电路。中间抽头接ESP32的ADC引脚(比如GPIO34)。
  • 电位器的两端分别接3.3V和GND,中间抽头接另一个ADC引脚(比如GPIO35)。

为什么光敏电阻要串联一个固定电阻?因为ADC测量的是电压,不是电阻。光敏电阻本身阻值变化范围很大,如果不加固定电阻分压,电压变化范围会非常窄,ADC读不出有效数据。

💡 经验之谈:分压电阻的选值

光敏电阻的暗阻和亮阻相差很大。我建议用万用表先测一下光敏电阻在典型光照下的阻值,然后选择与它阻值相近的固定电阻。这样分压电路的灵敏度最高。10kΩ是一个比较通用的起点,但如果你在暗光环境下使用,可能需要换成100kΩ甚至1MΩ。

4.4 代码实现:读取ADC值并转换为电压

好,理论说完了,我们直接上代码。ESP32的ADC驱动在Arduino框架下封装得很好,几行代码就能跑起来。

#include <driver/adc.h>
#include <esp_adc_cal.h>

// ADC通道定义
#define LDR_ADC_CHANNEL   ADC1_CHANNEL_6   // GPIO34
#define POT_ADC_CHANNEL   ADC1_CHANNEL_7   // GPIO35

// ADC特性结构体,用于电压校准
static esp_adc_cal_characteristics_t adc_chars;

void setup() {
  Serial.begin(115200);
  
  // 配置ADC1的位宽为12位
  adc1_config_width(ADC_WIDTH_BIT_12);
  
  // 配置衰减为11dB,测量范围0~3.3V
  adc1_config_channel_atten(LDR_ADC_CHANNEL, ADC_ATTEN_DB_11);
  adc1_config_channel_atten(POT_ADC_CHANNEL, ADC_ATTEN_DB_11);
  
  // 初始化ADC校准特性
  esp_adc_cal_characterize(
    ADC_UNIT_1, 
    ADC_ATTEN_DB_11, 
    ADC_WIDTH_BIT_12, 
    1100,  // 默认Vref,单位mV
    &adc_chars
  );
  
  Serial.println("ADC初始化完成");
}

void loop() {
  // 读取原始ADC值(0~4095)
  int ldr_raw = adc1_get_raw(LDR_ADC_CHANNEL);
  int pot_raw = adc1_get_raw(POT_ADC_CHANNEL);
  
  // 将原始值转换为电压(mV)
  uint32_t ldr_voltage = esp_adc_cal_raw_to_voltage(ldr_raw, &adc_chars);
  uint32_t pot_voltage = esp_adc_cal_raw_to_voltage(pot_raw, &adc_chars);
  
  // 打印结果
  Serial.print("光敏电阻 - 原始值: ");
  Serial.print(ldr_raw);
  Serial.print("  电压: ");
  Serial.print(ldr_voltage);
  Serial.println(" mV");
  
  Serial.print("电位器 - 原始值: ");
  Serial.print(pot_raw);
  Serial.print("  电压: ");
  Serial.print(pot_voltage);
  Serial.println(" mV");
  
  Serial.println("-------------------");
  delay(500);
}

这段代码做了三件事:配置ADC参数、读取原始值、转换为实际电压。注意我用了esp_adc_cal_raw_to_voltage()这个函数,它内部做了校准补偿,比直接用raw * 3300 / 4095要准确得多。

你可能会问:为什么不用简单的比例换算?因为ESP32的ADC内部有非线性和增益误差,直接用比例换算会引入较大偏差。官方提供的校准库就是用来修正这些误差的。

4.5 实验现象与数据分析

把代码烧录到ESP32,打开串口监视器,你会看到类似这样的输出:

光敏电阻 - 原始值: 2450  电压: 1980 mV
电位器 - 原始值: 1024  电压: 830 mV
-------------------
光敏电阻 - 原始值: 3100  电压: 2500 mV
电位器 - 原始值: 2048  电压: 1650 mV
-------------------

用手遮住光敏电阻,原始值会下降;用手电筒照射,原始值会上升。旋转电位器,数值会平滑变化。

这里有个细节值得注意:光敏电阻的ADC值变化是非线性的。因为光敏电阻的阻值与光照强度是对数关系,而不是线性关系。如果你需要精确的光照度值(单位lux),还需要做查表或曲线拟合。

📊 数据解读要点

  • ADC原始值范围:0~4095,对应0~3.3V
  • 电压值范围:0~3300 mV
  • 光敏电阻:电压越高,说明光照越强(电阻越小)
  • 电位器:电压与旋转角度成正比(线性关系)

4.6 避坑指南:我踩过的ADC坑

做ADC实验,有几个坑几乎每个人都会遇到。我把自己踩过的列出来,你遇到了可以直接对照排查。

  • 读数跳动太大:ESP32的ADC对电源噪声很敏感。如果供电不稳定,读数会上下跳动几十甚至上百。解决办法是在ADC引脚对地加一个0.1μF的滤波电容,或者用软件做多次采样取平均。
  • Wi-Fi开启后ADC2读数异常:ADC2的部分通道与Wi-Fi共用硬件资源。如果你同时使用Wi-Fi和ADC2,ADC2的读数会变成随机值。解决方案很简单——只用ADC1。
  • 电压超过3.3V烧坏引脚:这个我说过很多次了。ADC引脚没有过压保护,超过3.3V必烧。如果被测电压可能超过3.3V,必须用电阻分压或运放做电平转换。
  • 校准后仍有偏差:ESP32的ADC校准是基于芯片内部eFuse中存储的个体参数。但不同芯片之间仍有差异。如果要求高精度,建议外接精密参考电压源做两点校准。

4.7 本章知识结构图

下面这张图总结了ADC从物理量到数字量的完整链路,以及ESP32特有的注意事项。你可以把它当作本章的「思维导图」。

ESP32 ADC 知识结构图 模拟输入信号 光敏电阻 / 电位器 分压电路(0~3.3V) ESP32 ADC 模块 ADC1 ADC2 关键配置参数 • 位宽:12位(0~4095) • 衰减:ATTEN_DB_11 • 通道:ADC1_CHANNEL_6/7 数字输出(原始值+电压) ⚠️ 注意事项 • 有效范围:0.15~3.1V • 避免超过3.3V • ADC2与Wi-Fi冲突 • 建议加滤波电容 校准函数 esp_adc_cal_raw_to_voltage()

4.8 小结与思考

这一章我们走通了从模拟信号到数字信号的完整链路。你学会了:

  • ADC的工作原理——逐次逼近,用数字阶梯逼近模拟曲线。
  • ESP32的ADC特性——12位分辨率,注意线性度问题,优先用ADC1。
  • 硬件接线——光敏电阻需要分压电路,电位器直接分压。
  • 代码实现——配置、读取、校准、输出,四步走。
  • 常见坑——电源噪声、Wi-Fi冲突、过压、校准偏差。

现在你可以用光敏电阻做一个简单的光线检测器,或者用电位器做一个手动输入设备。这些基础能力,后面做传感器数据采集和云平台上报时都会用到。