Files
mckero 2667e046e8 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
2026-10-01 14:54:34 +08:00

205 lines
7.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""The BK4819 register interface must work, and RSSI must not read as zero.
Scope: this covers the register bus, not radio behaviour. The chip has no public
datasheet, so App/driver/bk4819.c is the only specification available and it can only
say which registers were written -- never what left the antenna. Keying envelopes,
spurious emissions and sensitivity need a real radio and a spectrum analyser.
What it does check:
1. The firmware boots. That is not a formality: App/app/app.c:910 and :1417 spin on
bit 0 of REG_0C with no timeout, so a model that leaves that bit set hangs the
guest outright. PB9 used to be idled low purely so reads returned 0 and those
loops could exit.
2. Registers written by the firmware read back with the values it wrote, which
proves the bit-banged transfer is being decoded rather than ignored.
3. RSSI is non-zero. It was previously hard 0 at 18 call sites, i.e. -160 dBm, so
the S-meter read empty and squelch and scan logic evaluated a dead band.
"""
import gzip
import json
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import uvk5_socket
import uvk5_testenv
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
QEMU = uvk5_testenv.qemu() # env QEMU/UVK5_QEMU, else PATH
ELF = uvk5_testenv.firmware() # env ELF/UVK5_FIRMWARE, else assets/firmware
PRISTINE = os.path.join(ROOT, "assets", "pristine", "flash-pristine.img.gz")
BOOT_SECONDS = 20
# From App/driver/bk4819.c. These are the ones the firmware reads back.
REG_INTERRUPT = 0x0C
REG_RSSI = 0x67
class Qmp:
def __init__(self, endpoint):
# A socket or an endpoint. QEMU's QMP accepts a single client, so whoever
# waited for it to appear hands its connection in rather than connecting a
# second time -- which hangs.
if hasattr(endpoint, "recv"):
self.sock = endpoint
else:
self.sock = uvk5_socket.connect(endpoint, timeout=30)
self.buf = b""
self._read()
self.cmd("qmp_capabilities")
def _read(self):
while b"\n" not in self.buf:
chunk = self.sock.recv(65536)
if not chunk:
raise RuntimeError("QMP closed")
self.buf += chunk
line, self.buf = self.buf.split(b"\n", 1)
return json.loads(line)
def cmd(self, name, **args):
msg = {"execute": name}
if args:
msg["arguments"] = args
self.sock.sendall(json.dumps(msg).encode() + b"\n")
while True:
reply = self._read()
if "return" in reply or "error" in reply:
return reply
def close(self):
try:
self.sock.close()
except OSError:
pass
def main():
absent = uvk5_testenv.missing([
(QEMU, "QEMU", "set QEMU=/path/to/qemu-system-arm, or put it on PATH"),
(ELF, "firmware", "run tools/fetch_firmware.py, or set ELF=/path/to/image"),
(PRISTINE, "pristine flash image", "it ships in assets/pristine/"),
])
if absent:
return uvk5_testenv.skip(absent)
workdir = tempfile.mkdtemp(prefix="uvk5-bk4819-")
image = os.path.join(workdir, "flash.img")
sock_path = uvk5_socket.server_endpoint("qmp", directory=workdir)
with gzip.open(PRISTINE, "rb") as src, open(image, "wb") as dst:
shutil.copyfileobj(src, dst)
child_env = dict(os.environ)
child_env["UVK5_FLASH_IMAGE"] = image
# stderr to a FILE, not a pipe: the model writes the firmware's serial output
# there, and a pipe nobody drains fills up, blocks the guest, and then QMP stops
# answering -- which reads as "the emulator never started".
qemu_log = os.path.join(workdir, "qemu.log")
log_fh = open(qemu_log, "w+b")
proc = subprocess.Popen(
[QEMU, "-M", "uv-k5-v3", "-nographic", "-monitor", "none",
"-qmp", sock_path, "-kernel", ELF],
stdout=subprocess.DEVNULL, stderr=log_fh, env=child_env)
failures = []
try:
qmp_sock = None
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
if proc.poll() is not None:
raise RuntimeError("QEMU exited during startup")
try:
# Connecting is the test: a unix path can be waited for as a file,
# a TCP endpoint cannot, and this works for both.
qmp_sock = uvk5_socket.connect(sock_path, timeout=2)
break
except OSError:
time.sleep(0.1)
if qmp_sock is None:
raise RuntimeError("QMP never accepted a connection at %s" % sock_path)
time.sleep(BOOT_SECONDS)
qmp = Qmp(qmp_sock)
# 1. Still running means the untimed REG_0C spin terminated.
status = qmp.cmd("query-status").get("return", {})
print(f"guest status: {status.get('status')}")
if status.get("status") != "running":
failures.append(
f"guest is {status.get('status')}, not running -- most likely stuck "
"in the untimed spin on REG_0C bit 0")
else:
print("PASS the firmware booted and is running")
# 2 and 3. Read the model's register file through QOM.
regs = {}
for name, num in (("interrupt", REG_INTERRUPT), ("rssi", REG_RSSI)):
reply = qmp.cmd("qom-get", path="/machine/bk4819",
property=f"reg{num:02x}")
if "error" in reply:
failures.append(f"cannot read reg{num:02x}: {reply['error']}")
else:
regs[name] = reply["return"]
if "rssi" in regs:
print(f"RSSI register: 0x{regs['rssi']:04X}")
if regs["rssi"] == 0:
failures.append(
"RSSI reads 0, i.e. -160 dBm: squelch and scan see a dead band")
else:
print("PASS RSSI is not stuck at zero")
if "interrupt" in regs:
print(f"REG_0C: 0x{regs['interrupt']:04X}")
if regs["interrupt"] & 1:
failures.append(
"REG_0C bit 0 is set; the firmware spins on it without a timeout")
else:
print("PASS REG_0C bit 0 is clear")
# The firmware writes plenty of registers during init, so a register file
# that is entirely zero means the bus decode never ran.
written = 0
for num in range(0x00, 0x80):
reply = qmp.cmd("qom-get", path="/machine/bk4819",
property=f"reg{num:02x}")
if "return" in reply and reply["return"] not in (0, None):
written += 1
print(f"non-zero registers: {written}")
if written < 5:
failures.append(
f"only {written} registers hold a value; the firmware writes dozens "
"during init, so the three-wire transfer is not being decoded")
else:
print("PASS the firmware's register writes were decoded")
qmp.cmd("quit")
qmp.close()
finally:
try:
proc.terminate()
proc.wait(timeout=15)
except Exception:
proc.kill()
shutil.rmtree(workdir, ignore_errors=True)
if failures:
print()
for f in failures:
print(f"FAIL {f}")
sys.exit(1)
print("\nthe BK4819 register interface works")
if __name__ == "__main__":
main()