mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
Emulator: multiboot slots from the page, flash controller, portable tests
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
This commit is contained in:
1 parent
ee80939c78
commit
2667e046e8
54 files changed
+5345
-347
No files matched your search
@@ -38,6 +38,7 @@ has no public datasheet, so its driver is the only specification available.
|
||||
| --- | --- |
|
||||
| Boot to main loop | works, ~5 s |
|
||||
| LCD contents | readable via `tools/screenshot.py` |
|
||||
| Display contrast / inversion | panel settings, read from the controller; inversion also changes the rendered picture |
|
||||
| SPI flash, settings, calibration | works, and persists across power cycles |
|
||||
| Frequency entry | works, stored per band and kept |
|
||||
| Keypad and menu navigation | works, including waking from power save |
|
||||
@@ -83,6 +84,9 @@ keypresses silently stop working. Run the test after touching that code;
|
||||
*.zh-CN.md Chinese translations, kept in step
|
||||
docs/screenshots/ LCD captures used in this README
|
||||
tools/ run, screenshot, inject keys, probe state
|
||||
bin2elf.py wrap a release .bin so QEMU can load it as a kernel
|
||||
make_flash.py build assets/flash.img; --blob puts extra data (the
|
||||
font packs a Chinese build needs) at chosen offsets
|
||||
keypad_test.py keypad regression test, boots its own instance
|
||||
test_flash_persist.py flash writes survive a power cycle
|
||||
test_freq_entry.py a typed frequency takes effect and persists
|
||||
@@ -114,6 +118,66 @@ keypresses silently stop working. Run the test after touching that code;
|
||||
kept because they are quick to reach for, not because they are polished)
|
||||
harness/, stubs/, shim/, tests/ host build of the CW timing chain (stage A)
|
||||
|
||||
## What is not in this repository
|
||||
|
||||
Two things are deliberately absent, and neither should be committed:
|
||||
|
||||
- **Firmware.** Released images, localised builds and bootloader dumps belong to whoever
|
||||
made them, not to this project. `tools/fetch_firmware.py` fetches a release from the
|
||||
upstream archive into `assets/firmware/` when a test or a run needs one, and that
|
||||
directory is ignored.
|
||||
- **Anything read off a real radio.** `work/data.bin` is an EEPROM dump -- settings and
|
||||
calibration from somebody's hardware. It is not a build artifact. It is ignored now, and
|
||||
the tests build their own flash images from `assets/pristine/` instead.
|
||||
|
||||
`assets/pristine/flash-pristine.img.gz` and `assets/calibration.bin` do ship: they are a
|
||||
2 KB synthetic pair that `tools/make_flash.py` assembles into a flash image, not data
|
||||
from a radio.
|
||||
|
||||
The rest of `work/` is scratch -- images, logs, captures from a debugging session. The
|
||||
four scripts in it are tracked on purpose, because they document how this machine is
|
||||
driven; everything else is ignored.
|
||||
|
||||
**If any of that is already in the history, removing it now is not enough.** The objects
|
||||
stay reachable, so publishing this repository needs the history filtered
|
||||
(`git filter-repo`) or a fresh one. Check before pushing:
|
||||
|
||||
git log --stat -- work/data.bin assets/firmware
|
||||
|
||||
## Quick start
|
||||
|
||||
Five minutes from a checkout to a running radio on a web page.
|
||||
|
||||
# 1. A QEMU 7.2 tree, patched and built. Building by hand means copying three files
|
||||
# into the tree, editing Kconfig and meson.build, then configure and ninja; this
|
||||
# is those same steps (see Building below for what it does).
|
||||
QEMU_SRC=~/src/qemu-7.2 bash tools/setup_qemu.sh
|
||||
|
||||
# 2. A firmware to run. Firmware is not redistributed here -- this fetches a release
|
||||
# from the upstream project's archive into assets/firmware/ and prints its hash.
|
||||
python3 tools/fetch_firmware.py
|
||||
|
||||
# 3. The external flash image the firmware keeps its settings in.
|
||||
python3 tools/make_flash.py
|
||||
|
||||
# 4. Run it.
|
||||
python3 tools/webui.py --qemu ~/src/qemu-7.2/build/qemu-system-arm \
|
||||
--elf assets/firmware/f4hwn.fieldops.v6.0.0.bin # then open http://127.0.0.1:8080/
|
||||
|
||||
`--frame-addr` and `--status-addr` default to one known build and move between builds;
|
||||
see [Web remote control](#web-remote-control) for how to find them. On Windows,
|
||||
`work/run-webui.ps1` wraps step 4 with this machine's paths.
|
||||
|
||||
Drop any `.bin` on the page to boot it. The page's **Firmware slots** table reads and
|
||||
writes the multi-system firmware's four slots in the flash image, and **Multiboot**
|
||||
restarts holding MENU so its boot menu comes up -- in a build that has one: the page
|
||||
labels builds that do not, because there the button can do nothing at all.
|
||||
|
||||
Then check it still works:
|
||||
|
||||
bash tools/run_tests.sh -q # ~15 s, no emulator
|
||||
bash tools/run_tests.sh # everything; needs the tree from step 1
|
||||
|
||||
## Building
|
||||
|
||||
Needs a QEMU 7.2 source tree, `meson`, `ninja`, `libfdt-dev`, `libglib2.0-dev`,
|
||||
@@ -155,6 +219,7 @@ that was never compiled. Individual tests still run standalone:
|
||||
python3 tools/test_flash_persist.py
|
||||
python3 tools/test_freq_entry.py
|
||||
python3 tools/test_serial_rx.py
|
||||
python3 tools/test_slot_serial.py
|
||||
python3 tools/test_bk4819.py
|
||||
bash tools/test_bk4819_readback.sh
|
||||
python3 tools/test_smeter.py
|
||||
@@ -241,10 +306,22 @@ Endpoints, if you want to script it:
|
||||
| `POST /api/release-all` | release every key and PTT, if one ever sticks |
|
||||
| `POST /api/power/<action>` | `on`, `off`, `reset`, `pause`, `resume` |
|
||||
| `GET /api/logs?since=N` | log entries after cursor N, with client IPs |
|
||||
| `GET /api/status` | QMP `query-status`, plus a `speaker` field |
|
||||
| `GET /api/status` | QMP `query-status`, plus `speaker`, `panel` and `firmware` |
|
||||
| `GET /api/firmware` | the loaded image, and how it will be loaded |
|
||||
| `POST /api/firmware` | body is a `.bin` or `.elf`; boots it and restarts the emulator |
|
||||
| `GET /api/slots` | the firmware slots in the flash image the emulator uses |
|
||||
| `POST /api/slots/<n>` | body is a `.bin`; writes it into slot `n` and restarts |
|
||||
| `POST /api/slots/<n>/erase` | erase slot `n` |
|
||||
| `POST /api/flash` | body is a flash image; use it from now on |
|
||||
|
||||
Frames are read with QMP `memsave`, about 1.35 ms each, and the guest keeps
|
||||
running throughout. Two details there are easy to get wrong:
|
||||
Frames now come from the display controller's own memory: a QMP `qom-get` on the
|
||||
panel's `gram` property, so the picture is right whoever wrote the firmware and
|
||||
wherever it keeps its buffers. Builds that share an ancestor still differ in their
|
||||
display logic -- the multi-system release keeps its image somewhere else entirely.
|
||||
|
||||
The older path reads `gFrameBuffer` and `gStatusLine` out of guest RAM with QMP
|
||||
`memsave` (~1.35 ms per frame) and remains as the fallback for an emulator built
|
||||
without the panel model. The two cautions below apply to that path:
|
||||
|
||||
- **`memsave`, not `pmemsave`.** The framebuffer symbols are CPU virtual
|
||||
addresses. `pmemsave` treats its argument as physical and returns a block of
|
||||
@@ -260,6 +337,64 @@ Two constraints worth knowing before you use it:
|
||||
- **There is no authentication.** Anyone who reaches the port has full control of
|
||||
the emulated radio. It binds loopback by default for that reason.
|
||||
|
||||
### Loading firmware from the page
|
||||
|
||||
Drop a `.bin` anywhere on the page, or use the Firmware control, and the server
|
||||
stores it and boots it. No ELF wrapping, no address to look up.
|
||||
|
||||
Two shapes exist and they need different load addresses:
|
||||
|
||||
| shape | how it is recognised | load address |
|
||||
| --- | --- | --- |
|
||||
| application | reset handler past `0x08002800` | `0x08002800` |
|
||||
| full-flash | reset handler inside the bootloader region (`0x08000000`..`0x080027ff`) | `0x08000000` |
|
||||
|
||||
An `.elf` carries its own program headers and needs neither. This is read out of the
|
||||
image's first two words -- `tools/uvk5_image.py` on the host, `uvk5_sniff_app_offset()`
|
||||
in the machine -- rather than taken from a flag or a file name, because getting it
|
||||
wrong is silent: the image lands `0x2800` bytes off and the first fetch reads whatever
|
||||
data is there. A file that is not a bootable image is refused with 400 and the radio
|
||||
keeps running what it had.
|
||||
|
||||
Uploads live in `work/firmware/` (`UVK5_UPLOAD_DIR` moves it), and while the emulator
|
||||
is running an upload restarts it, since the image is chosen when QEMU is spawned.
|
||||
|
||||
Both shapes are verified end to end here: an application `.bin`, an `.elf`, and a
|
||||
full-flash image whose bootloader entry branches to the application at `0x08002800`
|
||||
all reach the same drawn screen.
|
||||
|
||||
### Firmware slots, and the multi-system release
|
||||
|
||||
The v6.0.0 release keeps a boot menu and four firmware slots in the external flash: hold
|
||||
MENU at power-on and it lists them, and choosing one reflashes the internal flash from that
|
||||
slot and resets. Both halves are reachable from the page.
|
||||
|
||||
- **Firmware slots** shows one row per slot with the name, version, size and whether the
|
||||
header CRC-32 matches the image. Write a `.bin` into a slot, or erase one. Edits go to a
|
||||
working copy of the flash image (`work/firmware/flash-current.img`), never to the file the
|
||||
server was started with, and the emulator is restarted to pick them up.
|
||||
- **Multiboot** (or Shift+M) restarts the emulator with MENU held *from reset*. The page
|
||||
cannot do that with key events, because the firmware samples the keypad in the first
|
||||
milliseconds after reset. On the machine it is `-M uv-k5-v3,boot-key=MENU` or
|
||||
`UVK5_BOOT_KEY`, held for `UVK5_BOOT_KEY_MS` (8 s by default: the boot path can spend
|
||||
20 s adopting the running firmware into slot 0 before anything samples the keypad).
|
||||
- `tools/uvk5_slots.py` does the same offline: write a slot into a flash image, and print
|
||||
what each slot holds.
|
||||
|
||||
The layout is the firmware's, from `App/driver/mb_flash.h`: slot 0 at `0x020000` backs up
|
||||
the internal image, slots 1..4 follow at `0x040000` in 128 KiB steps, the image starts one
|
||||
4 KiB sector into the slot, and the 64-byte header carries magic `FMB1`, the image size and
|
||||
a CRC-32. The firmware's own `0x0720`..`0x0727` serial commands write slots the same way,
|
||||
which is what the Windows tools use.
|
||||
|
||||
Two behaviours look like the emulator misbehaving and are not. A **corrupt** active-state
|
||||
marker next to a valid slot 0 makes the firmware halt on a `STATE ERROR` screen to protect
|
||||
Main, and a **missing** marker makes it adopt the running firmware into slot 0 -- reflashing
|
||||
the external flash -- before the menu appears. Writing a slot erases those marker sectors so
|
||||
it can decide again. The internal flash is programmable in the model (`0x40022000`:
|
||||
unlock, page erase, program, EOP, never busy), so restoring a slot really does replace the
|
||||
image the CPU executes after the reset.
|
||||
|
||||
### Reaching it from elsewhere
|
||||
|
||||
The deployment here runs the server on loopback and puts nginx in front of it for
|
||||
@@ -336,7 +471,8 @@ Register layouts come from the vendor CMSIS header shipped with the firmware
|
||||
SPI2 0x40003800 flash
|
||||
ADC1 0x40012400
|
||||
|
||||
Modelled: RCC, GPIO, ADC, both SPI controllers, DMA1, TIM2, and the PY25Q16 flash.
|
||||
Modelled: RCC, GPIO, ADC, both SPI controllers, DMA1, TIM2, the PY25Q16 flash, and
|
||||
the ST7565 display controller's own settings (contrast, inversion, display on/off).
|
||||
Everything else answers through a logging catch-all — the log is how the next
|
||||
thing worth modelling gets identified.
|
||||
|
||||
@@ -405,6 +541,48 @@ on a host would let the tests drift from what the radio runs. The debounce in
|
||||
consecutive reads to register a press, immediate release) is part of the timing
|
||||
behaviour under test.
|
||||
|
||||
## The display controller's own settings
|
||||
|
||||
Contrast (`SetCtr`) and display inversion (`SetInv`) are commands to the ST7565, not
|
||||
framebuffer content — `0x81 <value>` and `0xA6`/`0xA7` — so `gFrameBuffer` does not
|
||||
change and anything that renders that buffer shows no effect at all. That is why there
|
||||
is a small `TYPE_ST7565` behind SPI1 (A0 on PA6, CS on PB2, the pins
|
||||
`App/driver/st7565.c` uses). It parses the command stream and exposes three
|
||||
read-only properties:
|
||||
|
||||
qom-get /machine/panel invert # last of 0xA6 / 0xA7
|
||||
qom-get /machine/panel contrast # the value following 0x81
|
||||
qom-get /machine/panel display-on # last of 0xAE / 0xAF
|
||||
|
||||
`tools/uvk5_lcd.py` applies the inversion when it renders, because that effect is
|
||||
fully determined, so the menu entry is visible in the web UI. Contrast is analogue —
|
||||
how dark the glass gets — and is only reported. `/api/status` carries all three and
|
||||
the page shows them beside the speaker glyph. `display-on` is reported but not acted
|
||||
on: whether a software reset (`0xE2`) clears that latch is not certain, and blanking
|
||||
the screen on a guess would be worse than leaving the image alone.
|
||||
|
||||
## On Windows
|
||||
|
||||
The emulator, the models and the tools are portable; the packaging was not. Four
|
||||
things differ, and all four are handled in-tree now:
|
||||
|
||||
- **QMP over TCP.** A Windows build of QEMU cannot create a unix socket, so an
|
||||
endpoint may be `host:port` as well as a path — `tools/uvk5_qmp.py`,
|
||||
`tools/key.py` and `tools/uvk5_supervisor.py` all accept both.
|
||||
- **One build fix.** MSYS2's mingw-w64 packages build QEMU 7.2 as-is except that
|
||||
`qemu/py32f071.c` needs `#include "qapi/visitor.h"` for `visit_type_uint64`, which
|
||||
a stock tree does not pull in transitively.
|
||||
- **A release `.bin` is not a kernel image.** `armv7m_load_kernel()` loads a raw
|
||||
binary at the address it is handed, which here is the flash *alias*, so a `.bin`
|
||||
lands 0x2800 bytes too high and never boots. `tools/bin2elf.py` wraps it in an
|
||||
ELF32/ARM header with the right program header, which is what `-kernel` wants.
|
||||
- **The Chinese font packs live in the SPI flash**, not in the firmware:
|
||||
`tools/make_flash.py --blob 0:pack.uf2` places every UF2 block at its own target
|
||||
address. Without that the font area reads as 0xFF.
|
||||
|
||||
`work/` holds a Windows bring-up record: the launcher scripts, the frame addresses
|
||||
proven against the firmware source, and the failures that cost time.
|
||||
|
||||
## Licence
|
||||
|
||||
Apache 2.0, see [LICENSE](LICENSE).
|
||||
|
||||
Reference in new issue
Block a user