mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
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:
commit
c0a09827ed
53 files changed
+5068
No files matched your search
@@ -0,0 +1,137 @@
|
||||
/* Baseline iambic keyer behaviour: element durations and decoded characters.
|
||||
*
|
||||
* These are the assertions that would have caught the "keyer never arms" class
|
||||
* of bug on a PC in milliseconds instead of on the radio by trial and error.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "harness/sim_keyer.h"
|
||||
#include "harness/sim_paddle.h"
|
||||
#include "harness/sim_record.h"
|
||||
#include "settings.h"
|
||||
|
||||
static int failures;
|
||||
|
||||
#define CHECK(cond, fmt, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf(" FAIL %s:%d " fmt "\n", __func__, __LINE__, __VA_ARGS__); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Element timing is generated from a 1 ms poll, so allow a tick of slack either
|
||||
// way rather than demanding an exact match.
|
||||
#define NEAR(actual, expected) \
|
||||
((actual) + 2 >= (expected) && (actual) <= (expected) + 2)
|
||||
|
||||
// Buttons mode: PTT is the dit paddle, SIDE1 the dah paddle.
|
||||
#define KEY_BUTTONS 0x04
|
||||
|
||||
static void test_single_dit(void)
|
||||
{
|
||||
SIM_KeyerBegin(KEY_BUTTONS, CW_IAMBIC_MODE_B, 20);
|
||||
const uint32_t dit = SIM_KeyerDitMs();
|
||||
|
||||
// Hold the dit paddle briefly; the keyer times the element itself.
|
||||
SIM_PaddleTap(SIM_CONTACT_TIP, dit / 2, 0);
|
||||
SIM_KeyerRun(10 * dit);
|
||||
|
||||
CHECK(SIM_RecordedElementCount() == 1, "expected 1 element, got %u",
|
||||
SIM_RecordedElementCount());
|
||||
const uint32_t len = SIM_RecordedElementMs(0);
|
||||
CHECK(NEAR(len, dit), "dit was %u ms, expected ~%u", len, dit);
|
||||
}
|
||||
|
||||
static void test_single_dah(void)
|
||||
{
|
||||
SIM_KeyerBegin(KEY_BUTTONS, CW_IAMBIC_MODE_B, 20);
|
||||
const uint32_t dit = SIM_KeyerDitMs();
|
||||
|
||||
SIM_PaddleTap(SIM_CONTACT_RING, dit / 2, 0);
|
||||
SIM_KeyerRun(10 * dit);
|
||||
|
||||
CHECK(SIM_RecordedElementCount() == 1, "expected 1 element, got %u",
|
||||
SIM_RecordedElementCount());
|
||||
const uint32_t len = SIM_RecordedElementMs(0);
|
||||
CHECK(NEAR(len, 3 * dit), "dah was %u ms, expected ~%u", len, 3 * dit);
|
||||
}
|
||||
|
||||
static void test_letter_a(void)
|
||||
{
|
||||
// "A" is dit-dah. Two taps with a gap shorter than the character gap.
|
||||
SIM_KeyerBegin(KEY_BUTTONS, CW_IAMBIC_MODE_B, 20);
|
||||
const uint32_t dit = SIM_KeyerDitMs();
|
||||
|
||||
SIM_PaddleTap(SIM_CONTACT_TIP, dit / 2, dit);
|
||||
SIM_PaddleTap(SIM_CONTACT_RING, dit / 2, 0);
|
||||
SIM_KeyerRun(10 * dit);
|
||||
|
||||
CHECK(SIM_RecordedElementCount() == 2, "expected 2 elements, got %u",
|
||||
SIM_RecordedElementCount());
|
||||
CHECK(NEAR(SIM_RecordedElementMs(0), dit), "element 0 was %u ms, expected ~%u",
|
||||
SIM_RecordedElementMs(0), dit);
|
||||
CHECK(NEAR(SIM_RecordedElementMs(1), 3 * dit), "element 1 was %u ms, expected ~%u",
|
||||
SIM_RecordedElementMs(1), 3 * dit);
|
||||
// Inter-element gap is one dit.
|
||||
CHECK(NEAR(SIM_RecordedGapMs(0), dit), "gap was %u ms, expected ~%u",
|
||||
SIM_RecordedGapMs(0), dit);
|
||||
}
|
||||
|
||||
static void test_wpm_scales_timing(void)
|
||||
{
|
||||
// Doubling the speed halves the dit. Guards the WPM plumbing, which the
|
||||
// menu writes and the keyer reads through a separate path.
|
||||
SIM_KeyerBegin(KEY_BUTTONS, CW_IAMBIC_MODE_B, 10);
|
||||
const uint32_t slow_dit = SIM_KeyerDitMs();
|
||||
SIM_PaddleTap(SIM_CONTACT_TIP, slow_dit / 2, 0);
|
||||
SIM_KeyerRun(10 * slow_dit);
|
||||
const uint32_t slow = SIM_RecordedElementMs(0);
|
||||
|
||||
SIM_KeyerBegin(KEY_BUTTONS, CW_IAMBIC_MODE_B, 20);
|
||||
const uint32_t fast_dit = SIM_KeyerDitMs();
|
||||
SIM_PaddleTap(SIM_CONTACT_TIP, fast_dit / 2, 0);
|
||||
SIM_KeyerRun(10 * fast_dit);
|
||||
const uint32_t fast = SIM_RecordedElementMs(0);
|
||||
|
||||
CHECK(slow > 0 && fast > 0, "missing elements: slow=%u fast=%u", slow, fast);
|
||||
CHECK(NEAR(slow, 2 * fast), "10 WPM dit %u ms should be ~2x 20 WPM dit %u ms",
|
||||
slow, fast);
|
||||
}
|
||||
|
||||
static void test_handkey_produces_no_elements(void)
|
||||
{
|
||||
// Handkey modes have no timing engine, so the iambic path must stay silent.
|
||||
// This is the behaviour that makes macro recording impossible with a straight
|
||||
// key -- worth pinning down so it does not change by accident.
|
||||
SIM_KeyerBegin(0x08 /* NO_KEYER */, CW_IAMBIC_MODE_B, 20);
|
||||
const uint32_t dit = SIM_KeyerDitMs();
|
||||
|
||||
SIM_PaddleTap(SIM_CONTACT_TIP, dit, dit);
|
||||
SIM_PaddleTap(SIM_CONTACT_RING, dit, dit);
|
||||
SIM_KeyerRun(10 * dit);
|
||||
|
||||
// The straight-key path keys the carrier directly from PTT rather than
|
||||
// producing timed elements, so no decoded characters should appear.
|
||||
CHECK(SIM_RecordedText()[0] == '\0', "handkey decoded '%s', expected nothing",
|
||||
SIM_RecordedText());
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("iambic keyer baseline\n");
|
||||
test_single_dit();
|
||||
test_single_dah();
|
||||
test_letter_a();
|
||||
test_wpm_scales_timing();
|
||||
test_handkey_produces_no_elements();
|
||||
|
||||
if (failures == 0) {
|
||||
printf(" all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
printf(" %d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
Reference in new issue
Block a user