Hacker News 中文摘要

RSS订阅

电子阅读器屏幕条纹消失探因 -- How my e-reader lost its stripes

我一生都是个读书人,可在这个被琐碎干扰充斥的时代,这习惯变得脆弱。书和电子墨水阅读器能对抗注意力的争夺,却比不过手机方便:口袋里的小设备让阅读更易,也更容易被通知打断。几天前我头一回听说新一代超小尺寸电子墨水阅读器,因为便宜,便由着好奇心买了一台 Xteink X3。它不仅小得惊人,还能立刻装上开源固件 CrossPoint。

正文

我一直是个终身读者,但在如今这个无尽琐碎干扰的时代,这个习惯很脆弱。书和墨水屏阅读器解决了注意力被争夺的问题,却比不过手机的便利。口袋里那台小设备让阅读变得更容易,也让阅读更容易被通知打断。

几天前,我第一次听说新一代微型墨水屏阅读器。它们很便宜,好奇心很容易被满足:我买了一台 Xteink X3。它小得惊人,而且我马上就能装上让人愉快的开源固件 CrossPoint。我很快装好了几本书。我也很喜欢能装自定义字体,只是对字体渲染的平淡有点意外(这是伏笔)。

CrossPoint 开箱就很好配置,我把一张照片转成抖动的黑白位图当作「设备睡眠」屏,效果挺好看。当我读到设备支持整整 4 级灰阶时,来了兴趣:灰度图会不会更好看?

掉进兔子洞:最初的图像 bug

这暴露出一个像是 bug 的问题:睡眠屏显示灰度图时暗部非常浑浊。要么是我这中年人眼睛终于不行了,要么是深灰被渲染成了黑色?我做了一张快速测试图,确认深灰确实就是黑色,而浅灰极浅(几乎发白)。有点烦人,但在廉价设备上不算意外,也容易绕开:那就做一张三色调图像。

重新生成的图像里大块黑色区域少了,这时我又看到一个新 bug:在 CrossPoint 的查看器应用里,上一屏的内容还以幽灵般的形态残留着(只出现在屏幕较浅的部分,所以第一次我没注意到)。但同一张图,睡眠屏却没有这个问题。这说明有两个图像渲染器在做不同的决定,而查看器应用的代码有 bug。

不过两种情况里,照片上都出现了位图文件中没有的明显垂直条纹。

我的三色调图像的手机照片。细垂直条纹在背景中最容易看到。

我对墨水屏、ESP32 开发、CrossPoint 几乎一无所知,于是用 2026 年末的常规方式开始调查:在 Codex 里用 GPT-6 Astra。我用手机拍下 X3 的屏幕,把图片丢进 Codex 会话。

墨水屏靠电压脉冲移动黑白色素颗粒,断电后颗粒会留在原位。更新不完整或电压不足,就会留下上一幅图像的鬼影。

要显示灰度图,CrossPoint 先画一层黑白底,连灰色像素一开始也是黑的。然后运行一段很短的电压脉冲波形,把选中的像素部分推向白色。这第二阶段,也就是「微推」,靠不同时长的驱动产生深浅不同的灰。

Astra 发现查看器只做了一次快速黑白更新就停了,从未执行灰度微推。它很快修好了。

条纹则顽固得多。Astra 起初乱撞,怪罪它用来准备我睡眠图时使用的 Floyd–Steinberg 抖动。换算法没区别。它又顺着调用栈往下找,指出微推波形值得调查。

但我们连自己看的、测的是哪种伪影都难以达成一致,这让我担心;我不想烧着最先进的 token 追幽灵。我一追问,Astra 就一头扎进去,报告说条纹图案宽两像素。这对我来说毫无道理,于是我让它标注照片,结果发现它挑出的是细抖动纹理,而不是我看到的横贯图像的条带。

Astra 的 FFT 发现了什么

在这里用快速傅里叶变换相当聪明:要挑出并量化肉眼难以测量的重复图案,它几乎是理想工具。Astra 对照片和源图的小块做二维 FFT,发现两者都在大约两个屏幕像素处有强重复。

