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:
mckero committed 2026-10-01 15:28:47 +08:00
1 parent e1ffdf8fdd
commit f4c9d343fc
4 files changed
+164

No files matched your search

+112
View File
@@ -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())