比 Vibe Coding 键盘更进一步:把 Claude Code 塞进便携微型键盘与彩色电子纸

最近圈子里很火的 Vibe Coding 键盘大多只是接在电脑上的几键宏键盘。这个项目干脆往前多走了一步:用异构双核的 Arduino UNO Q 直接在板载 Linux 上跑 Claude Code,配上微型键盘与 7.3 寸 E Ink Spectra 6 六色电子纸,做成一套能揣出门带走、强光大太阳底下越晒越清楚的便携 Vibe Coding 终端。本文记录从 Spectra 6 微杯显色原理、三线 SPI 逆向 4 KB OTP,到排查 2 倍超频伪影与局刷硅片分界的全过程。

比 Vibe Coding 键盘更进一步:把 Claude Code 塞进便携微型键盘与彩色电子纸

从去年底开始,开发者圈子里突然火起了一个叫 “Vibe Coding 键盘” 的概念:既然现在写代码主力变成了 Claude Code、Cursor 这类 AI Agent,人坐在电脑前干得最多的事不再是一行行敲语法,而是输入一段意图、看着 AI 自己读文件跑测试、最后按一下回车确认(Accept)——于是不少人做起了只有三四个按键的 USB 迷你宏键盘。

但这波网红 Vibe Coding 键盘有个共同的局限:它们本质上还是挂在台式机或笔记本后面的一个 USB 外设,人依然被拴在那块刺眼的 LCD 显示器前。用 Claude Code 跑大一点的任务时,人大部分时间其实是在等待和审阅;如果只是为了瞄一眼 AI 干到哪一步、敲几个字回个确认,为什么要一直盯着发光屏幕,或者背着一台沉甸甸、到了户外太阳底下就反光看不清的笔记本电脑?

四月中旬的时候我就动了念头:干脆比市面上的 Vibe Coding 键盘更进一步,直接把完整的 Claude Code 运行环境塞进“微型键盘 + 彩色电子纸屏幕”里,做成一台能揣进包里带走、强光下也能舒服使用的独立便携终端:

  • 真正的独立便携(插上充电宝就能带走):不用接电脑主机,巴掌大的开发板自己就跑着完整 Linux 和 Claude Code,配上微型键盘、连上手机热点或咖啡馆 Wi-Fi 就能随时随地开工;
  • 强光直晒可用,户外越晒越清楚:7.3 寸全反射式彩色电子纸,不靠背光发光,哪怕直接端到户外露台的大太阳底下,也像实体铜版纸书页一样字迹锐利、色彩饱满,长时间盯着看眼睛完全不累;
  • 天然契合 Vibe Coding 的思考与等待节奏:AI 正在长考或跑测试时,屏幕保持静态显示且零功耗;需要你拍板或任务完成时,抬眼扫一下颜色和文字,在微型键盘上敲几下就能继续推进。

很多人对电子纸的印象还停留在 Kindle 的黑白灰,或者早期彩屏那种像蒙了一层塑料布的暗淡颜色。所以在进正文之前,先放一张这块 7.3 寸彩色电子纸在自然光下实拍的色带测试图——没有任何前光或背光,全靠环境光在颜料表面的直接反射:

7.3 寸 E Ink Spectra 6 六色电子纸实拍色带
图 1:自然光下实拍的 7.3 寸 Spectra 6 电子纸色带测试图,无任何背光辅助,直接呈现出高净度的纸白底色与高饱和度的黑、白、黄、红、蓝、绿及过渡色带

为什么选 E Ink Spectra 6:原理、选型与强光表现

手头选的这块屏幕是微雪 / Good Display 出品的 7.3 寸六色电子纸 GDEP073E01,分辨率 800×480,基于元太科技(E Ink)最新的 Spectra 6 技术,裸屏约 70 美元,搭配一块 DESPI-C73 转接板(15 美元)。

既然要做一台能带出门的便携 Vibe Coding 终端,屏幕在室内自然光和户外强光下的可读性就是第一位的。在选型阶段,我对比过市面上常见的 Kaleido 3 彩屏和 Spectra 6,最终毫不犹豫选了后者,核心原因只有三个:白色净度高、原生色彩饱和度高、自然光与强光下显示效果好。

1. 告别滤光片:为什么 Spectra 6 白底更净、强光下更通透?

目前消费级彩色阅读器主流的 Kaleido 3 方案,本质上是在传统的黑白 Carta 墨水层上方,强行贴了一层 RGB 彩色滤光片(CFA)。这层滤光片像墨镜一样截留了超过 50% 的环境反射光——一旦关掉前光阅读灯,屏幕的“白色”底色看起来就像浸湿的旧报纸一样发灰,彩色分辨率也会因为 RGB 子像素并排而折半,颜色显得寡淡。

而 Spectra 6 彻底去掉了顶层的彩色滤光片,改用 Microcup(微杯) 结构,让带颜色的电泳颜料粒子直接浮到屏幕最顶层反射环境光。当二氧化钛(TiO₂)白色粒子全部浮到顶面时,没有任何滤光片挡路,白色净度和反射率极高,观感非常接近真实的铜版纸;当彩色粒子浮到顶面时,反射的也是原汁原味的颜料本色,原生色彩饱和度远超滤光片方案,对比度超过 30:1。更重要的是,因为它是纯反射式结构,在普通 OLED/LCD 屏幕彻底沦为“反光镜”的户外正午强光下,环境光越强,Spectra 6 反射回人眼的画面反而越亮、越清晰。

