先回答测到哪里,再谈它能代表什么
本篇标题中的“电量”是读者的问题,完成物只是一份带条件的电压记录。锂电池端电压受电流、温度、老化、化学体系和静置恢复影响,不能把某两个电压点直线映射为0–100%,更不能由一次读数生成剩余小时数。
这次换用有公开原厂分压设计的经典ESP32 Feather V2 PID5400 Rev F,不沿用本系列S3板设置。固定Eagle commit cbde2c6…可追踪R1/R3/C5和ADC点;实际板必须先核修订。固定设计含CP2102N,后来实物可能改用CH9102F;原厂不一致时先查对应图与桥驱动,不把板名相似当全部布局相同。S3 Feather的I2C电量计路线不适用。
参考:[1] Adafruit固定RevF原厂Eagle/Pinout[2] 原厂Feather V2引脚/电源/电池分压
200k不是唯一参数,C5与测表也在电路里
原厂Rev F:R1=200kΩ从VBAT到A13/I35,R3=200kΩ从该点到GND,C5=1µF并联到GND。GPIO35是经典ESP32 ADC1_CH7、输入专用,不是S3上可被存储器占用的同号脚。本板已有分压,不另把电池正端接GPIO,也不启用内部上拉或给它输出。
名义4.2V经2:1分压为2.1V;电阻支路4.2/400k=10.5µA,均为电路算值,未包含板子/ADC/电容实际漏电。Thevenin约100kΩ与C5形成约100ms时间常数;高源阻抗与ADC采样电容、实际C5状态可能改变误差,不能只加平均次数就称准确。程序等1.5秒、丢4次,再做32组raw与mV转换且每组等2ms,是公开采样方法而非固定精度保证。
DMM接BAT/GND可比较整板输出;若直接测分压点,10MΩ表会与R3并联,使名义分压点约下降1%,还改变动态过程。记录表输入阻抗/探头电容与测点,优先用BAT端参考,不为了测量反而改变被测点又把偏差归咎软件。
参考:[1] Adafruit固定RevF原厂Eagle/Pinout[2] 原厂Feather V2引脚/电源/电池分压
USB、实际电池和负载条件写入原始记录
例用PID258带保护1S 3.7V/4.2V1200mAh LiPo,JST-PH极性匹配。固定Rev F的MCP73831T-2ACI/OT与5.1kΩ R4对应约196mA名义充电设定;这是原手册公式的计算,实际还受容差/温度/热限制影响。电池厂要求充电不超过500mA,连接前核实际电池/极性/充电器修订,断USB再插拔电池,不用2S或4.35V电芯。
USB接入可能给电池充电并改变系统供电;无电池时充电器也可能使VBAT出现电压/灯闪,不能由一个合法电压推断“有电池”。所以程序只报VBAT_NODE_ONLY,不自动识别电池或USB。空观察表要求操作者确认battery_present、usb、REST/LOAD/CHARGE、静置分钟数和温度。
USB0记录需要独立、不反供的高阻RX/记录设备,不能插回USB看串口却仍填USB0。板自身仍有工作负载,关闭无线不叫完全开路静置。低端观察遵守电池原规格,本例到3.3V附近停止,不用保护截止来“标定0%”;本程序也不承担安全切断功能。
| 组别 | 必须记录 | 能解释的范围 |
|---|---|---|
| REST / USB0 | 实际有电池、静置时间/温度、板负载 | 该条件下的端电压对照 |
| LOAD / USB0 | 固定负载及电流/持续时间 | 负载压降与恢复,不等于容量 |
| CHARGE / USB1 | USB、电池、充电状态与温度 | 充电时端电压,不混入REST拟合 |
| 无电池 / USB1 | 明确battery_present=0 | 只观察VBAT节点,不能称电池读数 |
参考:[1] Adafruit固定RevF原厂Eagle/Pinout[2] 原厂Feather V2引脚/电源/电池分压[5] MCP73831 DS20001984H充电设定[6] PID258电池原规格与极性
芯片mV校准和整板万用表校正分开
Arduino core3.3.12的analogReadMilliVolts在经典ESP32上建立line-fitting校准,失败会返回0。源码配置没有给缺eFuse情况填一个猜测default_vref;本例先查eFuse,缺出厂校准则只报raw/NO_EFUSE_CAL,不能假定所有芯片参考电压都是1100mV。设置12bit与ADC_11db,raw和mV是不同转换,不按4095与3.3V直接比例换算。
本例ADC1200–2200mV策略窗覆盖正常1S分压用途,原始近端点或任一次mV=0使该帧无效;这不是全ADC推荐量程声明。mV×2只用了名义电阻比,默认DMM_CALIBRATED=false,整板校正输出NA。core启动会开GPIO2的NeoPixel/QT电源,本例setup明确关它及GPIO13 LED;不因此声称量得低功耗。
参考:[3] Arduino3.3.12 ADC原样实现[4] Arduino3.3.12 Feather V2 variant引脚与启动电源
用此板FQBN构建,先查无电池条件
固定CLI1.5.1与Arduino-ESP32 3.3.12,无第三方库。完整FQBN在versions.lock/README/sketch.yaml:adafruit_feather_esp32_v2、FlashSize8M、FlashFreq80、default_8MB、PSRAM disabled、CPU240、Upload115200;不能照抄S3的USBMode/CDC/OPI参数。sketch.yaml不设default_profile,常规用完整--fqbn构建,显式profile仍可独立复建。
在解压目录运行README完整compile/upload命令,PORT取此板实际USB串口,再115200 monitor。先仅USB、明确无电池,确认板型/eFuse/状态,不以此电压称电池剩余量;断USB后接指定电池,按已经核过的采集方式记录。程序不写电池/Flash参数,没有自动百分比或自动充电控制。
- 做到这里,你应该看到
- 你能分清出厂ADC校准、未启用的DMM校正,以及人工确认的电池/USB条件。
原样草图、同源计算与默认校正配置
下面三文件与下载一致。每帧记录raw均值、校准ADC mV、名义VBAT节点mV、可选自己的DMM校正与状态;没有电池存在检测。BatteryVoltage只负责数值/范围,host检查证明算术及拒绝输入,不能证明ADC/eFuse/分压/电池行为。
// ---- BatteryVoltage.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <cmath>
struct Voltage {bool valid=false,dmmValid=false;double nominal=0,corrected=0;};
inline Voltage voltageEstimate(double raw,double adcMv,bool efuse,bool dmm,double gain,double offset){
Voltage v;if(!efuse||!std::isfinite(raw)||!std::isfinite(adcMv)||raw<40||raw>4080||adcMv<1200||adcMv>2200)return v;
v.nominal=adcMv*2.0;v.valid=true;
if(dmm&&std::isfinite(gain)&&std::isfinite(offset)&&gain>=0.9&&gain<=1.1&&std::abs(offset)<=300){double x=gain*v.nominal+offset;if(x>=2400&&x<=4400){v.dmmValid=true;v.corrected=x;}}return v;
}
// ---- calibration.h ----
// SPDX-License-Identifier: MIT
#pragma once
// No site measurements exist. Keep disabled until YOUR board/meter/context has
// a reference dataset and independent check rows; tool output is not automatic proof.
constexpr bool DMM_CALIBRATED=false;
constexpr double DMM_GAIN=1.0;
constexpr double DMM_OFFSET_MV=0.0;
// ---- esp32-battery-voltage-monitor.ino ----
// SPDX-License-Identifier: MIT
#include <Arduino.h>
#include "esp_adc/adc_cali_scheme.h"
#include "BatteryVoltage.h"
#include "calibration.h"
static_assert(BATT_MONITOR==35,"Only Adafruit Feather ESP32 V2 GPIO35 divider is supported");
static bool factoryCalibration=false;
void setup(){
Serial.begin(115200);
pinMode(NEOPIXEL_I2C_POWER,OUTPUT);digitalWrite(NEOPIXEL_I2C_POWER,LOW); // core initVariant initially enabled this rail
pinMode(LED_BUILTIN,OUTPUT);digitalWrite(LED_BUILTIN,LOW);
analogReadResolution(12);analogSetPinAttenuation(BATT_MONITOR,ADC_11db);
adc_cali_line_fitting_efuse_val_t efuse;esp_err_t e=adc_cali_scheme_line_fitting_check_efuse(&efuse);
factoryCalibration=e==ESP_OK&&efuse!=ADC_CALI_LINE_FITTING_EFUSE_VAL_DEFAULT_VREF;
delay(1500); // Rth~100k and C5=1uF => nominal tau~100ms; physical settling still must be checked
Serial.printf("L07 Feather ESP32 V2 PID5400 RevF; ADC1 GPIO35; efuse_api=%s factory_cal=%u; no automatic battery/USB detection\n",esp_err_to_name(e),factoryCalibration);
Serial.println("elapsed_ms,raw_mean,adc_mv,vbat_node_mv,dmm_corrected_mv,factory_cal,dmm_cal,status");
}
void loop(){
// Raw and calibrated calls are separate conversions, not the same ADC sample.
for(int i=0;i<4;i++){analogRead(BATT_MONITOR);delay(2);}
uint32_t raw=0,mv=0;bool allConversions=true;for(int i=0;i<32;i++){uint16_t x=analogRead(BATT_MONITOR);raw+=x;if(x<40||x>4080)allConversions=false;if(factoryCalibration){uint32_t m=analogReadMilliVolts(BATT_MONITOR);mv+=m;if(!m)allConversions=false;}delay(2);}
double a=raw/32.0,b=mv/32.0;Voltage v=voltageEstimate(allConversions?a:-1,b,factoryCalibration,DMM_CALIBRATED,DMM_GAIN,DMM_OFFSET_MV);
Serial.printf("%lu,%.2f,",static_cast<unsigned long>(millis()),a);
if(!factoryCalibration){Serial.println("NA,NA,NA,0,0,NO_EFUSE_CAL");}
else if(!v.valid){Serial.printf("%.2f,NA,NA,1,0,RANGE_OR_ADC_ERROR\n",b);}
else{Serial.printf("%.2f,%.2f,",b,v.nominal);if(v.dmmValid)Serial.printf("%.2f",v.corrected);else Serial.print("NA");Serial.printf(",1,%u,VBAT_NODE_ONLY\n",v.dmmValid);}
delay(1000);
}
自己的5个拟合点,再加独立核验点
至少5个REST/USB0且确认有电池的点,正常使用区间跨度≥200mV,每点等待条件稳定,同步记录程序名义VBAT和DMM BAT/GND。表型号/校准状态/误差、温度、负载、休息时间和输入阻抗不可缺。另留至少2个未参与拟合的核验点;参与拟合的残差小并不能证明新数据的准确度。
把自己的参考行填入reference.csv,运行python3 tools/fit_reference.py reference.csv。空表、NaN、跨度窄、混充电组或离谱校正会拒绝;gain/offset只描述本数据,不能抄合成检查中的数。先排电路/探头/纹波,再决定是否校正。仅在独立核验达到自己的容差后,把真实系数写入calibration.h、启用标志并重建锁源码。
核验误差用估计值−DMM值,分别列REST/LOAD/CHARGE的分布、最大绝对误差和仪表不确定度,不把不同条件混成一个误差数字。reference.csv与observations.csv现在只有表头,本站没有电池读数、精度、漏电实测或续航结果。
参考:[1] Adafruit固定RevF原厂Eagle/Pinout[3] Arduino3.3.12 ADC原样实现[5] MCP73831 DS20001984H充电设定
超窗先排实物和条件,别夹成“满电”
全0/NO_EFUSE_CAL先查实际板型、eFuse/API与串口日志;合法节点电压却无电池时是供电条件问题,不代表电池突然满电。超窗先查BAT/GND、电池保护是否断开、USB/充电、分压修订与探头;不要把异常值夹到百分比0或100。噪声先看供电/负载和采样时刻,平均不能修增益或非线性。
上传失败先查该板串口桥驱动与PORT,按原厂boot恢复步骤加载已知固件,不先erase全部Flash。保留原始CSV与修改系数的构建信息。完成本实验的判据是条件可追踪、误差有自己的核验,不能因为计算出“3.8V”就声称知道剩余50%。回看P05把ADC输入范围与校准链路再核一遍,续航需另做完整周期能量与长时间记录。
资料来源与版本说明
逐项核固定源码、原厂电路与实际构建;电压、能量、无线、ADC和电池结果仍待物理实验。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。