可惜误差扩散抖动本身就会产生结构,天生常是高频噪声,在 FFT 里形成强信号。Astra 挑出的是 Floyd–Steinberg 的细点图案,正是它自己选来准备图像、调查早期又怪罪过的算法。我抱怨的那种更宽的条带只出现在阅读器上。我质疑它的估计后,它做了标注,确认我们看的是不同图案。

让条纹变得可测量

被 Astra 那些走不通的假设搞得有点疲惫,我换到 Claude Code 里的 Fable 5.1,换个视角。

我有种直觉,条纹的宽度有含义,但 Astra 连识别出条纹都做不到。这确实不容易,很多来源会引入图案和噪声:

  • 图像自身的抖动图案,骗过了 Astra
  • 引入条纹的东西
  • X3 屏幕自身的物理特性
  • 手持手机拍摄这一团乱麻:
    • 传感器噪声
    • 画面内对焦变化
    • 镜头畸变(要拍 259ppi 屏幕必须微距)
    • 光照和曝光变化、处理伪影
    • 我手抖造成的运动模糊

Fable 被提醒不要重蹈 Astra 天真的图像处理死路,它写代码把每列亮度做平均。下面这张图里,它用了 200 行高的滑动窗口:每个点变成它周围一小条垂直区域的均值。这样抖动纹理被平均掉,而沿列持续存在的亮度差异会留下来。图像里的大块形状还在,但条纹容易看得多:

应用滑动垂直平均后的裁切图。大块形状属于照片;细垂直带是缺陷。

Fable 接着对一维亮度剖面做 FFT,测量垂直图案的间距和强度。它第一次猜条纹间距约七个屏幕像素,但这是基于对我照片尺度的猜测。

它又把注意力转回抖动,这次是固件内部的抖动,提出反复的舍入误差可能对齐,产生垂直带。我说源图已经是抖动的,它更兴奋了,但这最终是 20 分钟的死路。另一次调查里,Fable 对图像的位深大动干戈,也没结果。至少它的调查比 Astra 新鲜点?

灰色有什么不同?

我也在设备上研究简单得多的图像。去掉灰色或去掉图案,条纹都会消失:

| 图像 | 有灰色像素 | 相邻像素处于不同状态 | 条纹 | | --- | --- | --- | --- | | 平坦灰场 | 是 | 否 | 无 | | 黑白抖动 | 否 | 是 | 无 | | 用两种方法之一的灰度抖动 | 是 | 是 | 有 |

灰色像素与不同色调的邻居结合,才是问题所在。Fable 把源图的小块与手机照片匹配,终于让它看到是浅灰像素携带了条纹——这个重要细节我之前都没看清,因为浅灰像素太淡了。

证据现在指向屏幕如何通过灰度微推产生灰色。相关代码在 CrossPoint 使用的硬件库 freeink-sdk 里。

Fable 起初不愿再深入:「我没法从这里设计或验证 LUT 改动。」LUT 是保存微推波形的查找表。当我指出 X3 就在我桌上,新构建显示什么我都能拍,它才回来提出可以做的实验。

X3 用磁吸触点充电这个古怪选择在这里帮了忙。Fable 可以在插电时通过 USB 刷写并重启设备。然后我可以拿起设备拍屏幕,再放回去,完全不用碰 USB-C 接口。

测试图案,以及第二个 bug

现有微推持续七个扫描周期:每次控制器逐行扫过面板,对每个像素施加电压序列的下一步。我们仍以为条纹间距约七像素。时序会不会表现为空间图案?改变波形时长就能给我们比较对象。

首先需要一张更好测量的图,我建议 Fable 生成一张测试图。它做了一个图案,包含四种灰阶的平坦色块、灰色与黑或白的混合、棋盘格,以及两个方向的线条。

测试图案。平坦色块确定四种灰阶;图案区域测试灰色与其他灰阶相邻时的表现。棋盘格位于第三行右侧。

