UV-K5 V3 emulator: QEMU machine for the PY32F071

Adds a QEMU machine for the Puya PY32F071 (Cortex-M0+) so Quansheng UV-K5 V3
firmware can run on a PC. The firmware boots to its main loop in about five
seconds and the LCD contents are readable.

Register layouts come from the vendor CMSIS header shipped with the firmware
rather than guesswork. Modelled: RCC, GPIO, ADC, both SPI controllers, DMA1 and
the PY25Q16 flash; everything else answers through a logging catch-all, which is
how the next thing worth modelling gets identified.

Seven things had to be right before it would boot, each found by watching where
the firmware stopped: flash aliased at the application offset, clock ready bits,
self-clearing ADC calibration, SPI transfer flags, DMA-driven flash reads,
SysTick poll acceleration, and the bit-banged transceiver bus idling low.

SysTick needs explanation. SYSTICK_DelayUs polls the counter and accumulates
differences; under emulation a register read costs far more relative to guest
time, so a measured 120 ms delay would have taken about 7.7 hours. Lowering the
clock does not help because the bottleneck is loop iterations, not counter speed.
Reporting a value that runs ahead of the real counter does, via a new poll-boost
property on SysTick. Guest time therefore runs fast during delays: fine for
exercising menus and control flow, wrong for judging signal timing.

Also includes the host build of the CW timing chain (harness, stubs, shim,
tests), which compiles app/cwkeyer.c and app/cwmacro.c unmodified against stub
drivers with a virtual clock and scripted paddle input.

Known gap: keypad rows reach the firmware's scan and KEYBOARD_Poll returns the
right key code, but the UI does not react yet.

Not modelled, and not intended to be: radio behaviour. The transceiver chip has
no public datasheet, so keying envelopes and emissions need real hardware.
This commit is contained in:
mckero committed 2026-08-27 14:59:21 +01:00
commit c0a09827ed
53 files changed
+5068

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
#include "sim_keyer.h"
#include "app/cwkeyer.h"
#include "app/cwmacro.h"
#include "misc.h"
#include "settings.h"
#include "sim_clock.h"
#include "sim_paddle.h"
#include "sim_record.h"
void SIM_EepromReset(void); // stubs/driver_stubs.c
void SIM_CaptureReset(void); // harness/sim_capture.c
void SIM_CapturePoll(void);
// The firmware's own constants (cwkeyer.c): 1200 ms / WPM is one dit.
#define SIM_TICKS_PER_MINUTE 60000U
#define SIM_DITS_PER_WORD 50U
static uint8_t s_wpm = 18;
// Translates one poll of the keyer into recorded carrier events. Mirrors the
// RF-path switch in app/cwapp.c: HOLD_ON means "already keyed, stay keyed", so
// only the ON/OFF transitions are edges.
static void tick(void)
{
switch (CW_HandleState()) {
case CW_ACTION_CARRIER_ON:
SIM_RecordCarrier(true);
break;
case CW_ACTION_CARRIER_OFF:
SIM_RecordCarrier(false);
break;
default:
break;
}
// Sample the firmware's display buffer for newly decoded characters.
SIM_CapturePoll();
}
void SIM_KeyerBegin(uint8_t key_input, uint8_t keyer_mode, uint8_t wpm)
{
SIM_ClockReset();
SIM_PaddleReset();
SIM_RecordReset();
SIM_EepromReset();
SIM_CaptureReset();
s_wpm = wpm;
gEeprom.CW_KEY_INPUT = key_input;
gEeprom.CW_KEY_WPM = wpm;
gEeprom.CW_KEYER_MODE = (CW_IambicMode_t)keyer_mode;
CW_KeyerResetRuntime();
CW_KeyerReconfigure(true);
SIM_ClockSetTick(&tick);
// CW_HandleState defers its pending init until it sees an idle word gap, so
// give it that before the scripted timeline starts. Without this the first
// element of every scenario would be swallowed by the configuration apply.
SIM_ClockRun(8U * SIM_KeyerDitMs());
SIM_RecordReset();
}
void SIM_KeyerRun(uint32_t tail_ms)
{
SIM_ClockRun(SIM_PaddleTotalMs() - SIM_ClockNow() + tail_ms);
}
uint32_t SIM_KeyerDitMs(void)
{
return SIM_TICKS_PER_MINUTE / ((uint32_t)s_wpm * SIM_DITS_PER_WORD);
}