Files
uv-k5-v3-emulator/work
mckero 1e9fdf685c Find the screen buffers in the firmware instead of hardcoding one build's
The page was told --frame-addr 0x200012BE --status-addr 0x2000163E and used them as a fallback. The firmware the user actually flashed keeps its buffers at 0x2000129E/0x2000161E, 32 bytes earlier, so every line landed 32 bytes off: that is the "other firmware looks shifted" report. The images here are minimal ELFs with no symbol table, so there is nothing to read -- but the firmware's own buffers hold the same bytes the controller holds, and tools/uvk5_buffers.py finds them by matching (1024/1024 bytes for that file).

The two address flags are optional now, work/run-webui.ps1 passes no machine-specific values at all, and the page reports what it found in /api/status and /api/panel. tools/uvk5_testenv.qemu() also looks in the sibling qemu-7.2/build the rest of the repo assumes. Fixed /api/panel's emulator-off branch, which called jsonify with both a dict and kwargs and 500'd.

Tests: test_uvk5_buffers (the search must count matches, not pairs -- its first version scored every offset full marks and always answered the first one).
2026-10-01 16:09:26 +08:00
..

让 f4hwn 5.9.0.CN 在本机(Windows)跑起来

本机实测记录。结论先说:这个项目并不是"只能跑在 Linux",只有外壳脚本和 unix socket 是 Linux 的;机器模型和工具本身跨平台。 我在本机用 MSYS2 原生编了 QEMU 7.2,把 qemu/py32f071.c 编进去,用户的固件已经跑起来、能按键、能出画面。

固件是什么

04-20260827_白头佬汉化版_f4hwn.5.9.0.bin,114,324 字节。

  • 身份串:UV-K5 Firmware, EGZUMER+F4HWN v5.9.0.CN
  • 向量表:SP=0x20004000,Reset=0x08002d49 —— 与仓库参考固件逐字节相同,说明它是 PY32F071 的应用镜像,链接基址 0x08002800(F:\pi 里的逆向报告独立确认了同一基址)
  • 它是应用镜像、不含 0..0x27FF 的出厂引导程序,所以可以直接当 -kernel 用

为什么要包一层 ELF(重要)

仓库 py32f071.c 的注释说 ".elf/.bin 都能启动",这句话对 .bin 不成立:

  • QEMU 7.2 的 armv7m_load_kernel(cpu, file, mem_base=0x2800, size) 对 ELF 用程序头里的地址, 对裸 .bin 则按 mem_base 加载 —— 也就是容器地址 0x2800;
  • 而容器地址 0..0x1D800 是 flash 的别名区(映射到 flash 偏移 0x2800 起),所以裸 bin 会被写到 flash 偏移 0x5000,整整偏移 0x2800,CPU 从地址 0 取向量表时读到的是空白,直接跑飞。

tools/bin2elf.py 就是补这个:把 .bin 包成一个 PT_LOAD 在 0x08002800、 入口取镜像自身复位向量的 ELF32/ARM。

中文字体在 SPI flash 里,不在固件里

发行包里 01/02/03 三个文件是首次刷机才要的:02 是 16x16、03 是 8x8 的 GB2312 点阵 字体包(UF2 容器)。它们烧到 SPI NOR 的固定位置:

UF2 头里的目标地址      内容            大小
0x000A0000            16x16 字模       261,888 B(8184 字模 × 32 B)
0x000E0000            8x8 字模          65,536 B(8192 字模 ×  8 B)

所以 make_flash.py 现在支持 --blob,.uf2 按自身块地址落盘:

python tools/make_flash.py \
    --blob 0:work/f4hwn/02-大字体16x16.uf2 \
    --blob 0:work/f4hwn/03-小字体8x8.uf2

不写进去会怎样:字体区是空的(0xFF),汉字全变实心块。

关键地址(已用固件源码交叉验证)

符号 地址 大小 说明
gFrameBuffer 0x200012BE 896 = 7 行 × 128 显示区,面板第 1..7 页
gStatusLine 0x2000163E 128 状态行,面板第 0 页

两个地址都不是 128 字节对齐,相差正好 896 字节(帧缓冲在前、状态行在后,与声明顺序相反; 参考固件的 0x200013DC / 0x2000175C 也是这个关系)。

这两个值是算出来的,不是看出来的,方法可复用到任何新固件:

  1. 抓 16 KB SRAM:python work/qmp.py dump 0x20000000 0x4000 work/s4.bin。
  2. 从源码里挑"内容已知、落点已知"的位图(App/driver/st7565.c、App/ui/status.c、 App/bitmaps.c):
    • gFontPowerSave 在状态行 +0、gFontDWR +18、gFontPttClassic +54、 BITMAP_BatteryLevel1 +111(= LCD_WIDTH - 17);
    • BITMAP_VFO_Default(VFO 箭头)由 memcpy(p_line0 + 0, ...) 画在帧行偏移 0。
  3. 在 SRAM 里搜这些字节串:状态行四个位图的间距要同时成立,只有一个基址满足; 箭头的落点直接给出帧缓冲基址。两者独立得出 0x2000163E 与 0x200012BE(相差 896,吻合)。