测试图让进展突然容易得多。Fable 知道图像应该长什么样,我照片和屏幕外观的变化就能被看见和解释。比如 Fable 原先的尺度估计把照片频谱里的一个特征误当成屏幕像素网格。用测试图案当尺子,条纹周期其实是八像素,不是七。

我同时提供手机的原始 DNG 文件和处理过的照片。对比显示手机处理把条纹幅度夸大约 70%,还改变了表观灰阶。之后我们都用原始文件。Fable 弄清了如何在取景、透视、镜头畸变变化下定位测试色块,从而用同样方式测量每次构建和照片。

我们试着把微推从七帧拉长到十帧,又试了驱动脉冲与休息交错的版本。两者都不影响条纹。这排除了帧数与条纹周期的关联。

三种,然后四种灰阶

测试图案还带回了一个我们之前绕开的问题。

原始波形下的测试图案。顶行前两个色块应为黑色和深灰,但都是黑色。第三行左侧深灰配黑的色块也消失了。

我们号称四灰阶的阅读器确定只显示三种灰阶;这不只是我眼睛老化。深灰就是黑。我幸运地在 Fable 思考时正好看着它发现这一点,因为它只是顺口提了一句。我明白这个发现的重要性,不得不插进去,让它停下,深挖。

Fable 追溯到 CrossPoint 和驱动对如何请求深灰有分歧。CrossPoint 为微推每像素发两位,选择四张波形表之一。它给深灰的代码选了一张什么都不做的表。想要的深灰驱动在另一张表里。Fable 修好错误,并向 freeink-sdk 提交了 PR。

修好后,深灰出现了,本该是深灰抖动叠在黑上的色块,从实心黑方块变成了可见的斑点。但浅灰驱动没变,条纹也没变。我们在为第一个 bug 做测试时顺手修了另一个 bug。

缺失的灰阶还让文字看起来很粗糙(还记得之前提到字体渲染平淡吗?)。CrossPoint 对文字做抗锯齿,本应用深灰柔化边缘的像素之前出来是黑的,字就显得粗胖。恢复这一灰阶后,整个阅读器的文字都改善了。

更慢的绘图方式

驱动里还有一段我们没试过的波形:厂商的四灰阶图像模式,叫 XTH4。它用长得多的脉冲序列产生四种灰阶,刷新约需一秒。每次翻页都等这么久会很烦,但打开图片或绘制睡眠屏似乎合理。

这张表已经在 freeink-sdk 里,另一款阅读器的驱动用它处理图像。我们可以直接在 X3 上试,不用从零发明波形。Fable 完全不确定这能有什么用,但我催它继续。

先要解决内存问题。微推只需区分深灰、浅灰和「别动这个像素」。黑白可以共用最后一条指令,因为第一遍已经画好它们。更长的波形需要为四种灰阶分别下指令。

另一款阅读器的驱动把黑白图像副本存在 RAM 里,再与灰度数据合并。我的 X3 用 ESP32-C3,约 380 KB RAM,最大空闲块只有 53 KB。屏幕 528×792 像素,哪怕一位的副本也要 52 KB。没有余量随便加缓冲区。

Fable 提议让 CrossPoint 一开始就按所需格式绘制数据。图像查看器本来就要为每次渲染解码文件;这些渲染可以产出每像素两位、标识全部四种灰阶的数据。驱动拿到需要的东西,又不用多一屏数据。

全部实现并刷机后,我打开引发调查的那张图,告诉 Fable:「条纹在我那张为什么有条纹?的原始图像上不再可见了。」

我的 X3 上的照片,修复前后。分界线以下使用原灰度波形,以上使用新波形。

测量结果一致。列变化从黑白范围的约 4% 降到 1%。八像素处的峰值从频谱中完全消失。

列亮度相对黑白范围,网格线每八个屏幕像素一条。上图中规律的振荡在下图中消失。

