Measure how a build renders, and keep the tool that does it

tools/panel_dump.py prints the display controller's own memory as ASCII or PNG for any firmware, with the four mappings, so two builds can be compared instead of glanced at. The panel model gained a bounded UVK5_PANEL_PROBE diagnostic along the way.

Measured: the 5.9.0.CN panel agrees with its own framebuffer 8188 of 8192 pixels with the data untouched, and the fetched 6.0.0 build renders identically. Both program the same geometry registers, which is why the mapping is a driver convention and cannot be derived from the controller: it has to be measured.

Noted, not yet fixed: the display start line (0x40|n) is ignored, and the model stores pixels at col-4 with the column counter wrapping at 128 instead of the controller's 132 columns.
This commit is contained in:
mckero committed 2026-10-01 15:22:02 +08:00
1 parent 542515d5ad
commit 4bddccfe7b
4 files changed
+116

No files matched your search

+22
View File
@@ -401,6 +401,28 @@ slot and resets. Both halves are reachable from the page.
- `tools/uvk5_slots.py` does the same offline: write a slot into a flash image, and print
what each slot holds.
### Which build renders correctly, and how that is decided
The page draws the display controller's **own memory**, not the firmware's framebuffer, so it
does not need to know where a given build keeps its screen -- and `tools/panel_dump.py` shows
the same thing from a shell, one character per pixel, so two builds can be diffed:
tools/panel_dump.py --qmp 127.0.0.1:4444 # ASCII
tools/panel_dump.py --qmp 127.0.0.1:4444 --png shot.png # scaled PNG
That path is faithful for the builds measured here. Against its own framebuffer the 5.9.0.CN
image agrees 8188 of 8192 pixels, and the fetched 6.0.0 build renders byte-identical to it.
Both program the same panel registers -- `0xA1` segment reverse, `0xC0`, `0xA6`, columns
0..127, start line 0 -- and that is the point: **the registers do not decide the mapping, the
driver does**, because one driver compensates for the panel's segment order in software and
another may not. So the mapping cannot be derived from the controller's settings; it has to be
measured (`--mapping` exists for that).
Two things the panel model does not yet honour, either of which will misplace pixels in a
build that uses them: the **display start line** (`0x40|n`, a vertical scroll), and the
controller's **132 columns** -- the model stores pixels at `col - 4` and wraps the column
counter at 128, so a driver that addresses 4..131 loses its first four pixels and shifts the
row. Both are on the list; neither affects the builds measured above.
### Moto/DFU flashing, and the flag this build does not set
The factory bootloader is on the machine and it does speak the flashing protocol: a real
+17
View File
@@ -356,6 +356,23 @@ v6.0.0 版把开机菜单和四个固件槽放在外部 flash 里:开机按住
决定(默认 8 秒:开机路径可能花 20 秒把当前固件"采纳"进槽 0,之后才会去采样键盘)。
- `tools/uvk5_slots.py` 做同样的事但离线:把槽写进 flash 镜像,并打印每个槽的内容。
### 哪份构建渲染正确,这件事怎么判定
网页画的是显示控制器**自己的显存**,不是固件的 framebuffer,所以它不需要知道某份固件把画面
放在哪 —— `tools/panel_dump.py` 在命令行里做同一件事,一个字符一个像素,方便把两份构建 diff:
tools/panel_dump.py --qmp 127.0.0.1:4444 # ASCII
tools/panel_dump.py --qmp 127.0.0.1:4444 --png shot.png # 放大后的 PNG
对这里量过的构建,这条路是忠实的:CN 5.9.0 的面板与它自己的 framebuffer **8192 个像素里吻合
8188 个**,而取回的 6.0.0 构建渲染出来**逐字节相同**。两者把面板寄存器配成**一样**的值 ——
`0xA1` 段反序、`0xC0`、`0xA6`、列 0..127、起始行 0 —— 这正是关键:**映射不是寄存器决定的,
而是驱动决定的**,因为有的驱动在软件里补偿面板的段顺序,有的不补偿。所以映射无法从控制器设置里
推导,只能**量**出来(`--mapping` 就是干这个的)。
面板模型目前还有两处没落实,任何用到它们的构建都会像素错位:**显示起始行**(`0x40|n`,纵向
滚动),以及控制器的 **132 列** —— 模型把像素存在 `col - 4`,并把列计数器在 128 处回绕,所以
按 4..131 寻址的驱动会丢掉前 4 个像素、整行左移。两条都在待办上;都不影响上面量过的构建。
### MOTO/DFU 刷机,以及这份构建没有打开的开关
出厂引导就在机器上,而且它**确实会说刷机协议**:真正的 `0x0518` / `0x0530` / `0x0519` 交互,
+17
View File
@@ -807,6 +807,23 @@ static void st7565_set_cs(void *opaque, int line, int level)
static uint8_t st7565_xfer(void *opaque, uint8_t out)
{
ST7565State *s = opaque;
/* Diagnostic probe (UVK5_PANEL_PROBE): what the driver actually tells the
* controller, bounded to the first few hundred bytes. Two firmware builds that
* disagree about the column offset or the scan direction render differently, and
* this is how that is measured rather than guessed. */
{
const char *panel_probe = g_getenv("UVK5_PANEL_PROBE");
static unsigned panel_probe_n;
if (panel_probe && panel_probe_n < 600) {
FILE *f = fopen(panel_probe, "a");
if (f) {
fprintf(f, "PANEL a0=%d cs=%d page=%d col=%d byte=%02x\n",
s->a0, s->selected, s->page, s->col, out);
fclose(f);
}
panel_probe_n++;
}
}
if (!s->selected) {
return 0xff;
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Render the display controller's own memory, for any firmware, without a browser.
The web page already does this; this is the same thing from a shell, which is what you
want when the question is "does *this* build render correctly" and the answer has to be
compared rather than glanced at. It prints ASCII at one character per LCD pixel, so two
builds can be diffed, and can also write a scaled PNG.
tools/panel_dump.py --qmp 127.0.0.1:4444 # ASCII, as the panel shows it
tools/panel_dump.py --qmp 127.0.0.1:4444 --mapping mirror-cols
tools/panel_dump.py --qmp 127.0.0.1:4444 --png work/screen.png
The --mapping option exists because which mapping is right is a property of the *driver*,
not of the controller. Two builds can program identical panel registers -- 0xA1 segment
reverse, 0xC0, 0xA6 -- and still arrive in different orders, because one compensates in
software and the other does not. Measured on the 5.9.0.CN build against its own
framebuffer: 8188 of 8192 pixels agree with the panel data untouched, 6954 with the
columns mirrored.
"""
import argparse
import sys
import uvk5_lcd
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--qmp", default="127.0.0.1:4444",
help="the emulator's QMP endpoint (host:port or unix:path)")
ap.add_argument("--mapping", default="identity",
choices=["identity", "mirror-cols", "mirror-rows", "both"])
ap.add_argument("--png", help="write a scaled PNG here instead of ASCII")
ap.add_argument("--scale", type=int, default=4)
args = ap.parse_args(argv)
from uvk5_qmp import QmpClient
# The frame addresses are unused on this path: the controller has its own memory.
grab = uvk5_lcd.FrameGrabber(QmpClient(args.qmp, timeout=20), 0, 0)
pixels = grab.panel_pixels()
if args.mapping in ("mirror-cols", "both"):
pixels = [list(reversed(row)) for row in pixels]
if args.mapping in ("mirror-rows", "both"):
pixels = list(reversed(pixels))
if args.png:
with open(args.png, "wb") as fh:
fh.write(uvk5_lcd.encode_png(pixels, args.scale))
print("wrote %s (%d x %d)" % (args.png, len(pixels[0]), len(pixels)))
return 0
print("panel %d x %d, %d pixels lit, mapping %s"
% (len(pixels[0]), len(pixels), sum(sum(r) for r in pixels), args.mapping))
for row in pixels:
print("".join("#" if value else "." for value in row))
return 0
if __name__ == "__main__":
sys.exit(main())