mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
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.
351 lines
11 KiB
Plaintext
351 lines
11 KiB
Plaintext
/*
|
|
* ARMv7M SysTick timer
|
|
*
|
|
* Copyright (c) 2006-2007 CodeSourcery.
|
|
* Written by Paul Brook
|
|
* Copyright (c) 2017 Linaro Ltd
|
|
* Written by Peter Maydell
|
|
*
|
|
* This code is licensed under the GPL (version 2 or later).
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "hw/timer/armv7m_systick.h"
|
|
#include "migration/vmstate.h"
|
|
#include "hw/irq.h"
|
|
#include "hw/sysbus.h"
|
|
#include "hw/qdev-clock.h"
|
|
#include "hw/qdev-properties.h"
|
|
#include "qemu/timer.h"
|
|
#include "qemu/log.h"
|
|
#include "qemu/module.h"
|
|
#include "qapi/error.h"
|
|
#include "trace.h"
|
|
|
|
#define SYSTICK_ENABLE (1 << 0)
|
|
#define SYSTICK_TICKINT (1 << 1)
|
|
#define SYSTICK_CLKSOURCE (1 << 2)
|
|
#define SYSTICK_COUNTFLAG (1 << 16)
|
|
|
|
#define SYSCALIB_NOREF (1U << 31)
|
|
#define SYSCALIB_SKEW (1U << 30)
|
|
#define SYSCALIB_TENMS ((1U << 24) - 1)
|
|
|
|
static void systick_set_period_from_clock(SysTickState *s)
|
|
{
|
|
/*
|
|
* Set the ptimer period from whichever clock is selected.
|
|
* Must be called from within a ptimer transaction block.
|
|
*/
|
|
if (s->control & SYSTICK_CLKSOURCE) {
|
|
ptimer_set_period_from_clock(s->ptimer, s->cpuclk, 1);
|
|
} else {
|
|
ptimer_set_period_from_clock(s->ptimer, s->refclk, 1);
|
|
}
|
|
}
|
|
|
|
static void systick_timer_tick(void *opaque)
|
|
{
|
|
SysTickState *s = (SysTickState *)opaque;
|
|
|
|
trace_systick_timer_tick();
|
|
|
|
s->control |= SYSTICK_COUNTFLAG;
|
|
if (s->control & SYSTICK_TICKINT) {
|
|
/* Tell the NVIC to pend the SysTick exception */
|
|
qemu_irq_pulse(s->irq);
|
|
}
|
|
if (ptimer_get_limit(s->ptimer) == 0) {
|
|
/*
|
|
* Timer expiry with SYST_RVR zero disables the timer
|
|
* (but doesn't clear SYST_CSR.ENABLE)
|
|
*/
|
|
ptimer_stop(s->ptimer);
|
|
}
|
|
}
|
|
|
|
static MemTxResult systick_read(void *opaque, hwaddr addr, uint64_t *data,
|
|
unsigned size, MemTxAttrs attrs)
|
|
{
|
|
SysTickState *s = opaque;
|
|
uint32_t val;
|
|
|
|
if (attrs.user) {
|
|
/* Generate BusFault for unprivileged accesses */
|
|
return MEMTX_ERROR;
|
|
}
|
|
|
|
switch (addr) {
|
|
case 0x0: /* SysTick Control and Status. */
|
|
val = s->control;
|
|
s->control &= ~SYSTICK_COUNTFLAG;
|
|
break;
|
|
case 0x4: /* SysTick Reload Value. */
|
|
val = ptimer_get_limit(s->ptimer);
|
|
break;
|
|
case 0x8: /* SysTick Current Value. */
|
|
val = ptimer_get_count(s->ptimer);
|
|
/*
|
|
* Busy-wait acceleration, off unless the machine opts in.
|
|
*
|
|
* Firmware delay loops of the form
|
|
* while (elapsed < ticks) { cur = VAL; elapsed += prev - cur; }
|
|
* depend on the counter moving appreciably between reads. On hardware it
|
|
* does. Under emulation a register read costs far more relative to guest
|
|
* time, so the counter barely moves and such a loop can need hours of
|
|
* wall time per millisecond of guest time.
|
|
*
|
|
* When poll_boost is set, each read of this register also advances the
|
|
* timer, which lets those loops converge. Guest time then runs fast
|
|
* while a poll loop is active: fine for exercising control flow, wrong
|
|
* for judging signal timing.
|
|
*/
|
|
if (s->poll_boost) {
|
|
/*
|
|
* Report a value that runs ahead of the real counter by an amount
|
|
* that grows with every read, so a polling loop sees steady forward
|
|
* motion. The timer itself is left alone: writing it back made each
|
|
* read re-anchor the count, the reported value stopped changing
|
|
* between reads, and the firmware's `if (cur != prev)` guard meant
|
|
* the loop accumulated nothing and hung.
|
|
*/
|
|
uint32_t period = ptimer_get_limit(s->ptimer) + 1;
|
|
|
|
s->poll_skew += s->poll_boost;
|
|
if (period > 1) {
|
|
uint32_t offset = s->poll_skew % period;
|
|
val = (val >= offset) ? (val - offset) : (val + period - offset);
|
|
}
|
|
}
|
|
break;
|
|
case 0xc: /* SysTick Calibration Value. */
|
|
/*
|
|
* In real hardware it is possible to make this register report
|
|
* a different value from what the reference clock is actually
|
|
* running at. We don't model that (which usually happens due
|
|
* to integration errors in the real hardware) and instead always
|
|
* report the theoretical correct value as described in the
|
|
* knowledgebase article at
|
|
* https://developer.arm.com/documentation/ka001325/latest
|
|
* If necessary, we could implement an extra QOM property on this
|
|
* device to force the STCALIB value to something different from
|
|
* the "correct" value.
|
|
*/
|
|
if (!clock_has_source(s->refclk)) {
|
|
val = SYSCALIB_NOREF;
|
|
break;
|
|
}
|
|
val = clock_ns_to_ticks(s->refclk, 10 * SCALE_MS) - 1;
|
|
val &= SYSCALIB_TENMS;
|
|
if (clock_ticks_to_ns(s->refclk, val + 1) != 10 * SCALE_MS) {
|
|
/* report that tick count does not yield exactly 10ms */
|
|
val |= SYSCALIB_SKEW;
|
|
}
|
|
break;
|
|
default:
|
|
val = 0;
|
|
qemu_log_mask(LOG_GUEST_ERROR,
|
|
"SysTick: Bad read offset 0x%" HWADDR_PRIx "\n", addr);
|
|
break;
|
|
}
|
|
|
|
trace_systick_read(addr, val, size);
|
|
*data = val;
|
|
return MEMTX_OK;
|
|
}
|
|
|
|
static MemTxResult systick_write(void *opaque, hwaddr addr,
|
|
uint64_t value, unsigned size,
|
|
MemTxAttrs attrs)
|
|
{
|
|
SysTickState *s = opaque;
|
|
|
|
if (attrs.user) {
|
|
/* Generate BusFault for unprivileged accesses */
|
|
return MEMTX_ERROR;
|
|
}
|
|
|
|
trace_systick_write(addr, value, size);
|
|
|
|
switch (addr) {
|
|
case 0x0: /* SysTick Control and Status. */
|
|
{
|
|
uint32_t oldval;
|
|
|
|
if (!clock_has_source(s->refclk)) {
|
|
/* This bit is always 1 if there is no external refclk */
|
|
value |= SYSTICK_CLKSOURCE;
|
|
}
|
|
|
|
ptimer_transaction_begin(s->ptimer);
|
|
oldval = s->control;
|
|
s->control &= 0xfffffff8;
|
|
s->control |= value & 7;
|
|
|
|
if ((oldval ^ value) & SYSTICK_CLKSOURCE) {
|
|
systick_set_period_from_clock(s);
|
|
}
|
|
|
|
if ((oldval ^ value) & SYSTICK_ENABLE) {
|
|
if (value & SYSTICK_ENABLE) {
|
|
ptimer_run(s->ptimer, 0);
|
|
} else {
|
|
ptimer_stop(s->ptimer);
|
|
}
|
|
}
|
|
ptimer_transaction_commit(s->ptimer);
|
|
break;
|
|
}
|
|
case 0x4: /* SysTick Reload Value. */
|
|
ptimer_transaction_begin(s->ptimer);
|
|
ptimer_set_limit(s->ptimer, value & 0xffffff, 0);
|
|
ptimer_transaction_commit(s->ptimer);
|
|
break;
|
|
case 0x8: /* SysTick Current Value. */
|
|
/*
|
|
* Writing any value clears SYST_CVR to zero and clears
|
|
* SYST_CSR.COUNTFLAG. The counter will then reload from SYST_RVR
|
|
* on the next clock edge unless SYST_RVR is zero.
|
|
*/
|
|
ptimer_transaction_begin(s->ptimer);
|
|
if (ptimer_get_limit(s->ptimer) == 0) {
|
|
ptimer_stop(s->ptimer);
|
|
}
|
|
ptimer_set_count(s->ptimer, 0);
|
|
s->control &= ~SYSTICK_COUNTFLAG;
|
|
ptimer_transaction_commit(s->ptimer);
|
|
break;
|
|
default:
|
|
qemu_log_mask(LOG_GUEST_ERROR,
|
|
"SysTick: Bad write offset 0x%" HWADDR_PRIx "\n", addr);
|
|
}
|
|
return MEMTX_OK;
|
|
}
|
|
|
|
static const MemoryRegionOps systick_ops = {
|
|
.read_with_attrs = systick_read,
|
|
.write_with_attrs = systick_write,
|
|
.endianness = DEVICE_NATIVE_ENDIAN,
|
|
.valid.min_access_size = 4,
|
|
.valid.max_access_size = 4,
|
|
};
|
|
|
|
static void systick_reset(DeviceState *dev)
|
|
{
|
|
SysTickState *s = SYSTICK(dev);
|
|
|
|
ptimer_transaction_begin(s->ptimer);
|
|
s->control = 0;
|
|
if (!clock_has_source(s->refclk)) {
|
|
/* This bit is always 1 if there is no external refclk */
|
|
s->control |= SYSTICK_CLKSOURCE;
|
|
}
|
|
ptimer_stop(s->ptimer);
|
|
ptimer_set_count(s->ptimer, 0);
|
|
ptimer_set_limit(s->ptimer, 0, 0);
|
|
systick_set_period_from_clock(s);
|
|
ptimer_transaction_commit(s->ptimer);
|
|
}
|
|
|
|
static void systick_cpuclk_update(void *opaque, ClockEvent event)
|
|
{
|
|
SysTickState *s = SYSTICK(opaque);
|
|
|
|
if (!(s->control & SYSTICK_CLKSOURCE)) {
|
|
/* currently using refclk, we can ignore cpuclk changes */
|
|
}
|
|
|
|
ptimer_transaction_begin(s->ptimer);
|
|
ptimer_set_period_from_clock(s->ptimer, s->cpuclk, 1);
|
|
ptimer_transaction_commit(s->ptimer);
|
|
}
|
|
|
|
static void systick_refclk_update(void *opaque, ClockEvent event)
|
|
{
|
|
SysTickState *s = SYSTICK(opaque);
|
|
|
|
if (s->control & SYSTICK_CLKSOURCE) {
|
|
/* currently using cpuclk, we can ignore refclk changes */
|
|
}
|
|
|
|
ptimer_transaction_begin(s->ptimer);
|
|
ptimer_set_period_from_clock(s->ptimer, s->refclk, 1);
|
|
ptimer_transaction_commit(s->ptimer);
|
|
}
|
|
|
|
static void systick_instance_init(Object *obj)
|
|
{
|
|
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
|
|
SysTickState *s = SYSTICK(obj);
|
|
|
|
memory_region_init_io(&s->iomem, obj, &systick_ops, s, "systick", 0xe0);
|
|
sysbus_init_mmio(sbd, &s->iomem);
|
|
sysbus_init_irq(sbd, &s->irq);
|
|
|
|
s->refclk = qdev_init_clock_in(DEVICE(obj), "refclk",
|
|
systick_refclk_update, s, ClockUpdate);
|
|
s->cpuclk = qdev_init_clock_in(DEVICE(obj), "cpuclk",
|
|
systick_cpuclk_update, s, ClockUpdate);
|
|
}
|
|
|
|
static void systick_realize(DeviceState *dev, Error **errp)
|
|
{
|
|
SysTickState *s = SYSTICK(dev);
|
|
s->ptimer = ptimer_init(systick_timer_tick, s,
|
|
PTIMER_POLICY_WRAP_AFTER_ONE_PERIOD |
|
|
PTIMER_POLICY_NO_COUNTER_ROUND_DOWN |
|
|
PTIMER_POLICY_NO_IMMEDIATE_RELOAD |
|
|
PTIMER_POLICY_TRIGGER_ONLY_ON_DECREMENT);
|
|
|
|
if (!clock_has_source(s->cpuclk)) {
|
|
error_setg(errp, "systick: cpuclk must be connected");
|
|
return;
|
|
}
|
|
/* It's OK not to connect the refclk */
|
|
}
|
|
|
|
static const VMStateDescription vmstate_systick = {
|
|
.name = "armv7m_systick",
|
|
.version_id = 3,
|
|
.minimum_version_id = 3,
|
|
.fields = (VMStateField[]) {
|
|
VMSTATE_CLOCK(refclk, SysTickState),
|
|
VMSTATE_CLOCK(cpuclk, SysTickState),
|
|
VMSTATE_UINT32(control, SysTickState),
|
|
VMSTATE_INT64(tick, SysTickState),
|
|
VMSTATE_PTIMER(ptimer, SysTickState),
|
|
VMSTATE_END_OF_LIST()
|
|
}
|
|
};
|
|
|
|
static Property systick_properties[] = {
|
|
/* See the comment on poll_boost in the header; 0 means exact behaviour. */
|
|
DEFINE_PROP_UINT32("poll-boost", SysTickState, poll_boost, 0),
|
|
DEFINE_PROP_END_OF_LIST(),
|
|
};
|
|
|
|
static void systick_class_init(ObjectClass *klass, void *data)
|
|
{
|
|
DeviceClass *dc = DEVICE_CLASS(klass);
|
|
|
|
dc->vmsd = &vmstate_systick;
|
|
dc->reset = systick_reset;
|
|
dc->realize = systick_realize;
|
|
device_class_set_props(dc, systick_properties);
|
|
}
|
|
|
|
static const TypeInfo armv7m_systick_info = {
|
|
.name = TYPE_SYSTICK,
|
|
.parent = TYPE_SYS_BUS_DEVICE,
|
|
.instance_init = systick_instance_init,
|
|
.instance_size = sizeof(SysTickState),
|
|
.class_init = systick_class_init,
|
|
};
|
|
|
|
static void armv7m_systick_register_types(void)
|
|
{
|
|
type_register_static(&armv7m_systick_info);
|
|
}
|
|
|
|
type_init(armv7m_systick_register_types)
|