diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ffa981a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,136 @@ +# Working on this repo + +Notes for whoever picks this up next. Focused on what is not obvious from the +code, and on mistakes that already cost time here. + +## What this is + +A QEMU machine for the Puya PY32F071 (Cortex-M0+), so Quansheng UV-K5 V3 +firmware runs on a PC. Boots to the main loop in ~5 s; the LCD is readable. + +The machine and every device model live in one file, `qemu/py32f071.c`. That is +deliberate: the models are small and tightly coupled to each other's wiring, and +splitting them would spread the board layout out without making any of it +clearer. + +## Ground rules + +**Never edit the firmware to make the emulator work.** The firmware is the +reference. If something does not run, the model is wrong. A fix that changes +firmware source makes every later test meaningless, because you are no longer +testing what the radio runs. + +**Register layouts come from the vendor CMSIS header**, not from a datasheet +search and not from inference: + + /Drivers/CMSIS/Device/PY32F071/Include/py32f071xB.h + +When you need a bit position, read it from there. Several details are +unintuitive — `LL_ADC_FLAG_EOS` is really `ADC_SR_EOC` on this part — and +guessing produces models that look right and hang. + +**Find the next thing to model by watching where the firmware stops**, not by +reading the datasheet front to back. Every peripheral here was added because the +firmware demonstrably waited on it: + + tools/where.sh 4 # sample the call stack a few times + +A stack that repeats in the same function across samples is a spin loop. Look at +what it reads. + +## How to run it + + python3 tools/make_flash.py # once; builds assets/flash.img + tools/run.sh # GDB stub on :1234, QMP on /tmp/uvk5-qmp.sock + + tools/where.sh # where execution is + tools/gpiob_dump.sh # GPIOB registers + python3 tools/key.py MENU # inject a keypress + python3 tools/screenshot.py --frame-addr 0x200013DC \ + --status-addr 0x2000175C --port 1234 --out screen.png + +Screenshot addresses move between firmware builds. Get the current ones with: + + arm-none-eabi-nm firmware.elf | grep -E 'gFrameBuffer|gStatusLine' + +Rebuild after editing the machine: + + cd $QEMU/build && ninja qemu-system-arm # ~10 s incremental + +## Things that already went wrong + +**GDB breakpoints halt the guest.** A key held across a breakpoint session is +never processed, because the main loop is not running. This produced a whole +round of "the keypress does nothing" that was really "the machine is stopped". +Use `tools/press_and_shot.sh` — it presses, lets the machine run, then reads the +framebuffer, with no breakpoints anywhere. + +**Do not write the SysTick counter back when accelerating it.** Two attempts did +that. Each read re-anchored the count, so the value the firmware saw stopped +changing, its `if (cur != prev)` guard never fired, and the delay loop hung +outright — worse than the slowness being fixed. The working approach reports a +value that runs ahead of the real counter and leaves the timer alone. + +**Lowering the clock does not speed up delay loops.** The bottleneck is loop +iterations per second, not counter speed. 48 MHz to 200 Hz bought 32x and was +nowhere near enough. Measured, not assumed. + +**Unnamed qdev in and out lines share one namespace.** A device with both +unnamed `qdev_init_gpio_in` and `qdev_init_gpio_out` makes `qdev_get_gpio_in()` +ambiguous, and board wiring silently attaches to the wrong line. The GPIO model +uses `"pin-in"` and `"pin-out"` for this reason. Keep it that way. + +**Key hold times must be generous.** Guest time runs fast, so 400 ms of wall +clock was too short for the firmware's debounce to complete. `key.py` holds for +2500 ms. If a press seems ignored, lengthen it before suspecting the wiring. + +**Verify a tool's own parsing before trusting its output.** `gpio_watch.py` +reported `IDR=0x0000` for several rounds because its regex did not match gdb's +output format at all. The register was fine; the reader was broken. Cross-check +with `tools/gpiob_dump.sh`, which uses a different path. + +## Known gap: the keypad + +Keypresses reach the firmware but the UI does not react. What is established: + +- The keypad model holds the right state (`qom-get press` reads back the key) +- Row lines are driven: TRACE shows `row0 -> 0` while MENU is held +- The firmware's scan sees it: at the IDR read inside `KEYBOARD_Poll`, + `ODR=033c IDR=7fbf` — column 1 low, row 0 low +- `KEYBOARD_Poll` returns 10, which is `KEY_MENU` in `driver/keyboard.h` + +So the matrix works and the scan decodes correctly. Whatever is wrong is +downstream, in how `app.c` debounces or dispatches the returned key. That is +where to look — not at the wiring, which has been checked more than enough. + +Useful here: `tools/scan_trace.sh` (what the scan reads), `tools/key_result.sh` +(what Poll returns), `tools/trace_run.sh` (the TRACE points, currently compiled +in). + +There is `fprintf(stderr, "TRACE ...")` instrumentation in `qemu/py32f071.c` at +three points. Remove it once the keypad works. + +## What this cannot do + +It reproduces what the firmware *commanded* — frequency, power step, carrier +keying in time. It does not reproduce the analogue result: keying envelopes, +spurious emissions, sensitivity. + +That is not a gap to close later. The BK4819/BK4829 transceiver has no public +datasheet, so its driver is the only specification available, and a driver tells +you which registers were written, never what left the antenna. Those questions +need a real radio and a spectrum analyser. Do not let anyone conclude otherwise +from a passing emulator test. + +Timing is also deliberately wrong — see the SysTick section in README.md. Fine +for menus and control flow; useless for signal timing. + +## If you add a peripheral + +1. Read the register layout from the CMSIS header +2. Model only what the firmware actually touches; the logging catch-all + (`py32-stub`) shows you what that is +3. Watch for spin loops: any flag the firmware polls must be able to change, and + write-1-to-start bits (like `ADC_CR2_CAL`) must never be stored set +4. Rebuild, run, and check with `tools/where.sh` that the firmware moved past + where it used to stop