Spectra 6 无滤光片微杯结构与四粒子混色原理
图 2:传统 Kaleido 3 滤光片方案与 E Ink Spectra 6 四粒子微杯架构的物理对比,以及四种带电粒子拼出六种基色与原生橙色的光学混色机理

2. 四种带电粒子,怎么拼出六种颜色?

更有意思的是它的底层物理结构。根据 E Ink 在 SID 2023 发表的技术论文(Paper 88-2: Highly Saturated Color Electrophoretic Display),这块号称“六色”的电子纸,每个微杯里其实只装了 4 种带电颜料粒子,悬浮在透明介电液中,靠电荷极性(正/负)和电荷量大小(高/低迁移率阈值)来区分:

  • 带正电(+)的两种粒子:蓝色粒子(带高正电荷,且具有半透明 Translucent 光学特性)、红色粒子(带低正电荷,散射反光型)。
  • 带负电(−)的两种粒子:黄色粒子(带高负电荷,散射反光型)、白色粒子(带低负电荷,高反射散射基底)。

驱动芯片在 ±15 V 范围内提供 6 档非零电压轨,通过“中间态清洗 → 目标态分离”的两段式脉冲序列,就能用 4 种粒子组合出 6 种甚至更多的颜色:

  1. 白、黄、红、蓝(单色独立登顶):利用高低幅值的正负脉冲和迁移速度差,把同极性里的快粒子和慢粒子拉开,让单一颜色粒子占据微杯顶面。
  2. 绿色(半透明蓝 + 反光黄):先将反光黄色粒子拉到顶面附近,再用特定窄脉冲把高迁移率的半透明蓝色粒子也拉到顶层。入射光穿过半透明蓝、在黄色粒子表面反射回来,光学叠色后就呈现出纯正的绿色。
  3. 黑色(半透明蓝 + 反光红,无黑色颜料):微杯里根本没有黑色炭黑粒子。当施加高正压脉冲时,半透明蓝色粒子与散射红色粒子同时聚集在顶面,蓝与红叠加吸收了绝大部分可见光波段,直接合成出对比度 >30:1 的深邃纯黑。
  4. 橙色(反光红 + 反光黄):如果把红色粒子往上拉的同时,中间档电压推力不足、没能把黄色粒子彻底压回杯底,红黄粒子在顶层均匀混合,就会呈现饱和的原生橙色——这个物理特性,恰好成了我们后面破解“隐藏橙色配方”的关键伏笔。
户外阳光直射下的 7.3 寸六色电子纸与 UNO Q 整机实拍
图 3:在户外木桌阳光直射下实拍的 7.3 寸电子纸终端与 Arduino UNO Q——强烈的太阳光把旁边的阴影打得极深,但屏幕上的白底黑字与蓝色/黄色状态标签反而像纸张一样清晰锐利

硬件架构:为什么选能“整机揣走”的异构双核 Arduino UNO Q?

很多做电子纸副屏的项目会顺手拿一块 ESP32,但 ESP32 只能当一个依赖电脑主机喂画面的 Wi-Fi 显示傀儡——一旦离开家里的台式机,整套东西就歇菜了。既然目标是做一台把微型键盘、电子纸和 AI Agent 合为一体的便携 Vibe Coding 终端,主控就必须自己能跑完整的 Linux 和 Claude Code,同时还能搞定底层的硬实时 SPI 波形驱动。

所以我最终选了刚推出不久的异构双核开发板 Arduino UNO Q(约 44 美元)。它虽然只有巴掌大、长着标准 UNO 的外形,但板子上实际塞了两颗各司其职的芯片:

  • MPU 侧(高通 QRB2210):跑完整的 Debian Linux,带 2 GB 内存和 Wi-Fi / 蓝牙,可以直接接上便携微型键盘,在板子上原生运行 Node.js 和 Claude Code。
  • MCU 侧(意法半导体 STM32U585):跑 Zephyr RTOS,负责微秒级硬实时 GPIO 时序与 10 MHz 硬件 SPI 刷新,绝不会因为 Linux 侧跑大模型网络请求或垃圾回收而卡顿掉帧。
  • 板内互联:两颗芯片在板内通过高速串口(ttyHS1)直连,官方提供了 arduino-router 服务,两侧直接通过 msgpack-RPC 互相调用函数。
UNO Q 板内的数据通路
图 4:UNO Q 板内数据通路——Claude Code、微型键盘输入与 Python 渲染守护进程直接跑在板载 Linux 上,STM32 专职负责底层 SPI 时序输出

这样一来,整套装置只需要一根普通的 USB-C 线接上充电宝就能独立工作(五月初我甚至直接把这块板子加电子纸揣进包里带去星巴克,连上咖啡馆 Wi-Fi 边喝咖啡边调代码)。我把 Claude Code 直接装在 UNO Q 自己的 Linux 系统里(工程目录 /home/arduino/eink/),写了一个 Python 守护进程 eink-daemon,并在 ~/.claude/settings.json 中挂载了 UserPromptSubmit、PreToolUse、PostToolUse、Stop 四个生命周期 Hook。

