1. 项目缘起与整体设计思路
1.1 为什么要在冰箱上贴一块电子墨水屏
家里有冰箱的人都懂那种场景:站在冰箱前面,门开着,脑子里飞速回忆“到底缺什么”,然后掏出手机翻备忘录,翻着翻着就被一条推送带走了,等回过神来已经刷了十分钟短视频,购物清单还是没写。纸质便签倒是直接,但写完贴上去,过两天就掉地上,或者被油污糊得看不清,更别提两个人同时想往上加东西的时候,只能靠喊。
PaperMono 这个项目就是冲着这个场景去的。它的核心形态是一块贴在冰箱门上的电子墨水屏磁贴,常驻显示一份购物清单,家里人谁想加东西,掏出手机打开一个网页就能同步上去,屏幕几秒后刷新,清单就更新了。没有 App 要装,没有账号要注册,没有云端账号体系,局域网内一个网页搞定。
我第一眼看到这个标题的时候,判断它属于典型的“低功耗常显设备 + 轻量 Web 同步”组合。电子墨水屏的特性决定了它只在刷新时耗电,静态显示几乎不耗电,这跟冰箱贴“常年挂着、偶尔更新”的使用节奏完美契合。而“手机网页同步”这个设计选择,直接绕开了 App 开发和分发的一堆麻烦事,用浏览器这个所有人都有的客户端,把同步这件事做到了零门槛。
适合参考这个项目的人其实挺广的:玩 ESP32 的嵌入式爱好者、想给家里做点实用小玩意的 DIY 玩家、对电子墨水屏刷新机制好奇的开发者,以及想理解“设备端 + 网页端”最小同步架构的人。哪怕你手上没有现成的墨水屏,读完整个思路,换成 LCD 或者别的显示方案,逻辑也是通的。
1.2 方案选型的几个关键取舍
做这类设备,第一个要拍板的就是主控。市面上能驱动电子墨水屏的方案不少,从树莓派到各种单片机都有。PaperMono 这类项目通常会选 ESP32 系列,原因很实在:它自带 Wi-Fi,省掉外挂网络模块的麻烦;GPIO 数量够驱动墨水屏的 SPI 接口;深度睡眠模式下功耗能压到微安级别,配合墨水屏的断电保持特性,一块小电池能撑很久。如果用树莓派,性能是过剩的,但待机功耗和成本都上去了,对“贴冰箱上常年不动”的设备来说不划算。
第二个取舍是同步架构。这里有个分岔路:走公网云同步,还是走局域网直连。云同步的好处是人在外面也能加东西,但代价是要维护服务器、处理账号、考虑安全,项目复杂度直接翻倍。PaperMono 选择的是局域网方案,设备自己起一个轻量 Web 服务,手机连同一个 Wi-Fi 就能访问。这个选择背后的逻辑是:购物清单这个场景,绝大多数添加动作都发生在家里,人在超市的时候往往是看清单而不是加清单,局域网足够覆盖核心需求,而且隐私数据不出家门,省心。
第三个取舍是墨水屏的刷新策略。电子墨水屏有个众所周知的毛病:全刷会黑屏闪一下,局部刷新虽然不闪但残影会累积。PaperMono 需要在“刷新体验”和“显示寿命”之间找平衡。常见的做法是局部刷新为主,累积到一定次数后做一次全刷清残影。这个阈值设多少,直接影响到观感和屏幕寿命,后面会详细展开。
提示:选型阶段最容易被忽略的是墨水屏的驱动电压和时序要求。不同型号的墨水屏对 SPI 时序、忙信号处理的要求差异很大,买屏之前一定要确认有没有现成的驱动库支持,否则光调时序就能耗掉你一周。
2. 核心细节解析与硬件实操要点
2.1 硬件清单与选型参数
先把硬件摊开说。PaperMono 这类项目的硬件构成其实很精简,核心就几块:
| 部件 | 常见选型 | 关键参数 | 选型理由 |
|---|---|---|---|
| 主控 | ESP32 系列 | 双核 240MHz,内置 Wi-Fi | 自带网络,深度睡眠功耗低 |
| 墨水屏 | 2.13 寸或 2.9 寸 SPI 屏 | 分辨率 250x122 或 296x128 | 尺寸适合冰箱贴,单色够用 |
| 供电 | 锂电池或干电池组 | 3.7V 锂电 + 充电管理 | 配合深睡可撑数周至数月 |
| 外壳 | 3D 打印或亚克力 | 背面嵌磁铁 | 贴合冰箱门,方便取下充电 |
主控这块,ESP32 的型号选择有个细节值得说。如果只是驱动墨水屏加跑个 Web 服务,普通的 ESP32-WROOM 模组完全够用。但如果想省电到极致,可以考虑带 ULP 协处理器的型号,让主核在等待期间彻底睡死。不过对于购物清单这种“一天刷新几次”的场景,普通型号的深度睡眠已经足够,没必要为了那点功耗去折腾更复杂的架构。
墨水屏的尺寸选择也有讲究。2.13 寸的屏分辨率是 250x122,能显示大概 8 到 10 行文字,对于一份购物清单来说刚好。2.9 寸的 296x128 稍微宽一点,能多显示几行,但价格和体积都上去了。我的建议是先用 2.13 寸起步,等确认整个流程跑通了,再考虑换大屏。因为墨水屏的驱动逻辑在不同尺寸间是相通的,先跑通再升级,风险最低。
2.2 墨水屏驱动的关键原理
电子墨水屏的工作原理值得花点篇幅讲清楚,因为这直接决定了你的刷新策略。屏幕里悬浮着无数微小的黑白颗粒,每个颗粒带电荷,靠电场控制它们翻到正面还是背面。翻一次需要时间,这就是为什么墨水屏刷新慢;翻完之后断电,颗粒位置不变,这就是为什么它能静态保持不耗电。
驱动层面,SPI 接口负责传数据,但真正麻烦的是时序。墨水屏每次刷新前要先发一堆命令配置寄存器,然后传图像数据,最后发一个刷新命令,接着要等一个“忙”信号拉低才能进行下一步。这个忙信号如果处理不好,要么刷新不完整,要么屏幕卡死。常见的驱动库比如 GxEPD2 已经把这些时序封装好了,直接用就行,但你要理解它内部在干什么,出问题的时候才知道往哪查。
局部刷新和全刷的区别也在这里。全刷是把所有颗粒强制翻一遍,画面干净但会闪黑;局部刷新只翻转需要变化的区域,不闪但旧颗粒位置可能有残留。PaperMono 的清单场景,文字变化其实不大,大部分时候只是某一行加了个字或者删了一行,局部刷新完全够用。但连续局部刷新几十次后,残影会明显到影响阅读,这时候就需要插一次全刷。
2.3 供电与功耗的实测账
功耗这块我算过一笔账,挺有意思。ESP32 在活跃状态跑 Wi-Fi 的时候,电流大概在 80 到 120 毫安之间波动。墨水屏刷新一次的峰值电流能到 10 毫安左右,但持续时间只有一两秒。深度睡眠模式下,ESP32 能压到 10 微安级别。
假设一天刷新 10 次,每次活跃 5 秒,那么一天的活跃耗电大约是 100mA × 5s × 10 = 5000mA·s,换算成毫安时是 5000/3600 ≈ 1.39mAh。剩下的时间都在深睡,24 小时减去 50 秒,约等于 24 小时,按 10 微安算,一天耗电 0.24mAh。加起来一天不到 2mAh。一块 2000mAh 的锂电池,理论上能撑 1000 天。当然实际有自放电、电压转换损耗、电池老化等因素,打个对折也有 500 天。这个账算下来,你就明白为什么墨水屏方案适合做常显设备了。
注意:上面这个账是理想值。实际中 Wi-Fi 连接建立的过程很耗电,如果每次唤醒都要重新连 Wi-Fi,那 5 秒可能不够。优化方向是让设备记住 Wi-Fi 信道和 BSSID,缩短重连时间,或者干脆用定时唤醒而不是事件唤醒。
3. 固件实现与手机网页同步的完整流程
3.1 固件整体架构拆解
PaperMono 的固件跑在 ESP32 上,整体架构可以分成四层:硬件驱动层、网络服务层、业务逻辑层、电源管理层。这个分层不是为了好看,而是为了改起来方便。比如你换了一块不同型号的墨水屏,只需要动驱动层;想改网页界面,只动网络服务层。
硬件驱动层负责跟墨水屏、按键(如果有)、电池检测电路打交道。网络服务层起一个 HTTP 服务器,处理手机浏览器的请求,返回网页和处理表单提交。业务逻辑层维护那份购物清单的数据结构,处理增删改查。电源管理层决定什么时候进深睡、什么时候唤醒。
数据存储这块,购物清单本身不大,几十条顶天了,直接存在 ESP32 的 NVS(非易失性存储)里就行,掉电不丢。每次修改后写一次 NVS,刷新屏幕。不需要文件系统,也不需要外部存储芯片,简单可靠。
3.2 手机网页同步的实现细节
手机网页同步是整个项目的交互核心,它的实现比想象中简单。ESP32 起一个 HTTP 服务,监听 80 端口。手机连上同一个 Wi-Fi 后,浏览器访问设备的 IP 地址,设备返回一个 HTML 页面。这个页面就是一个简单的表单,上面列出当前清单,每条后面有个删除按钮,下面有个输入框加添加按钮。
表单提交用 POST 请求,设备收到后解析参数,更新内存里的清单,写 NVS,触发屏幕刷新,然后返回更新后的页面。整个过程没有任何 JavaScript 框架,就是最朴素的 HTML 表单。为什么不用 AJAX 做无刷新体验?因为对于这个场景,页面刷新一下也就几百毫秒的事,而且实现简单,不容易出 bug。少一层抽象,就少一堆调试的麻烦。
设备 IP 地址怎么让手机知道?两个办法。一是设备启动后在屏幕上显示自己的 IP,你手动输一次,存成书签。二是用 mDNS,给设备起个名字比如 papermono.local,手机浏览器直接访问这个名字就行。mDNS 在大多数家用路由器下都能用,但偶尔会有解析不稳定的情况,所以建议两个都配上,mDNS 不行就手动输 IP。
// 简化的 HTTP 处理逻辑示意 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ String html = buildListPage(shoppingList); request->send(200, "text/html", html); }); server.on("/add", HTTP_POST, [](AsyncWebServerRequest *request){ if (request->hasParam("item", true)) { String item = request->getParam("item", true)->value(); shoppingList.push_back(item); saveToNVS(); refreshDisplay(); } request->redirect("/"); });上面这段是逻辑示意,实际代码里还要处理清单长度上限、空输入过滤、特殊字符转义这些细节。特别是转义,用户输入里如果有尖括号或者引号,不转义直接拼进 HTML 会出问题,虽然是自己家用,但养成好习惯没坏处。
3.3 屏幕刷新与数据同步的联动
数据变了要刷新屏幕,但刷新不能太频繁。如果用户连续添加五条,你刷新五次,屏幕闪五次,体验很差。合理的做法是加一个防抖:数据变更后启动一个短定时器,比如 3 秒,3 秒内没有新变更就刷新一次。这样连续操作只会触发一次刷新。
刷新的时候还要考虑局部还是全刷。我的做法是维护一个局部刷新计数器,每做一次局部刷新就加一,超过 20 次就做一次全刷并清零。这个 20 次是实测下来的经验值,低于这个数残影不明显,高于这个数就开始影响阅读了。当然不同批次的屏幕残影表现有差异,你可以根据自己的屏调整。
还有一个细节是刷新时的电源状态。刷新过程中不能进深睡,否则刷一半断电屏幕会花。所以刷新前要确保电源管理模块知道“现在忙,别睡”,刷完再允许睡眠。这个状态同步如果没做好,会出现屏幕偶尔显示不全的诡异现象。
3.4 从零搭建的实操步骤
如果你要复现这个项目,我建议按下面的顺序来,每一步都验证通过再往下走:
点亮屏幕:先不管网络,用现成的驱动库把墨水屏点亮,显示一行“Hello”。这一步验证接线和驱动没问题。接线注意 SPI 的 CS、DC、RST、BUSY 四个引脚,不同库对引脚定义要求不同,照着库的示例改。
连上 Wi-Fi:写一段代码让 ESP32 连上你家 Wi-Fi,串口打印出获取到的 IP。这一步验证网络模块正常。
起一个最简单的网页:用 WebServer 库起一个服务,访问根路径返回一个“Hello Web”。手机浏览器能打开就说明网络服务通了。
把清单逻辑加上:定义清单数据结构,实现添加和删除,先在串口里打印验证逻辑正确。
网页和清单打通:网页显示清单,表单提交能修改清单,修改后串口能看到变化。
网页修改触发屏幕刷新:把前面几步串起来,网页提交后刷新屏幕显示最新清单。
加电源管理:最后再加深度睡眠和唤醒逻辑,因为这一步最容易引入难查的问题,放最后做,前面的功能都稳定了再动。
这个顺序的核心逻辑是:先让每个独立模块跑通,再做集成。很多新手一上来就把所有代码写在一起,出了问题不知道是哪部分的,排查起来非常痛苦。
4. 常见问题与排查技巧实录
4.1 屏幕刷新异常的问题排查
墨水屏最让人头疼的就是刷新异常,表现五花八门:花屏、残影严重、刷新一半卡住、完全不刷新。我把踩过的坑整理成一张速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全不刷新 | 忙信号没接或接错 | 检查 BUSY 引脚,用万用表测电平 |
| 刷新一半卡住 | 刷新期间进了睡眠 | 检查电源管理逻辑,刷新时禁止睡眠 |
| 残影严重 | 局部刷新次数过多 | 降低全刷阈值,或增加全刷频率 |
| 花屏 | SPI 时序不对或电压不足 | 检查供电电压,确认 SPI 速率 |
| 显示内容错位 | 图像缓冲区尺寸不对 | 核对屏幕分辨率与缓冲区大小 |
花屏这个问题我遇到过好几次,最后发现是供电不足。墨水屏刷新瞬间电流需求比较大,如果电源走线太细或者电池电量低,电压会瞬间跌落,导致刷新不完整。解决办法是在墨水屏的电源脚旁边并一个大电容,比如 100 微法,给瞬间电流做个缓冲。这个细节很多教程不会提,但实际做的时候很关键。
4.2 网络同步的典型故障
网络这块的问题相对好排查,因为现象比较明确。最常见的是手机连不上设备。先确认手机和设备是不是在同一个网段,有些路由器开了访客网络或者 AP 隔离,设备之间互相 ping 不通。再确认设备的 IP 有没有变,DHCP 分配的地址可能会变,建议在路由器里给设备绑定一个固定 IP。
mDNS 解析失败也很常见。有些路由器不支持 mDNS 转发,或者手机系统对 mDNS 支持不好。这时候就老老实实输 IP。我自己的做法是在设备外壳上贴一张小标签,写上 IP 地址,换网络的时候更新一下,比每次去路由器后台查方便。
还有一个隐蔽的问题是 HTTP 请求超时。如果设备正在刷新屏幕,而刷新期间 CPU 被占用,HTTP 响应可能会延迟。手机浏览器等不及就报错了。解决办法是把屏幕刷新放到一个独立的任务里,别阻塞网络服务。ESP32 是双核的,一个核跑网络,一个核跑显示,互不干扰,这个架构值得采用。
4.3 功耗与稳定性的平衡经验
做低功耗设备,最怕的是“看起来睡了其实没睡”。ESP32 的深度睡眠有几个前提条件:所有 GPIO 要设置好状态,不能有悬空的引脚导致漏电;Wi-Fi 要彻底关闭;外设要断电。我遇到过屏幕驱动芯片在睡眠时仍然耗电的情况,后来加了一个 MOS 管控制屏幕供电,睡眠时彻底切断,功耗才降下来。
另一个经验是关于唤醒频率。如果设成每分钟唤醒一次检查更新,功耗会上去;如果设成每小时一次,又不够及时。折中方案是用一个物理按键唤醒,想加东西的时候按一下,设备醒来连 Wi-Fi,你手机刷新页面就能看到。这样既省电又及时。按键用中断唤醒,ESP32 支持 GPIO 中断唤醒深睡,这个功能很好用。
提示:调试功耗的时候,万用表测的是平均电流,看不出瞬间峰值。如果想精确分析,可以用一个采样电阻配合示波器看电流波形,这样能发现那些短暂的耗电尖峰。
5. 项目扩展与个人实操体会
5.1 可以继续往下做的几个方向
PaperMono 这个基础版本跑通之后,能扩展的方向不少。最直接的是加一个“已购买”状态,清单里的条目可以勾选,勾选后显示删除线或者移到下方。这个功能在网页端实现很简单,加个复选框,设备端多存一个状态位就行。
再进一步可以做多清单切换,比如“本周采购”“日用品”“零食”分开。网页上加个标签页,设备端存多份清单,屏幕上显示当前选中的那份。这个扩展会让数据结构复杂一点,但逻辑还是清晰的。
如果想让设备更独立,可以加一个低功耗的温湿度传感器,顺便在屏幕上显示冰箱内的温湿度。这个属于锦上添花,但确实实用,尤其是对食材保存比较在意的人。传感器选 I2C 接口的,接线简单,驱动也成熟。
还有一个方向是离线可用性。现在的方案依赖 Wi-Fi,如果路由器挂了,网页就打不开。可以考虑在设备上做一个简单的物理按键,按一下直接添加一个预设条目,比如“牛奶”,不依赖网络。这个降级方案在关键时刻能救急。
5.2 我在实际搭建中的几点体会
这个项目我从头到尾搭过一遍,最大的体会是:简单方案往往最可靠。一开始我想用 WebSocket 做实时同步,觉得那样才“现代”,结果调试了半天连接稳定性,最后还是换回了最朴素的表单提交。表单提交虽然每次要刷新页面,但它没有状态,不会断连,不会内存泄漏,对于这个场景就是最优解。
第二个体会是关于屏幕尺寸和字体的。2.13 寸的屏,如果字体选得不好,显示效果会大打折扣。我试过好几种字体,最后发现等宽字体在墨水屏上最清晰,因为笔画粗细均匀,不容易糊。字号也别太小,最小 12 像素,再小就费眼了。清单条目之间留点行距,阅读起来舒服很多。
第三个体会是外壳的重要性。电子设备裸露着放冰箱上,既不美观也不安全。我用 3D 打印做了一个简单的外壳,背面嵌了四块小磁铁,吸在冰箱门上很稳。外壳上留了充电口和按键孔。如果你没有 3D 打印机,用亚克力板加磁铁也能凑合,关键是让设备有个“成品感”,这样你才会愿意一直用它,而不是玩两天就扔抽屉里。
最后分享一个小技巧:设备第一次配网的时候,如果不想硬编码 Wi-Fi 密码,可以用 ESP32 的 SmartConfig 或者 AP 配网模式。设备启动后如果连不上已知 Wi-Fi,就自己起一个热点,手机连上去填 Wi-Fi 密码,填完设备重启连网。这个功能加上之后,换路由器或者搬家都不用重新烧录固件,省事很多。