mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 11:07:31 +00:00
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
168 lines
5.8 KiB
Python
Executable File
168 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""millis() must advance, because 17 call sites build timeouts on it.
|
|
|
|
Found by auditing what the emulator genuinely reproduces rather than what merely runs.
|
|
TIM2 was covered by the catch-all stub, which returns the last value written -- so
|
|
|
|
uint32_t millis(void) { return LL_TIM_GetCounter(TIM2); }
|
|
|
|
returned 0 forever and no elapsed-time check could ever fire. A silent wrong answer,
|
|
not a hang, which is the harder kind to spot.
|
|
|
|
Checked here:
|
|
1. the counter is non-zero once the firmware has been running a while
|
|
2. it increases between two samples
|
|
3. it increases by roughly the elapsed wall time, not some arbitrary amount
|
|
|
|
Point 3 is what separates a working counter from one that merely changes: a counter
|
|
ticking at the wrong rate would satisfy 1 and 2 and still break every timeout.
|
|
"""
|
|
|
|
import gzip
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
import uvk5_socket
|
|
import uvk5_testenv
|
|
|
|
SIM = pathlib.Path(__file__).resolve().parent.parent
|
|
QEMU = uvk5_testenv.qemu()
|
|
ELF = uvk5_testenv.firmware()
|
|
PRISTINE = SIM / "assets/pristine/flash-pristine.img.gz"
|
|
|
|
BOOT_SECONDS = 24
|
|
TIM2_CNT = 0x40000024 # TIM2 base + CNT offset
|
|
GAP = 5.0
|
|
|
|
|
|
class Qmp:
|
|
def __init__(self, path):
|
|
self.s = uvk5_socket.connect(path, timeout=25)
|
|
self.buf = b""
|
|
self._read()
|
|
self.cmd("qmp_capabilities")
|
|
|
|
def _read(self):
|
|
while b"\n" not in self.buf:
|
|
chunk = self.s.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.s.sendall(json.dumps(msg).encode() + b"\n")
|
|
while True:
|
|
reply = self._read()
|
|
if "return" in reply or "error" in reply:
|
|
return reply
|
|
|
|
|
|
def read_counter(port):
|
|
"""TIM2's CNT, via gdb.
|
|
|
|
Read through the CPU so the peripheral's read handler runs -- the count is derived
|
|
on access, not stored, so dumping memory another way would miss it.
|
|
"""
|
|
out = subprocess.run(
|
|
[str(uvk5_testenv.gdb()), "-batch",
|
|
"-ex", "set confirm off", "-ex", "set pagination off",
|
|
"-ex", f"target remote :{port}",
|
|
"-ex", f'printf "CNT=%u\\n", *(unsigned int*){TIM2_CNT}',
|
|
"-ex", "detach", "-ex", "quit", str(ELF)],
|
|
capture_output=True, text=True, timeout=90)
|
|
for line in out.stdout.splitlines():
|
|
if line.startswith("CNT="):
|
|
return int(line[4:])
|
|
return None
|
|
|
|
|
|
def main():
|
|
if uvk5_testenv.gdb() is None:
|
|
return uvk5_testenv.skip("gdb-multiarch is missing; this test reads firmware "
|
|
"globals over a gdb attach, which has not been ported to "
|
|
"the QMP memsave route the page uses")
|
|
for tool, what in ((QEMU, "QEMU"), (ELF, "firmware"), (PRISTINE, "pristine flash image")):
|
|
if tool is None or not tool.exists():
|
|
return uvk5_testenv.skip("%s is missing (%s); see the README Quick start"
|
|
% (what, tool or "not found"))
|
|
|
|
port = 1263
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
img = pathlib.Path(tmp) / "flash.img"
|
|
img.write_bytes(gzip.decompress(PRISTINE.read_bytes()))
|
|
sock = uvk5_socket.server_endpoint("qmp", directory=str(tmp))
|
|
|
|
proc = subprocess.Popen(
|
|
[str(QEMU), "-M", f"uv-k5-v3,flash-image={img}",
|
|
"-nographic", "-monitor", "none",
|
|
"-qmp", sock,
|
|
"-kernel", str(ELF), "-gdb", f"tcp::{port}"],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
try:
|
|
# Qmp() below connects, and uvk5_socket retries until QEMU's QMP answers, so
|
|
# there is no socket path to wait for -- on Windows there would not be one.
|
|
time.sleep(BOOT_SECONDS)
|
|
time.sleep(BOOT_SECONDS)
|
|
|
|
failures = 0
|
|
|
|
first = read_counter(port)
|
|
print(f"millis() sample 1: {first}")
|
|
if first is None:
|
|
print("FAIL could not read TIM2")
|
|
return 1
|
|
if first == 0:
|
|
print("FAIL the counter is still 0; TIM2 is not running")
|
|
failures += 1
|
|
else:
|
|
print("PASS the counter is running")
|
|
|
|
time.sleep(GAP)
|
|
second = read_counter(port)
|
|
print(f"millis() sample 2: {second} (after {GAP:.0f}s)")
|
|
|
|
delta = second - first
|
|
if delta > 0:
|
|
print(f"PASS it advanced by {delta} ms")
|
|
else:
|
|
print(f"FAIL it did not advance ({first} -> {second})")
|
|
failures += 1
|
|
|
|
# Generous bounds: the guest is paused twice by gdb, and the emulator does
|
|
# not track wall time exactly. The point is to catch a counter running at
|
|
# completely the wrong rate, not to measure precision.
|
|
low, high = GAP * 1000 * 0.3, GAP * 1000 * 3.0
|
|
if low <= delta <= high:
|
|
print(f"PASS the rate is plausible "
|
|
f"({delta} ms for {GAP:.0f}s of wall time)")
|
|
else:
|
|
print(f"FAIL the rate is wrong: {delta} ms elapsed over "
|
|
f"{GAP:.0f}s, expected roughly {int(GAP * 1000)}")
|
|
failures += 1
|
|
|
|
if failures:
|
|
return 1
|
|
print("\nmillis() advances at a usable rate")
|
|
return 0
|
|
finally:
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|