更长波形还带来一个意外好处:灰阶本身也改善了。恢复深灰让我们有了四级;这还让浅灰更远离白色(记得我说它很淡吗?)。深灰比我想要的稍暗,所以还有些调校要做。

原始固件、恢复深灰后、更长波形下的实测灰阶。虚线标记等间距亮度作对比。

我们得到答案了吗?

所以这是一次半满意的调查:换代码解决了问题,但我们真的知道它最初为什么发生吗?不完全。

以下是我基于新学到、仍然非常有限的电泳显示知识做出的最佳猜测。墨水屏面板的行由蚀刻在玻璃上的栅极驱动器切换。几路交错的时钟驱动栅极,每路每隔 n 行打开一次,时长略有不同。被强力驱动到黑或白的颗粒对时序变化不敏感。在灰度微推中花三帧走到中间灰色的像素,只得到它所在行时间窗允许的电荷,于是每第八行会略亮或略暗。

列被充电时,这个过程一次一行,其电压只在相邻行是不同灰阶时才需要变化。如果一个灰色像素紧跟在黑色像素后,这次电压摆动必须在短暂且可变的时间窗内完成。平坦灰场从不摆动,黑白会摆动,但光学响应饱和,时序没那么重要。只有灰度抖动时,我们才同时看到摆动和对时序的敏感,条纹就出现了。

我知道这是真的吗?不知道,但它符合电气架构和我手头的证据。我本可以继续追下去,但图像不再有条纹已经让我松了口气。

像素化的圆满结局

驱动改动成了 freeink-sdk#95。我的两个 freeink-sdk PR 都在几小时内合并。我也提交了几个 CrossPoint PR。有趣的是,有人抢在我前面,就在我的改动被审查之前合并了一套几乎相同的改动。

我觉得这一切的时间尺度有点荒唐。拆箱第一台墨水屏设备几小时后,我就在测试它的显示波形修复。Astra 和 Fable 处理陌生的代码、构建工具和测量脚本,而我可以专注于实验该做什么,提供关键提示比如用 FFT 和测试图案,在死路上质疑并转向,以及分享屏幕实际显示的内容。

这种能跳进未知领域并立刻取得进展的经历相当令人兴奋。尽管一开始我对墨水屏、CrossPoint、ESP32 所知不多,几小时内就学到了很多。它确实勾起了我对更多这类底层设备改造的兴趣。

评论

社区对 Xteink X3 的便携低价与开源属性普遍好评,并欣赏原帖真实记叙,但少数用户遗憾文章未讲清开箱阅读体验;另有人惊叹 AI 自调显示查找表,并批评大模型生成图表缺乏读者视角。

称赞 X3 便携低价且原帖记录真实

  • 用户实测设备便宜小巧、小屏阅读舒适易携带,开源 CrossPoint 可同步页码,长期用封闭设备的用户也感新鲜;文章被赞真诚手写而非机器生成(较多人) > 这小玩意儿便宜到离谱,放口袋里碎片时间读起来竟挺顺手(@asveikau)

不满文章未交代开箱即用的阅读体验

  • 有用户虽对新产品心动,但指出体验文没说明作为电子阅读器开箱后到底如何,让人难以决策(少数人) > 要是能说说它开箱当阅读器究竟好不好用就省得猜了(@PerilousD)

赞叹用 AI 自反馈调显示器查找表

  • 从厂商获取显示查找表向来最难,让 AI 借图像反馈自行调优被赞不可思议(少数人) > 没想到让 AI 看图像自己把最难弄的查找表调了出来,绝了(@jareklupinski)

批评 LLM 生成图表和文章时缺乏读者意识

  • 图表爱好者指出模型做图会把对话私货(如每8格网格线说明)硬塞进标签,像急于显摆的新生而非考虑读者需求;写文同理(少数人) > 模型产出的图表总把对话里的过程细节塞给读者,仿佛不懂对方并不需要那些(@ainch)

有价值的补充

  • 曾有开发者构建 Linux 打印驱动,能直接将文件打印并上传至这类阅读器。