每次 Claude Code 收到提问、准备调用工具或停下来等待输入时,Hook 就往本地 Unix Socket 发一条轻量通知。守护进程收到后用 Pillow 渲染出一张 800×480 的五分区状态图,量化抖动成 4 bpp 字节流(一帧恰好 192,000 字节),通过内部 RPC 分块推给 STM32,再由 STM32 走硬件 SPI 刷进电子纸。

从第一天写方案起我就定了一条架构原则:不在 STM32 固件里写死任何屏幕初始化序列,只暴露 raw_cmd、raw_data、raw_reset、raw_read 四个最底层的原子接口。所有寄存器配置和波形表实验全部在 Linux 侧用 Python 发送,改任何寄存器参数都不需要重新编译烧录单片机固件。

五分区状态屏跑通后的实拍
图 5:四个生命周期 Hook 跑通后的面板实拍,画面按顶栏、主状态块、最近工具调用、24 小时活动热力图和费用预算条分为五个区域

听起来三四天就能收工。结果从 4 月 15 日写下第一版计划,一直折腾到 6 月初才算把这颗驱动芯片的脾气彻底摸透。

项目两个月完整时间线
图 6:两个月完整推进时间线:从前期的资料交叉核验、首日点亮排障、三线 SPI 逆向 OTP,到中途误开 2 倍超频的十天弯路,最终收敛出 7 套稳定配方

上板前的插曲:两个 AI 联手制造的“幻觉冤案”

屏幕还在快递路上时,我先把厂家给的 STM32 和 ESP32 参考代码翻了一遍。看完心里挺凉:出厂默认波形全屏刷新一次要 28 到 30 秒,而且附带的 ESP32 局刷例程(ESP32-GDEP073E01-jushua)打开一看,里面的 EPD_PartialWindow 函数干脆是被整段 /* ... */ 注释掉的半成品,连最基本的写显存命令 0x10 和刷新命令 0x12 都没写。

要想缩短刷新时间,或者调出出厂六色之外的过渡色(比如 Claude 标志性的橙色),唯一的办法就是绕开芯片内部的出厂 OTP 波形,自己往寄存器里写自定义查找表(LUT)。

我先拿这个问题去问 Gemini。Gemini 很笃定地告诉我:只要把 PSR (0x00) 寄存器的高位置 1 切到外部 LUT 模式,就能往 0x20–0x29 寄存器里塞自定义波形表;它还顺手给了一个出处,说是硬件黑客 Zephray 在 GitHub 的 zephray/ACeP-Driver 仓库里逆向出来的。

我让 Claude Code 去查这个仓库,结果直接撞上 404,翻遍 Zephray 的博客也没找到半个字。Claude 当场在设计文档 PLAN.md 里给这条路判了死刑,断定 0x20–0x29 是大模型的幻觉,GDEP073E01 的波形锁死在芯片 OTP 里无法从外部修改。

第二天我不死心,拿着这串寄存器编号去全网搜,才在 Hackaday.io 上翻到一篇 2021 年 4 月的旧日志。逆向出这套寄存器的根本不是 Zephray,而是意大利开发者 Manuel Tosone(ts-manuel),他在调试七色屏控制器 SPD1656 时把 0x20–0x29 的未公开定义和代码全贴出来了。最哭笑不得的是,回头翻 Claude 前一天的搜索日志,这篇帖子明明就排在搜索结果第一页,只因为原作者把标题里的 Technology 少打了一个 h 拼成了 Understanding ACeP Tecnology,就被 Claude 当成不靠谱的拼写错误野鸡帖直接略过了。

总结下来就是:Gemini 记错了作者名字、记对了寄存器地址;Claude 核对了作者发现查无此人,就连带着把正确的寄存器也当成幻觉给枪毙了。


首日点亮排障:三个独立环节同时假报“成功”

4 月 20 日下午硬件到齐,我照着转接板丝印把杜邦线接好:BUSY 接 D7,RST 接 D8,DC 接 D9,片选 CS 接 D10,MOSI 和 SCK 接 D11 和 D13,供电接 3.3V。

首日接线表与引脚核对记录
图 7:4 月 20 日上板时的引脚对应表,最初按常规 Arduino 习惯把片选 CS 接在了 D10

写好测试固件传进板子,终端一片绿字提示执行成功,然而屏幕纹丝不动,BUSY 引脚读回来永远是高电平。那天从下午一路查到夜里十一点半,之所以排查得这么曲折,是因为板子上有三个完全独立的环节同时在骗我“一切正常”:

  1. 后台守护进程静默覆盖固件:刚注册好的 RPC 接口,每次跑几秒钟就自己消失,串口还报 ttyHS1 忙。顺着进程表去抓,才发现 UNO Q 的系统里预装了一个叫 claude-state-watcher.service 的服务(运行 claude-crab-led.py)。它只要检测到系统里有 claude 进程在跑,就会每隔几秒往单片机发一个 draw 请求,想在板载点阵灯上画一只螃蟹。因为我的电子纸固件里没有 draw 函数,它调不通就判定单片机固件损坏,在后台自动调用烧录脚本,把出厂的螃蟹灯固件重新刷回去。我前脚刷完,它后脚就静默盖掉。
  2. 官方烧录脚本写错 Flash 分区地址:停掉螃蟹灯服务后,我用板子自带的 /opt/openocd/bin/arduino-flash.sh 手动刷固件。每次 OpenOCD 都打印 Verified OK,但新代码死活不生效。去扒 Zephyr 编译生成的设备树分区表才发现:STM32U585 上的用户程序作为动态模块存放在 0x08100000,而官方脚本里写死的烧录地址是 0x080F0000——那个地址落在 0x080D0000 起始的 bootanimation(开机动画)分区里。OpenOCD 确实把字节分毫不差地写进了 Flash 并校验通过,但写进的是开机动画区,CPU 永远不会去那里取指令。改用标准 arduino-app-cli app start 流程后,固件才算真正跑起来。
  3. D10 引脚被硬件 SPI2 外设强行接管:固件跑起来后屏幕依然没反应。晚上七点多我去翻这块板子的 Zephyr 设备树源文件 zephyr-arduino_uno_q_stm32u585xx.dts,在第 782 行找到了真凶:
