1. 项目缘起与整体设计思路
1.1 为什么要做一块墨水屏手表
智能手表这个品类已经很成熟了,但市面上大多数产品都有一个共同的痛点:续航焦虑。一天一充甚至一天两充的使用体验,让很多人最终把手表扔进了抽屉。我自己就是典型例子,前前后后买过三四块智能手表,没有一块能坚持超过半年还在戴的,核心原因就是充电太频繁。
墨水屏(EPD,Electrophoretic Display)这个技术路线恰好能解决这个问题。它的物理特性决定了只在刷新画面时才耗电,静态显示时功耗几乎为零。这就意味着,如果设计得当,一块墨水屏手表做到30天甚至更长的续航是完全可行的。再加上现在ESP32系列芯片价格便宜、生态成熟、自带Wi-Fi和蓝牙,用Arduino框架开发门槛也不高,自己动手做一块开源墨水屏手表就成了一件很有性价比的事情。
这个项目的核心目标很明确:做一块免费开源的、用Arduino编程的墨水屏智能手表,续航做到30天级别,支持自定义表盘、蓝牙和Wi-Fi连接、实时天气显示、番茄闹钟、指南针等实用功能。适合有基本Arduino开发经验的创客、嵌入式爱好者,以及想学ESP32实战的开发者参考。
1.2 硬件选型的底层逻辑
整个项目最关键的决策就是硬件选型,这直接决定了续航、功能和开发难度。我前后试过好几套方案,最终确定的配置如下:
| 模块 | 选型 | 选型理由 |
|---|---|---|
| 主控 | ESP32-S3 Mini(N16R8) | 16MB Flash + 8MB PSRAM,双核240MHz,自带Wi-Fi和BLE 5.0,体积小 |
| 显示屏 | 1.54寸或2.13寸墨水屏(SPI接口) | 功耗极低,阳光下可读,刷新后断电保持画面 |
| 电源管理 | TP4056充电 + 低功耗LDO | 充电简单可靠,LDO静态电流低 |
| 电池 | 3.7V 1000mAh锂聚合物电池 | 容量与体积的平衡点 |
| 传感器 | MPU6050(加速度+陀螺仪) | 指南针和抬腕亮屏都需要 |
| 时钟 | PCF8563或DS3231 RTC | 深度睡眠时维持时间,功耗微安级 |
| 按键 | 3个轻触按键 | 功能切换、刷新、复位 |
选ESP32-S3而不是普通ESP32,主要考虑是S3的深度睡眠功耗更低,而且支持更多的GPIO和PSRAM,跑LVGL之类的图形库更从容。N16R8这个版本16MB Flash可以放不少字库和图片资源,8MB PSRAM在刷新大尺寸墨水屏时做帧缓冲非常有用。
墨水屏我建议新手从1.54寸开始,分辨率200x200,SPI接口,驱动芯片常见的是SSD1681或DEPG0154。2.13寸的250x122分辨率更高,但刷新时间更长,对续航有一定影响。如果你追求极致续航,1.54寸是更好的选择。
注意:墨水屏分局部刷新和全局刷新两种模式。局部刷新快但有残影,全局刷新慢但画面干净。手表场景下建议表盘用局部刷新,整点或切换页面时做一次全局刷新清残影。
1.3 软件架构的分层设计
软件层面我采用的是分层架构,从上到下大致分为四层:
- 应用层:表盘渲染、天气获取、番茄钟逻辑、指南针计算、菜单交互
- 服务层:Wi-Fi管理、蓝牙HID、时间同步(NTP)、电源管理
- 驱动层:墨水屏驱动、RTC驱动、MPU6050驱动、按键驱动
- 硬件抽象层:Arduino ESP32核心库、SPI/I2C总线封装
这样分层的好处是每一层职责清晰,换屏幕或者换传感器时只需要改驱动层,应用层代码基本不用动。比如你一开始用1.54寸屏,后来想换成2.13寸,只要驱动层适配好分辨率,上层渲染逻辑几乎不用改。
开发环境我用的是Arduino IDE 2.x配合ESP32开发板包。这里有个坑要提醒:ESP32的板包下载速度在国内经常很慢,建议提前下载好离线包。安装方法是在Arduino IDE的“开发板管理器”里添加ESP32的板包地址,然后搜索“esp32”安装。如果网络不好,可以手动下载离线包放到Arduino的hardware目录下。版本建议用2.0.11或更新的稳定版,太老的版本对S3支持不好。
2. 核心功能模块的细节拆解
2.1 墨水屏驱动与表盘渲染
墨水屏的驱动是整个项目里最需要耐心调试的部分。以常见的SSD1681为例,它通过SPI接口通信,需要控制几个关键引脚:CS(片选)、DC(数据/命令)、RST(复位)、BUSY(忙检测)。初始化流程大致是:硬件复位→软复位→设置驱动输出→设置数据入口模式→设置显示更新控制→设置RAM内容→上电→刷新。
表盘渲染的核心思路是维护一个帧缓冲区(framebuffer),在内存里把要显示的内容画好,然后一次性推送到屏幕。1.54寸200x200分辨率,1位色深,一帧需要200*200/8 = 5000字节,ESP32-S3的PSRAM完全放得下。渲染时用Adafruit GFX库或者自己写简单的绘图函数都可以。
自定义表盘的关键在于把表盘元素抽象成可配置的结构。我定义了一个表盘描述结构,包含背景图、时间位置、日期位置、电量图标位置、天气图标位置等。用户想换表盘时,只需要提供一张背景图和一份坐标配置,不用改代码。背景图可以用Image2Lcd之类的工具转成C数组,或者存到SPIFFS/LittleFS文件系统里按需读取。
// 表盘配置结构示例 struct WatchFace { const uint8_t* background; int16_t timeX, timeY; int16_t dateX, dateY; int16_t batteryX, batteryY; int16_t weatherX, weatherY; uint8_t timeFontSize; };局部刷新的实现要利用墨水屏的“局部刷新”模式,只更新变化的区域。比如秒针区域、电量区域。但要注意,局部刷新次数多了会有残影,建议每刷新20-30次局部后做一次全局刷新。这个阈值可以根据实际观感调整。
实操心得:墨水屏的BUSY引脚一定要接,而且要用中断或者轮询方式等待它变低。我一开始偷懒没接BUSY,用固定延时,结果不同批次的屏幕刷新时间不一样,经常出现花屏。接了BUSY之后稳定多了。
2.2 蓝牙与Wi-Fi连接实战
ESP32-S3支持Wi-Fi和蓝牙双模,但两者同时工作时会有资源竞争,功耗也会上升。我的策略是分时复用:需要同步天气和时间时开Wi-Fi,同步完立刻关闭;蓝牙则用于和手机App通信,按需开启。
Wi-Fi部分主要是连接路由器获取天气数据。我用的是心知天气或者和风天气的免费API,HTTP GET请求返回JSON,然后用ArduinoJson库解析。关键代码如下:
#include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> void fetchWeather() { WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) delay(100); HTTPClient http; http.begin("https://api.seniverse.com/v3/weather/now.json?key=YOUR_KEY&location=beijing&language=zh-Hans&unit=c"); int code = http.GET(); if (code == 200) { String payload = http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, payload); // 解析温度、天气现象等 } http.end(); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); }蓝牙部分我建议用BLE而不是经典蓝牙,因为BLE功耗低得多。ESP32-S3支持BLE 5.0,可以做BLE HID设备,把手表模拟成蓝牙键盘或者媒体控制器。比如双击按键切歌、长按呼出语音助手。BLE HID的实现需要用到ESP32的BLE库,定义HID描述符,然后通过BLE连接发送按键报告。
如果你只是想用手机App控制手表,那用BLE串口(Nordic UART Service)更简单。手机端用nRF Connect或者自己写App,发送指令给手表,手表解析后执行相应操作。
注意:Wi-Fi和BLE同时开启时,ESP32的射频部分会分时切换,可能导致连接不稳定。建议在代码里做好状态机管理,同一时间只开一个射频功能。
2.3 实时天气与时间同步
天气显示是智能手表的高频功能。我的做法是每2小时同步一次天气,同步时开Wi-Fi,获取数据后关闭。天气图标我预先转成位图数组存在Flash里,根据API返回的天气代码映射到对应图标。
时间同步用NTP协议,连接Wi-Fi后从pool.ntp.org获取UTC时间,然后根据时区偏移转换成当地时间,写入RTC芯片。RTC芯片在深度睡眠期间继续走时,唤醒后从RTC读取时间,不需要每次都联网。
#include <time.h> void syncTime() { configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); struct tm timeinfo; if (getLocalTime(&timeinfo)) { // 写入RTC rtc.setTime(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); rtc.setDate(timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900); } }这里有个细节:NTP同步需要Wi-Fi连接稳定,如果信号弱可能超时。建议设置重试机制,最多重试3次,每次间隔2秒。如果都失败就跳过本次同步,用RTC的旧时间继续走。
2.4 番茄闹钟与指南针实现
番茄闹钟的逻辑很简单:25分钟工作+5分钟休息,循环4次后长休息15分钟。用ESP32的定时器或者RTC闹钟实现。我倾向于用RTC闹钟,因为深度睡眠时定时器会停,而RTC闹钟可以把芯片从深度睡眠中唤醒。
// 设置RTC闹钟,25分钟后唤醒 rtc.setAlarm(25 * 60); esp_deep_sleep_start();唤醒后蜂鸣器响或者振动马达振动,提示时间到。蜂鸣器用PWM驱动,可以调音调。
指南针用MPU6050的磁力计数据(如果你用的是MPU9250或者加了HMC5883L)计算航向角。MPU6050本身没有磁力计,只能算俯仰和横滚,要做指南针需要额外的磁力计模块。我建议直接用MPU9250或者QMC5883L,省事。
航向角计算公式:
float heading = atan2(my, mx) * 180 / PI; if (heading < 0) heading += 360;然后做倾斜补偿和磁偏角校正。实际使用中,磁力计容易受手表内部电池和电机的干扰,需要做硬铁和软铁校准。校准方法是在水平面上旋转手表几圈,记录最大最小值,计算偏移量。
实操心得:指南针功能在手表上其实用得不多,但做出来很有成就感。校准的时候远离电脑、手机等磁场源,否则数据会飘得厉害。
3. 完整实操流程与关键环节
3.1 硬件焊接与组装步骤
硬件组装我按以下顺序进行,每一步都验证后再进行下一步:
- 电源部分先焊:TP4056充电模块、电池保护板、LDO稳压。焊好后用万用表测输出电压,确保3.3V稳定。
- 主控最小系统:ESP32-S3 Mini焊到转接板或者直接焊到洞洞板上,测试能否正常烧录程序。
- 墨水屏连接:按SPI接线,CS、DC、RST、BUSY、CLK、MOSI。注意墨水屏是3.3V逻辑,不要接5V。
- RTC和传感器:I2C总线连接,SCL和SDA加上拉电阻(4.7kΩ)。
- 按键和蜂鸣器:按键一端接GPIO,另一端接地,启用内部上拉。蜂鸣器用三极管驱动,避免GPIO电流不够。
- 整体组装:3D打印外壳,把屏幕、主板、电池分层放置,注意电池不要被挤压。
焊接时用细焊锡丝和尖头烙铁,温度320度左右。墨水屏的FPC排线很脆弱,焊接时间不要超过3秒。
3.2 开发环境搭建与烧录
Arduino IDE的ESP32环境搭建步骤如下:
- 下载Arduino IDE 2.3.x,安装。
- 打开“文件→首选项”,在“附加开发板管理器网址”里填入ESP32的板包地址。
- 打开“工具→开发板→开发板管理器”,搜索“esp32”,安装最新稳定版。
- 安装完成后,在开发板列表里选择“ESP32S3 Dev Module”。
- 配置参数:Flash Size 16MB,PSRAM OPI PSRAM,Partition Scheme 16M Flash (3MB APP/9.9MB FATFS),Upload Speed 921600。
烧录时如果遇到“等待上电同步”一直卡住,检查以下几点:USB线是否支持数据传输(有些线只能充电)、驱动是否安装(CH340或CP2102)、烧录模式是否正确(有些板子需要按住BOOT再按RESET进入下载模式)。
# 如果用esptool命令行烧录 esptool.py --chip esp32s3 --port /dev/ttyUSB0 --baud 921600 write_flash 0x0 firmware.bin3.3 电源管理与续航优化
续航是墨水屏手表的核心卖点,要做到30天,必须把平均电流控制在1mA以下。1000mAh电池,30天=720小时,平均电流要小于1.39mA。
各模块功耗估算:
| 模块 | 工作电流 | 工作占比 | 平均电流 |
|---|---|---|---|
| ESP32-S3深度睡眠 | 10μA | 95% | 9.5μA |
| ESP32-S3活跃 | 80mA | 5% | 4mA |
| 墨水屏刷新 | 15mA | 1% | 0.15mA |
| RTC | 1μA | 100% | 1μA |
| LDO静态 | 5μA | 100% | 5μA |
| 合计 | 约4.2mA |
4.2mA对应1000mAh电池只能撑238小时,约10天。要延长到30天,需要进一步优化:
- 减少ESP32活跃时间:表盘刷新从每分钟一次改成每5分钟一次,只在按键时立即刷新。
- 降低活跃电流:关闭不用的外设,Wi-Fi和BLE用完即关。
- 选用更低静态电流的LDO,比如HT7333(静态4μA)。
- 墨水屏刷新频率降低,局部刷新代替全局刷新。
优化后平均电流可以降到1.2mA左右,1000mAh电池能撑约34天。实际使用中因为电池老化、温度影响,30天是比较现实的预期。
注意:深度睡眠时GPIO状态要设置好,悬空的引脚可能漏电。把不用的引脚设为输入下拉或者输出低电平。
3.4 表盘自定义与资源管理
表盘资源我建议存在LittleFS文件系统里,而不是编译进固件。这样用户可以通过Wi-Fi或者蓝牙上传新表盘,不用重新烧录固件。
LittleFS的使用方法:
#include <LittleFS.h> void setup() { if (!LittleFS.begin(true)) { Serial.println("LittleFS mount failed"); return; } // 读取表盘文件 File f = LittleFS.open("/face1.bin", "r"); if (f) { f.read(framebuffer, f.size()); f.close(); } }表盘图片格式我用的是1位BMP转成的原始二进制,200x200分辨率正好5000字节。转换工具可以用Python的Pillow库:
from PIL import Image img = Image.open("face.png").convert("1") img = img.resize((200, 200)) with open("face1.bin", "wb") as f: f.write(img.tobytes())上传表盘可以通过BLE文件传输或者Wi-Fi HTTP服务器。ESP32可以起一个简单的Web服务器,手机浏览器打开上传页面,选择文件上传到LittleFS。
4. 常见问题与排查技巧实录
4.1 墨水屏刷新异常排查
墨水屏最常见的问题就是花屏、残影、刷新不全。我整理了一个排查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全白屏 | 接线错误、初始化失败 | 检查SPI接线,确认RST时序 |
| 花屏 | BUSY未等待、SPI速率过高 | 接BUSY引脚,降低SPI频率到10MHz |
| 残影严重 | 局部刷新过多 | 增加全局刷新频率 |
| 刷新一半卡住 | 电源供电不足 | 加大电容,检查LDO电流能力 |
| 显示偏移 | 分辨率配置错误 | 核对屏幕型号和分辨率 |
我遇到最坑的一次是屏幕偶尔花屏,查了半天发现是SPI线太长导致信号完整性不好。把线缩短到5cm以内就稳定了。所以PCB设计或者连线时,SPI线尽量短,必要时加串联电阻。
4.2 蓝牙连接不稳定处理
蓝牙连接不上或者频繁断开,通常有这几个原因:
- 天线问题:ESP32-S3的板载天线如果被电池或者金属外壳遮挡,信号会很差。外壳设计时天线区域要留空。
- 电源噪声:蓝牙对电源噪声敏感,LDO输出要加滤波电容。
- 协议栈冲突:Wi-Fi和BLE同时开容易冲突,分时使用。
- 配对信息残留:手机端删除配对记录,手表端清除绑定信息后重试。
如果手机搜不到手表,先确认BLE广播是否正常开启。可以用nRF Connect扫描,看能不能发现设备。如果广播正常但连不上,检查服务UUID和特征值配置是否正确。
4.3 续航不达标的优化思路
续航不达标是最常见的问题。我的排查顺序是:
- 测实际电流:用万用表或者电流表串在电池回路,测深度睡眠电流。如果大于100μA,说明有漏电。
- 查GPIO状态:悬空引脚、上拉电阻、外设供电都要检查。
- 查LDO静态电流:有些LDO静态电流几mA,直接拖垮续航。
- 查RTC闹钟频率:如果每分钟唤醒一次,累积功耗很可观。
- 查Wi-Fi/BLE开启时间:同步天气时间尽量短,用完立刻关。
我实测发现,把表盘刷新从每分钟改成每5分钟,续航从12天提升到了28天。所以刷新频率对续航影响巨大。
4.4 编译与烧录常见错误
Arduino IDE编译ESP32项目时经常遇到各种错误,我列几个典型的:
- “Board not found”:板包没装好,重新安装ESP32板包。
- “PSRAM not available”:开发板配置里没开PSRAM,选OPI PSRAM。
- “Partition too small”:分区表选小了,换成16M Flash的大分区方案。
- “Timed out waiting for packet header”:烧录时没进入下载模式,按住BOOT再点烧录。
- “undefined reference to xxx”:库没装全,用库管理器安装依赖。
编译速度慢的话,可以在首选项里开启“编译缓存”,第二次编译会快很多。Windows下编译ESP32确实比Mac和Linux慢,这是正常的。
4.5 传感器数据漂移与校准
MPU6050和磁力计用久了会有漂移。加速度计和陀螺仪的零偏可以通过静止时采样求平均来校准。磁力计需要做椭圆拟合校准,消除硬铁和软铁干扰。
校准代码思路:
// 静止时采集1000个样本求平均作为零偏 float ax_offset = 0, ay_offset = 0, az_offset = 0; for (int i = 0; i < 1000; i++) { ax_offset += mpu.getAccX(); ay_offset += mpu.getAccY(); az_offset += mpu.getAccZ(); delay(2); } ax_offset /= 1000; ay_offset /= 1000; az_offset = az_offset / 1000 - 1.0; // 减去重力指南针校准则是水平旋转手表,记录mx和my的最大最小值,计算偏移和缩放:
float mx_offset = (mx_max + mx_min) / 2; float my_offset = (my_max + my_min) / 2; float mx_scale = (mx_max - mx_min) / 2; float my_scale = (my_max - my_min) / 2;校准后航向角精度能到±5度以内,日常使用足够了。
4.6 外壳设计与佩戴体验
外壳我用Fusion 360设计,3D打印PLA或者PETG。设计要点:
- 屏幕开窗要精确,留0.2mm公差。
- 按键位置要对应PCB上的轻触开关,行程要够。
- 表带接口用标准的20mm生耳,方便换表带。
- 背面留充电口和传感器开孔。
- 整体厚度控制在12mm以内,太重了戴着不舒服。
打印时层高0.15mm,填充20%,外壳壁厚1.2mm。打印完用砂纸打磨一下,喷一层哑光漆,质感会好很多。
实操心得:第一版外壳我设计得太紧凑,电池放进去后盖不上盖。后来把内部空间加大了1mm,问题解决。所以设计外壳时一定要留足余量,尤其是电池和排线。
5. 功能扩展与进阶玩法
5.1 接入更多传感器
ESP32-S3的GPIO很丰富,可以接很多传感器扩展功能:
- 温湿度:SHT30或者DHT22,显示环境温湿度。
- 气压计:BMP280,做高度计和天气趋势预测。
- 心率血氧:MAX30102,做健康监测。
- 环境光:BH1750,自动调节屏幕刷新策略。
接传感器时注意I2C地址冲突,多个设备地址不能一样。如果冲突,可以用I2C多路复用器TCA9548A。
5.2 低功耗蓝牙HID应用
把手表做成BLE HID设备,可以当蓝牙键盘或者媒体控制器。比如:
- 单击按键:播放/暂停
- 双击按键:下一曲
- 长按按键:唤醒语音助手
实现时需要定义HID报告描述符,然后通过BLE发送按键报告。ESP32的BLE库有HID示例,改一改就能用。
5.3 OTA无线升级
每次改代码都要插线烧录很麻烦,OTA(Over-The-Air)升级可以解决这个问题。ESP32支持通过Wi-Fi更新固件,Arduino IDE里也有OTA功能。
#include <ArduinoOTA.h> void setup() { ArduinoOTA.begin(); } void loop() { ArduinoOTA.handle(); }配置好之后,在Arduino IDE的工具菜单里选择网络端口,就能无线烧录了。注意OTA时手表要连上Wi-Fi,而且电量要充足。
5.4 与智能家居联动
手表连上Wi-Fi后,可以通过MQTT协议和智能家居平台联动。比如:
- 手表显示家里温湿度
- 按键控制客厅灯
- 番茄钟结束时推送通知到手机
MQTT用PubSubClient库,连接MQTT broker,订阅和发布消息。这样手表就不只是一个独立设备,而是智能家居的一个终端。
6. 个人实操体会与建议
这个项目我从立项到最终成型大概花了三个月,中间踩了不少坑,也学到了很多东西。最大的体会是:墨水屏手表的核心难点不在功能实现,而在功耗控制和稳定性。功能代码网上都能找到参考,但把功耗压到1mA以下、让设备连续跑一个月不出问题,需要反复调试和优化。
另一个体会是硬件和软件的配合很重要。比如墨水屏的刷新时序、电源的纹波、天线的布局,这些硬件细节直接决定了软件能不能稳定运行。我建议做类似项目的朋友,不要急着堆功能,先把最小系统跑通,把功耗测准,再逐步加功能。
最后分享一个小技巧:调试阶段可以用USB供电,但测续航一定要用电池。USB供电时LDO的静态电流和电池供电不一样,测出来的数据不准。另外,墨水屏在低温下刷新会变慢,冬天户外使用时要注意。
这个项目后续还可以扩展很多方向,比如加太阳能充电板做永久续航、加NFC做门禁卡、加GPS做轨迹记录。硬件上ESP32-S3的性能还有很大余量,软件上Arduino生态也很丰富,想怎么玩都可以。