mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 11:07:31 +00:00
Notes for whoever works on this next, weighted toward what the code does not say: that the firmware is the reference and must never be edited to suit the emulator, that register layouts come from the vendor CMSIS header rather than inference, and that the way to find the next peripheral worth modelling is to watch where the firmware stops. Records the mistakes that already cost time here, each with the symptom that made it look like something else: GDB breakpoints halting the guest (which reads as 'the keypress does nothing'), writing the SysTick counter back while accelerating it (which hangs the delay loop outright), lowering the clock to speed up busy-waits (measured, 32x, nowhere near enough), unnamed qdev GPIO lines sharing one namespace, and a probe script whose own regex silently matched nothing. Also states plainly what the emulator cannot answer, so a passing test is not mistaken for evidence about radio behaviour.
137 lines
6.1 KiB
Markdown
137 lines
6.1 KiB
Markdown
# 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:
|
|
|
|
<firmware>/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
|