mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
Emulator: multiboot slots from the page, flash controller, portable tests
flash controller: store ACR/OPTKEYR instead of swallowing them, which is what stopped the factory bootloader from starting slots over the firmware's own serial protocol (0x0720 family); uvk5_socket/uvk5_testenv so a fresh checkout skips instead of failing; web UI slot table and Multiboot button; quick start, CONTRIBUTING, and stop tracking firmware images and radio dumps
This commit is contained in:
1 parent
ee80939c78
commit
2667e046e8
54 files changed
+5345
-347
No files matched your search
+209
-3
@@ -65,7 +65,8 @@ bootloader 区域,所以应用在它之后。`armv7m_load_kernel()` 之所以
|
||||
没有校验和 —— 只有一个代码和数据必须约定一致的地址。**所以某个设置读回来不对时,
|
||||
先怀疑偏移,再怀疑传输层。**
|
||||
|
||||
进入主循环要约 15 秒,那是模拟开销。真机大约一秒就起来了。
|
||||
启动耗时是模拟开销。本机实测:QEMU 启动后约 1.6 秒出现第一个像素、约 3.6 秒画出主界面,
|
||||
也就是 README 里写的"约 5 秒"。真机大约一秒就起来了。
|
||||
|
||||
## 基本规则
|
||||
|
||||
@@ -126,6 +127,28 @@ bootloader 区域,所以应用在它之后。`armv7m_load_kernel()` 之所以
|
||||
它的测试:`tools/test_uvk5_*.py` 和 `tools/test_webui.py` 不需要模拟器,
|
||||
`tools/test_webui_e2e.py` 会自己启动一个。
|
||||
|
||||
固件也可以**从页面加载**,不必走命令行:`POST /api/firmware` 把请求体当作镜像,存进
|
||||
`work/firmware/` 并启动它(模拟器正在运行就先重启)。镜像的**形态**是从镜像自己读出来的
|
||||
(主机侧 `tools/uvk5_image.py`,机器侧 `uvk5_sniff_app_offset()`):*应用镜像*链接在
|
||||
`0x08002800`,*整片镜像*从 `0x08000000` 开始,而地址 0 必须别名到对应的基址。判断错的
|
||||
症状是**静默**的 —— 镜像整体偏 0x2800 字节,第一次取指读到的是随便什么数据 —— 所以这里
|
||||
既不用标志位,也不按文件名约定。不是镜像的文件会被拒绝,且不打扰正在运行的电台。
|
||||
|
||||
这条路上有两件事是踩出来的:
|
||||
|
||||
- **flash 镜像走环境变量,不走 `-M`。** 经启动器启动时,QEMU 会用
|
||||
"unsupported machine type" 拒绝 `-M uv-k5-v3,flash-image=...`:同一份 argv 我手跑就
|
||||
正常,`-M help` 在**同一上下文**里能列出这台机器,argv 的 `repr` 干净,环境变量也
|
||||
逐项比过、没有定论。属性本身仍然可用,所以两条路都保留;启动器现在只传机器名,另用
|
||||
`UVK5_FLASH_IMAGE`,模型把它作为属性缺省的回退。根因未知 —— 没重新验证过"从页面开机"
|
||||
之前,别把它当成冗余"清理"掉。
|
||||
- **画面取自显示控制器,而不是 guest RAM。** 面板模型保存控制器自己的显示 RAM
|
||||
(8 页 × 128 列),页面渲染的是它,所以**任何**固件都显示正确 —— 同一祖先改出的各个版本
|
||||
显示逻辑不同,多系统那版更是把图像放在完全不同的位置。**不要**在数据之上再叠加驱动的
|
||||
`0xA1` 段反转:同一时刻与 guest 自己的帧缓冲对比,不镜像时 8192 像素里对上 8153,
|
||||
镜像后只剩 6557。读 `gFrameBuffer`(`memsave`)的旧路径保留为"没有面板模型的模拟器"
|
||||
的回退。
|
||||
|
||||
## flash 的那些 bug:四个故障,一个症状
|
||||
|
||||
"频率改不了"和"关机后 flash 什么都不记得"看起来是两个抱怨。实际是**一个根因加上路上顺带
|
||||
@@ -211,12 +234,120 @@ bootloader 区域,所以应用在它之后。`armv7m_load_kernel()` 之所以
|
||||
`IDR=0x0000`,因为它的正则**根本不匹配** gdb 的输出格式。寄存器是好的;读取器是坏的。
|
||||
用走另一条路径的 `tools/gpiob_dump.sh` 交叉验证。
|
||||
|
||||
同一个陷阱再往外一层:**重定向会改变编码。** 三次 `qemu ... 2> probe.log` 的探针都报告
|
||||
SPI 零传输、flash 零读取、片选零变化,而"固件根本不碰 SPI"被当成了结论写下来。
|
||||
PowerShell 5.1 的 `2>` 按 UTF-16LE 写文件,于是探针打出的每一行 ASCII 里每个字符之间都夹着
|
||||
NUL,`startswith("LCDW")` 永远匹配不上。把同一个文件按 UTF-16 解码,看到的是完整的 ST7565
|
||||
初始化序列和 48 个不同的设置读取。**在相信一个"什么都没发生"的探针之前,先确认探针能被看见**:
|
||||
读一下文件、数一下字节,或者用不会重新编码的 `cmd /c` 来写。
|
||||
|
||||
**QMP `pmemsave` 是物理地址,`memsave` 是虚拟地址。** 帧缓冲符号是 CPU 虚拟地址,
|
||||
所以对 `gFrameBuffer` 用 `pmemsave` 会返回一整块零**并报告成功** —— 一片空白屏幕,
|
||||
而且哪里都没有日志。网页界面最初就是建在 `pmemsave` 上的,因为一个计时基准说它更快;
|
||||
**那个基准从来没有检查过内容**。要测量你真正在意的东西:这个 bug 是在渲染出的一帧
|
||||
返回 0 个亮像素、而 gdb 路径报告 1693 时才浮出水面的。
|
||||
|
||||
### 页面是 f-string 生成的,所以要检查它真正吐出的脚本
|
||||
|
||||
网页 UI 是一整条 f-string。JavaScript 字符串字面量里多一个或少一个反斜杠,**整段**
|
||||
`<script>` 就无法解析,而唯一的现象是状态栏永远停在 "connecting..."——同时所有接口用
|
||||
`curl` 测又都正常。这真的发布过一次:`.split('\\')` 变成了 `.split('\')`,一个没闭合的
|
||||
字符串,于是从浏览器看页面是死的,而每个测试都通过。
|
||||
|
||||
现在 `test_webui.TestPageScriptParses` 会把服务端真正输出的脚本抽出来交给 `node --check`。
|
||||
**要测你交付出去的那个产物,而不是生成它的代码。**
|
||||
|
||||
### 探针必须真的能看到它在找的东西
|
||||
|
||||
有三轮"固件从不碰 flash"的结论,全是探针的错,而且每一轮看起来都像个发现:
|
||||
|
||||
- 探针按 `address >= 0x0C0000` 过滤,于是所有**没有地址**的帧——写使能、以及真正执行
|
||||
擦除的扇区擦除——都被丢掉了。"0 次写"是过滤器造成的,不是固件。
|
||||
- 握手只给了 1.5 秒,而固件需要约 4 秒才进入串口模式。"没有应答"是超时造成的。
|
||||
- 探针为什么会完全看不见:PowerShell 5.1 的 `2>` 会写成 UTF-16LE,每行字符之间都夹
|
||||
一个 NUL,任何过滤都匹配不上。
|
||||
|
||||
在相信一个空探针之前,先让它打印出一个你确定存在的东西。
|
||||
|
||||
### flash 模型每释放一次片选就把整份镜像写回
|
||||
|
||||
对一次设置保存来说 2 MB 不算什么;但对多系统的主机接口就是灾难——它是按 200 字节一块
|
||||
编程槽位的(`App/app/uart.c` 的 `0x0724`,12 字节头 + 数据):一份 114 KB 固件会变成约
|
||||
600 次全量重写,而且跑在 vCPU 线程上,于是**客人**和所有跟它对话的主机工具都要等。实测:
|
||||
一次 64 字节的槽位写入花了六秒。
|
||||
|
||||
第一版修法是加 200 毫秒节流,那是错的:它用"强杀时静默丢掉最后一段写入"换来了测试变快。
|
||||
现在模型记录**改动区间**并只写那一段(就地写),既快又更忠实——真机 NOR 的编程本来就不是
|
||||
原子的。退出时的通知器仍然整份写回。
|
||||
|
||||
### 串口链路上跑着固件自己的屏幕流
|
||||
|
||||
`K5Viewer` 会把屏幕从 USART1 推出去。主机客户端如果只在等应答时才读,socket 就会积压,
|
||||
**客人随后会阻塞**在写串口上:槽位传输会在中途开始丢应答,一次很小的写入也要好几秒。
|
||||
正确做法是用一个读线程持续排空,等待逻辑只去看已经重组好的帧——在电台那一侧,任何跟它
|
||||
对话的程序都适用同一条。
|
||||
|
||||
这条路上还有两点:固件的接收缓冲是 256 字节
|
||||
(`App/driver/uart.c: UART_DMA_Buffer[256]`),240 字节数据加上帧头就超了,于是每一帧都被
|
||||
静默丢弃,200 就合适;以及串口**会话**在约 6 秒没有 `0x0514` 后会超时
|
||||
(`gSerialConfigCountDown_500ms = 12`),长传输一定会跨过它——用重新握手实测:写入立刻恢复。
|
||||
|
||||
这四点都修好后,`tools/uvk5_slots_serial.py` 能通过固件本身写入槽位,并由设备校验 CRC。
|
||||
|
||||
### 开机后才连上的串口客户端会错过一切
|
||||
|
||||
`-serial tcp:host:port,server=on,wait=off` 在**没有客户端连接时会丢弃**客人写出的字节。
|
||||
固件的横幅是在最初几秒打印的,所以"等 QEMU 起来再连"(比如四秒后)会看到一个空端口——
|
||||
和"客人根本没启动"长得一模一样。有四轮"引导什么也不发"其实是这个原因,不是引导。
|
||||
|
||||
先连接,再让客人跑。同一条陷阱也适用于引导等待主机时持续发出的 0x0518 洪水:它是持续的,所以
|
||||
晚连的客户端**也能看到**——这正是这个错误能存活这么久的原因,它只对一次性启动输出显形。
|
||||
|
||||
另外两个在这里学到的习惯:
|
||||
|
||||
- **先确认探针能看到一个你确定存在的东西。** 一个 USART 寄存器探针报告零次访问,最自然的读法
|
||||
是"引导从不配置串口"。而用同一个探针跑应用,得到 2239 次——这才说明探针是好的、引导确实是沉默的。
|
||||
- **当一条记录下来的观察不再能复现时,把它当成未验证。** 引导的 Moto 洪水是从一次现在已无法用
|
||||
当前构建与镜像复现的运行里写下来的;要依赖它之前先重新推导。
|
||||
|
||||
### 裸的 host:port 不是"scheme"
|
||||
|
||||
`uvk5_socket.connect` 会按 ":" 切分参数来找 scheme,于是监管进程、网页和 README 一直传的
|
||||
`127.0.0.1:4444` 被读成:scheme 是 `127.0.0.1`、端口为空、主机为空。连接就一直等到超时。
|
||||
从外面看到的现象是"网页开不了机",而用完全相同命令行手敲的 QEMU 半秒就应答 QMP,客人则在后台
|
||||
好好跑着。
|
||||
|
||||
两件事让它难被发现:同一个助手也接受 `tcp:host:port`,所以用那种写法的测试都是通的;而失败
|
||||
形式是**超时**而不是报错,看起来像"模拟器很慢或卡住"。现在 `test_uvk5_socket` 覆盖了所有会走到
|
||||
`connect` 的写法。推广一句:**当一个助手接受多种写法时,每一种都要测**——没人测的那一种,往往
|
||||
就是所有人都在用的那一种。
|
||||
|
||||
同一处的另一半是**另一个独立真问题**:QEMU 的 stderr 必须从启动那一刻就被排空。固件把屏幕流从
|
||||
那根管道推出去,约一秒就能填满 64 KB,QEMU 写不进去就阻塞——主循环停了,QMP 自然也不应答。
|
||||
两种做法都量过:排空时 QMP 0.5 秒应答;不排空时永远不应答。
|
||||
|
||||
### 你吞掉的寄存器,就是下一个程序死等的东西
|
||||
|
||||
出厂引导**完全起不来**:没有任何串口输出,PC 探针每一次采样都停在 `0x08000f38`。那地址在引导
|
||||
区里,两条指令是:
|
||||
|
||||
0x0f38: ldr r2, [r1] ; r1 = 0x40022000,FLASH 控制器
|
||||
0x0f3a: lsls r2, r2, #30
|
||||
0x0f3c: lsrs r2, r2, #30 ; r2 = ACR & 3,就是 LATENCY 字段
|
||||
0x0f3e: cmp r2, #1 ; 等 1 个等待周期
|
||||
0x0f40: bne 0x0f38
|
||||
|
||||
早前加进来的 FLASH 控制器模型把 `ACR` 与 `OPTKEYR` 当成"可忽略的写"——直接提前 return,
|
||||
于是通用路径从未保存它们,`ACR` 永远回读 0,引导在配置串口之前就死等。改成 `return false`
|
||||
让数值被保存后,PC 立刻进入应用区(`0x08013ea0`),串口也开始有输出。
|
||||
|
||||
两条都通用:
|
||||
|
||||
- **只写寄存器也是寄存器。** 应用从不回读 `ACR`,所以只要只有应用在跑,这个缺陷就是隐形的;
|
||||
下一个碰同一个外设的程序立刻撞上。
|
||||
- **"以前是好的"就是一条二分指令。** 引导的 Moto 洪水在 FLASH 控制器建模**之前**被观察到,
|
||||
之后就不再复现。正确的动作是问"这两次之间改了什么",而不是怀疑早先的记录。
|
||||
|
||||
## 键盘:两个真 bug,都已修复
|
||||
|
||||
这里原来的笔记写的是"按键到达了固件但界面不反应",并且归咎于机器模型。结果发现有**两个
|
||||
@@ -349,8 +480,12 @@ GCC 能看到全部调用者。如果一个模型的输出神秘地不起作用
|
||||
`SETTINGS_InitEEPROM` 会比较 flash `0x00A160` 处的版本字符串,在一个新镜像上发现不匹配,
|
||||
于是写入设置扇区。而 `PY25Q16_WriteBuffer` 会在重新编程之前**擦除整个 4 KB 扇区**,
|
||||
所以埋在 `0x00A00B` 的字节在 `settings.c:169` 的读取看到它之前就已经没了。
|
||||
- **guest 侧的设置改动不会持久化。**(历史条目:现在 flash 会写回文件了,
|
||||
见 flash 章节的第 1 条修复。)
|
||||
- **guest 侧的设置改动现在会持久化,所以测试方式要变。** PY25Q16 模型在 realize 时读入镜像、
|
||||
留在 RAM 里,并在片选释放或进程退出时经临时文件写回,于是**每个会话都会改动
|
||||
`assets/flash.img`**。实测跑完一次真实会话之后:本来全是 `0xFF` 的设置区 `0x00A000`
|
||||
已经装着 guest 的设置,`0x8000..0x8800` 也变了,整个文件与开会话前的副本相差 2239 字节。
|
||||
所以不要假设镜像是干净的 —— 要和 `assets/pristine/`(或你自己留的副本)对比,还原前先断电。
|
||||
在 Windows 上这一步直到把 `rename()` 换成 `g_rename()` 之前都是静默失败的,见可移植性一节。
|
||||
|
||||
这里有用的工具:`tools/scan_trace.sh`(扫描读到了什么)、`tools/key_result.sh`
|
||||
(Poll 返回了什么)、`tools/trace_run.sh`(那些 TRACE 点)。
|
||||
@@ -427,6 +562,10 @@ GCC 能看到全部调用者。如果一个模型的输出神秘地不起作用
|
||||
文档传给某个工具的每个长参数是否真的存在于该工具中、
|
||||
以及文档里的固件 `file:line` 引用是否仍指向正文声称的东西。
|
||||
|
||||
校验器自己也需要两处改动才能在作者的机器之外运行:所有读取都显式用 `encoding="utf-8"`
|
||||
(默认是区域编码,Windows 上是 GBK,中文文档根本解不开),以及固件树路径来自 `UVK5_FW_DIR`
|
||||
环境变量而不是写死,这样 `file:line` 那几项检查可以指向你手上任何一份固件树。
|
||||
|
||||
参数检查本身也留下了一个教训。它的第一版只匹配到行尾,所以对一条这样换行的命令
|
||||
|
||||
python3 tools/screenshot.py --frame-addr 0x200013DC \
|
||||
@@ -732,3 +871,70 @@ guest 照样在跑,但事后 `REG_0C` bit 0 仍然是置位的:固件没有
|
||||
3. 警惕自旋循环:任何固件会轮询的标志都必须**能够变化**,
|
||||
而写 1 启动的位(比如 `ADC_CR2_CAL`)**绝不能被存成置位状态**
|
||||
4. 重新构建、运行,并用 `tools/where.sh` 确认固件越过了它原来停住的地方
|
||||
5. 往 `py32_stubs[]` 里加条目时**必须同时改 `PY32_NUM_STUB`**。忘了改以前是**静默**的:
|
||||
那个设备根本不会被 realize,地址仍是未映射,唯一症状就是"什么都没变"。现在表后面有一条
|
||||
`QEMU_BUILD_BUG_ON(ARRAY_SIZE(...) != PY32_NUM_STUB)`,会在编译期拦住。
|
||||
这样找到过两个洞,都对多系统固件是致命的(见可移植性一节):`0x40007400` = `DAC1_BASE`、
|
||||
`0x1FFF3000` = `UID_BASE`,两者都不在任何面向固件的清单里。所以现在整个 APB/AHB 外设空间
|
||||
在具名设备之后还有一层**低优先级兜底**:没名字的寄存器会应答并记日志,而不是抛异常 ——
|
||||
真机有应答、模拟器却数据异常,那是模型的 bug,不是新发现。
|
||||
|
||||
## 在新固件里找显示缓冲区的地址
|
||||
|
||||
`gFrameBuffer` 和 `gStatusLine` 在不同构建之间会移动,而且**两者都不是 128 字节对齐的**,
|
||||
所以按对齐地址去猜,渲染出来的图会以"像字体问题"的方式出错:偏 0x3E 字节时,每一行都变成
|
||||
"上一行的尾部 + 本行的头部",字形会被固定在某一列劈开,状态行也被帧内容盖住。别用眼睛判断
|
||||
—— 固件源码会明确告诉你它们在哪。
|
||||
|
||||
1. 抓 SRAM(QMP `memsave`,或 `python work/qmp.py dump 0x20000000 0x4000 out.bin`)。
|
||||
2. 挑"内容和落点都能从源码确定"的位图(`App/bitmaps.c` 配合 `App/ui/status.c`、
|
||||
`App/driver/st7565.c`):`gFontPowerSave` 复制到状态行 +0、`gFontDWR` +18、
|
||||
`gFontPttClassic` +54、`BITMAP_BatteryLevel1` +111(`LCD_WIDTH - 17`);
|
||||
`BITMAP_VFO_Default` 由 `memcpy` 写到**帧行**偏移 0。
|
||||
3. 在 SRAM 里搜这些字节串。只有唯一一个基址能让那四个状态行偏移同时成立,而 VFO 箭头的地址
|
||||
**就是**帧缓冲基址。两者必须正好相差 `FRAME_LINES * LCD_WIDTH` = 896 —— 这就是"搜索收敛了"
|
||||
的判据。5.9.0.CN 这份固件:帧缓冲 `0x200012BE`,状态行 `0x2000163E`。
|
||||
|
||||
拿到之后的自检:帧行 3 是界面 `memset` 掉的中缝,应当整行全 0;两个 VFO 同频时,
|
||||
帧行 0/1 与 4/5 相同,而 2 与 6 不同(只有活动 VFO 的说明行有内容)。
|
||||
|
||||
## 可移植性:Windows 到底弄坏了什么
|
||||
|
||||
机器和工具本身是可移植的 C 与 Python;**包装**是 Linux 专用的。四个失败,每一个在有人依赖它
|
||||
之前都是不可见的:
|
||||
|
||||
* **`rename()` 在 Windows 上不覆盖已存在的文件。** flash 回写是"写临时文件再改名覆盖",
|
||||
于是每一次保存设置都失败(`cannot replace`),设置永远到不了磁盘,而 stderr 刷屏把主循环
|
||||
挤住到 QMP 连问候语都发不出来 —— 最终只表现为 "power on failed: timed out"。
|
||||
`g_rename()`(需要 `<glib/gstdio.h>`)在两个平台上都给出 POSIX 语义。
|
||||
* **Windows 版 QEMU 无法创建 unix socket**,所以 QMP 必须以 `tcp:host:port` 传输;
|
||||
`uvk5_qmp.py`、`key.py` 和 supervisor 的启动器现在两种形式都接受。
|
||||
* **`qemu/py32f071.c` 对原版 QEMU 7.2 编译不过**,缺 `#include "qapi/visitor.h"`
|
||||
(`visit_type_uint64`);`qom/object.h` 不会间接带入它。
|
||||
* **`-kernel foo.bin` 会加载到错的地方。** `armv7m_load_kernel()` 把裸二进制加载到给它的基址,
|
||||
而在本机那个基址是 flash 的**别名区**,于是镜像高 0x2800 字节,第一次取指就 fault。
|
||||
`tools/bin2elf.py` 把发行版 `.bin` 套上带正确程序头的 ELF32/ARM。
|
||||
|
||||
**外置 flash 是有分区的,而且主固件确实在读它。** 有一次 26 秒的抓取里只看到 `0x00A0xx`,
|
||||
于是"固件根本不用 flash"被当成结论写下来 —— 错了两次:第一次被 PowerShell 的 UTF-16 重定向
|
||||
骗了,第二次是我把探针**截断在 80 次读**。把上限放到 4000、并打开菜单让固件画汉字之后,
|
||||
一次启动就产生 3168 次读:设置区、`0x0A0000`/`0x0E0000` 用户字体包里的单个字模,以及
|
||||
**`0x1E0000` 处一张 32 KB 字体表被按 32 字节一步连续走完 1024 次**。分区如下(由
|
||||
`gitee.com/oldlicn/betula-multi-system-tool` 的配套数据反推,不是靠它那张分区图):
|
||||
|
||||
| 偏移 | 大小 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| 0x000000 | 128 KB | 引导 + 设置(`0x00A0xx`)+ 校准(`0x010000`) |
|
||||
| 0x020000 | 4 × 128 KB | 固件槽(工具里正好有"清空 0x20000-0x40000"到"0x80000-0xA0000") |
|
||||
| 0x0A0000 | 256 KB | 用户字体包 16x16 |
|
||||
| 0x0E0000 | 64 KB | 用户字体包 8x8 |
|
||||
| 0x100000 | 1 MB | 出厂资源区,其中 `0x1E0000` 是那张 32 KB 字体表 |
|
||||
|
||||
官方十六份 128 KB 恢复文件可以拼回真机整片 2 MB 数据。把它的 `0x100000-0x200000` 区补进
|
||||
`assets/flash.img` 之后,**固件画出来的内容会变** —— 说明那块数据是活的,不是装饰。
|
||||
还没定论的是"哪块文字由哪个来源提供":屏幕上的 16 像素字模与 `0xA0000` 的字模包
|
||||
(24 格里对 2 格)、与 `0x1E0000` 的表(8 格里 0 格)都不能逐字节对上。
|
||||
|
||||
面板设置是第五种、不一样的情况:对比度和反显根本不在帧缓冲里,所以任何渲染 `gFrameBuffer`
|
||||
的东西都显示不出来。`TYPE_ST7565` 建模控制器自己的寄存器,`tools/uvk5_lcd.py` 把反显应用到
|
||||
画面上;见 README.md。
|
||||
Reference in new issue
Block a user