用 Codex 把一块吃灰的七色墨水屏救活
我手上有一块老的七色墨水屏,600 × 448,Pico 驱动。它其实一直没坏,只是一直躺着吃灰。
原因很简单:这东西看起来只是 SPI 屏,真要做成一个长期能用的小项目,后面会牵出一长串活:清 Waveshare 例程、接 Pico SDK、写构建脚本、做 PC 上位机、设计 USB 传图协议、处理图片量化,最后还要面对最烦人的颜色问题。
这次我没再硬着头皮手写,而是让 Codex 当工程搭档,一路把固件、上位机、扫描仪取样和颜色校准都推完。最后这块吃灰屏真的活过来了。
先把厂商例程瘦下来
Waveshare 的例程能跑,但它是典型的“全家桶”:examples/ 下面塞了很多不同尺寸的测试文件,lib/e-Paper/ 下面也有一堆屏幕型号驱动,甚至还有不少预编译 UF2。对我这块 600 × 448 七色屏来说,绝大多数都是噪音。
Codex 先做的是把项目拆成一个干净的小工程。目标屏幕只保留 EPD_5in65f.c/h,Pico GPIO/SPI 底层、GUI 画图库和必要字体留下,其它屏幕型号、示例合集、预编译 UF2 都清掉。CMakeLists.txt 也重新写了一遍,不再靠目录扫描把无关文件偷偷编进去。
构建这边也顺手整理了:build.ps1 会自动接入本机 Pico SDK 和 ARM GNU Toolchain;SPI 初始化里 CLK/MOSI 也被修正成 GPIO_FUNC_SPI。这一步结束后,项目终于不再像厂商资料包,而是一个明确的 Pico 固件工程。
固件先做稳
硬件上我加了三个按键:KEY0 接 GP15 预留,KEY1 接 GP17 用来刷本地 demo 图,KEY2 接 GP2 用来清白屏。
启动行为后来也改了。Pico 上电后不再自动刷新测试图,只初始化屏幕和 USB 串口,然后等 PC 指令。墨水屏刷新一次很慢,调试时每次上电都自动刷图,纯粹是在消耗耐心。
PC 传图走 Pico 的 USB CDC 串口,协议很朴素:帧头 magic 是 EPD7,固定尺寸 600 × 448,payload 是 4bpp,两个像素一个字节,高 4 位是左像素,低 4 位是右像素。帧头里还带 CRC32,Pico 校验通过后才真正刷新屏幕。
七个颜色索引保持和 Waveshare 驱动一致:
| 颜色 | 索引 |
|---|---|
| Black | 0x0 |
| White | 0x1 |
| Green | 0x2 |
| Blue | 0x3 |
| Red | 0x4 |
| Yellow | 0x5 |
| Orange | 0x6 |
这个协议的好处是 Pico 端很轻。PC 已经把图片转成屏幕原生 4bpp 索引数据,Pico 收到之后直接 EPD_5IN65F_Display()。
后面查 Waveshare 的注意事项时,又补了一轮保护:开机会自动清白屏一次;每次真正刷新前都重新 EPD_5IN65F_Init();刷新或清屏结束后立刻 EPD_5IN65F_Sleep()。七色墨水屏刷新时要高压驱动,不刷新时一直挂在工作状态并不健康。这个改动不酷,但它很重要。
固件更新也被顺手修舒服了。Pico SDK 的 USB stdio 本来支持 1200 baud 触发 BOOTSEL,也支持 picotool 的 vendor reset。Codex 把这些显式打开,又写了 flash.ps1:先编译 epd.uf2,再让正在运行的 Pico 进 ROM BOOTSEL,最后 picotool load -x 写入并重启。后面改固件终于不用每次按住 BOOTSEL 再拖 UF2。
上位机不是只发图
PC 端工具先是命令行版,后来又做成了 Tkinter GUI。它最开始只是为了把图片转成屏幕格式并发到 Pico,后来慢慢变成了整个项目的控制台。
GUI 里可以打开图片,选择 fit 或 fill,在 PC 端做 0/90/180/270 度旋转,生成七色预览,保存 .epd 原始数据,再选择串口发送到 Pico。抖动算法也有几种可选:Floyd-Steinberg、Sierra-2、palette-aware blue-noise,或者干脆不抖动。
后面常用操作也都塞进去了:一键清屏,一键发送 Color Test 校准图,一键重新编译并刷写 Pico 固件。按钮布局也整理过,左边放串口、刷新端口和固件更新,右边放生成预览、保存、发送图片、Color Test 和清屏。小改动,但用起来明显少绕路。
fit 和 fill 的区别也很实际。fit 保留整张图,空白补白;fill 铺满屏幕,居中裁切。旋转发生在缩放之前,最终都会变成 600 × 448,再量化成七色屏能显示的索引数据。
这是生成出来的纯 7 色校准图:

