三色屏最让人抓狂的一类问题是:代码看起来全对,屏幕也确实刷新了,但红色那一块要么整片变成红色,要么干脆一点红都不剩。我在这上面耗了整整两个周末,最后发现是三个互相独立的原因叠在一起,单独修任何一个都不会让画面正常。这篇把三个都记下来,省得下次再从头摸。
真凶一:驱动对颜色平面做了取反
我用的是 GxEPD2 的 GxEPD2_420c。它的 writeImage(black, color, …) 在把颜色平面写进显存之前,会对颜色数据做一次取反。也就是说,你以为传 invert=false 是"不取反",实际上它走的是另一条分支,把整面红平面填成了红——屏幕上一刷新,整个画面被红色盖住,你画的黑字全没了。
我一开始反复检查自己的点阵数据,甚至把服务器上的 PNG 导出来一个像素一个像素数,都没问题。问题完全在驱动这一侧:三色屏的颜色平面和黑平面极性是相反的,驱动内部为了适配这个,把取反做进了函数里。正确做法是顺着它的约定传参,而不是按字面意思去理解 invert 这个名字。这一条我在源码里来回读了两遍才确认。
真凶二:红平面会盖住黑平面
这是三色屏的物理特性,不是 bug。刷屏时红平面如果某个像素是"有颜色",它会盖住黑平面上同一个像素的内容。所以在黑底上画红字的时候,红字那一小块区域的黑面必须被清掉;反过来,在白底或红底上画黑色的元素,红面那块也得清掉。否则你看到的就是黑底把红字吃掉,或者红底把黑字吃掉。
我最初的代码是分别往两个 Image 上画、然后在 C3 上分别 drawBitmap,两个平面完全独立,自然互相打架。改法很简单:画红色元素时,在黑面上用白色把那块区域涂掉;画黑色元素时,在红面上用白色涂掉。听起来是重复劳动,但这是三色屏唯一正确的画法。
真凶三:PIL '1' 模式里 fill=255 画白色几乎不生效
为了做上面那个"在对侧平面清掉",我在 '1' 模式的 Image 上调用 rectangle 画白色,参数给了 fill=255。奇怪的是,清不干净,有的地方清掉了有的地方没有,边界还留着一圈黑边。
原因是 '1' 模式的像素只有 0 和 1 两个值,传 255 时它的处理并不像 RGB 模式那样明确,实际落盘的值可能不是你想要的。改成 fill=1 之后,一切正常。这个坑很小,但它让我前两个问题的排查一直无法收敛——因为即使我把取反和覆盖都改对了,剖面上还是残留一条黑边,让我以为驱动还有别的问题。
我现在的排查顺序
被这类问题折腾过之后,我固定了这个顺序,按顺序走一般十分钟内能定位:
- 先确认服务器点阵本身是好的。统计黑面和红面里非零字节的数量——如果红面非零字节数是 0,那设备端再怎么改都没用,问题在渲染脚本。这一步能立刻把问题从"算法"和"传输"里切出来。
- 再看
invert参数的传法,对照驱动的源码确认它走的到底哪条分支,不要信名字。 - 检查两个平面之间有没有互相清理,尤其是画在对侧底色上的元素。
- 都不对,再考虑换
drawBitmap的调用方式,比如是否用了整窗刷新、是否每个 page 都画了同一份数据。
顺带记一下验证的小工具
前面说的"统计非零字节数",在服务器上就一行:
black_bits, red_bits = frame[:15000], frame[15000:]
print("black non-zero:", sum(1 for b in black_bits if b))
print("red non-zero:", sum(1 for b in red_bits if b))
红面是 0 就说明脚本压根没画出红色;红面接近 15000 就说明被整面填满了,几乎一定是取反参数的问题。这两个数字比盯着屏幕看半天有用得多。后来我把这两行直接放进了生成脚本的开头,每次生成都打印一次,异常能提前发现。
最后提醒一句:三色屏的刷新本来就慢,一次整屏刷新要好几秒,这是正常现象,不要在这上面怀疑代码。真正该怀疑的是刷新之后画面内容对不对,而不是刷得多快。