mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-03 19:47:22 +00:00
Probe what a BK4819 read presents, and say plainly that it is not the guard yet
UVK5_BK4819_PROBE reassembles the sixteen bits a read clocks out and logs them against the register's own value: 1566 of 1566 agree on the working model. That agreement is not proof -- removing the skip_falling fix, which is exactly the historical left-shift regression, leaves the reassembled word unchanged, so this observation point is not the one the guest samples at. tools/test_bk4819_readback.sh therefore stays the guard. tools/test_bk4819_readback.py is the working draft of a portable replacement (no source patch, no rebuild, no ARM gdb) and is deliberately NOT registered in run_tests.sh, so a proven guard is not swapped for an unproven one. Both the file and the two READMEs say so.
This commit is contained in:
1 parent
e1ffdf8fdd
commit
f4c9d343fc
4 files changed
+164
No files matched your search
@@ -401,6 +401,15 @@ 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.
|
||||
|
||||
### The readback guard, and a draft that is not one yet
|
||||
|
||||
`tools/test_bk4819_readback.sh` guards the reading-shift bug -- a register read delivering its
|
||||
value one bit off. It needs an ARM gdb, so it only runs where one is installed.
|
||||
`tools/test_bk4819_readback.py` is the portable replacement in progress, and **it is not the
|
||||
guard yet**: it passes on the working model, but removing the fix does not make it fail, so it
|
||||
does not observe what the guest actually samples. It is deliberately not registered in
|
||||
`tools/run_tests.sh`, so that a proven guard is not replaced by an unproven one. The
|
||||
observation point has to move to the driver's sampling edge before it takes over.
|
||||
### 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
|
||||
|
||||
@@ -356,6 +356,13 @@ v6.0.0 版把开机菜单和四个固件槽放在外部 flash 里:开机按住
|
||||
决定(默认 8 秒:开机路径可能花 20 秒把当前固件"采纳"进槽 0,之后才会去采样键盘)。
|
||||
- `tools/uvk5_slots.py` 做同样的事但离线:把槽写进 flash 镜像,并打印每个槽的内容。
|
||||
|
||||
### 读回守卫,以及一个**还不算守卫**的草稿
|
||||
|
||||
`tools/test_bk4819_readback.sh` 守着「读回左移一位」那个 bug —— 读寄存器交出的值偏一位。它需要
|
||||
ARM gdb,所以只在装了它的机器上跑。`tools/test_bk4819_readback.py` 是正在做的可移植替代,但
|
||||
**它现在还不是守卫**:在正确的模型上它通过,可是**把修复拿掉它并不会失败**,说明它观测的位置
|
||||
不是客人真正采样的那一刻。因此它**刻意没有注册进** `tools/run_tests.sh` —— 不能用一个未经证明的
|
||||
检查换掉一个已经证明的。要等观测点移到驱动真正的采样边沿,它才能接手。
|
||||
### 哪份构建渲染正确,这件事怎么判定
|
||||
|
||||
网页画的是显示控制器**自己的显存**,不是固件的 framebuffer,所以它不需要知道某份固件把画面
|
||||
|
||||
@@ -1055,6 +1055,21 @@ struct BK4819State {
|
||||
bool reading;
|
||||
bool skip_falling; /* the command byte's trailing edge, not a data bit */
|
||||
|
||||
/*
|
||||
* Diagnostic probe (UVK5_BK4819_PROBE) -- what this read presents, reassembled from
|
||||
* the bits clocked out, because the model's own register file is right by
|
||||
* construction and says nothing about what the guest was handed.
|
||||
*
|
||||
* It logs "sent" (the sixteen bits clocked out) against "reg" (the register). On the
|
||||
* working model every read agrees: 1566 of 1566. That agreement is *not* proof that it
|
||||
* would catch the historical left-shift -- removing the skip_falling fix below leaves
|
||||
* the reassembled word unchanged, so this is not the point the guest samples at. Until
|
||||
* that is understood, tools/test_bk4819_readback.sh remains the guard and
|
||||
* tools/test_bk4819_readback.py is a draft.
|
||||
*/
|
||||
uint32_t out_seen;
|
||||
unsigned out_bits;
|
||||
|
||||
/*
|
||||
* Interrupt flags awaiting collection, held apart from REG_02 because the firmware
|
||||
* writes that register to acknowledge and then reads it back for the flags, so the
|
||||
@@ -1370,6 +1385,8 @@ static void bk4819_set_scl(void *opaque, int line, int level)
|
||||
bk4819_eval_receiver(s);
|
||||
}
|
||||
s->shift_out = s->regs[s->cmd];
|
||||
s->out_seen = 0;
|
||||
s->out_bits = 0;
|
||||
/*
|
||||
* The command byte's own trailing falling edge must not consume
|
||||
* bit 15. Each firmware bit is read/raise/lower, so the eighth
|
||||
@@ -1429,6 +1446,25 @@ static void bk4819_set_scl(void *opaque, int line, int level)
|
||||
if (falling && s->skip_falling) {
|
||||
s->skip_falling = false;
|
||||
} else if (falling && s->have_cmd && s->reading) {
|
||||
/*
|
||||
* The bit being presented right now is what the guest samples. Reassemble the
|
||||
* sixteen of them so the probe can report the word the guest received.
|
||||
*/
|
||||
s->out_seen = (s->out_seen << 1) | ((s->shift_out >> 15) & 1u);
|
||||
s->out_bits++;
|
||||
if (s->out_bits == 16) {
|
||||
const char *bk_probe = g_getenv("UVK5_BK4819_PROBE");
|
||||
if (bk_probe) {
|
||||
FILE *bf = fopen(bk_probe, "a");
|
||||
if (bf) {
|
||||
fprintf(bf, "READ cmd=%02x sent=%04x reg=%04x skip=%d\n",
|
||||
s->cmd, (unsigned)s->out_seen, s->regs[s->cmd],
|
||||
s->skip_falling ? 1 : 0);
|
||||
fclose(bf);
|
||||
}
|
||||
}
|
||||
s->out_bits = 0;
|
||||
}
|
||||
/*
|
||||
* Advance on the falling edge so the next bit is settled before the guest
|
||||
* samples it. BK4819_ReadU16 sets SCL low, reads, then sets it high.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A register read must deliver the value the register holds.
|
||||
|
||||
DRAFT -- not yet a guard, and deliberately not registered in tools/run_tests.sh.
|
||||
|
||||
It passes on the working model (238 reads, every one agreeing), but it does *not* fail when
|
||||
the historical fix is removed: with skip_falling left unset the reassembled word is unchanged,
|
||||
so this observation point is not the one the guest samples at. Until that is understood,
|
||||
tools/test_bk4819_readback.sh remains the guard for this bug and this file is the working
|
||||
draft of its portable replacement.
|
||||
|
||||
This is the guard for a bug that was invisible for a long time: reads arrived shifted
|
||||
one place left, so seeding REG_0C with 0x1248 delivered 0x2490 to the firmware. Writes
|
||||
were always fine and the registers the firmware polls hardest were legitimately zero,
|
||||
and reading zero and getting zero looks like success.
|
||||
|
||||
It used to be tools/test_bk4819_readback.sh, which seeded a register by *patching
|
||||
qemu/py32f071.c*, rebuilt QEMU, and read the value out of the running guest with
|
||||
gdb-multiarch. Three dependencies -- a source tree, a compiler and an ARM gdb -- for a
|
||||
guard whose whole job is to protect whoever is editing the model right now. On a machine
|
||||
without an ARM gdb it skipped, which is to say the guard did not exist there.
|
||||
|
||||
It now watches the bits the model presents to the guest (UVK5_BK4819_PROBE) and asserts
|
||||
that the sixteen bits a read delivers are the register's own value. No rebuild, no gdb,
|
||||
no source patch: plain Python, so it runs wherever the emulator does.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
import uvk5_testenv # noqa: E402
|
||||
|
||||
# The firmware reads REG_0C about 1000 times in the first seconds, REG_31 and REG_48
|
||||
# hundreds of times, so a sample is guaranteed without seeding anything.
|
||||
MIN_SAMPLES = 200
|
||||
|
||||
|
||||
def main():
|
||||
qemu = uvk5_testenv.qemu()
|
||||
firmware = uvk5_testenv.firmware()
|
||||
missing = uvk5_testenv.missing([
|
||||
(qemu, "QEMU", "set QEMU=/path/to/qemu-system-arm, or put it on PATH"),
|
||||
(firmware, "firmware", "run tools/fetch_firmware.py, or set ELF=..."),
|
||||
])
|
||||
if missing:
|
||||
return uvk5_testenv.skip(missing)
|
||||
|
||||
workdir = tempfile.mkdtemp(prefix="uvk5-readback-")
|
||||
probe = os.path.join(workdir, "reads.log")
|
||||
env = dict(os.environ)
|
||||
env["UVK5_BK4819_PROBE"] = probe
|
||||
env.setdefault("UVK5_FLASH_IMAGE", os.path.join(HERE, "..", "assets", "flash.img"))
|
||||
env["PATH"] = "F:/msys64/mingw64/bin;" + env.get("PATH", "")
|
||||
|
||||
# No QMP needed: the probe writes to a file and the firmware asks the chip on its own.
|
||||
proc = subprocess.Popen([str(qemu), "-M", "uv-k5-v3", "-nographic", "-monitor", "none",
|
||||
"-serial", "null", "-kernel", str(firmware)],
|
||||
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
deadline = time.time() + 25
|
||||
while time.time() < deadline:
|
||||
time.sleep(1.0)
|
||||
if os.path.exists(probe) and len(open(probe, encoding="utf-8").read().splitlines()) >= MIN_SAMPLES:
|
||||
break
|
||||
finally:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
|
||||
reads = []
|
||||
if os.path.exists(probe):
|
||||
with open(probe, encoding="utf-8") as fh:
|
||||
reads = [line.split() for line in fh if line.startswith("READ ")]
|
||||
if len(reads) < MIN_SAMPLES:
|
||||
print("FAIL only %d reads captured (wanted %d): did the firmware boot?"
|
||||
% (len(reads), MIN_SAMPLES))
|
||||
return 1
|
||||
|
||||
bad = []
|
||||
for fields in reads:
|
||||
values = dict(field.split("=", 1) for field in fields[1:] if "=" in field)
|
||||
sent, reg = values.get("sent"), values.get("reg")
|
||||
if sent is not None and reg is not None and sent != reg:
|
||||
bad.append((values.get("cmd"), sent, reg))
|
||||
|
||||
print(" %d register reads, %d delivered something other than the register's value"
|
||||
% (len(reads), len(bad)))
|
||||
if not bad:
|
||||
print("")
|
||||
print("register reads are bit-aligned")
|
||||
return 0
|
||||
|
||||
print("")
|
||||
for cmd, sent, reg in bad[:5]:
|
||||
direction = ""
|
||||
if sent == "%04x" % ((int(reg, 16) << 1) & 0xffff):
|
||||
direction = " (shifted one place left; the command byte's trailing falling edge is eating bit 15)"
|
||||
elif sent == "%04x" % (int(reg, 16) >> 1):
|
||||
direction = " (shifted one place right; a data bit is being presented twice)"
|
||||
print("FAIL REG_%s: register holds %s, guest received %s%s" % (cmd, reg, sent, direction))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user