它没有文字,也没有抖动,每条色带都是 600 × 64 像素。目的不是好看,而是方便后面用扫描仪稳定取样。
颜色这件事最麻烦
七色墨水屏不是普通 RGB 显示器。显示器是发光,墨水屏是反光;屏幕上的“红”和图片里的 #ff0000 不是一回事。它的白不是发光白,黑也不是显示器黑,蓝绿黄橙都有各自的反射特性。
如果直接拿理想 RGB 去做最近色匹配,结果会非常粗糙。理论上的红是 (255, 0, 0),蓝是 (0, 0, 255),白是 (255, 255, 255);但这块墨水屏在真实环境里给出来的颜色,完全不是这么回事。
所以 Codex 帮我把校准闭环也做了出来:GUI 上传 Color Test 到 Pico,屏幕显示七条纯色色带,我把屏幕放到 EPSON L6290 网络扫描仪上,Codex 用 Windows WIA 直接启动扫描,保存扫描图,再自动定位色带区域、取样每个色块的扫描 RGB,最后生成 epaper_calibration.json。
扫描图是这样的:

这次扫描得到的中位 RGB 大概是:
| 颜色 | 扫描 RGB |
|---|---|
| Black | (53, 54, 74) |
| White | (161, 157, 158) |
| Green | (60, 81, 61) |
| Blue | (68, 67, 103) |
| Red | (126, 65, 69) |
| Yellow | (155, 134, 77) |
| Orange | (141, 81, 70) |
这组数字一看就知道问题在哪:屏幕的“白”在扫描仪里也就 (161,157,158) 左右,不是 (255,255,255);黑也不是 (0,0,0),而是带一点蓝紫倾向的暗色。
转换算法也跟着改
现在的图片转换流程大致是:读取输入图片的 sRGB,按需要旋转,再按 fit/fill 缩放到 600 × 448;然后把 sRGB gamma 解码到线性 RGB,根据扫描得到的黑白点把输入动态范围映射到屏幕实际反射动态范围;候选颜色再转到 OKLab 里做最近色匹配或调色板混色,最后输出 4bpp 七色索引数据。
这里解决了两个很现实的问题。第一,不再拿发光 RGB 的理想色值硬套反光屏。第二,不再在 gamma 压缩后的 RGB 里直接做误差扩散。
下面是同一张测试图,用理想 RGB 色板和扫描校准色板生成的预览对比:

右边不一定更鲜艳,但它更接近这块屏实际会给你的颜色空间。对七色屏来说,这比盲目追求饱和度更有意义。
抖动算法也迭代了一轮。最开始只有 Floyd-Steinberg,后来加了 Sierra-2 和 blue-noise ordered。第一次 blue-noise 很直觉:给线性 RGB 加一点蓝噪声,再找最近的 7 色。它看起来确实有 ordered dither 的味道,但浅色区域很容易被推向黄/橙,尤其是动漫图里的肤色和浅灰。
后来改成 palette-aware blue-noise。每个像素先在校准后的七色调色板里找最能混出目标色的 1/2/3 色组合,再用 blue-noise mask 按权重分配这些颜色。这样 blue-noise 不再直接扰动 RGB,而是只负责把调色板颜色铺开,平均颜色就稳得多。

从左到右分别是 Floyd-Steinberg、Sierra-2 和 palette-aware blue-noise。误差扩散通常更细腻,但会有方向性的误差传播;blue-noise 颗粒感明显一点,不过没有长拖尾,在某些照片上反而更像印刷网点。
Codex 参与了什么
这次有意思的不是“写了一个驱动”,而是 Codex 把一堆原本会让我拖延的工程环节串了起来。
它清理 Waveshare 例程,搭 Pico SDK 工程,改 CMake,写固件、按键逻辑、USB CDC 传图协议和安全刷新流程;然后继续写 PC 图片转换工具、Tkinter GUI、固件刷写脚本;再往后,它又接入扫描仪取样、颜色校准、OKLab 匹配和多种抖动算法。
这些事情如果手工做,当然也不是做不到。但每一步都很容易让人“明天再说”:清工程、接 SDK、调 CMake、写 Python、做 GUI、跑串口、扫图、校准颜色、回头修固件。项目真正难的地方不是某一个算法,而是一直不掉线。
如果只是普通网页 AI 对话,也会卡在同样的问题上。它可以给建议、给代码片段、解释算法,但关键反馈都发生在真实环境里:编译器报什么错、串口有没有响应、扫描图有没有旋转、预览图有没有偏色、屏幕刷新后实际长什么样。没有执行和感知能力,就很容易停在“听起来合理”的层面。
最后
以前这类项目最烦的不是某一段代码,而是上下文切换。写嵌入式,切到 CMake;CMake 通了,切到 Python;Python 能处理图了,又要搞串口;串口通了,又发现颜色不对;颜色不对,还得碰扫描仪、Lab、误差扩散。每一步都不难,但每一步都足够让人明天再说。
这次 Codex 把这些“明天再说”压缩成了一个连续 session。
最后得到的是一套真的能用的东西:Pico 固件能编译,屏幕能按键刷新和清屏,每次刷新后会自动休眠,下一次刷新前自动初始化;固件能通过 GUI 一键更新,不用总按 BOOTSEL 拖 UF2;PC 能上传图片,GUI 能旋转、预览、发送和刷固件;扫描仪能取样,颜色转换能基于实测反射颜色校准,抖动算法也从“能用”迭代到更适合七色调色板。
一块老墨水屏,从“懒得写代码”变成“有固件、有上位机、有校准流程”,没有花掉一个周末。
所以结论还是那句:Codex 好。
用 Codex 把一块吃灰的七色墨水屏救活
