diff --git a/README.md b/README.md index 0d4cc4a..052dc9a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 7417568..e0654c2 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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,所以它不需要知道某份固件把画面 diff --git a/qemu/py32f071.c b/qemu/py32f071.c index 51f19c4..e2e77a7 100644 --- a/qemu/py32f071.c +++ b/qemu/py32f071.c @@ -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. diff --git a/tools/test_bk4819_readback.py b/tools/test_bk4819_readback.py new file mode 100644 index 0000000..5d1951b --- /dev/null +++ b/tools/test_bk4819_readback.py @@ -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())