4、ADC模拟输入:电位器读取、光敏电阻、电池电压检测、ADC校准技巧
ADC,模数转换器,说白了就是把电压值变成数字。
ESP32 有两个 12 位的 ADC,精度是 4096 级。也就是 0~3.3V 对应 0~4095。听起来挺简单,对吧?但实际用起来,坑不少。我最早用 ESP32 做电池电压检测时,就吃过亏——读出来的电压值飘得厉害,后来才发现是参考电压没校准。
4.1 电位器读取:最简单的模拟输入
电位器,就是可调电阻。三个引脚,中间那个是滑动端,输出分压值。
接线很简单:
- 一脚接 3.3V
- 一脚接 GND
- 中间脚接 ADC 引脚(比如 GPIO34)
代码也很直接:
#include <driver/adc.h>
void setup() {
Serial.begin(115200);
adc1_config_width(ADC_WIDTH_BIT_12); // 12位精度
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11); // 衰减11dB,量程0~3.3V
}
void loop() {
int val = adc1_get_raw(ADC1_CHANNEL_6);
float voltage = val * (3.3 / 4095.0);
Serial.printf("ADC值: %d, 电压: %.2fV\n", val, voltage);
delay(500);
}
嗯,这里要注意:ESP32 的 ADC 引脚有编号限制。GPIO34~39 只能做输入,不能输出。我刚开始就犯过这个错,把 GPIO32 当 ADC 用,结果死活读不到数据。
4.2 光敏电阻:环境光检测
光敏电阻,阻值随光照变化。暗的时候阻值大,亮的时候阻值小。
用法很简单:光敏电阻和固定电阻(比如 10kΩ)串联,中间点接 ADC 引脚。这就构成了一个分压电路。
电路连接:
- 3.3V → 光敏电阻 → ADC 引脚 → 10kΩ 电阻 → GND
代码示例:
#define LIGHT_SENSOR_PIN 34
void setup() {
Serial.begin(115200);
pinMode(LIGHT_SENSOR_PIN, INPUT);
}
void loop() {
int lightValue = analogRead(LIGHT_SENSOR_PIN);
// 注意:analogRead() 默认是 12 位,但内部实现有差异
float voltage = lightValue * (3.3 / 4095.0);
// 根据电压反推光敏电阻阻值
float r_light = (3.3 / voltage - 1) * 10000; // 10kΩ 固定电阻
Serial.printf("光照ADC: %d, 电压: %.2fV, 光敏阻值: %.0fΩ\n",
lightValue, voltage, r_light);
delay(1000);
}
我个人习惯用 analogRead() 而不是 adc1_get_raw(),因为前者更简单。但要注意,analogRead() 内部其实调用了 ADC 驱动,精度和 adc1_get_raw() 是一样的。
4.3 电池电压检测:分压电路与低功耗设计
电池电压检测,是智能家居安防摄像头的标配功能。你得知道电池还剩多少电,才能决定要不要充电或者关机。
但 ESP32 的 ADC 输入范围是 0~3.3V,而锂电池满电是 4.2V。所以必须用分压电路把电压降下来。
典型分压电路:
- 电池正极 → 100kΩ 电阻 → ADC 引脚 → 100kΩ 电阻 → GND
这样分压比是 1:2,4.2V 变成 2.1V,在 ADC 范围内。
代码实现:
#define BATTERY_PIN 35
float readBatteryVoltage() {
int raw = analogRead(BATTERY_PIN);
float voltage = raw * (3.3 / 4095.0);
// 分压比 1:2,所以实际电压要乘以 2
float batteryVoltage = voltage * 2.0;
return batteryVoltage;
}
void setup() {
Serial.begin(115200);
// 设置衰减,确保量程准确
analogSetAttenuation(ADC_11db);
}
void loop() {
float vbat = readBatteryVoltage();
Serial.printf("电池电压: %.2fV\n", vbat);
if (vbat < 3.3) {
Serial.println("电量低,建议充电!");
} else if (vbat < 3.0) {
Serial.println("电量严重不足,即将关机!");
// 这里可以执行关机操作
}
delay(5000); // 5秒检测一次,省电
}
低功耗设计方面,我建议:
- 不要连续采样,用定时唤醒
- 采样前先让 ADC 稳定,加 10ms 延时
- 用深度睡眠模式,只在需要时唤醒
4.4 ADC校准技巧:让数据更准确
ESP32 的 ADC 有个通病——非线性。尤其是靠近 0V 和 3.3V 两端,误差很大。我测过一批 ESP32,同样的输入电压,不同芯片读出来的值能差 5%~10%。
所以,校准是必须的。
方法一:软件校准(两点校准)
找两个已知电压点,比如 0V 和 3.3V,测量实际 ADC 值,然后做线性修正。
// 校准参数,需要实际测量
#define CAL_0V_RAW 0 // 0V 时的 ADC 值(理论是 0)
#define CAL_3V3_RAW 3950 // 3.3V 时的 ADC 值(实际测量值)
float calibratedRead(int pin) {
int raw = analogRead(pin);
// 线性校准公式
float voltage = (raw - CAL_0V_RAW) * (3.3 / (CAL_3V3_RAW - CAL_0V_RAW));
return voltage;
}
方法二:使用 ESP32 内置 eFuse 校准
ESP32 出厂时,会在 eFuse 中存储两个校准点。我们可以读取这些值,自动校准。
#include <esp_adc_cal.h>
void setup() {
Serial.begin(115200);
// 配置 ADC
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
// 读取 eFuse 校准值
esp_adc_cal_characteristics_t adc_chars;
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11,
ADC_WIDTH_BIT_12, 1100, &adc_chars);
// 采样
uint32_t raw = adc1_get_raw(ADC1_CHANNEL_6);
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
Serial.printf("校准后电压: %dmV\n", voltage);
}
方法三:多次采样取平均
这是最简单的降噪方法。ADC 噪声通常服从高斯分布,取平均能有效抑制。
int stableAnalogRead(int pin, int samples = 16) {
long sum = 0;
for (int i = 0; i < samples; i++) {
sum += analogRead(pin);
delayMicroseconds(10); // 采样间隔
}
return sum / samples;
}
我个人习惯取 16 次平均,既保证稳定,又不影响响应速度。
4.5 综合示例:智能安防摄像头的电池管理
最后,我把这些知识点整合成一个完整的电池管理模块。这是我在一个安防摄像头项目中实际用过的代码。
class BatteryManager {
private:
int adcPin;
float r1, r2; // 分压电阻值
float calFactor; // 校准系数
public:
BatteryManager(int pin, float res1, float res2) {
adcPin = pin;
r1 = res1;
r2 = res2;
calFactor = 1.0; // 默认不校准
pinMode(adcPin, INPUT);
}
void calibrate(float knownVoltage) {
float measured = readRawVoltage();
calFactor = knownVoltage / measured;
}
float readRawVoltage() {
int raw = 0;
for (int i = 0; i < 16; i++) {
raw += analogRead(adcPin);
delayMicroseconds(10);
}
raw /= 16;
float voltage = raw * (3.3 / 4095.0);
return voltage * (r1 + r2) / r2; // 补偿分压
}
float getBatteryVoltage() {
return readRawVoltage() * calFactor;
}
int getBatteryPercent() {
float v = getBatteryVoltage();
// 锂电池电压与电量关系(近似)
if (v >= 4.2) return 100;
if (v >= 4.0) return 80;
if (v >= 3.8) return 60;
if (v >= 3.6) return 40;
if (v >= 3.4) return 20;
if (v >= 3.2) return 5;
return 0;
}
};
// 使用示例
BatteryManager battery(35, 100000, 100000); // GPIO35, 100k+100k分压
void setup() {
Serial.begin(115200);
battery.calibrate(4.2); // 用万用表测实际电压,校准
}
void loop() {
float v = battery.getBatteryVoltage();
int pct = battery.getBatteryPercent();
Serial.printf("电池: %.2fV, %d%%\n", v, pct);
delay(10000);
}
嗯,这一章就到这里。下一章我们会讲 PWM 输出,控制 LED 亮度、舵机角度,还有电机调速。到时候见。