dts
pinctrl-0 = <&spi2_nss_pb9 &spi2_sck_pd1 &spi2_miso_pc2 &spi2_mosi_pc3>;

UNO Q 上的 D10 对应芯片的 PB9,在设备树里被默认绑定成了硬件 SPI2 的片选脚 spi2_nss_pb9。只要代码里一调用 SPI.begin(),硬件 SPI 外设就直接接管了 PB9 引脚的复用权,后续代码里写的 digitalWrite(10, LOW) 完全不起作用,引脚电平根本不会翻转。

排查 D10 被 spi2_nss_pb9 硬件接管的终端记录
图 8:定位到设备树第 782 行 PB9 被绑定在 spi2_nss_pb9 上,随后直接将蓝色 CS 杜邦线从 D10 改插到 D6

最稳妥的办法就是不跟设备树较劲,直接动手把那根蓝色的 CS 杜邦线从 D10 拔下来,改插到没有任何复用冲突的 D6 引脚。

线一挪完,当晚 23:25 重新下发初始化序列,屏幕终于“咔哒咔哒”闪烁起来,稳稳推出了出厂色带:

面板跑通出厂色带测试实拍
图 9:将片选 CS 改接到 D6 后,面板首次成功响应命令并刷出测试色带

点亮屏幕后紧接着算了一笔传输账(图 10)。一帧画面 192 KB,如果靠 Zephyr 默认仅 64 字节的串口接收缓冲,从 Linux 推往 STM32 经常丢包卡死。4 月 24 日我花半天时间重新编译了板载 Zephyr 底包,将串口环形缓冲区直接扩大 64 倍到 4096 字节,在 921600 波特率下整帧推送才彻底稳定在 3 秒以内。

传输与刷新耗时预算拆解记录
图 10:192 KB 帧数据从 Linux 经串口到 STM32、再经 SPI 写入屏幕的传输耗时拆解

三线双向 SPI 读回:确认 SPD1657A 身份与 4 KB OTP 波形结构

屏幕点亮之后,下一步就是摸清这颗驱动芯片的真实型号和内部波形表格式。

此前开源社区里(包括常被引用的 codeart1st 逆向笔记)普遍认为微雪/Good Display 的 7.3 寸彩屏用的是晶门科技的 SPD1656 或 UC8159 变种。但我试着发送了网上流传的 SPD1656 解锁魔数(0x33 -> 0xAB -> 0xCD),芯片毫无反应。

1. 没有 MISO 引脚,如何从单根数据线读回寄存器?

要想读取芯片内部的 ID 寄存器和出厂 OTP 波形表,首先碰到一个硬件限制:DESPI-C73 转接板只有 8 根排针,只引出了一根单向的数据输入线 SDI (MOSI),根本没有引出 MISO 引脚。

好在电子纸驱动芯片底层走的是 3 线双向半双工 SPI(3-Wire Bidirectional SPI)——主控发命令和芯片回传数据,实际上共用同一根物理引脚 SDA。于是我在 STM32 固件 sketch.ino 里实现了一个软件切换方向的 raw_read 函数:

  1. 先通过硬件 SPI 把 1 字节的读取指令(如读版本指令 0x70 或读 OTP 指令 0x92)发送出去;
  2. 发送完毕的瞬间,立即调用 SPI.end() 释放硬件 SPI 外设对引脚的控制权;
  3. 将 D11 (MOSI) 引脚动态重配为 INPUT 输入模式,D13 (SCK) 设为 OUTPUT 输出模式;
  4. 用软件翻转 SCK 时钟线,在每个时钟上升沿从 D11 逐位读取芯片吐回来的电平,拼成字节返回,读完再重新调用 SPI.begin() 恢复硬件 SPI。

靠这个土办法下发 0x70 读版本指令,芯片干脆利落地吐回了三个字节:01 01 04。

对照晶门科技的芯片编码规则,身份水落石出:这颗芯片根本不是七色屏时代的 SPD1656,而是专门针对 Spectra 6 六色微杯屏设计的 SPD1657A(硬件修订版 0x04)。两代芯片的指令集只有约 85% 重合,前人在 SPD1656 上总结的解锁序列在 SPD1657A 上完全是空操作。

2. 拆解 4 KB 出厂 OTP:揭开 20 字节状态机格式

打通双向读回通道后,我紧接着下发 0x92 (ROTP) 指令导出出厂波形表。起初我写了个循环一口气读了 96 KB 存为 otp_full.bin,结果过了两天写解析脚本时发现不对劲:怎么每隔 4096 字节(0x1000),后面的数据就跟文件开头一模一样?

