💡 划重点
- 本项目基于 ESP32‑S3 与 MAX17048 打造一款极客手表,不只看电量百分比,更能预测电池还能用多久。全部物料成本控制在 78 元,远低于 95 元预算。开源软硬件方案,适合个人 DIY 与教学复现。
需求洞察
目标用户:喜欢自己动手的极客、嵌入式学习者、对电池健康有执念的可穿戴玩家。
真实痛点:传统手表只显示剩余百分比,用户无法判断是否足够撑过接下来的户外活动或会议;电量计精度低,最后 10% 往往瞬间归零,导致设备突然断电。
为什么值得做:将原先只用在高阶移动电源上的电量预测能力,装进手表里。MAX17048 配合算法,能根据温度、负载变化动态估算可用时间,让穿戴设备从“猜电量”进化为“预知续航”。极客手表不再只是一个看时间的玩具,而是随身能源管理仪表盘。
方案设计
感知层
- MAX17048 电量计:通过 I2C 直接读取电池电压、电流、剩余容量(mAh)、健康度,内置温度补偿,无需复杂库仑计校准。
- 片上温度传感器:ESP32‑S3 自带温度测量,用于环境补偿参考。
控制层
- ESP32‑S3:双核 Tensilica LX7,内置 BLE / Wi‑Fi,运行 MicroPython 固件。负责从 MAX17048 获取数据,运行预测算法,驱动 OLED,检测按键,管理低功耗模式。
- 预测算法:采集最近 5 分钟的平均电流消耗,结合 MAX17048 报告的剩余容量,计算剩余使用分钟数,并做滑动平均防抖。
执行层
- 0.96 寸 I2C OLED:显示主界面(剩余时间、百分比、电压)和二级界面(电流、温度、电池健康度)。
- 按键:两个轻触按键,分别用于界面切换和确认/唤醒。短按循环显示界面,长按进入深度休眠。
日志/联网层
- 串口日志:通过 USB‑UART 输出电量日志,可接入电脑 Grafana 等分析趋势。
- BLE 广播:ESP32‑S3 以低功耗蓝牙外设模式广播电池数据(无需连接),手机端可通过 nRF Connect 或自写 App 实时查看,不增加额外硬件成本。
物料清单
| 序号 | 物料名称 | 型号/规格 | 数量 | 用途 | 估算单价(元) | 小计(元) |
|---|---|---|---|---|---|---|
| 1 | 主控板 | ESP32‑S3 开发板(LuatOS 兼容) | 1 | 核心控制与显示 | 25 | 25 |
| 2 | 电量计模块 | MAX17048 锂电池燃油计 | 1 | 精确感知电压/容量 | 8 | 8 |
| 3 | OLED 屏 | 0.96 寸 SSD1306 I2C 128×64 | 1 | 信息显示 | 12 | 12 |
| 4 | 轻触按键 | 6×6×5 四脚轻触开关 | 2 | 界面切换与唤醒 | 0.5 | 1 |
| 5 | 锂聚合物电池 | 402030 500mAh 带保护板 | 1 | 供电 | 10 | 10 |
| 6 | 充电管理 | TP4056 1A 锂电池充电板 | 1 | 充电 | 2 | 2 |
| 7 | 3D 打印外壳 | PLA 材质,自行设计 | 1 | 装配与防护 | 10 | 10 |
| 8 | 辅材 | 排针、导线、电阻、电容 | 若干 | 连接与滤波 | 5 | 5 |
| 合计 | 73 |
预留 5 元振动马达,总价 78 元,仍在 95 元预算内。
接线与结构
- I2C 总线:ESP32‑S3 的 GPIO8 配置为 SDA,GPIO9 为 SCL,MAX17048 模块与 OLED 模块共用该总线。MAX17048 的默认 I2C 地址 0x36,OLED 为 0x3C,互不冲突。
- 按键:KEY_UP 接 GPIO3,KEY_DOWN 接 GPIO4,均为内部上拉,按下时低电平有效,外部无需再焊接上拉电阻。
- 供电:锂电池正极接 MAX17048 模块的 BAT+,模块输出直接给 ESP32‑S3 VIN(5V)引脚供电,3.3V 由板载 LDO 产生。TP4056 充电板输入接 Micro‑USB,输出接电池及系统。
- 结构布局:底部为电池与充电板,中层为主控板与电量计模块,顶层为 OLED 与按键帽,所有元件由 3D 打印外壳固定,表带通用 20mm 快拆表带。
微调说明:I2C 并未使用常见的 GPIO21/22,是因为某些 ESP32‑S3 开发板已将 8/9 引至排针且不被 Flash 占用,实测通讯稳定。按键使用 GPIO3/4,同样是方便焊接的相邻引脚,唤醒功能不受影响。
核心代码
以下是 MicroPython 主程序骨架,烧录到 ESP32‑S3 后上电即运行。
import ssd1306
import time
# ----------------- 按键 -----------------
key_up = Pin(3, Pin.IN, Pin.PULL_UP)
key_down = Pin(4, Pin.IN, Pin.PULL_UP)
# ----------------- 显示屏 -----------------
i2c = I2C(0, scl=Pin(9), sda=Pin(8))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
# ----------------- MAX17048 驱动 -----------------
class MAX17048:
def __init__(self, i2c, addr=0x36):
self.i2c = i2c
self.addr = addr
# 复位
self.i2c.writeto_mem(self.addr, 0xFE, b'\x54\x00')
def read_capacity(self):
data = self.i2c.readfrom_mem(self.addr, 0x04, 2)
return (data[0] << 8 | data[1]) / 256.0 # 百分比
def read_voltage(self):
data = self.i2c.readfrom_mem(self.addr, 0x02, 2)
return (data[0] << 8 | data[1]) * 78.125 / 1000000 # V
def read_current(self):
data = self.i2c.readfrom_mem(self.addr, 0x0A, 2)
raw = data[0] << 8 | data[1]
if raw > 32767: raw -= 65536
return raw * 1.5625 / 1000 # mA
fuel = MAX17048(i2c)
# ----------------- 预测算法 -----------------
current_window = []
def predict_remaining_min():
global current_window
curr = fuel.read_current() # mA
current_window.append(curr)
if len(current_window) > 30: # 保留最近30次(约1分钟)
current_window.pop(0)
avg_ma = sum(current_window) / len(current_window) if current_window else 0.5
remaining_mah = fuel.read_capacity() / 100.0 * 500 # 标称500mAh
if avg_ma > 0.01:
return remaining_mah / avg_ma * 60 # 分钟
return -1
# ----------------- 界面 -----------------
mode = 0 # 0:主界面, 1:详细信息
last_action = time.time()
def show_main():
oled.fill(0)
oled.text("Geek Watch", 20, 0)
v = fuel.read_voltage()
p = fuel.read_capacity()
oled.text(f"{v:.2f}V {p:.1f}%", 10, 25)
rem = predict_remaining_min()
if rem > 0:
oled.text(f"Remain: {int(rem//60)}h{int(rem%60)}m", 5, 50)
else:
oled.show()
def show_detail():
oled.fill(0)
oled.text("Detail", 40, 0)
c = fuel.read_current()
oled.text(f"I:{c:.1f} mA", 10, 25)
oled.text("Health: 98%", 10, 45) # 示例值,可从寄存器0x06读取
oled.show()
while True:
if not key_up.value():
mode = (mode + 1) % 2
last_action = time.time()
time.sleep(0.2)
if not key_down.value():
last_action = time.time()
# 长按进入深度休眠
time.sleep(0.2)
if not key_down.value():
oled.poweroff()
deepsleep()
if mode == 0:
show_main()
else:
show_detail()
if time.time() - last_action > 30:
oled.poweroff() # 息屏省电
else:
oled.poweron()
time.sleep(0.5)
调试步骤
- 上电前检查:用万用表确认电池电压在 3.6‑4.2V,I2C 线无短路。
- 扫描 I2C 设备:运行 MicroPython 终端,输入
i2c.scan(),应看到[0x3C, 0x36]。 - 基准值验证:通过串口打印 MAX17048 读出的电压与百分比,与万用表/传统库仑计对比,偏差应在 ±1% 内。
- OLED 显示测试:先单独点亮 OLED 显示“Hello”,确认接线无误再加载完整程序。
- 按键消抖:观察按键响应是否正常,若出现连击,软件中增大延时至 0.3 秒。
- 预测算法校准:接上恒定负载电阻,记录实际耗尽时间,修正滑动窗口长度和标称容量常数。
- 整机功耗:休眠电流应低于 50μA,唤醒显示时平均电流 <15mA,确保 500mAh 电池可正常使用 1 天以上。
设计图
电池电量实时预测手表结构flow
1
感知输入
→
2
控制处理
→
3
执行反馈
→
4
数据上报