[PATCH v2 1/3] hw/timer: add k230 dwapb timer model

raoyi <[email protected]> Thu, 30 Jul 2026 21:37:09 +0800
Newsgroups org.nongnu.qemu-riscv,org.nongnu.qemu-devel
Message-ID <[email protected]>
Add DesignWare APB timer model for K230 machine.
The timer uses the Clock framework for frequency input,
preparing for future CMU integration.

Signed-off-by: raoyi <[email protected]>
---
 hw/timer/Kconfig                    |   4 +
 hw/timer/k230_dwapb_timer.c         | 317 ++++++++++++++++++++++++++++
 hw/timer/meson.build                |   1 +
 hw/timer/trace-events               |   9 +
 include/hw/timer/k230_dwapb_timer.h |  72 +++++++
 5 files changed, 403 insertions(+)
 create mode 100644 hw/timer/k230_dwapb_timer.c
 create mode 100644 include/hw/timer/k230_dwapb_timer.h

diff --git a/hw/timer/Kconfig b/hw/timer/Kconfig
index b3d823ce2c..9d1f20f5cd 100644
--- a/hw/timer/Kconfig
+++ b/hw/timer/Kconfig
@@ -65,3 +65,7 @@ config STELLARIS_GPTM
 
 config AVR_TIMER16
     bool