计算 SHA-256 哈希一比对,所谓的 96 KB 数据,其实是同一个 4096 字节的页面原封不动重复了 24 遍(图 11)。原因在于 SPD1657A 的 0x92 指令不支持地址偏移续读,每次拉低片选发起 0x92 命令,芯片内部的 OTP 读取指针都会强制归零。芯片里真正存储的出厂核心 LUT 只有开头的 4 KB。

发现 96 KB OTP 实为同一份 4 KB 页面重复 24 次
图 11:编写脚本比对后确认,0x92 指令每次调用都会重置内部地址指针,96 KB 文件实为同一个 4 KB 页面重复 24 次

把这 4096 字节的 otp_stock.bin 按非零段落拆开,内部布局极其规整:

  • 共包含 7 张波形表,每张表精确占 400 字节,相邻两张表之间用 200 字节的全 0x00 空白区隔开;
  • 开头的 0..399 字节是公共电极波形 VCM_LUT(对应外部寄存器 0x20),后面依次排列着黑、白、黄、红、蓝、绿六种颜色的驱动表(对应 0x21–0x26);
  • 单张 400 字节波形表的内部编码:不同于旧款 UC8159c“每状态 13 字节”的结构,SPD1657A 的每张 400 字节表由 20 个状态(State)组成,每个状态精确占 20 字节:
    • Byte 0(Repeat):当前状态的循环执行次数;
    • Byte 1–4(Level):以每 3-bit 为一组,紧凑打包了该状态下 Phase A 至 Phase H 共 8 个相位的电压轨档位编码;
    • Byte 5–12(Frame Count):这 8 个相位各自持续的帧数(1 字节/相位);
    • Byte 13–19(Tail):SPD1657A 新增的 7 字节微时序与相间切换控制字段。

自定义波形实验:时域截断、槽位限制与原生橙色机理

摸清了“20 状态 × 20 字节”的波形格式后,四月底到五月初那两周,桌上就一直摆着这套实验台(图 12),每天往 0x20–0x28 寄存器里塞各种手搓的波形数组。

桌面实验台全貌:UNO Q、DESPI-C73 与正在跑多色条带实验的 7.3 寸面板
图 12:五月初的桌面实验台,UNO Q 通过杜邦线和 DESPI-C73 转接板驱动面板运行自定义波形测试

1. Repeat = 0x00 死循环与“时间快门”过渡色

动手改波形撞上的第一个坑是屏幕频繁假死。有时我只写了前五六个状态,把 400 字节剩余部分全填成 0x00,结果下发 0x12 (DRF) 启动刷新后,屏幕闪了几下就停了,但 BUSY 引脚死死拉高五六十秒都不释放。

排查后发现:SPD1657A 的波形状态机读到 Byte 0 (Repeat) == 0x00 时,并不会将其视为终止符,而是当成循环 256 次处理。尾巴填满 0x00,芯片就会在零电压输出的空状态里老老实实空转上千帧。

为了把卡在死循环里的屏幕救回来,我在 Python 里加了一行硬复位:下发 0x12 刷新指令后,主控用 time.sleep(t) 掐表等待指定秒数,不等芯片自己跑完,直接调用 raw_reset() 拉低硬件 RST 引脚将芯片强制断电重启。

没想到这一掐,掐出了一个意外的物理效果。电子纸变色靠的是微杯里四种带电粒子在二三十秒内缓慢迁移到位;当你半途拉低 RST,片内升压电荷泵瞬间断电,正在半路上游动的红、黄、蓝、白粒子立刻失去电场力,在高度粘稠的介电液里原地“冻结”在那一刻的混合位置上。

时域截断实验记录:在不同秒数触发 raw_reset 冻出过渡色
图 13:时域截断(Time-Domain Abort)实验记录——在不同秒数拉低硬件 RST 打断刷新,将粒子冻结在半途从而获得过渡色

这相当于给慢速电泳粒子加了一个“时间快门”。我们在 0.2 秒到 25 秒之间按 0.5 秒步进做了一轮扫描,从原厂基底波形里冻出了粉橙、浅珊瑚红、柠檬黄、薄荷绿、板岩蓝等十几种出厂规格书上没有的过渡色(图 13、图 14)。

自定义寄存器与波形实验刷出的多色带与柠檬黄、橙色实拍
图 14:通过自定义寄存器与波形调制,在面板上稳定刷出的高饱和柠檬黄、原生橙、深海蓝与翠绿条带实拍

不过做完跨天重复性验证后,时域截断的局限也很明确:像黄色波形在第 20 秒截断出柠檬黄这种平台期超过 3 秒的宽窗口,每次掐出来的颜色都很稳定;但小于 2 秒的窄窗口稍微偏差几百毫秒,色相就会漂移。而且由于中途断电破坏了直流平衡(DC Balance),粒子有强烈的历史位置记忆,下次刷新前如果不先跑一遍完整的 30 秒出厂清洗把粒子归位,颜色就会串味。因此它适合用来刷长期静止的背景图,不能作为日常高频刷新手段。

2. 自定义 LUT 的硬件槽位上限:仅 0x20–0x24 可写

第二个摸清的硬件边界是可写槽位数量。按照早年社区在 SPD1656 上的经验,0x21–0x28 对应 8 个颜色通道。但我专门写脚本对 0x20–0x28 做了逐槽位独立写入验证(图 15),确认在 SPD1657A 上:只有 0x20(公共电极 VCM)和 0x21–0x24(像素代码 0..3)这 5 个槽位是真的开放给外部 RAM 写入并生效的;0x25–0x28(像素代码 4..7)无论外部写入什么数据,芯片在刷新时都会直接忽略,固定回退读取 OTP 内部波形。

