1. 项目缘起与整体设计思路1.1 为什么要折腾一块墨水屏手表智能手表这个品类已经非常成熟了但市面上大多数产品都有一个共同的痛点续航。一天一充甚至半天一充的体验让很多人最终把手表扔进了抽屉。而墨水屏的特性恰好能解决这个问题——它只在刷新画面的时候耗电静态显示几乎不耗电。这意味着如果你做一个以看时间为主的手表充一次电撑几周甚至一个月是完全可行的。这个项目的核心思路就是用 Arduino 生态里最成熟的墨水屏驱动方案搭配低功耗蓝牙和 Wi-Fi 模块做一块功能齐全但续航夸张的智能手表。它不需要你从头画 PCB也不需要你精通嵌入式底层开发只要你会基本的 Arduino 编程就能跟着做出来。适合的人群包括想入门嵌入式开发的爱好者、喜欢 DIY 可穿戴设备的创客、以及想给自己做一块“不焦虑续航”手表的人。整个项目的设计哲学是“够用就好”。屏幕不追求高刷新率功能不追求大而全但每一个功能都要做到真正可用。表盘可以自定义说明它不是一个固定功能的产品而是一个可以持续折腾的平台。蓝牙和 Wi-Fi 连接让它能获取实时天气、同步时间番茄闹钟和指南针则是在低功耗前提下增加实用性的功能模块。1.2 硬件选型的核心逻辑选型这件事直接决定了项目的成败和复现难度。我在这个项目里遵循的原则是优先选生态成熟、资料多、驱动完善的模块而不是追求极致的低功耗或最小的体积。主控方面ESP32 系列是首选。原因很直接它自带 Wi-Fi 和蓝牙不需要额外加模块而且 Arduino 核心支持非常完善社区里能找到大量现成的库和示例代码。相比 STM32 加外挂蓝牙的方案ESP32 的集成度更高代码量更少对新手友好得多。虽然 ESP32 的深度睡眠功耗不是最低的但配合墨水屏的静态零功耗特性整体续航依然能做到 30 天级别。墨水屏选的是 1.54 英寸或 2.13 英寸的黑白屏分辨率 200x200 或 250x122。这个尺寸在手表上刚好够用能显示时间、日期、天气图标和简单的文字信息。驱动芯片通常是 SSD1608 或 IL0373这两种在 Arduino 社区都有成熟的库支持。刷新方式支持全刷和局刷全刷会有明显的黑闪局刷则更平滑但次数有限需要根据使用场景合理搭配。电池方面选的是 3.7V 锂聚合物电池容量在 500mAh 到 800mAh 之间。这个容量配合 ESP32 的深度睡眠和墨水屏的静态显示撑 30 天是有希望的。关键是要把 ESP32 的睡眠电流控制在 10μA 级别同时墨水屏在不刷新的时候要彻底断电。注意墨水屏的驱动板在断电后仍会保留画面这是它省电的核心原理。但如果你选的驱动板没有断电保持功能那续航会大打折扣选型时一定要确认这一点。1.3 软件架构的分层设计软件部分我分成了三层硬件驱动层、功能服务层和应用层。硬件驱动层负责初始化墨水屏、读取传感器数据、控制电源管理芯片。功能服务层包括时间同步、天气获取、蓝牙连接、闹钟逻辑等。应用层就是表盘绘制和用户交互。这种分层的好处是当你想换一块不同尺寸的墨水屏时只需要改驱动层的初始化参数上层逻辑完全不用动。同样如果你想增加一个新的传感器也只需要在服务层加一个模块应用层按需调用即可。时间同步走的是 Wi-Fi 下的 NTP 协议开机连上 Wi-Fi 后自动校准时间然后断开 Wi-Fi 进入低功耗模式。天气数据通过 HTTP 请求获取解析 JSON 后提取温度和天气状况绘制成简单的图标和文字。蓝牙部分用的是 BLE主要用来做配置和传输自定义表盘功耗比经典蓝牙低很多。2. 核心细节解析与实操要点2.1 墨水屏的刷新策略与功耗平衡墨水屏的刷新方式是整个项目里最需要仔细处理的地方。全刷Full Refresh会先黑屏再白屏再显示内容整个过程大约 2-3 秒视觉上会有明显的闪烁但显示效果最干净而且对屏幕寿命影响最小。局刷Partial Refresh只更新变化区域速度快、无闪烁但连续局刷次数多了之后会出现残影需要定期做一次全刷来清理。我的策略是每分钟局刷一次时间区域每 30 分钟全刷一次清理残影其他区域如天气、日期只在数据变化时才刷新。这样一天下来局刷大约 1440 次全刷 48 次对屏幕寿命的影响在可接受范围内。实际操作中局刷的坐标计算很容易出错。墨水屏的显存是按页管理的局刷时需要指定起始页和结束页以及每页内的起始字节和结束字节。如果你只更新时间区域需要精确计算出这个区域对应的页范围和字节范围。我踩过的坑是一开始没算对页边界导致局刷时把旁边的日期区域也刷花了。// 局刷时间区域的示例代码 void partialRefreshTime(int hour, int minute) { // 时间区域坐标x50, y30, 宽度100, 高度40 int x 50; int y 30; int w 100; int h 40; // 计算页范围每页8像素高 int startPage y / 8; int endPage (y h - 1) / 8; // 计算字节范围每字节8像素宽 int startByte x / 8; int endByte (x w - 1) / 8; // 调用局刷函数 display.partialRefresh(startPage, startByte, endPage, endByte); }提示不同驱动芯片的局刷参数定义可能不同有的用页和字节有的用绝对坐标。拿到屏幕后第一件事就是翻数据手册确认局刷的接口定义。2.2 电源管理与深度睡眠配置30 天续航的关键在于把平均电流压到极低。ESP32 在深度睡眠模式下可以做到 10μA 左右但前提是你要把所有外设都处理好。墨水屏在刷新完成后要进入休眠模式蓝牙在不需要的时候要彻底关闭传感器不用的要断电。我的做法是用一个 MOS 管来控制墨水屏的电源刷新完成后直接切断供电。ESP32 本身设置定时器唤醒每分钟醒来一次更新时间醒来后先连 Wi-Fi 校准时间如果距离上次校准超过 6 小时然后刷新屏幕再进入深度睡眠。这里有个细节ESP32 的深度睡眠唤醒后程序是从头开始执行的相当于一次软重启。所以你不能把状态保存在变量里必须用 RTC 内存或者外部 Flash 来保存。RTC 内存在深度睡眠期间仍然供电适合保存时间戳、闹钟状态这类小数据。// 深度睡眠配置示例 #define uS_TO_S_FACTOR 1000000ULL #define TIME_TO_SLEEP 60 // 睡眠60秒 void setup() { // 初始化外设 initDisplay(); initRTC(); // 读取RTC内存中的状态 esp_sleep_wakeup_cause_t wakeup_reason esp_sleep_get_wakeup_cause(); if (wakeup_reason ESP_SLEEP_WAKEUP_TIMER) { // 定时器唤醒更新时间 updateTime(); } // 刷新屏幕 refreshDisplay(); // 关闭外设电源 powerOffPeripherals(); // 进入深度睡眠 esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); esp_deep_sleep_start(); }实测下来这套配置下整机平均电流在 150μA 左右800mAh 的电池理论上能撑 220 天。但实际因为 Wi-Fi 校准和天气获取时会消耗较大电流最终续航在 30-40 天之间。如果你把 Wi-Fi 校准频率降低到一天一次续航还能再延长。2.3 蓝牙 BLE 的自定义表盘传输协议自定义表盘这个功能本质上是把一张图片传输到手表上然后手表把这张图片转换成墨水屏能显示的格式。传输走 BLE因为 BLE 的低功耗特性适合这种小数据量传输。协议设计上我把一张表盘图片分成多个包每个包 128 字节通过 BLE 的特征值写入。手表端收到包后先缓存到 Flash全部收完后做一次校验校验通过再切换到新表盘。这样即使传输中断也不会影响当前表盘的正常显示。图片格式我选的是 1 位黑白 BMP因为墨水屏本身就是 1 位色深不需要做灰度转换。手机端可以用任何绘图软件画好 200x200 的图导出为单色 BMP然后通过一个简单的手机 App 或者网页蓝牙工具发送。注意BLE 的 MTU 默认是 23 字节实际可用载荷只有 20 字节。传输前需要先协商更大的 MTUESP32 支持最大 517 字节的 MTU协商成功后每包可以传 512 字节左右传输效率会高很多。传输 200x200 的 1 位图数据量是 200*200/8 5000 字节。按每包 512 字节算大约 10 个包就能传完整个过程不到 2 秒。这个速度对于表盘切换来说完全够用。3. 实操过程与核心环节实现3.1 硬件连接与供电方案先把硬件连起来。ESP32 开发板选的是带电池管理功能的型号这样可以直接插锂电池不需要额外的充电模块。墨水屏通过 SPI 接口连接接线如下墨水屏引脚ESP32 引脚说明VCC3.3V供电建议加 MOS 管控制GNDGND共地DINGPIO 23SPI MOSICLKGPIO 18SPI SCKCSGPIO 5片选DCGPIO 17数据/命令切换RSTGPIO 16复位BUSYGPIO 4忙状态检测供电部分锂电池正极接开发板的 BAT 引脚开发板自带的充电芯片会管理充电。墨水屏的 VCC 不要直接接 3.3V而是通过一个 P 沟道 MOS 管控制MOS 管的栅极接 ESP32 的一个 GPIO刷新时拉低导通刷新完拉高断电。指南针用的是 QMC5883L 或 HMC5883L 三轴磁力计通过 I2C 连接SDA 接 GPIO 21SCL 接 GPIO 22。这个传感器只在用户主动查看指南针时才上电平时完全断电。3.2 开发环境搭建与库依赖Arduino IDE 里需要安装 ESP32 的开发板支持然后在库管理器里装以下几个库GxEPD2墨水屏驱动库支持多种驱动芯片局刷功能完善Adafruit GFX图形绘制库提供画线、画圆、写字等基础功能ArduinoJson解析天气 API 返回的 JSON 数据NTPClient时间同步库ESP32 BLE ArduinoBLE 服务端和客户端库安装完库之后先在 Arduino IDE 里跑一个最简单的墨水屏示例确认屏幕能正常显示。这一步很重要因为不同批次的屏幕驱动芯片可能不一样如果示例跑不起来后面的一切都白搭。// 墨水屏初始化示例 #include GxEPD2_BW.h #include Adafruit_GFX.h // 根据屏幕型号选择驱动 GxEPD2_BWGxEPD2_154_D67, GxEPD2_154_D67::HEIGHT display( GxEPD2_154_D67(/*CS*/5, /*DC*/17, /*RST*/16, /*BUSY*/4) ); void setup() { display.init(115200); display.setRotation(0); display.setTextColor(GxEPD_BLACK); display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); display.setCursor(10, 50); display.setTextSize(2); display.print(Hello Watch); } while (display.nextPage()); // 刷新完成后让屏幕休眠 display.hibernate(); }提示display.hibernate()这个调用非常关键它会让屏幕进入深度休眠电流降到微安级别。如果你忘了调这个屏幕会一直保持在刷新后的状态虽然画面不变但驱动电路仍在耗电。3.3 表盘绘制与自定义实现表盘绘制用的是 Adafruit GFX 的绘图函数。默认表盘我设计了三种布局数字时钟加日期、模拟时钟加天气、极简只显示时间。每种布局都封装成一个函数通过一个变量来切换。自定义表盘的实现方式是手表从 Flash 里读取一张 1 位位图然后逐像素写入墨水屏的显存。GxEPD2 库提供了drawBitmap()函数可以直接把位图数组画到指定位置。// 自定义表盘绘制示例 void drawCustomWatchface(const uint8_t* bitmap) { display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); // 在(0,0)位置绘制200x200的位图 display.drawBitmap(0, 0, bitmap, 200, 200, GxEPD_BLACK); } while (display.nextPage()); display.hibernate(); }位图数据的生成我写了一个简单的 Python 脚本把 PNG 图片转换成 C 数组。脚本用 Pillow 库读取图片缩放到 200x200转成 1 位黑白然后按墨水屏的显存格式输出字节数组。from PIL import Image def png_to_bitmap(filepath): img Image.open(filepath).convert(1) img img.resize((200, 200)) pixels img.load() bitmap [] for y in range(200): byte 0 for x in range(200): if pixels[x, y] 0: # 黑色 byte | (0x80 (x % 8)) if x % 8 7: bitmap.append(byte) byte 0 return bitmap这个脚本生成的数组可以直接粘贴到 Arduino 代码里或者存到 Flash 的 SPIFFS 文件系统中运行时读取。3.4 天气获取与番茄闹钟逻辑天气数据我用的是一个免费的天气 API通过 HTTP GET 请求获取指定城市的实时天气。返回的 JSON 里包含温度、湿度、天气状况代码。解析后提取需要的字段映射成图标和文字显示在屏幕上。// 天气获取示例 #include HTTPClient.h #include ArduinoJson.h void fetchWeather() { HTTPClient http; http.begin(http://api.example.com/weather?citybeijing); int httpCode http.GET(); if (httpCode 200) { String payload http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, payload); float temp doc[main][temp]; int weatherId doc[weather][0][id]; // 保存到RTC内存或全局变量 currentTemp temp; currentWeather weatherId; } http.end(); }番茄闹钟的逻辑很简单用户通过蓝牙设置一个 25 分钟的倒计时手表在倒计时结束后震动提醒如果装了震动马达或者刷新屏幕显示提醒信息。倒计时期间手表每分钟检查一次剩余时间但不刷新屏幕只在最后 1 分钟开始每秒刷新。注意番茄闹钟的倒计时不能依赖 ESP32 的定时器因为深度睡眠会重置定时器。正确做法是把结束时间戳存在 RTC 内存里每次唤醒时比较当前时间和结束时间判断是否该触发提醒。4. 常见问题与排查技巧实录4.1 墨水屏刷新异常问题速查现象可能原因解决方法全屏黑白闪烁后无显示SPI 接线错误或驱动芯片不匹配检查接线确认驱动型号局刷后出现残影局刷次数过多增加全刷频率每30分钟全刷一次屏幕显示一半或错位显存页边界计算错误检查局刷的页和字节范围刷新后屏幕变灰供电不足或刷新电压不够检查电池电量确认3.3V稳定深度睡眠后无法唤醒唤醒源配置错误检查定时器唤醒使能位残影这个问题我遇到过很多次尤其是在显示大面积黑色区域后切换成白色背景时特别明显。解决办法除了定期全刷之外还可以在局刷前先写入一次全白再写入新内容相当于做一次局部清理。但这样会增加刷新时间需要权衡。4.2 蓝牙连接不稳定与传输失败BLE 传输表盘时最容易出现的问题是连接断开或数据丢包。我总结了几条经验第一传输前先协商 MTU。ESP32 默认 MTU 是 23协商到 512 后传输效率提升 20 倍以上。协商代码在 BLE 库的示例里有直接抄就行。第二每包数据之间加一个短延时。虽然 BLE 协议本身有流控但实际测试下来连续快速写入容易导致缓冲区溢出。我在每包之间加了 10ms 延时丢包率从 5% 降到了 0.1% 以下。第三接收端要做校验。我在每包数据里加了序号和 CRC 校验收完后检查序号是否连续、CRC 是否匹配。如果不匹配就请求重传虽然协议变复杂了但可靠性大幅提升。4.3 续航不达标的排查思路如果你做出来的手表只能撑几天而不是 30 天按以下顺序排查先测深度睡眠电流。用万用表串联在电池和开发板之间进入深度睡眠后看电流读数。如果大于 1mA说明有外设没关干净。最常见的是墨水屏没调hibernate()或者 BLE 没彻底关闭。再测 Wi-Fi 工作时的电流和时间。Wi-Fi 连接和 HTTP 请求是耗电大户一次完整的天气获取大约消耗 100mA 电流持续 3-5 秒。如果你每分钟都获取一次天气那续航肯定崩。我的做法是每 6 小时获取一次天气平时显示的是缓存数据。最后检查电池容量是否虚标。很多便宜的小锂电池标称 800mAh实际只有 300-400mAh。换一块靠谱的电池续航直接翻倍。提示ESP32 的深度睡眠电流在官方数据手册里是 10μA但实际做出来通常在 50-100μA 之间因为开发板上还有 LDO、USB 转串口芯片等外围电路在耗电。如果你追求极致续航可以自己画一块最小系统板去掉所有不必要的外围电路。4.4 指南针校准与精度提升磁力计很容易受到干扰尤其是靠近电机、扬声器、金属外壳的时候。我在实际使用中发现如果手表外壳是金属的指南针基本没法用。换成塑料或 3D 打印外壳后精度明显改善。校准方法是把手表拿在手里在空中画几个“8”字让磁力计采集到各个方向的数据然后计算硬铁和软铁补偿值。QMC5883L 自带校准寄存器写入补偿值后精度能到 1-2 度。如果不校准误差可能有 10-20 度基本没法用。另外指南针功能只在用户主动打开时才启用平时磁力计完全断电。这样既省电又避免了磁力计持续工作带来的干扰。5. 外壳制作与佩戴体验优化5.1 3D 打印外壳的设计要点外壳我前前后后改了五版才定型。第一版太厚戴在手上像块砖头。第二版太薄屏幕和电池挤在一起后盖盖不上。第三版按键位置不对手指按不到。第四版表带接口太脆摔了一次就断了。最终版的尺寸是 45mm x 40mm x 12mm这个厚度在墨水屏手表里算中等但考虑到里面塞了 800mAh 电池和 ESP32 开发板已经很难再压缩了。表带接口用的是标准的 20mm 生耳可以直接换市面上任何 20mm 的表带。打印材料选的是 PETG比 PLA 更耐温和耐摔。填充率 30%壁厚 1.2mm这个参数下外壳既有足够的强度又不会太重。打印完后用砂纸打磨一下边缘手感会好很多。注意如果你用的是带 Wi-Fi 和蓝牙的 ESP32外壳不要用金属材料否则信号会被屏蔽。塑料或树脂材料对信号的影响很小可以放心用。5.2 佩戴舒适度与日常使用感受墨水屏手表最大的优势是“不打扰”。它不会像 LCD 或 OLED 那样主动发光在暗处需要借助外部光源才能看清。这听起来像缺点但实际用下来晚上看时间时反而更舒服不会刺眼。白天在户外墨水屏的反射式显示比透射式屏幕更清晰阳光越强看得越清楚。重量方面整机大约 60 克比大多数智能手表轻。戴在手上几乎感觉不到负担睡觉时戴着也不硌手。表带我用的是硅胶材质透气性好出汗了也不会难受。充电接口我留了一个磁吸触点用磁吸线充电不需要插拔。充电时手表会自动开机显示充电进度充满后自动断电。这个体验比插 USB 线好太多尤其是对手表这种小设备来说。5.3 后续可扩展的功能方向这个项目的框架已经搭好了后续想加什么功能都很方便。比如加一个心率传感器通过 I2C 连接在表盘上显示实时心率。或者加一个环境光传感器根据光线自动调整屏幕刷新策略。再或者加一个蜂鸣器做整点报时。软件层面可以把表盘做成可配置的用户通过手机 App 选择显示哪些信息、放在什么位置。甚至可以做一个简单的表盘编辑器让用户拖拽组件来设计自己的表盘。我个人最想加的功能是电子书阅读。墨水屏看文字非常舒服如果能把 txt 文件传到手表上用按键翻页就是一个极简的阅读器。虽然屏幕小了点但胜在便携和续航通勤路上看几页还是很惬意的。这个项目最吸引我的地方是它把“低功耗”和“可编程”结合得很好。你不需要在续航和功能之间做取舍而是可以通过合理的电源管理和刷新策略让两者兼得。每次看到自己做的表盘在屏幕上显示出来那种成就感是买成品手表给不了的。