尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ESP32-C3 DIY智能电池盒:ADC电压检测与手机电量显示实战

ESP32-C3 DIY智能电池盒:ADC电压检测与手机电量显示实战 1. 从零拆解一个智能电池盒的真实需求1.1 为什么我要做这个东西去年冬天我迷上了户外露营带出去的设备越来越多——手机、运动相机、露营灯、蓝牙音箱每一样都要充电。市面上卖的电池盒要么只能看个大概电量要么贵得离谱一个带数显的 18650 电池盒动辄上百块。我手头正好有几块 ESP32-C3 的开发板就想着干脆自己做一个能实时检测电池电压、把电量百分比显示在手机上、成本控制在三十块以内。这个项目听起来简单但真正动手之后才发现坑不少。ESP32-C3 的 ADC 精度一直被大家吐槽分压电阻选不好误差能到 10% 以上手机端显示又涉及到蓝牙或者 WiFi 通信的选型。我把整个过程完整记录了下来包括分压电阻的计算方法、ADC 采样的滤波处理、以及手机端电量显示的几种实现方案。如果你也在做类似的电池监测项目或者单纯想搞明白 ADC 到底怎么用这篇内容应该能帮你省下不少试错时间。1.2 这个电池盒能做什么先把这个项目的功能边界说清楚。核心功能有三个第一通过 ADC 实时采集电池电压采样频率大概每秒一次就够了第二把采集到的电压换算成电量百分比这里需要根据电池的放电曲线做映射第三把电量数据发送到手机上显示可以用蓝牙 BLE 也可以用 WiFi 网页我两种都试过后面会详细对比。适合谁来参考呢如果你有基本的 Arduino 或者 ESP-IDF 开发经验能看懂简单的 C 语言代码这个项目完全可以直接复现。如果你连电阻分压是什么都不知道也没关系我会从最基础的分压原理讲起保证你能跟上。整个项目的硬件成本大概在二十五到三十五块之间取决于你用什么样的电池和外壳。1.3 核心关键词速览在正式开始之前先把几个核心概念快速过一遍方便你建立整体认知。ESP32-C3是乐鑫推出的一款 RISC-V 架构的 WiFiBLE 芯片价格便宜开发环境成熟缺点是 ADC 只有 12 位且线性度一般。ADC是模数转换器把模拟电压信号转换成数字值ESP32-C3 的 ADC 量程可以通过衰减器配置默认 0-3.3V 左右。分压电阻计算器是我自己写的一个小工具输入电池最高电压和 ADC 参考电压自动算出两个电阻的比值和推荐阻值。手机电量显示这块我最终选择了 BLE 方案因为功耗低、连接稳定手机端用现成的 BLE 调试助手就能看到数据。2. 硬件选型与分压电路设计2.1 为什么选 ESP32-C3 而不是其他芯片市面上能做电池监测的芯片太多了STM32、GD32、甚至 51 单片机都能干这活。我选 ESP32-C3 的理由很实际第一它自带 WiFi 和 BLE不用额外加通信模块省了硬件成本和布线麻烦第二它的开发板便宜我买的那块不到十五块烧录器都省了直接用 USB 线就能下载程序第三它的低功耗模式做得不错深度睡眠电流可以做到几十微安对于电池盒这种需要长期待机的设备来说很关键。当然它也有明显的短板。ESP32-C3 的 ADC 只有 12 位分辨率理论精度是 3.3V/4096 约等于 0.8mV但实际有效位数可能只有 9 到 10 位也就是说实际精度在 3mV 到 6mV 左右。另外它的 ADC 线性度在接近量程两端的时候比较差所以分压电路的设计要尽量让电池电压落在 ADC 量程的中间区域。这些坑我在后面会详细说怎么规避。2.2 分压电阻的计算逻辑电池电压通常高于 ESP32-C3 的 ADC 量程比如单节锂电池满电是 4.2V而 ADC 默认量程只有 3.3V 左右直接接上去会烧引脚。所以必须用两个电阻组成分压电路把电池电压按比例降下来。分压公式很简单Vout Vin × R2 / (R1 R2)。其中 Vin 是电池电压Vout 是送到 ADC 引脚的电压R1 是上端电阻R2 是下端电阻。我们需要确定的是 R1 和 R2 的比值让 Vin 最大时 Vout 不超过 ADC 的安全量程。假设电池最高电压是 4.2VADC 最大安全输入是 3.3V那么分压比就是 3.3/4.2 ≈ 0.786。也就是说 R2/(R1R2) 0.786反过来 R1/R2 (1-0.786)/0.786 ≈ 0.272。如果选 R2 100kΩ那么 R1 ≈ 27.2kΩ取标准值 27kΩ 或者 30kΩ 都可以。但这里有个关键点电阻不能选太大也不能选太小。选太大比如 R1 和 R2 都是兆欧级别ADC 输入阻抗会受影响采样值会偏低选太小比如几百欧姆电阻上会持续消耗电流电池待机时间会缩短。我实测下来R1 在 20kΩ 到 50kΩ 之间R2 在 68kΩ 到 150kΩ 之间比较合适。最终我选了 R1 30kΩR2 100kΩ分压比是 100/130 ≈ 0.7694.2V 输入时 ADC 端电压是 3.23V留了一点余量很安全。2.3 分压电阻计算器的实现思路手动算电阻虽然不难但每次换电池类型都要重新算一遍太麻烦。我就用 Python 写了一个小脚本输入电池最高电压和 ADC 参考电压自动输出推荐阻值和分压比。核心逻辑就是上面那个公式再加上标准电阻系列的筛选。def calc_divider(vbat_max, vadc_max, r2_standardNone): ratio vadc_max / vbat_max r1_over_r2 (1 - ratio) / ratio if r2_standard is None: r2_standard [10, 22, 47, 68, 100, 150, 220] results [] for r2 in r2_standard: r1 r1_over_r2 * r2 # 找最接近的标准电阻值 e24 [10, 11, 12, 13, 15, 16, 18, 20, 22, 24, 27, 30, 33, 36, 39, 43, 47, 51, 56, 62, 68, 75, 82, 91] r1_std min(e24, keylambda x: abs(x * 1000 - r1)) * 1000 actual_ratio r2 / (r1_std r2) vout_max vbat_max * actual_ratio results.append((r1_std, r2, actual_ratio, vout_max)) return results这个脚本跑出来的结果会列出几组可选方案你根据手头有的电阻挑一组就行。比如输入 4.2V 和 3.3V它会推荐 30kΩ100kΩ、27kΩ100kΩ、33kΩ120kΩ 等组合每一组都标注了实际分压比和最大输出电压一目了然。2.4 硬件清单与接线要点整个项目的硬件清单如下ESP32-C3 开发板一块、18650 电池盒一个、18650 锂电池一节、电阻若干30kΩ 和 100kΩ 各一个精度建议 1%、电容若干0.1μF 和 10μF 各一个用于 ADC 输入滤波、杜邦线若干、USB 数据线一根。接线的时候有几个要点。电池正极接 R1 的一端R1 的另一端接 R2 的一端和 ADC 引脚R2 的另一端接 GND。ADC 引脚我选的是 GPIO0因为它在 ESP32-C3 上对应 ADC1 的通道 0用起来最方便。滤波电容要尽量靠近 ADC 引脚放置0.1μF 的陶瓷电容并联在 ADC 引脚和 GND 之间10μF 的电解电容可以放在电池输出端用来平滑电池电压的波动。注意ESP32-C3 的 ADC1 有多个通道但 ADC2 在 WiFi 开启时不能使用所以一定要选 ADC1 的引脚。GPIO0 到 GPIO4 都是 ADC1 的通道随便选一个就行。3. ADC 采样与数据处理的实战细节3.1 ESP32-C3 的 ADC 到底怎么用ESP32-C3 的 ADC 使用起来比 STM32 简单一些但也有一些特有的坑。在 Arduino 环境下直接用analogRead()就能读取 ADC 值返回的是 0 到 4095 的整数。但默认的参考电压和衰减器配置可能不适合你的分压电路需要手动调整。ESP32-C3 的 ADC 有四个衰减档位0dB、2.5dB、6dB、11dB。0dB 时量程大约是 0 到 750mV11dB 时量程大约是 0 到 3.3V。我们分压后的最大电压是 3.23V所以必须用 11dB 档位。在 Arduino 代码里这样设置#include driver/adc.h void setup() { adc1_config_width(ADC_WIDTH_BIT_12); adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11); }然后在循环里用adc1_get_raw(ADC1_CHANNEL_0)读取原始值。注意不要用analogRead()因为它的默认配置可能不是 11dB会导致读数不准。3.2 采样值到电压的换算读到的原始值是一个 0 到 4095 的整数需要换算成实际电压。换算公式是Vadc raw / 4095.0 × Vref。这里的 Vref 不是精确的 3.3V而是跟衰减档位和芯片个体差异有关。我实测下来11dB 档位的实际满量程电压在 3.1V 到 3.4V 之间波动所以直接用 3.3V 算会有误差。更准确的做法是用万用表测一个已知电压然后反推 Vref。比如我输入一个精确的 2.000V 到 ADC 引脚读到的原始值是 2480那么 Vref 2.000 / (2480/4095) ≈ 3.302V。把这个值写死在代码里精度会好很多。得到 Vadc 之后再根据分压比反推电池电压Vbat Vadc × (R1 R2) / R2。用我选的 30kΩ 和 100kΩ分压比是 0.769所以 Vbat Vadc / 0.769 ≈ Vadc × 1.3。3.3 软件滤波让读数不再跳来跳去直接读 ADC 的话你会发现数值一直在跳有时候能差几十个 LSB。这是因为电源噪声、WiFi 射频干扰、以及 ADC 本身的噪声导致的。我试过几种滤波方法最后选了一个简单但有效的方案滑动平均加中值滤波。具体做法是连续采样 11 次去掉最大值和最小值剩下的 9 个取平均。这样既能滤掉突发的尖峰噪声又能平滑随机噪声。代码实现如下#define SAMPLE_COUNT 11 float read_battery_voltage() { int samples[SAMPLE_COUNT]; for (int i 0; i SAMPLE_COUNT; i) { samples[i] adc1_get_raw(ADC1_CHANNEL_0); delay(2); } // 冒泡排序 for (int i 0; i SAMPLE_COUNT - 1; i) { for (int j 0; j SAMPLE_COUNT - 1 - i; j) { if (samples[j] samples[j1]) { int temp samples[j]; samples[j] samples[j1]; samples[j1] temp; } } } // 去掉最大最小各一个剩下的取平均 long sum 0; for (int i 1; i SAMPLE_COUNT - 1; i) { sum samples[i]; } float raw_avg sum / (float)(SAMPLE_COUNT - 2); float vadc raw_avg / 4095.0 * 3.302; float vbat vadc * (30.0 100.0) / 100.0; return vbat; }实测下来滤波后的读数波动在 ±5mV 以内对于电量显示来说完全够用了。3.4 电压到电量的映射曲线锂电池的放电曲线不是线性的满电 4.2V 到 3.7V 之间电量下降很慢3.7V 到 3.4V 之间下降很快3.4V 以下基本就没电了。如果直接用线性映射显示的电量会很不准。我的做法是分段线性映射把放电曲线分成几段每段用不同的斜率。电池电压电量百分比4.20V100%4.00V85%3.85V65%3.75V40%3.65V20%3.50V8%3.30V0%在代码里用一个查找表加线性插值就能实现。比如读到 3.80V落在 3.75V 和 3.85V 之间对应电量在 40% 和 65% 之间线性插值算出来大概是 52%。提示不同品牌、不同老化程度的锂电池放电曲线会有差异上面这张表是我用自己手头的电池测出来的你可以根据自己的电池实测调整。4. 手机端电量显示的三种方案对比4.1 方案一BLE 蓝牙广播BLE 方案是我最推荐的功耗低、连接快、手机端不用装额外 App。ESP32-C3 的 BLE 可以工作在广播模式不需要建立连接就能把电量数据发出去手机端用任何 BLE 调试助手都能看到。实现思路是定义一个自定义的广播数据包把电量百分比放在厂商自定义字段里。代码大概长这样#include BLEDevice.h #include BLEUtils.h #include BLEAdvertising.h BLEAdvertising *pAdvertising; void setup_ble() { BLEDevice::init(SmartBatteryBox); pAdvertising BLEDevice::getAdvertising(); BLEAdvertisementData advData; advData.setName(BatteryBox); pAdvertising-setAdvertisementData(advData); pAdvertising-start(); } void update_battery_level(int percent) { BLEAdvertisementData advData; advData.setName(BatteryBox); std::string manufacturerData; manufacturerData (char)0xFF; manufacturerData (char)0xFF; manufacturerData (char)percent; advData.setManufacturerData(manufacturerData); pAdvertising-setAdvertisementData(advData); pAdvertising-start(); }手机端用 nRF Connect 或者 LightBlue 这类 App 就能看到广播包里的电量数据。优点是功耗极低广播一次大概几百微安一秒广播一次的话平均电流不到 1mA。缺点是需要手机端主动扫描不能后台自动显示。4.2 方案二WiFi 网页服务器WiFi 方案的好处是手机不用装任何 App打开浏览器输入 IP 地址就能看到电量。ESP32-C3 跑一个简单的 HTTP 服务器返回一个 HTML 页面页面上用 JavaScript 定时刷新电量数据。#include WiFi.h #include WebServer.h WebServer server(80); float current_battery 0; void handle_root() { String html htmlheadmeta charsetutf-8; html meta http-equivrefresh content5; html title电池电量/title/headbody; html h1电池电量: String(current_battery, 1) %/h1; html /body/html; server.send(200, text/html, html); } void setup_wifi() { WiFi.begin(你的WiFi名称, 你的WiFi密码); while (WiFi.status() ! WL_CONNECTED) { delay(500); } server.on(/, handle_root); server.begin(); }这个方案的缺点是 WiFi 功耗高平均电流在 80mA 左右电池盒待机时间会大幅缩短。如果你打算用这个方案建议加一个物理开关需要看电量的时候才打开 WiFi。4.3 方案三蓝牙串口透传蓝牙串口透传是介于前两者之间的方案。ESP32-C3 模拟一个串口设备手机端用蓝牙串口 App 连接后会持续收到电量数据。优点是连接稳定、数据实时性好缺点是手机端需要装 App而且连接状态下功耗比广播模式高。我实测下来蓝牙串口连接状态的平均电流在 15mA 左右比 WiFi 低很多但比 BLE 广播高。如果你需要实时监控电量变化这个方案比较合适。4.4 三种方案的最终选择建议方案功耗手机端要求实时性适用场景BLE 广播极低需扫描 App一般长期待机、偶尔查看WiFi 网页高浏览器好固定场所、频繁查看蓝牙串口中等需串口 App好实时监控、调试我最终选择了 BLE 广播作为默认方案因为电池盒大部分时间都是放在包里待机偶尔拿出来看一下电量广播模式完全够用。如果你要做成桌面摆件WiFi 网页方案会更方便。5. 烧录、调试与常见问题排查5.1 ESP32-C3 烧录失败的几种原因ESP32-C3 烧录失败是新手遇到最多的问题我至少踩过五六次坑。最常见的原因是 USB 线的问题有些线只能充电不能传数据换一根线就好了。其次是驱动问题Windows 上需要装 CH343 或者 CP210x 驱动Mac 和 Linux 一般免驱。还有一个容易被忽略的点是烧录模式。ESP32-C3 需要进入下载模式才能烧录具体操作是按住 BOOT 键按一下 RESET 键然后松开 BOOT 键。有些开发板有自动下载电路不用手动操作但如果你用的是最小系统板就必须手动进下载模式。如果烧录时报错 Failed to connect to ESP32-C3先检查串口是否被其他程序占用比如 Arduino IDE 的串口监视器没关掉。然后检查波特率ESP32-C3 支持的最高烧录波特率是 921600但有些板子跑不到这么高降到 115200 试试。5.2 ADC 读数不准的排查思路ADC 读数不准通常有三个原因分压电阻精度不够、Vref 设置不对、或者电源噪声太大。排查的时候先用万用表量一下分压后的实际电压跟代码里算出来的 Vadc 对比如果差得多说明 Vref 不对需要重新校准。如果读数跳动很大先检查滤波电容有没有焊好然后检查 WiFi 是不是开着。ESP32-C3 的 WiFi 射频会对 ADC 产生明显干扰如果不需要 WiFi在代码里关掉它读数会稳定很多。还有一个隐藏的坑是 GPIO 的上下拉配置。如果 ADC 引脚被配置成了上拉或者下拉读数会完全不对。确保在代码里没有对 ADC 引脚做pinMode设置让它保持默认的高阻态。5.3 功耗优化的几个实用技巧如果你想让电池盒待机时间长一点功耗优化是必须的。ESP32-C3 支持深度睡眠模式睡眠电流可以降到 5μA 左右。我的做法是每 10 秒唤醒一次采样一次电压更新 BLE 广播然后继续睡眠。esp_sleep_enable_timer_wakeup(10 * 1000000); esp_deep_sleep_start();注意深度睡眠会重置所有外设所以每次唤醒后都要重新初始化 ADC 和 BLE。另外 BLE 广播在睡眠期间会停止手机端需要重新扫描才能看到数据。如果你需要持续广播就不能用深度睡眠只能用轻度睡眠或者降低广播频率来省电。5.4 常见问题速查表问题现象可能原因解决方法烧录失败USB 线不支持数据传输换一根数据线烧录失败未进入下载模式按住 BOOT 再按 RESETADC 读数始终为 0引脚配置错误检查是否用了 ADC1 通道ADC 读数跳动大电源噪声加滤波电容关闭 WiFi电量显示不准放电曲线不匹配重新实测电池放电曲线BLE 搜不到设备广播未启动检查 BLE 初始化代码待机时间短未启用睡眠模式配置深度睡眠定时唤醒注意ESP32-C3 的 ADC2 在 WiFi 开启时不可用这是硬件限制不是代码问题。如果你同时需要 WiFi 和 ADC只能用 ADC1。6. 外壳组装与最终成品展示6.1 外壳选型和开孔位置外壳我用的是一个 18650 电池盒的成品塑料壳尺寸大概是 75mm × 22mm × 22mm刚好能塞下一节 18650 电池和一块 ESP32-C3 最小系统板。开孔位置很关键USB 接口要露出来方便充电和烧录ADC 引脚和电池正负极的接线要留出空间。我在外壳侧面开了三个孔一个 USB Type-C 孔、一个 2mm 的电源开关孔、一个 3mm 的指示灯孔。USB 孔用美工刀慢慢修不要一次切太多边切边试。电源开关我加了一个小型的拨动开关用来控制整个电路的供电不用的时候直接关掉省电。6.2 焊接和组装注意事项焊接的时候有几个细节要注意。分压电阻尽量靠近 ESP32-C3 的 ADC 引脚引线越短越好减少干扰。滤波电容的接地端要跟 ESP32-C3 的 GND 就近连接不要绕远路。电池的正负极线要用稍微粗一点的线比如 24AWG减少压降。组装顺序是先焊电阻和电容再焊电池线最后装外壳。焊完之后先用万用表测一下分压点的电压确认没有短路再通电。我第一次做的时候就是没测直接通电结果电阻焊反了ADC 引脚直接吃了 4.2V芯片当场冒烟。这个教训希望大家记住。6.3 实测数据与精度评估成品做出来之后我用可调电源模拟电池电压从 3.0V 到 4.2V 每隔 0.1V 测一次跟万用表的读数对比。实测下来最大误差在 30mV 左右对应电量百分比误差大概 3% 到 5%。对于日常使用来说完全够用了毕竟手机自己显示的电量也有误差。功耗方面BLE 广播模式下平均电流是 1.2mA深度睡眠加定时唤醒模式下平均电流是 0.15mA。一节 3000mAh 的 18650 电池如果一直广播能撑 100 天左右如果用睡眠模式能撑将近两年。当然实际使用中还有自放电和温度影响打个七折比较保守。6.4 后续可以扩展的方向这个电池盒目前只做了电压检测和电量显示其实还有很多可以扩展的地方。比如加一个电流检测电阻用 INA219 或者 ACS712 模块就能同时监测充放电电流算出实际容量。再比如加一个 OLED 小屏幕不用手机也能直接看电量。还可以加一个蜂鸣器电量低于 10% 的时候报警提醒。软件方面可以把电量数据通过 MQTT 上传到服务器做长期的电量记录和曲线分析。或者用 ESP32-C3 的蓝牙 Mesh 功能把多个电池盒组网统一监控。这些扩展我在后续的文章里会陆续分享感兴趣的话可以持续关注。最后分享一个我在调试过程中发现的小技巧如果你觉得 ADC 读数还是不够稳可以在读取之前先把引脚配置成输出模式拉低一下再切回输入模式相当于给 ADC 输入电容放电能消除上一次采样残留的电荷影响。这个技巧在采样频率较高的时候特别有用实测能再降低一半的波动。
返回列表