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

+9
View File
@@ -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
+7
View File
@@ -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,所以它不需要知道某份固件把画面
+36
View File
@@ -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.
+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())