1. 为什么电池电压监测值得单独拿出来做锂电池电压监测这件事看起来简单——不就是拿个ADC读一下电压吗但真正做过电池供电项目的人都知道这里面的坑比想象中多得多。ESP32的ADC本身精度就一般加上锂电池电压范围3.0V~4.2V超出了ESP32 ADC的默认量程必须做分压处理而分压电路的设计又直接影响测量精度和功耗。更麻烦的是ESP32的ADC在不同衰减档位下线性度差异很大WiFi开启时噪声还会干扰采样结果。我最近在做一个便携式数据采集终端用18650锂电池供电需要实时监测电池电量并在低电量时触发告警。一开始想得很简单直接拿ADC读不就完了结果实测下来误差能到0.3V以上电量显示跳来跳去根本没法用。后来花了整整两天时间从分压电阻选型、ADC衰减配置、软件滤波到校准曲线一步步把精度做到了±0.02V以内才算真正能用。这篇文章就是把这套完整的方案分享出来。不管你是刚接触ESP32的新手还是已经用过ADC但被精度问题困扰的开发者都能从里面找到可以直接复现的步骤和参数。我会从分压电路的数学推导开始讲到ESP32 ADC的硬件特性、软件配置、滤波算法最后给出完整的校准方法和实测数据。所有代码都经过实际验证电路参数可以直接抄作业。2. 锂电池电压监测的核心需求与ESP32 ADC的匹配分析2.1 锂电池电压范围与ESP32 ADC量程的矛盾先搞清楚一个基本事实单节锂电池的电压范围通常在3.0V放电截止到4.2V充满之间。而ESP32的ADC引脚在默认配置下0dB衰减满量程只有大约1.1V。即使开到最大衰减11dB满量程也就在3.3V左右而且线性度会明显变差。这意味着什么如果你直接把锂电池正极接到ADC引脚4.2V的电压会直接超过量程上限不仅读出来的值不准长期过压还可能损坏引脚。所以分压电路是必须的没有商量余地。那分压比例怎么定理论上只要把4.2V分到3.3V以下就行。但实际选择时要考虑几个因素分压后的电压最好落在ADC线性度较好的区间分压电阻的功耗要尽量小避免影响电池续航电阻的精度和温漂要可控。综合这些因素我最终选择了100kΩ和100kΩ的等值分压分压比1:24.2V分压后是2.1V留足了余量。2.2 ESP32 ADC的衰减档位与线性度实测对比ESP32的ADC有四个衰减档位0dB、2.5dB、6dB、11dB。官方文档给出的建议量程分别是1.1V、1.5V、2.2V、3.3V。但文档不会告诉你的是不同档位的线性度差异非常大。我拿手头的ESP32-WROOM-32模块做了一组实测用高精度可调电源输出标准电压记录ADC读数并换算成电压值对比实际输入电压。结果如下衰减档位标称量程实测最佳线性区间该区间最大误差0dB1.1V0.1V~1.0V±8mV2.5dB1.5V0.15V~1.4V±12mV6dB2.2V0.2V~2.0V±18mV11dB3.3V0.3V~2.8V±35mV可以看到11dB档位虽然量程最大但误差也最大。而我们的分压后电压在2.1V左右如果直接用11dB档误差会比较大。更好的选择是用6dB档量程2.2V刚好覆盖误差也小很多。这就是为什么我选择1:2分压比而不是更大的分压比——让分压后的电压尽量落在ADC线性度最好的区间。注意不同批次的ESP32芯片ADC特性有差异以上数据仅供参考。建议你拿到板子后自己实测一遍建立校准曲线。2.3 分压电阻的选型逻辑与功耗计算分压电阻选多大这看起来是个小问题但选错了要么功耗太大要么抗干扰能力差。先算功耗。假设用两个100kΩ电阻串联分压电池电压4.2V时分压电路电流是4.2V / 200kΩ 21μA。这个电流对锂电池来说几乎可以忽略——一节2000mAh的18650理论上可以供这个分压电路工作超过10万小时。但如果用两个10kΩ电阻电流就是210μA虽然也不算大但对于需要长期待机的设备来说日积月累的消耗就不可忽视了。那是不是电阻越大越好也不是。电阻太大ADC采样时的输入阻抗匹配会出问题。ESP32的ADC输入阻抗不是无穷大采样时会有漏电流如果分压电阻太大这个漏电流会在电阻上产生额外压降导致测量误差。根据我的实测经验分压电阻的总阻值在100kΩ到500kΩ之间比较合适。我最终选了100kΩ 100kΩ的组合兼顾了低功耗和测量稳定性。电阻精度方面普通5%精度的电阻就够用因为后面还有软件校准。但如果想减少校准工作量可以用1%精度的金属膜电阻温漂也更小。我用的就是1%精度的1/4W金属膜电阻成本也就几毛钱。3. 分压电路的具体设计与PCB布局要点3.1 分压电路原理图与关键参数计算分压电路本身很简单两个电阻串联中间节点接ADC引脚。但有几个细节需要注意。首先是滤波电容。在分压节点和GND之间并联一个0.1μF的陶瓷电容可以滤掉高频噪声。这个电容的位置要尽量靠近ADC引脚否则滤波效果打折扣。我一开始没加这个电容ADC读数跳动幅度有±50mV加上之后跳动降到±10mV以内。其次是串联保护电阻。在分压节点和ADC引脚之间串一个1kΩ电阻可以限制流入引脚的电流起到保护作用。这个电阻和ADC引脚的输入电容会形成一个低通滤波器截止频率大约在1.6MHz左右对电池电压这种直流信号没有影响但能进一步抑制高频干扰。完整的电路连接是这样的电池正极接R1100kΩ一端R1另一端接R2100kΩ一端和C10.1μF一端R2另一端接GNDC1另一端接GNDR1和R2的连接点通过R31kΩ接到ESP32的ADC引脚比如GPIO34。分压比的计算Vadc Vbat × R2 / (R1 R2) Vbat × 100k / 200k Vbat / 2。所以Vbat Vadc × 2。这个系数后面在代码里会用到。3.2 PCB布局中容易被忽略的接地与走线细节如果你是把分压电路搭在面包板上那布局影响不大。但如果是画PCB有几个点值得注意。第一分压电路的GND要尽量靠近ESP32的GND引脚走线要短而粗。ADC测量的是相对GND的电压如果GND走线有压降测量就不准。我一般会在ADC引脚附近放一个GND过孔直接连到板子的地平面。第二分压节点到ADC引脚的走线要尽量短避免经过高频信号线附近。ESP32的WiFi天线区域、晶振区域都是噪声源走线要避开。如果实在避不开可以在走线两侧包地减少耦合。第三滤波电容C1要尽量靠近ADC引脚放置而不是靠近分压电阻。因为电容的作用是滤掉引脚处的噪声放在引脚旁边效果最好。第四如果你用的是ESP32模组而不是裸芯片注意模组的ADC引脚可能已经内部连接了其他电路。比如某些开发板的GPIO34可能接了其他外设使用前要确认原理图。3.3 实测对比有无滤波电容的ADC读数差异为了让你直观感受滤波电容的作用我做了一组对比测试。电池电压稳定在3.85V分压后理论值1.925V用6dB衰减档连续采样100次记录读数的波动范围。条件平均值最大偏差标准差无滤波电容1.918V±52mV18.3mV加0.1μF电容1.924V±9mV3.1mV加0.1μF1kΩ串联电阻1.925V±6mV2.2mV可以看到加上滤波电容后波动幅度从52mV降到了9mV效果非常明显。再串上1kΩ电阻后进一步降到6mV。这个精度对于电池电量监测来说已经足够了。提示如果你发现加了电容后读数反而偏大或偏小可能是电容漏电流导致的。换用质量好一点的C0G或X7R材质陶瓷电容避免用Y5V材质的。4. ESP32 ADC的软件配置与采样策略4.1 Arduino环境下ADC衰减与分辨率的正确设置在Arduino环境下使用ESP32的ADC最常用的函数是analogRead()。但这个函数默认使用11dB衰减分辨率是12位0~4095。如果你直接用它读电池电压精度不会太好。更好的做法是用analogSetAttenuation()手动设置衰减档位并用analogReadResolution()设置分辨率。我的配置是这样的// 设置ADC衰减为6dB适合0~2.2V输入 analogSetAttenuation(ADC_6db); // 设置分辨率为12位 analogReadResolution(12); // 设置ADC引脚 const int ADC_PIN 34;这里有个细节analogSetAttenuation()必须在analogRead()之前调用而且对所有ADC引脚生效。如果你同时用多个ADC引脚且需要不同衰减那就得用ESP-IDF的底层API了Arduino封装不支持按引脚单独设置。另外ESP32的ADC2在WiFi开启时无法使用这是硬件限制。所以如果你需要同时用WiFi和ADC必须用ADC1的引脚GPIO32~GPIO39。我用的GPIO34就是ADC1的通道WiFi开启时也能正常采样。4.2 多次采样与中值滤波的代码实现单次采样受噪声影响大必须做多次采样然后滤波。我试过几种滤波方案算术平均、中值滤波、中值平均组合。实测下来中值平均的组合效果最好既能去掉脉冲噪声又能平滑随机噪声。具体做法是连续采样15次去掉最大和最小的3个值剩下的9个值取平均。这样既避免了极端值的影响又保留了足够的样本做平均。代码实现如下#define SAMPLE_COUNT 15 #define DISCARD_COUNT 3 float readBatteryVoltage() { int samples[SAMPLE_COUNT]; // 连续采样 for (int i 0; i SAMPLE_COUNT; i) { samples[i] analogRead(ADC_PIN); delayMicroseconds(100); } // 冒泡排序 for (int i 0; i SAMPLE_COUNT - 1; i) { for (int j 0; j SAMPLE_COUNT - 1 - i; j) { if (samples[j] samples[j 1]) { int temp samples[j]; samples[j] samples[j 1]; samples[j 1] temp; } } } // 去掉最大和最小的各3个值取中间9个的平均 long sum 0; for (int i DISCARD_COUNT; i SAMPLE_COUNT - DISCARD_COUNT; i) { sum samples[i]; } float avgRaw (float)sum / (SAMPLE_COUNT - 2 * DISCARD_COUNT); // 转换为电压12位ADC6dB衰减参考电压约2.2V float vadc avgRaw / 4095.0 * 2.2; // 分压比1:2电池电压是ADC电压的2倍 float vbat vadc * 2.0; return vbat; }这段代码里delayMicroseconds(100)是为了让ADC采样保持电路有足够时间稳定。如果你采样太快相邻两次采样会相互影响。实测下来100μs的间隔比较合适15次采样总共耗时约1.5ms对大多数应用来说完全可以接受。4.3 采样时机与WiFi共存时的噪声规避如果你的项目同时用了WiFiADC采样时机会影响结果。WiFi射频工作时会产生周期性电流脉冲在电源和GND上产生噪声进而影响ADC读数。我的做法是在WiFi发送数据的间隙采样并且采样时暂时关闭WiFi的省电模式。具体来说可以在loop()里用一个状态机WiFi发送完成后置一个标志位ADC采样在标志位置位后进行。另外WiFi.setSleep(false)可以关闭WiFi省电模式减少射频开关带来的噪声代价是功耗会高一些。如果你对功耗不敏感还可以在ADC采样前短暂关闭WiFi射频采样完成后再打开。不过这个操作比较复杂需要调用底层API一般项目没必要这么做。实测数据WiFi开启且不采取任何措施时ADC读数波动约±30mV采用上述时机规避后波动降到±8mV左右。效果还是很明显的。5. 从原始ADC值到准确电压的校准方法5.1 两点校准法的原理与操作步骤即使做了滤波原始ADC读数换算出的电压和实际电压之间还是有偏差。这个偏差来自多个方面ADC参考电压的个体差异、分压电阻的精度、ADC非线性等。要消除这个偏差必须做校准。最实用的校准方法是两点校准法。原理很简单ADC的输入输出关系近似线性用两个已知电压点确定这条直线就可以推算出任意电压对应的实际值。操作步骤用高精度可调电源输出3.0V接到分压电路输入端记录ADC换算出的电压值V1_meas实际值为V1_real3.0V。调整电源输出4.2V记录ADC换算值V2_meas实际值V2_real4.2V。计算校准系数gain (V2_real - V1_real) / (V2_meas - V1_meas)offset V1_real - gain × V1_meas。之后每次测量实际电压 gain × 测量值 offset。我实测的一组数据V1_meas2.94VV2_meas4.15V。计算得gain (4.2-3.0)/(4.15-2.94) 1.2/1.21 0.9917offset 3.0 - 0.9917×2.94 0.084V。校准后在3.0V~4.2V范围内最大误差从原来的±0.15V降到了±0.02V。5.2 校准数据的存储与自动加载校准系数算出来后不能每次上电都重新校准。可以把它们存在ESP32的NVS非易失性存储里开机时自动加载。Arduino环境下可以用Preferences库来操作NVS#include Preferences.h Preferences prefs; void saveCalibration(float gain, float offset) { prefs.begin(battery, false); prefs.putFloat(gain, gain); prefs.putFloat(offset, offset); prefs.end(); } bool loadCalibration(float gain, float offset) { prefs.begin(battery, true); if (!prefs.isKey(gain)) { prefs.end(); return false; } gain prefs.getFloat(gain, 1.0); offset prefs.getFloat(offset, 0.0); prefs.end(); return true; }第一次使用时通过串口发送校准命令触发校准流程并保存系数。之后每次上电loadCalibration()会自动读取。如果读取失败比如第一次使用就用默认值gain1.0、offset0.0并提示用户需要校准。注意NVS有写入寿命限制约10万次不要频繁写入。校准系数只在手动校准时写入一次完全没问题。5.3 温度漂移对长期监测的影响与应对如果你做的设备需要在不同温度环境下工作还要考虑温漂。分压电阻的温漂、ADC参考电压的温漂都会影响测量结果。普通电阻的温漂在100ppm/°C左右温度变化30°C阻值变化0.3%对应电压测量误差约0.3%。对于精度要求不高的电量显示这个误差可以接受。但如果要做精确的电池容量计量就需要用低温漂电阻如25ppm/°C的精密电阻或者加入温度补偿。ESP32芯片内部有一个温度传感器虽然精度不高±5°C左右但用来做趋势补偿足够了。可以在校准数据里同时记录校准时的温度运行时根据当前温度做线性补偿。不过这个做法比较复杂一般项目用不上这里就不展开了。6. 电量百分比换算与低电量告警的工程实现6.1 锂电池放电曲线与电压到电量的映射电压测准了接下来要把电压换算成电量百分比。很多人直接用线性映射3.0V对应0%4.2V对应100%。但锂电池的放电曲线不是线性的中间有一段很平坦的区域电压从3.9V降到3.6V电量可能从70%降到了20%。如果用线性映射电量显示会很不准。更合理的做法是用分段线性映射根据锂电池的典型放电曲线设置几个关键点。我用的映射表如下电池电压电量百分比4.20V100%4.05V90%3.95V75%3.85V55%3.75V35%3.65V20%3.50V8%3.30V3%3.00V0%在代码里用查表线性插值实现float voltageToPercent(float vbat) { const float voltageTable[] {4.20, 4.05, 3.95, 3.85, 3.75, 3.65, 3.50, 3.30, 3.00}; const float percentTable[] {100, 90, 75, 55, 35, 20, 8, 3, 0}; const int TABLE_SIZE 9; if (vbat voltageTable[0]) return 100.0; if (vbat voltageTable[TABLE_SIZE - 1]) return 0.0; for (int i 0; i TABLE_SIZE - 1; i) { if (vbat voltageTable[i] vbat voltageTable[i 1]) { float ratio (vbat - voltageTable[i 1]) / (voltageTable[i] - voltageTable[i 1]); return percentTable[i 1] ratio * (percentTable[i] - percentTable[i 1]); } } return 0.0; }这个映射表是根据多节18650电池的实测放电数据整理的不同品牌和容量的电池会有差异你可以根据自己的电池型号做微调。6.2 低电量告警的阈值设定与迟滞处理低电量告警的阈值设定要考虑两个问题一是避免电量显示在阈值附近反复跳变二是给用户留出足够的反应时间。我一般设两级告警低于20%时黄色预警低于5%时红色告警并触发保护动作比如保存数据后关机。但直接用固定阈值会有抖动问题——电量在20%附近波动时告警会反复触发和解除。解决办法是加迟滞触发阈值和解除阈值分开。比如20%触发预警但只有回到25%以上才解除预警。代码实现bool lowBatteryWarning false; bool criticalBatteryWarning false; void checkBattery(float percent) { // 预警低于20%触发高于25%解除 if (!lowBatteryWarning percent 20.0) { lowBatteryWarning true; Serial.println(低电量预警); } else if (lowBatteryWarning percent 25.0) { lowBatteryWarning false; Serial.println(电量恢复); } // 严重告警低于5%触发高于8%解除 if (!criticalBatteryWarning percent 5.0) { criticalBatteryWarning true; Serial.println(严重低电量准备关机); // 这里可以触发保存数据和关机流程 } else if (criticalBatteryWarning percent 8.0) { criticalBatteryWarning false; } }迟滞窗口的大小要根据实际波动情况调整。如果ADC读数波动大迟滞窗口就要宽一些。我一般设5%的迟滞实测下来不会出现反复跳变。6.3 完整代码整合与实测效果展示把前面所有部分整合起来完整的电池监测代码大约150行左右。主循环里每秒钟采样一次更新电压和电量显示检查告警条件。实测下来整套方案的精度和稳定性都很好。我在3.0V~4.2V范围内取了10个测试点每个点用高精度万用表测量实际电压对比ESP32监测值。校准后最大误差0.018V平均误差0.007V。电量百分比显示在放电过程中平滑下降没有跳变。低电量告警准确触发迟滞逻辑工作正常。功耗方面分压电路持续消耗约21μAESP32在WiFi开启时平均电流约80mAADC采样本身增加的功耗可以忽略。整体方案对电池续航的影响很小。提示如果你需要进一步降低功耗可以在不采样时用MOS管切断分压电路的供电采样前再打开。不过这样会增加电路复杂度一般项目没必要。7. 实际部署中遇到的几个典型问题与解决思路7.1 ADC读数跳变严重时的排查顺序如果你发现ADC读数跳变很大不要急着改代码先按这个顺序排查第一步检查硬件连接。分压电阻是否焊好滤波电容是否装反ADC引脚是否接触良好我遇到过好几次是面包板接触不良导致的换了杜邦线就好了。第二步用万用表测量分压节点的实际电压和ADC读数对比。如果万用表读数稳定而ADC读数跳变说明问题在ADC或软件如果万用表读数也跳说明问题在电源或分压电路。第三步检查电源质量。如果电池直接供电通常没问题如果经过DC-DC转换器开关噪声可能耦合到ADC。可以在分压电路前加一个LC滤波器试试。第四步检查WiFi是否开启。如前所述WiFi开启时ADC2不可用ADC1也会受噪声影响。尝试关闭WiFi看读数是否稳定。第五步检查采样代码。是否设置了正确的衰减档位采样间隔是否足够滤波算法是否正确按这个顺序排查基本能定位到问题所在。7.2 不同ESP32开发板ADC特性差异的应对不同厂家、不同批次的ESP32开发板ADC特性可能有差异。我手头有三块不同的ESP32开发板用同样的代码和电路读出的电压值能差0.1V以上。这不是代码问题是芯片本身的个体差异。应对方法很简单每块板子单独校准。校准系数存在各自的NVS里互不影响。如果你做的是小批量产品每台出厂前都做一次两点校准把系数写入NVS就能保证一致性。另外ESP32-S3和ESP32-C3的ADC特性又和ESP32不同量程和线性度都有变化。如果你换芯片型号校准参数要重新测。ESP32-S3的ADC在6dB衰减下量程约2.5V比ESP32略大分压比可以适当调整。7.3 长期运行中的数值漂移与自恢复策略设备连续运行几周后你可能会发现电压读数慢慢偏了。这可能是温度变化导致的也可能是ADC参考电压漂移。如果偏差不大几十毫伏可以忽略如果影响到了电量显示就需要处理。我的做法是加一个自恢复逻辑设备每次重启时如果检测到电池电压在4.1V以上说明刚充满或接近充满就自动用这个点做一次单点校准修正offset。这样不需要人工干预设备就能保持长期准确。当然这个逻辑的前提是你知道电池什么时候是满电的。如果你的设备有充电管理芯片可以读取充电状态引脚在充满时触发校准。如果没有就只能在固件里定期提醒用户手动校准了。8. 写在最后的一些实操体会这套方案我从最初的想法到最终稳定运行前后改了四五版。最大的体会是ADC测量这件事硬件和软件同样重要。分压电路没设计好软件再怎么滤波也白搭软件没做校准硬件再精密也有偏差。另外不要迷信官方文档给的参数。ESP32的ADC文档写得很粗略实际特性要靠自己实测。我建议每个做电池监测的人都花半个小时用可调电源和万用表做一组实测数据建立自己板子的校准曲线。这半个小时的投资能帮你省掉后面无数次的调试。最后说一个容易被忽略的点电池电压监测的目的是为了知道剩余电量而电量估算本身就有不确定性。锂电池的放电曲线受温度、放电倍率、老化程度影响同样的电压在不同条件下对应的电量可能差很多。所以电量显示不用追求绝对精确能做到趋势正确、不跳变、低电量及时告警就已经很好了。
阅读完成 · 觉得有帮助?