扫描 0x20 至 0x28 槽位可写性的终端实验记录
图 15:逐槽位测试写入效果,证实 SPD1657A 仅有 0x20–0x24 五个槽位支持外部自定义波形覆写

3. 一次空白对照实验,引出“原生橙色”的电压轨机理

在这期间还闹过一次乌龙,却意外引出了这块屏最实用的一个隐藏特性。

4 月 28 日那天,我把面板设置寄存器设为 PSR = [0x5F, 0xE9](置位 LUT_EN = 1 启用外部 LUT),然后写入了一段自己拼接的波形数组。刷出来一看,原本显示红色的像素代码 3 变成了一整块非常纯正、均匀的橙色,原本白色的代码 1 变成了深蓝色(图 16)。

4 月 28 日在 LUT_EN=1 模式下跑出的橙色与深蓝条带实拍
图 16:在 PSR = [0x5F, 0xE9] 与单字节 PWRR = [0x3F] 配置下,无需传输任何自定义波形即可稳定刷出均匀的原生橙色与深蓝

起初我以为是自己写的波形参数调出了橙色。为了定位到底是哪几个字节起作用,我把自定义 LUT 的写入代码一行行删掉做消融实验——结果删到最后一行自定义波形都没写、只发了 PSR = [0x5F, 0xE9],那块漂亮的橙色依然稳稳地刷了出来!

这件事给我提了个醒:做硬件实验看到意外的好结果时,第一反应不该是庆祝,而是先把自己的输入全删光跑一次空白对照(Blank Control)。

但为什么不写波形、光打开 LUT_EN = 1 就会把正红变成橙色?五月份时我们只能把它当成芯片内置的一套“备用调色板”,直到六月初拿到晶门科技 44 页的 SPD1657A.pdf 原厂数据手册,对着寄存器表做完单变量验证,才彻底搞清底层的电路原因:

LUT_EN = 1 原生橙色的电路机理:

PSR 寄存器中的 LUT_EN 位不仅控制波形查找表的来源,还同时控制模拟驱动电压轨(Voltage Rails)的配置来源。

  • 当 LUT_EN = 0(默认出厂模式)时,芯片自动从 OTP 中加载全套校准好的高、中、低驱动电压;
  • 当设为 LUT_EN = 1 时,芯片改为读取主控写入 PWRR (0x01) 寄存器的电压参数。由于微雪/Good Display 官方例程里只向 PWRR 发送了单字节 [0x3F],漏发了后续 5 个电压幅值字节,导致控制中间档驱动电压的 VSPL 与 VSNL(第 3、4 字节)一直停留在芯片上电复位默认的 ±3.0 V(远低于出厂 OTP 校准值)。
  • 在偏低的 ±3.0 V 中间电压轨下,负责将红色粒子与黄色粒子彻底拉开的那一相脉冲推力不足(呼应前文图 2 的微杯物理模型),红色粒子与黄色粒子在微杯顶面形成稳定的均匀混合态,跑完整个 30 秒直流平衡周期后,呈现出的就是极其干净的原生橙色。一旦将 PWRR 补齐为完整的 6 字节 [0x3F, 0x00, 0x32, 0x2A, 0x0E, 0x2A] 抬升中间电压,或者将 PSR 次字节改为 0xF9(置位 VCMZ),红色粒子推力恢复,就立刻切回正红色。

那两周里唯一一次真正让人惊出冷汗的操作,是试探 0x2A (XON_LUT) 栅极全开控制寄存器。有天做边界测试,我随手往 XON_LV 字段填了 0xFF。这个字节一下发,相当于命令栅极驱动器在同一瞬间将全屏 480 行扫描线全部强制导通,巨大的容性负载当场把片内升压电荷泵拉塌(电荷泵欠压 Brownout 伴随局部栓锁),屏幕顶端瞬间窜出一条花花绿绿的马赛克横带,看着跟排线烧毁一模一样。赶紧断电静置几分钟,重新上电切回出厂 OTP 模式连续刷了两遍全白,被锁死的粒子才慢慢散开恢复正常。


踩坑复盘:误开 2 倍超频(PLL = 0x07)白忙活十天

到了五月上旬,我的实验笔记里已经攒下了一堆看起来非常亮眼的“突破”:

  • 全屏刷新时间被我从 30 秒“优化”到了 14–15 秒;
  • 像素代码 4 和 7 能稳定刷出一种规格书上没有的“肤色(tan)”;
  • 排列组合不同寄存器位,记录了 30 多种“新调色板”;
  • 5 月 13 日那天,我发现只要把 PSR (0x00) 放在硬件复位后的第一条指令发送,刷新时间就能从 30 秒砍半到 15 秒,还在笔记里郑重其事地给它命名为“PSR-first 定律”。

为了防止 Claude Code 顺着我的假设自说自话,5 月 8 日我还尝试在 UNO Q 的板载 Linux 上同时跑起一个 Gemini CLI,想让两个大模型在板子上直接交叉审查实验记录。结果板载内存只有 2 GB,Claude Code 加上系统服务本就吃得差不多了,Gemini 的 Node.js 进程一启动当场把内存撑爆,抛出 JavaScript heap out of memory 崩溃退出(图 17)。