之前我把基址取成 128 对齐的 0x20001280 / 0x20001600,偏了 0x3E = 62 字节。后果不是整体平移, 而是每行 62 字节环绕折行:渲染出的每一行 = 上一真实行的尾部 62 列 + 本行的头部 66 列, 字会从第 66 列被劈开,状态行还混进了帧行尾部——看起来就是"没对准"。

快速自检(对齐正确时应成立):帧行 3 是源码 memset(gFrameBuffer[3], 0, 128) 清掉的中缝, 应整行全 0;上/下 VFO 的帧行 0≡4、1≡5;帧行 2 与 6 只该在活动 VFO 的说明文字上不同。

面板侧两个细节(不影响取帧,但解释列号为什么 +4):驱动写 Line + 176、Column + 4, cmds[] 用 0xA1(SEG 反向)+ 0xC0(COM 正常)。

怎么跑

powershell -File work\run-emulator.ps1      # 起 QEMU(QMP tcp:4444,GDB tcp:1234)
powershell -File work\run-webui.ps1         # 起网页遥控,然后开 http://127.0.0.1:8080/
powershell -File work\restore-flash.ps1     # 还原 flash.img(先停模拟器)

命令行按键(走 TCP):

python tools/key.py --socket 127.0.0.1:4444 MENU DOWN DOWN

本机上的构件