+
+config K230_TIMER
+    bool
+    select PTIMER
diff --git a/hw/timer/k230_dwapb_timer.c b/hw/timer/k230_dwapb_timer.c
new file mode 100644
index 0000000000..623f84cafd
--- /dev/null
+++ b/hw/timer/k230_dwapb_timer.c
@@ -0,0 +1,317 @@
+/*
+ * K230 DW APB timer
+ *
+ * Copyright (c) 2026 raoyi <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/bitops.h"
+#include "qemu/module.h"
+#include "qapi/error.h"
+#include "migration/vmstate.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/ptimer.h"
+#include "hw/core/qdev-clock.h"
+#include "hw/timer/k230_dwapb_timer.h"
+#include "trace.h"
+
+static void k230_timer_clk_update(void *opaque, ClockEvent event)
+{
+    K230TimerState *s = K230_TIMER(opaque);
+    for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+        if (!s->timers[i].ptimer) {
+            continue;
+        }
+        ptimer_transaction_begin(s->timers[i].ptimer);
+        ptimer_set_period_from_clock(s->timers[i].ptimer, s->pclk, 1);
+        ptimer_transaction_commit(s->timers[i].ptimer);
+    }
+}
+
+static void k230_timer_enable(K230Timer *t)
+{
+    ptimer_transaction_begin(t->ptimer);
+    ptimer_set_limit(t->ptimer, t->load ? t->load : 1, 1);
+    ptimer_run(t->ptimer, 1);
+    ptimer_transaction_commit(t->ptimer);
+
+    trace_k230_timer_enable(t->id, t->load);
+}
+
+static void k230_timer_disable(K230Timer *t)
+{
+    ptimer_transaction_begin(t->ptimer);
+    ptimer_stop(t->ptimer);
+    ptimer_transaction_commit(t->ptimer);
+
+    t->int_status = 0;
+    qemu_set_irq(t->irq, 0);
+
+    trace_k230_timer_disable(t->id);
+}
+
+static void k230_timer_tick(void *opaque)
+{
+    K230Timer *t = opaque;
+    uint32_t reload;
+
+    trace_k230_timer_tick(t->id);
+
+    t->int_status = 1;
+
+    if (!(t->control & K230_APBTMR_CONTROL_INT)) {
+        qemu_set_irq(t->irq, 1);
+        trace_k230_timer_interrupt(t->id);
+    }
+
+    if (t->control & K230_APBTMR_CONTROL_MODE_PERIODIC) {
+        reload = t->load ? t->load : 1;
+    } else {
+        reload = UINT32_MAX;
+    }
+
+    ptimer_set_limit(t->ptimer, reload, 1);
+    ptimer_run(t->ptimer, 1);
+}
+
+static uint64_t k230_timer_read(void *opaque, hwaddr addr, unsigned int size)
+{
+    K230TimerState *s = K230_TIMER(opaque);
+    uint32_t value = 0;
+
+    addr &= 0xfff;
+
+    if (addr < K230_APBTMRS_INT_STATUS) {
+        int idx = addr / K230_APBTMR_STRIDE;
+        int reg = addr % K230_APBTMR_STRIDE;
+
+        if (idx < K230_APBTMR_NUM_TIMERS) {
+            switch (reg) {
+            case K230_APBTMR_N_LOAD_COUNT:
+                value = s->timers[idx].load;
+                break;
+            case K230_APBTMR_N_CURRENT_VALUE:
+                if (s->timers[idx].control & K230_APBTMR_CONTROL_ENABLE) {
+                    value = ptimer_get_count(s->timers[idx].ptimer);
+                } else {
+                    value = 0;
+                }
+                break;
+            case K230_APBTMR_N_CONTROL:
+                value = s->timers[idx].control;
+                break;
+            case K230_APBTMR_N_EOI:
+                s->timers[idx].int_status = 0;
+                qemu_set_irq(s->timers[idx].irq, 0);
+                trace_k230_timer_irq_clear(s->timers[idx].id);
+                value = 0;
+                break;
+            case K230_APBTMR_N_INT_STATUS:
+                if (s->timers[idx].control & K230_APBTMR_CONTROL_INT) {
+                    value = 0;
+                } else {
+                    value = s->timers[idx].int_status;
+                }
+                break;
+            }
+        }
+    } else if (addr >= K230_APBTMR_N_LOAD2 &&
+               addr < K230_APBTMR_N_LOAD2 +
+                      K230_APBTMR_NUM_TIMERS * K230_APBTMR_LOAD2_STRIDE) {
+        int idx = (addr - K230_APBTMR_N_LOAD2) / K230_APBTMR_LOAD2_STRIDE;
+        value = s->timers[idx].load2;
+    } else {
+        switch (addr) {
+        case K230_APBTMRS_INT_STATUS:
+            for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+                if (!(s->timers[i].control & K230_APBTMR_CONTROL_INT)) {
+                    value |= s->timers[i].int_status << i;
+                }
+            }
+            break;
+        case K230_APBTMRS_EOI:
+            for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+                s->timers[i].int_status = 0;
+                qemu_set_irq(s->timers[i].irq, 0);
+            }
+            value = 0;
+            break;
+        case K230_APBTMRS_RAW_INT_STATUS:
+            for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+                value |= s->timers[i].int_status << i;
+            }
+            break;
+        case K230_APBTMRS_COMP_VERSION:
+            value = K230_APBTMR_COMP_VERSION_VAL;
+            break;
+        }
+    }
+
+    trace_k230_timer_read(addr, value);
+    return value;
+}
+
+static void k230_timer_write(void *opaque, hwaddr addr,
+                             uint64_t value, unsigned int size)
+{
+    K230TimerState *s = K230_TIMER(opaque);
+
+    addr &= 0xfff;
+
+    if (addr < K230_APBTMRS_INT_STATUS) {
+        int idx = addr / K230_APBTMR_STRIDE;
+        int reg = addr % K230_APBTMR_STRIDE;
+
+        if (idx < K230_APBTMR_NUM_TIMERS) {
+            switch (reg) {
+            case K230_APBTMR_N_LOAD_COUNT:
+                s->timers[idx].load = value;
+                break;
+            case K230_APBTMR_N_CONTROL: {
+                uint32_t old_ctrl = s->timers[idx].control;
+                s->timers[idx].control = value & K230_APBTMR_CONTROL_RW_MASK;
+                if ((value ^ old_ctrl) & K230_APBTMR_CONTROL_ENABLE) {
+                    if (value & K230_APBTMR_CONTROL_ENABLE) {
+                        k230_timer_enable(&s->timers[idx]);
+                    } else {
+                        k230_timer_disable(&s->timers[idx]);
+                    }
+                }
+                if ((value ^ old_ctrl) & K230_APBTMR_CONTROL_INT) {
+                    if (value & K230_APBTMR_CONTROL_INT) {
+                        qemu_set_irq(s->timers[idx].irq, 0);
+                    } else if (s->timers[idx].int_status) {
+                        qemu_set_irq(s->timers[idx].irq, 1);
+                    }
+                }
+                break;
+            }
+            default:
+                break;
+            }
+        }
+    } else if (addr >= K230_APBTMR_N_LOAD2 &&
+               addr < K230_APBTMR_N_LOAD2 +
+                      K230_APBTMR_NUM_TIMERS * K230_APBTMR_LOAD2_STRIDE) {
+        int idx = (addr - K230_APBTMR_N_LOAD2) / K230_APBTMR_LOAD2_STRIDE;
+        s->timers[idx].load2 = value;
+    }
+
+    trace_k230_timer_write(addr, value);
+}
+
+static const MemoryRegionOps k230_timer_ops = {
+    .read  = k230_timer_read,
+    .write = k230_timer_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+static void k230_timer_realize(DeviceState *dev, Error **errp)
+{
+    K230TimerState *s = K230_TIMER(dev);
+    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+
+    if (!clock_has_source(s->pclk)) {
+        error_setg(errp, "K230 DW APB timer: pclk clock must be connected");
+        return;
+    }
+
+    for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+        s->timers[i].id = i;
+        s->timers[i].ptimer = ptimer_init(k230_timer_tick, &s->timers[i],
+            PTIMER_POLICY_NO_IMMEDIATE_TRIGGER |
+            PTIMER_POLICY_NO_IMMEDIATE_RELOAD |
+            PTIMER_POLICY_NO_COUNTER_ROUND_DOWN);
+        ptimer_transaction_begin(s->timers[i].ptimer);
+        ptimer_set_limit(s->timers[i].ptimer, UINT32_MAX, 1);
+        ptimer_transaction_commit(s->timers[i].ptimer);
+        sysbus_init_irq(sbd, &s->timers[i].irq);
+    }
+
+    k230_timer_clk_update(s, ClockUpdate);
+
+    memory_region_init_io(&s->mmio, OBJECT(dev), &k230_timer_ops,
+                          s, TYPE_K230_TIMER, K230_APBTMR_MMIO_SIZE);
+    sysbus_init_mmio(sbd, &s->mmio);
+}
+
+static void k230_timer_reset(DeviceState *dev)
+{
+    K230TimerState *s = K230_TIMER(dev);
+
+    for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+        K230Timer *t = &s->timers[i];
+
+        ptimer_transaction_begin(t->ptimer);
+        ptimer_stop(t->ptimer);
+        ptimer_transaction_commit(t->ptimer);
+
+        t->load = 0;
+        t->load2 = 0;
+        t->control = 0;
+        t->int_status = 0;
+        qemu_set_irq(t->irq, 0);
+    }
+}
+
+static const VMStateDescription vmstate_k230_timer_channel = {
+    .name = "k230-dwapb-timer-channel",
+    .fields = (const VMStateField[]) {
+        VMSTATE_PTIMER(ptimer, K230Timer),
+        VMSTATE_UINT32(load, K230Timer),
+        VMSTATE_UINT32(load2, K230Timer),
+        VMSTATE_UINT32(control, K230Timer),
+        VMSTATE_UINT32(int_status, K230Timer),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static const VMStateDescription vmstate_k230_timer = {
+    .name = "k230-dwapb-timer",
+    .fields = (const VMStateField[]) {
+        VMSTATE_CLOCK(pclk, K230TimerState),
+        VMSTATE_STRUCT_ARRAY(timers, K230TimerState,
+                             K230_APBTMR_NUM_TIMERS, 0,
+                             vmstate_k230_timer_channel, K230Timer),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void k230_timer_init(Object *obj)
+{
+    K230TimerState *s = K230_TIMER(obj);
+    s->pclk = qdev_init_clock_in(DEVICE(obj), "pclk", k230_timer_clk_update,
+                                 s, ClockUpdate);
+}
+
+static void k230_timer_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    dc->realize = k230_timer_realize;
+    dc->vmsd = &vmstate_k230_timer;
+    dc->desc = "K230 DW APB timer";
+    device_class_set_legacy_reset(dc, k230_timer_reset);
+}
+
+static const TypeInfo k230_timer_info = {
+    .name          = TYPE_K230_TIMER,
+    .parent        = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(K230TimerState),
+    .instance_init = k230_timer_init,
+    .class_init    = k230_timer_class_init,
+};
+
+static void k230_timer_register_type(void)
+{
+    type_register_static(&k230_timer_info);
+}
+type_init(k230_timer_register_type)
diff --git a/hw/timer/meson.build b/hw/timer/meson.build
index 201b5d8316..d0a834e127 100644
--- a/hw/timer/meson.build
+++ b/hw/timer/meson.build
@@ -34,3 +34,4 @@ specific_ss.add(when: 'CONFIG_IBEX', if_true: files('ibex_timer.c'))
 system_ss.add(when: 'CONFIG_SIFIVE_PWM', if_true: files('sifive_pwm.c'))
 
 system_ss.add(when: 'CONFIG_AVR_TIMER16', if_true: files('avr_timer16.c'))
+system_ss.add(when: 'CONFIG_K230_TIMER', if_true: files('k230_dwapb_timer.c'))
diff --git a/hw/timer/trace-events b/hw/timer/trace-events
index 634ba1da27..89c619bc87 100644
--- a/hw/timer/trace-events
+++ b/hw/timer/trace-events
@@ -1,5 +1,14 @@
 # See docs/devel/tracing.rst for syntax documentation.
 
+# k230_dwapb_timer.c
+k230_timer_read(uint64_t addr, uint32_t val) "K230 timer read: [0x%" PRIx64 "] -> 0x%" PRIx32
+k230_timer_write(uint64_t addr, uint64_t val) "K230 timer write: [0x%" PRIx64 "] <- 0x%" PRIx64
+k230_timer_tick(int idx) "K230 timer %d tick"
+k230_timer_interrupt(int idx) "K230 timer %d IRQ asserted"
+k230_timer_irq_clear(int idx) "K230 timer %d IRQ cleared"
+k230_timer_enable(int idx, uint32_t load) "K230 timer %d enabled load=0x%" PRIx32
+k230_timer_disable(int idx) "K230 timer %d disabled"
+
 # slavio_timer.c
 slavio_timer_get_out(uint64_t limit, uint32_t counthigh, uint32_t count) "limit 0x%"PRIx64" count 0x%x0x%08x"
 slavio_timer_irq(uint32_t counthigh, uint32_t count) "callback: count 0x%x0x%08x"
diff --git a/include/hw/timer/k230_dwapb_timer.h b/include/hw/timer/k230_dwapb_timer.h
new file mode 100644
index 0000000000..3aba178dfa
--- /dev/null
+++ b/include/hw/timer/k230_dwapb_timer.h
@@ -0,0 +1,72 @@
+/*
+ * K230 DW APB timer
+ *
+ * Copyright (c) 2026 raoyi <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * K230 Technical Reference Manual V0.3.1 (2024-11-18):
+ * https://github.com/revyos/external-docs/blob/master/K230/en-us/K230_Technical_Reference_Manual_V0.3.1_20241118.pdf
+ */
+
+#ifndef K230_DWAPB_TIMER_H
+#define K230_DWAPB_TIMER_H
+
+#include "qemu/bitops.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/irq.h"
+#include "hw/core/clock.h"
+#include "qom/object.h"
+
+#define TYPE_K230_TIMER "riscv.k230.timer"
+OBJECT_DECLARE_SIMPLE_TYPE(K230TimerState, K230_TIMER)
+
+#define K230_APBTMR_MMIO_SIZE    0x800
+#define K230_APBTMR_NUM_TIMERS   6
+#define K230_APBTMR_STRIDE       0x14
+#define K230_APBTMR_DEFAULT_FREQ 6250000
+#define K230_APBTMR_COMP_VERSION_VAL 0x3231312A
+#define K230_APBTMR_LOAD2_STRIDE  0x04
+
+/* Per-timer register offsets */
+#define K230_APBTMR_N_LOAD_COUNT    0x00
+#define K230_APBTMR_N_CURRENT_VALUE 0x04
+#define K230_APBTMR_N_CONTROL       0x08
+#define K230_APBTMR_N_EOI           0x0c
+#define K230_APBTMR_N_INT_STATUS    0x10
+/* Global register offsets */
+#define K230_APBTMRS_INT_STATUS        0xa0
+#define K230_APBTMRS_EOI               0xa4
+#define K230_APBTMRS_RAW_INT_STATUS    0xa8
+#define K230_APBTMRS_COMP_VERSION      0xac
+/* PWM LoadCount2 registers */
+#define K230_APBTMR_N_LOAD2       0xb0
+
+/* Control register bits */
+#define K230_APBTMR_CONTROL_ENABLE        BIT(0)
+#define K230_APBTMR_CONTROL_MODE_PERIODIC BIT(1)
+#define K230_APBTMR_CONTROL_INT           BIT(2)
+#define K230_APBTMR_CONTROL_PWM           BIT(3)
+#define K230_APBTMR_CONTROL_RW_MASK     0xf
+
+typedef struct K230Timer {
+    struct ptimer_state *ptimer;
+    qemu_irq irq;
+    int id;
+    uint32_t load;
+    uint32_t load2;
+    uint32_t control;
+    uint32_t int_status;
+} K230Timer;
+
+struct K230TimerState {
+    /* <private> */
+    SysBusDevice parent_obj;
+
+    /*< public >*/
+    MemoryRegion mmio;
+    K230Timer timers[K230_APBTMR_NUM_TIMERS];
+    Clock *pclk;
+};
+
+#endif
-- 
2.53.0