在 2 GB 内存的 UNO Q 板载 Linux 上同时跑 Claude 与 Gemini CLI 触发 OOM
图 17:5 月 8 日尝试在 2 GB 内存的 UNO Q 上同时启动 Gemini CLI 做交叉复核,直接触发 Node.js 堆内存溢出

最终把前面那堆得意成果全盘推翻的,既不是 Claude 也不是板子上的 Gemini,而是 5 月 16 日下午我自己去对照了一眼厂家的 C 语言源码。

那天为了核对一个时序细节,我打开 Display_EPD_W21.cpp 逐行比对。看到第 61 行配置内部时钟分频的 PLL (0x30) 寄存器时,后背瞬间一凉:厂家源码里清清楚楚写着 0x08(对应 50 Hz 标准帧率),而我从 5 月 6 日修改实验脚本起,连续十天发给屏幕的一直是 0x07!

在 SPD1657A 中,0x07 对应的内部时钟频率是 100 Hz,恰好是标准值 0x08(50 Hz)的整整两倍。也就是说,过去十天我所有的实验,全是在把驱动芯片强行 2 倍超频的状态下跑出来的。

我立刻把脚本里的 PLL 改回标准值 0x08,硬着头皮把前十天的核心实验全部重跑了一遍(图 18)。结果非常残酷,三条核心结论当场作废:

5 月 15 至 16 日重测 PLL=0x08 基准调色板的终端记录
图 18:将 PLL 改回原厂标准值 0x08 后重跑基线测试,确认像素代码 4 和 7 在正常时钟下就是标准的备用黄色
  1. “15 秒快速全刷”作废:改回 PLL = 0x08 后,全屏刷新老老实实回到了 30.0 秒。此前之所以能跑进 15 秒,根本不是什么寄存器序列优化得好,纯粹是因为时钟快了一倍,芯片用两倍的速度把固定帧数硬跑完了。
  2. “肤色(tan)”与 30 多种新调色板作废:在 2 倍超频下,每个电压脉冲的持续时间被强行腰斩一半,微杯里的颜料粒子还没游到位就被切到了下一相位,半途停下才混出了灰扑扑的“肤色”。时钟恢复 50 Hz 后,脉冲时间充足,粒子稳稳走到底,出厂 OTP 里 8 个像素代码(0..7)对应的真实颜色是 黑 / 白 / 黄 / 红 / 黄 / 蓝 / 绿 / 黄——代码 4 和 7 压根不是隐藏肤色,只是两份备用的黄色槽位。此前记下的 30 多种调色板里,除了 PSR.A = 0x7F(置位 bit 5) 确实能切出芯片内置的另一套暖色映射外,绝大多数都只是超频导致的粒子欠迁移伪影。
  3. “PSR-first 定律”作废:5 月 19 日在 PLL = 0x08 下严格重测了指令顺序:无论把 PSR (0x00) 放在第一条还是放在 PWRR (0x01) 之后,清洗和渲染都精确耗时 30.0 秒,一秒都省不下来;而且回头看厂家自己的 EPD_init(),原本就是先发 PWRR 再发 PSR。之前看到的“刷新时间砍半”,只是超频状态下芯片偶发复位不完全引起的提前终止。
一个发错的 PLL 字节如何连带推翻三条结论
图 19:一个写错的 PLL 时钟字节(0x07 vs 0x08)如何像多米诺骨牌一样连带推翻了刷新速度、新调色板和指令顺序三条结论

这种软故障最容易让人栽跟头的地方,就在于它从来不报错。屏幕既不黑屏也不花屏,每次跑都能稳定复现一模一样的“肤色”和“15 秒刷新”,让你产生一种每天都在取得重大进展的错觉。

那天晚上我把知识库里受波及的 7 份实验文档全部打上 SUPERSEDED(作废)标记,并立了一条死规矩:往后记录任何一条实验结论,同一行必须强制带齐当时的 8 个完整状态字段(PLL、PSR、PWRR、TCON、VCOM、EVB、前置清洗方式、实测秒数)。和 AI 一起做硬件逆向尤其需要这种严苛的纪律——因为大模型帮你归纳总结、起名字写文档的速度太快了,一旦上游混进一个没记录的错误基线参数,它会顺理成章地帮你盖起一整栋逻辑自洽的歪楼。


收官:最终留下的 7 套实用配方与局刷物理分界

把时钟锁定回标准值 PLL = 0x08 后,我对所有做过空白对照和跨天复现的配置做了一轮清洗,最终在工程里保留了 7 套真正稳定可用的驱动配方(编号跳过 5,是因为原 Recipe 5 在恢复正常时钟后与 Recipe 3 完全等价,已合并):