路径 说明
F:\dsh-build\qemu-7.2.0 QEMU 7.2.0 源码 + 打入的模型、patched SysTick、uv-k5-v3 注册
F:\dsh-build\qemu-7.2.0\build\qemu-system-arm.exe 编译产物;运行需要 F:\msys64\mingw64\bin 在 PATH
F:\msys64 MSYS2(gcc 16.2 / glib 2.90 / pixman / meson / ninja / gdb / perl)
work/f4hwn/*.elf 由 .bin 包的 ELF
assets/flash.img 校准 + 字体;work/flash-base.img 是干净副本

为了在 Windows 上跑起来,对仓库做了什么

  1. qemu/py32f071.c:补 #include "qapi/visitor.h"。原文件用 visit_type_uint64 却没包含声明它的 头,对原版 QEMU 7.2 编译不过(作者的环境里应该是被别的头间接带入的)。
  2. tools/bin2elf.py:新增(见上)。
  3. tools/make_flash.py:新增 --blob ADDR:FILE,.uf2 按块地址解析。
  4. tools/uvk5_qmp.py、tools/key.py:QMP 端点除 unix 路径外接受 host:port。
  5. tools/uvk5_supervisor.py:wait_for_socket 支持 TCP 端点。
  6. tools/uvk5_lcd.py、tools/uvk5_stream.py:帧暂存目录不再硬编码 /dev/shm(Windows 没有), 退回系统临时目录。

Linux 上的行为都没变:地址默认值仍是 unix 路径,/dev/shm 存在时仍用 /dev/shm。

面板级设置:对比度与反显

菜单里的 SetCtr(对比度) 和 SetInv(反显) 属于面板,不属于帧缓冲:

gSetting_set_ctr = ...;                    // App/app/menu.c
ST7565_ContrastAndInv();                   // → 0xE2、0x81 + (21 + set_ctr)、0xA6|set_inv

它们只往 ST7565 发命令,gFrameBuffer 一个字节都不改。网页渲染的是帧缓冲,所以"改了没反应"是必然的 ——除非把控制器本身也建模。现在 SPI1 后面挂了最小的 ST7565 模型(A0 接 PA6、CS 接 PB2,与 App/driver/st7565.c 一致),解析 0xA6/0xA7(反显)、0xAE/0xAF(开屏)、0x81 <值>(对比度), 并暴露三个只读属性:

qom-get /machine/panel invert        反显位(0xA7 之后为真)
qom-get /machine/panel contrast      0x81 后面那个值
qom-get /machine/panel display-on    0xAF / 0xAE
  • 反显会真的作用到画面:tools/uvk5_lcd.py 渲染时按该位取反,所以 60 号设置现在看得见。
  • 对比度只报告不渲染:那是模拟量(玻璃多黑),渲染不出来。本机实测值 36 = 21 + 15, 正好等于当时存着的对比度设置,说明命令一直都在发。
  • display-on 只报告不动作:软复位 0xE2 是否清掉开屏位我无法确证,拿不确定的语义去把用户画面 变黑比不动作更糟。

固件日志为什么要在网页里看得见

模型把串口按 SERIAL <行> 打到 stderr,而只有服务器自己启动 QEMU 时才会去读这个管道 (uvk5_supervisor 在连接成功后 pump_stream 到日志缓冲)。所以:

  • work/run-webui.ps1 默认自己启动模拟器(页面上的 On/Off 也就是真的了), 日志面板因此能看到固件串口(启动横幅 UV-K5 Firmware, EGZUMER+F4HWN v5.9.0.CN 就在里面);
  • 加 -Attach 则回到"附着到别处启动的模拟器",此时服务器看不到那个 stderr 流,面板里只有 QEMU/电源事件——这正是之前看不到 debug 日志的原因。

这条线上一根线上跑着两种东西:固件自己的可读输出,和 CPS 编程协议(二进制)。后者按文本解码会 把面板刷成一屏控制字符、把可读的那行埋掉。所以 uvk5_logs.describe_line() 现在对"大部分字节不可打印" 的行只给长度 + 十六进制头,可读行照旧——两种数据都还在,只是不再互相盖住:

[serial] UV-K5 Firmware, EGZUMER+F4HWN v5.9.0.CN
[serial] <binary 15 bytes> f0 aa 55 02 04 80 01 02 03 04 05 06 07 08 09
[serial] <binary 255 bytes> 0e 0f 10 11 12 13 14 60 0c 1f 06 f0 15 c3 07 … +231 bytes

回归测试在 tools/test_uvk5_logs.py::test_pump_stream_summarises_binary_serial。

我在这一路上搞错和修掉的东西

  • 更正:固件确实在读 SPI flash。 我先前写的"26 秒零访问"是测量假象——PowerShell 的 2> 重定向把 QEMU 的 stderr 写成了 UTF-16LE,而我的过滤器在找 FLASHREAD/LCDW 这样的 ASCII 行,于是"什么都没找到"被我当成了"什么都没发生"。按 UTF-16 解码后:SPI1 上是完整的 ST7565 序列(e2 a2 c0 a1 a6 a4 24 81 1f 2b…),SPI2 上读的全是 0x00A0xx(设置区,48 个不同 地址,片选翻转 30 次)。

  • 再更正一次(同一个坑的第二种形态):字体包确实被读了。 上面那句"字体包没被读"是我把探针 截断在前 80 次读得出的——正是 AGENTS 里"没弄清数据形态之前不要截断诊断日志"警告过的错。 把上限放到 4000 次、并顺手在菜单里转一圈让固件画汉字之后,实测 3168 次读里: 0x0A0000(3)、0x0B0000(11)、0x0C0000(6)、0x0E0000(15) —— 都在字体区;另外 0x1E0000 有 1024 次、每步正好 32 字节(= 连续走完 32 KB 的一张字体表)。

  • 外置 SPI flash 的分区(由 gitee.com/oldlicn/betula-multi-system-tool 的官方数据反推, 不靠那张分区图):

    偏移 大小 内容
    0x000000 128 KB BL/多系统引导 + 设置(固件读 0x00A0xx)+ 校准(0x010000,与 make_flash.py 一致)
    0x020000 4 × 128 KB 四个固件槽(官方"清空 0x20000-0x40000 … 0x80000-0xA0000"正好四段)
    0x0A0000 256 KB 用户字体包 16x16(UF2 目标地址就是这里)
    0x0E0000 64 KB 用户字体包 8x8
    0x100000 1 MB 出厂资源区:含拼音串,且 0x1E0000 处有 32 KB 字体表,固件开机会整张走过

    仓库里 16 份官方恢复数据(每份 128 KB)可以直接拼回真机整片 2 MB 数据; 把这些字节拼出来放在 F:\dsh-build\flashdump\factory-2MB.img,补上它之后再跑, 画面上的字会变——说明模拟器原来的镜像缺了固件真正在用的字体数据 (work/flash-with-factory-resource.img 就是这个"原厂资源区 + 用户字体包 + 校准"的合成)。 剩下没落实的是:屏幕上的 16 像素大字与 0xA0000 的字模包仍不能逐字节对上 (2/24),与 0x1E0000 那张表也对不上(0/8),所以"哪个来源供哪一块文字"还没定论。

  • 修:Windows 上 rename() 不覆盖已存在的文件。 flash 回写走"写临时文件再改名", 于是每一次回写都失败(cannot replace),设置因此从不落盘,而反复的告警把主循环挤住, 连 QMP 的问候语都发不出来(表现为"power on failed: timed out")。改用 g_rename() (需 <glib/gstdio.h>;Windows 上是 MoveFileEx+替换,Unix 上就是 rename)。现在电源能开、设置能存。

  • 修:power on 失败时看不到原因。 supervisor 只在连接成功后才去读 QEMU 的 stderr, 于是失败只剩一句"QMP 端口没出现"。现在会把 QEMU stderr 的尾部记进日志——上面那个 rename 问题正是这样浮出来的。

  • 修:启动器在 TCP 端点下用错参数。 default_launcher 现在按端点类型生成 unix:… 或 tcp:host:port(Windows 的 QEMU 根本没有 unix socket)。