
1. 为什么锂电池项目都绕不开ADC电压监测做嵌入式项目只要涉及到电池供电电压监测就是一个绕不过去的坎。你想想一块锂电池从满电4.2V放到3.3V中间这段电压曲线直接反映了剩余电量如果连电压都读不准后面的低电量报警、自动休眠、电量百分比显示全都是空中楼阁。我见过太多项目电池保护板都装了结果固件里连个电压采集都没做最后设备突然断电数据丢得一塌糊涂。ESP32这颗芯片自带12位SAR型ADC理论上能读出0到4095的原始值看起来够用。但实际用起来尤其是测锂电池电压的时候坑一个接一个。首先ESP32的ADC输入范围默认是0到3.3V左右而锂电池满电4.2V直接接上去轻则读数溢出重则损伤引脚。其次ESP32的ADC线性度在接近量程两端的时候相当糟糕尤其是高电压段误差能到几十毫伏甚至上百毫伏。再一个WiFi和蓝牙开启的时候射频电路会往ADC上耦合噪声读数跳得跟心电图似的。所以这个项目的核心就三件事第一设计一个分压电路把4.2V降到ADC能安全采集的范围第二在固件里做合理的采样和滤波把噪声压下去第三把原始ADC值换算回真实的电池电压并且做校准。这三件事环环相扣任何一环没做好最终读出来的电压都是不可信的。这篇文章适合谁看如果你正在用ESP32做电池供电的便携设备、物联网节点、手持仪表或者单纯想搞清楚ESP32的ADC到底怎么用才靠谱那接下来的内容应该能帮你省下不少调试时间。我会从分压电阻的选型计算开始一路讲到固件里的滤波算法和校准方法中间穿插我实际踩过的坑和验证过的参数。2. 分压电路设计电阻选型不是随便凑两个就行2.1 分压比的计算逻辑锂电池的电压范围一般在3.0V到4.2V之间取个安全余量按2.8V到4.3V来设计。ESP32的ADC在衰减设置为11dB的时候满量程大约在3.1V到3.3V之间但为了留出线性度较好的工作区间我一般把ADC输入电压控制在2.5V以下。这样分压比就是分压比 VADC_max / VBAT_max 2.5V / 4.3V ≈ 0.581也就是说分压后的电压应该是电池电压的58.1%左右。用两个电阻R1和R2组成分压电路R1接电池正极和ADC引脚之间R2接ADC引脚和地之间那么VADC VBAT × R2 / (R1 R2)所以R2/(R1R2) 0.581反过来R1/R2 (1-0.581)/0.581 ≈ 0.721。也就是说R1大约是R2的0.72倍。常见的电阻值组合比如R1100kΩR2150kΩ算下来分压比是150/(100150)0.6满电4.2V时分压后是2.52V刚好在安全范围内。或者R182kΩR2120kΩ分压比0.594满电分压后2.49V也很合适。这里有个细节要注意电阻值不能选太小否则分压电路本身会持续耗电。比如用10kΩ和15kΩ电池4.2V的时候分压支路的电流是4.2V/25kΩ168μA对于大容量电池来说不算什么但对于那种用纽扣电池或者小容量锂电的项目这个静态功耗就很可观了。所以我一般选100kΩ以上的电阻把静态电流压到几十微安以下。但电阻也不能太大否则ADC采样的时候内部采样电容充电时间会变长导致读数不准。ESP32的ADC采样保持时间可以通过analogSetCycles()调整但电阻太大还是会影响精度。实测下来100kΩ到200kΩ这个区间是比较平衡的。2.2 电阻精度与温漂的影响分压电阻的精度直接决定了电压测量的精度。如果你用5%精度的普通碳膜电阻分压比本身就有±5%的误差换算到4.2V上就是±0.21V这还没算ADC本身的误差。所以至少要用1%精度的金属膜电阻最好是用0.1%的。我试过用5%的电阻同一块板子换两个电阻读数能差0.15V根本没法做电量显示。温漂也是个容易被忽略的点。普通电阻的温漂在100ppm/℃到200ppm/℃左右如果设备工作在户外温度从-10℃到50℃变化60℃分压比可能漂移0.6%到1.2%对应到4.2V就是25mV到50mV的误差。对于电量显示来说这个误差还能接受但如果要做精确的电压报警比如3.3V截止那就得选温漂更小的电阻或者做温度补偿。还有一个实操中的小技巧分压电阻尽量选同一批次、同一阻值系列的这样它们的温漂方向一致分压比的变化会相互抵消一部分。比如都用厚膜电阻或者都用薄膜电阻不要混着用。2.3 滤波电容的配置在ADC引脚和地之间并一个电容可以滤掉高频噪声尤其是WiFi工作时耦合过来的干扰。电容值一般选0.1μF到1μF之间。0.1μF对高频噪声效果好但对低频波动没什么用1μF能平滑掉更多的低频波动但响应速度会变慢。我一般用0.1μF和1μF并联一个管高频一个管低频。但要注意电容太大会导致ADC采样时充电不足。ESP32的ADC输入阻抗不是无穷大采样的时候会从分压电路抽取电流如果电容太大每次采样前电容上的电压还没恢复到稳定值读数就会偏低。实测下来1μF以内问题不大10μF以上就明显影响响应速度了。另外如果分压电阻选得比较大比如200kΩ以上电容的充电时间常数会变大这时候要么减小电容要么增加ADC的采样保持时间。ESP32的Arduino核心提供了analogSetCycles()和analogSetSamples()来调整采样参数后面固件部分会详细说。3. ESP32 ADC的固件配置与采样策略3.1 ADC衰减与量程设置ESP32的ADC默认量程是0到1.1V左右要测更高的电压必须设置衰减。Arduino核心提供了analogSetAttenuation()函数常用的参数有衰减设置满量程电压适用场景ADC_0db1.1V小信号采集ADC_2_5db1.5V一般传感器ADC_6db2.2V中等电压ADC_11db3.3V电池电压监测对于锂电池监测选ADC_11db满量程约3.3V。但前面说了为了线性度实际工作区间控制在2.5V以下。设置代码很简单analogSetAttenuation(ADC_11db);但这里有个坑不同批次的ESP32芯片ADC的参考电压和衰减特性有差异同一段代码在不同板子上读出来的值可能差5%到10%。所以量产的时候必须做校准这个后面会讲。3.2 采样次数与滤波算法ESP32的ADC读数跳动是出了名的尤其是WiFi开启的时候。我实测过单次采样的话同一个电压点连续读100次最大值和最小值能差200个LSB约160mV。所以必须做多次采样然后滤波。最简单的做法是读N次取平均。N一般取16到64。但平均值滤波对脉冲噪声效果不好如果某次采样刚好赶上WiFi发射的瞬间那个异常值会把平均值拉偏。所以我一般用中值滤波加平均滤波的组合先连续读N次去掉最大和最小的几个值剩下的取平均。#define SAMPLE_COUNT 32 int readADC() { 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 i 1; j SAMPLE_COUNT; j) { if (samples[i] samples[j]) { int temp samples[i]; samples[i] samples[j]; samples[j] temp; } } } // 去掉最大最小各4个剩下的取平均 long sum 0; for (int i 4; i SAMPLE_COUNT - 4; i) { sum samples[i]; } return sum / (SAMPLE_COUNT - 8); }这个函数读32次排序后去掉头尾各4个剩下24个取平均。实测下来读数波动能控制在±5个LSB以内对应电压波动约±4mV完全够用了。3.3 采样时机的选择采样时机也很关键。如果设备同时开了WiFi最好在WiFi空闲的时候采样或者干脆在WiFi关闭的间隙采样。我试过在WiFi持续传输的时候采样即使做了滤波读数还是会有10mV左右的周期性波动跟WiFi的信标帧周期对得上。如果项目对电压精度要求高可以在采样前临时关闭WiFi采样完再打开。但这样会影响网络连接的实时性需要根据具体场景权衡。另一个办法是增加采样次数用更长的采样窗口来平滑掉周期性干扰。还有一个细节ADC采样的时候CPU最好别干别的事。如果采样过程中发生了中断或者任务切换可能会影响采样保持电路的时序。我一般把采样放在一个独立的函数里采样期间关中断采完再开。4. 电压换算与校准从ADC值到真实电压4.1 理论换算公式拿到滤波后的ADC值换算成电压的公式是VADC ADC值 / 4095 × Vref其中Vref是ADC的参考电压理论上3.3V但实际值每个芯片都不一样。然后根据分压比反推电池电压VBAT VADC × (R1 R2) / R2把两个公式合起来VBAT ADC值 / 4095 × Vref × (R1 R2) / R2假设Vref3.3VR1100kΩR2150kΩ那么VBAT ADC值 / 4095 × 3.3 × 250 / 150 ADC值 / 4095 × 5.5也就是说ADC值每变化1电池电压变化约1.34mV。满电4.2V对应ADC值约31273.3V对应ADC值约2457。但这个公式算出来的值只能作为参考实际误差可能有好几十毫伏。原因有三个一是Vref不是精确的3.3V二是分压电阻有误差三是ADC本身有非线性。4.2 实测校准方法校准的方法很简单用高精度万用表测电池的实际电压同时读ESP32的ADC值然后拟合一条曲线。我一般取两个点一个高电压点比如4.1V和一个低电压点比如3.4V分别记录ADC值然后算斜率和截距。// 校准参数 float slope 0.00134; // 每个ADC值对应的电压 float intercept 0.0; // 截距 float getBatteryVoltage() { int adcValue readADC(); return adcValue * slope intercept; }校准的时候要注意电池电压要稳定最好在开路状态下测不要带负载。带负载的时候电池内阻会导致端电压下降测出来的值会偏低。如果非要在带负载的时候校准那就要记录负载电流后面做内阻补偿。我一般会在固件里留一个校准模式通过串口输入实际电压固件自动计算slope和intercept并保存到NVS里。这样每块板子出厂前校准一次精度能到±10mV以内。4.3 多点校准与分段线性化如果对精度要求更高比如要做电量计那就需要多点校准。因为ESP32的ADC在不同电压段的非线性程度不一样低电压段误差小高电压段误差大。我一般取5个点3.3V、3.5V、3.7V、3.9V、4.1V分别记录ADC值然后在固件里做分段线性插值。// 校准点表 const int calPoints 5; float calVoltages[calPoints] {3.3, 3.5, 3.7, 3.9, 4.1}; int calADCValues[calPoints] {2457, 2606, 2755, 2904, 3053}; float getCalibratedVoltage(int adcValue) { // 分段线性插值 for (int i 0; i calPoints - 1; i) { if (adcValue calADCValues[i] adcValue calADCValues[i1]) { float ratio (float)(adcValue - calADCValues[i]) / (calADCValues[i1] - calADCValues[i]); return calVoltages[i] ratio * (calVoltages[i1] - calVoltages[i]); } } // 超出范围的处理 if (adcValue calADCValues[0]) return calVoltages[0]; return calVoltages[calPoints-1]; }这个方法实测下来全电压范围内误差能控制在±5mV以内对于电量显示来说绰绰有余。5. 常见问题与避坑指南5.1 ADC读数跳动严重怎么办这是最常见的问题。首先检查分压电阻和滤波电容是否焊好尤其是电容虚焊的话等于没有滤波。然后检查采样代码确保做了多次采样和滤波。如果还是跳看看WiFi是不是开着尝试在WiFi空闲时采样。最后检查电源如果电池供电的LDO输出纹波大ADC参考电压也会跟着抖读数自然不稳。我遇到过一次读数跳得特别厉害查了半天发现是分压电阻的一头虚焊接触电阻忽大忽小。所以焊接质量一定要保证尤其是高阻值的电阻虚焊的影响比低阻值更明显。5.2 满电时读数偏低锂电池满电4.2V分压后2.52V按理说应该在ADC量程内。但如果读数明显偏低比如只有3.9V那可能是分压比算错了或者电阻值不对。用万用表测一下分压点的实际电压如果跟理论值差很多那就是电阻的问题。另一个可能是ADC衰减设置不对如果设成了6db满量程只有2.2V2.52V就溢出了读数会卡在最大值附近。还有一种情况电池刚充满的时候端电压会虚高静置一段时间后会降到4.15V左右。如果刚充完电就测读数偏高是正常的。5.3 WiFi开启后电压读数漂移WiFi发射的时候瞬间电流能到几百毫安电池内阻和PCB走线电阻上会产生压降导致电池端电压瞬间下降。如果采样刚好赶上这个瞬间读数就会偏低。解决办法一是增加采样次数用平均滤波平滑掉二是采样前临时关闭WiFi三是在电池和LDO之间加一个大电容比如100μF把瞬间压降补上。我一般用第二种和第三种结合采样前关WiFi同时硬件上加电容。实测下来WiFi开启和关闭时的电压读数差异能控制在5mV以内。5.4 低电量时读数不准锂电池放电末期电压曲线变得很陡从3.5V到3.3V可能只放出5%的电量。这时候ADC的微小误差会导致电量显示跳变。解决办法是在低电量段增加校准点或者用电压加电流积分的方式做电量计而不是单纯看电压。另外低电量时电池内阻变大带负载时端电压下降更明显。如果设备有电机或者屏幕背光这种大电流负载低电量时电压读数会明显偏低导致提前报警。这时候需要做内阻补偿或者在大电流负载关闭的间隙采样。5.5 常见问题速查表现象可能原因排查方法解决措施读数跳动大滤波不足、WiFi干扰、虚焊示波器看ADC引脚波形增加滤波、关WiFi采样、补焊满电读数偏低分压比错误、衰减设置不对万用表测分压点电压重新计算电阻、设ADC_11dbWiFi开启后漂移射频干扰、电池压降对比WiFi开关时的读数加电容、采样前关WiFi低电量跳变电压曲线陡、内阻大记录放电曲线增加校准点、内阻补偿读数始终为0引脚配置错误、电阻开路检查代码和硬件重设引脚、更换电阻读数始终为4095分压比过小、衰减不够测分压点电压调整电阻、增大衰减6. 进阶技巧让电压监测更可靠6.1 使用内部参考电压校准ESP32有一个内部的1.1V参考电压虽然精度也不高但它的温漂比外部3.3V参考小。可以用这个内部参考来反推Vref的实际值从而提高测量精度。具体做法是先测内部参考的ADC值然后根据已知的1.1V反推Vref。// 读取内部参考电压的ADC值 int refADC analogRead(ADC_INTERNAL_REF); // 反推Vref float vref 1.1 * 4095.0 / refADC;这个方法我实测过能把Vref的误差从±5%降到±1%以内。但要注意内部参考的1.1V本身也有误差所以这个方法只能提高一致性不能提高绝对精度。要绝对精度还是得外部校准。6.2 电池内阻补偿如果设备工作电流变化大电池内阻会导致端电压波动。可以在固件里记录不同电流下的电压拟合出内阻然后做补偿。简单做法是在已知负载电流I的情况下补偿电压 I × R_internal。R_internal对于18650电池一般在50mΩ到150mΩ之间对于软包电池在100mΩ到300mΩ之间。float getCompensatedVoltage(float loadCurrent) { float rawVoltage getBatteryVoltage(); float internalResistance 0.1; // 100mΩ return rawVoltage loadCurrent * internalResistance; }这个方法在电机启动、屏幕亮起等大电流场景下特别有用能避免因为瞬间压降导致的误报警。6.3 低功耗采样策略对于电池供电的设备电压监测本身也要省电。分压电阻的静态电流虽然只有几十微安但长期累积也不小。我一般用MOS管控制分压电路的通断只在采样的时候打开采完就关。这样静态电流能降到1μA以下。#define DIVIDER_EN_PIN 25 float readBatteryVoltage() { digitalWrite(DIVIDER_EN_PIN, HIGH); delay(10); // 等待分压稳定 int adcValue readADC(); digitalWrite(DIVIDER_EN_PIN, LOW); return adcValue * slope intercept; }这个技巧对于太阳能供电或者长期部署的传感器节点特别实用能把电池寿命延长好几倍。6.4 温度补偿锂电池的电压曲线跟温度有关低温下内阻变大端电压偏低。如果设备工作在宽温范围最好加一个温度传感器根据温度修正电压读数。简单的做法是在低温时给电压读数加一个正的偏移量高温时加一个负的偏移量。具体偏移量需要根据电池型号实测。我一般用ESP32内置的温度传感器做粗略补偿虽然精度不高但比不补偿强。如果要精确补偿就外挂一个DS18B20或者NTC热敏电阻。7. 我的实操体会这个项目我从头到尾做了大概三版硬件第一版分压电阻用了5%精度的读数飘得没法看第二版换了1%电阻加了滤波电容好多了但WiFi一开还是跳第三版加了MOS管控制分压电路采样前关WiFi终于稳定了。固件方面从最简单的单次采样到平均滤波再到中值加平均最后到分段线性校准每一步都是被实际问题逼出来的。如果你刚开始做我的建议是电阻至少用1%精度100kΩ以上ADC衰减设11db采样至少32次去掉头尾取平均固件里留校准接口每块板子单独校准。做到这几点电压监测的精度和稳定性就够用了。还有一个容易被忽略的点PCB布局。分压电阻尽量靠近ADC引脚走线尽量短远离WiFi天线和电源开关节点。我试过把分压电阻放在板子另一头走线长了10厘米读数噪声明显变大。后来改到ADC引脚旁边噪声立刻降下来了。最后分享一个调试技巧如果读数不对先用万用表测分压点的实际电压跟ADC读数换算出来的电压对比。如果万用表读数和理论值一致但ADC读数不对那就是ADC配置或校准的问题如果万用表读数就跟理论值不一致那就是分压电路的问题。这样能快速定位问题出在硬件还是固件。