配方编号关键寄存器配置(均在标准时钟 PLL = 0x08 下)实测耗时实际显示效果与工程用途
Recipe 1(出厂六色)PSR = [0x5F, 0x69] 或 [0xDF, 0x69]
配 6 字节完整 PWRR
~30 s 清洗 + ~30 s 渲染标准 黑/白/黄/红/黄/蓝/绿/黄,色彩最正、残影最小,用作日常全刷基线
Recipe 2(反相七色)PSR = [0xCF, 0x69](DDX = 0)+ 微调 VSNL~30 s反转色表映射并保住高净度白底,适合暖色调静态海报表盘
Recipe 3(原生橙色)PSR = [0x5F, 0xE9](LUT_EN = 1)
配单字节 PWRR = [0x3F]
~30 s利用默认 ±3.0 V 中间电压轨让代码 3 直出均匀原生橙色、代码 1 出深蓝,无需传外部 LUT
Recipe 4(1.5 秒局刷清白)0x83 (PTLW) 设局部窗口 + 极短全白自定义 LUT1.5 s在 1.5 秒内将指定小窗口快速擦白,用于刷新文字前擦除旧字
Recipe 6(暖黄反相)PSR = [0x4F, 0xE9]~30 s在 LUT_EN = 1 模式下反转高低半字节映射
Recipe 7(纯黑 + 原生橙)PSR = [0x5F, 0xEB](开启 NORG / TIEG)~30 s保留代码 3 原生橙色的同时,让代码 0 的黑色更扎实浓郁,最适合 Claude Code 界面
Recipe 8(VCMZ 切回正红)PSR = [0x5F, 0xF9](置位 VCMZ = 1)~30 s在 LUT_EN = 1 模式下将代码 3 从橙色切回高饱和正红色

到了最后一步往状态屏上落地局部刷新(命令 0x83 PTLW)时,我们还摸清了这颗芯片最后两个绕不开的硬件脾气,连同前面的 OTP 格式和橙色电压轨一起整理成了下面这张机制图(图 20):

SPD1657A 三个底层硬件机制图解
图 20:两个月摸清的三个 SPD1657A 底层硬件机制:4 KB OTP 的 20 字节状态格式、原生橙色的 PWRR 电压轨机理,以及 y ≈ 248 的局部刷新物理分界

1. 局部刷新的速度边界

很多开发者刚拿到这块屏时都希望能做出“三五秒的彩色局部刷新”。但实测证实:只要进入 0x83 (PTLW) 局部窗口模式,SPD1657A 在彩色渲染时就会直接忽略外部写入 0x21–0x24 的自定义彩色波形表,强制回退执行内部 30 秒波形。

因此局部刷新在工程上只有两条路可走:

  • 要么用 Recipe 4,在 1.5 秒内把局部小窗口快速刷白(用来擦除旧状态文字);
  • 要么老老实实跑完 30 秒的局部窗口彩色刷新(图 21)——虽然耗时仍然是 30 秒,但好处是窗口以外的其余屏幕区域纹丝不动,不会跟着全屏剧烈闪烁,放在桌面眼角余光里舒服得多。
在色带背景上执行 0x83 PTLW 局部窗口红色刷新的实拍
图 21:在多色条带背景上开启 0x83 (PTLW) 局部窗口刷出正红色块实拍,窗口外部背景保持静止不闪烁;注意观察红框下边缘处那条 1 像素宽的浅色边界线

2. y ≈ 248:消除不掉的 1 像素局刷边界线与硅片物理分界

仔细看上面图 21 中红色局部窗口的底边,就能看到这颗芯片最隐蔽的一个硬件现象:当你在屏幕上开一个局部窗口做彩色刷新时,只要窗口的下边界 VRED 落在屏幕上半区(y ≲ 248),刷新完成后在窗口底边 y = VRED 那一行,必定会留下一条 1 像素宽的浅色水平细线。

六月初拿到原厂手册后,我起初怀疑是刷新收尾阶段电荷没放干净,于是把手册里所有跟收尾放电、高阻态、边界浮空相关的控制位(FOPT、NORG、VCMZ、Border)排列组合测了个遍,甚至导出了完整的测试矩阵和 Gemini 一起推演。结果紧盯着屏幕刷新过程才发现三个铁证:

  1. 这条 1 像素细线根本不是刷新结束断电时留下的,而是在刷新刚开始的第一阶段(Phase 1)就已经出现了;
  2. SPD1657A 的 0x83 (PTLW) 指令只有 9 个参数字节,最后一个字节只是单纯的局刷开关 PMODE(写 0 就直接退回全屏刷新,不像单色屏控制器 UC8179c 那样有第 10 字节可以翻转局刷扫描方向);
  3. 最一锤定音的证据:哪怕把分辨率寄存器 VRES 从 480 改成 448,y = 240 处照样出线;可只要把局部刷新窗口挪到下半屏(y > 248),不管怎么刷都干干净净,连半点印子都没有。

说白了,y ≈ 248 就是这颗芯片内部栅极驱动器(Gate Driver)上下两个物理分块的硅片接缝。当局部窗口底边停在上半屏的扫描段内时,相邻未选中行的栅极耦合电容在长达 30 秒的高压脉冲下产生了微弱的边缘串扰。

想通这是硅片级的物理分界之后,我立刻停掉了所有寄存器穷举测试,只改了 eink-daemon 的一行 UI 排版逻辑:把需要跟着 Claude Code 工具调用频繁局部刷新的任务列表、进度条全部排在 y > 248 的下半屏,而 y < 248 的上半屏只放全屏刷新时才变动的大状态标题。不跟硬件物理规律死磕,顺着它的脾气做完布局调整,这套能够揣进包里随时带走、在户外大太阳底下依然像纸页一样清晰的“微型键盘 + 六色电子纸” Vibe Coding 终端,终于安安静静地跑稳了。

← 上一篇 用 Claude Code 驱动 ANSYS Maxwell:轴向磁通电机仿真的两个月 下一篇 → 让 Claude Code 自己去 1688 采数据,再给产品打分:两天实验记录