Add DesignWare APB timer peripheral for K230 machine.
The timer uses the Clock framework for frequency input,
preparing for future CMU integration.

Signed-off-by: raoyi <[email protected]>
---
 MAINTAINERS                         |   3 +
 hw/riscv/Kconfig                    |   1 +
 hw/riscv/k230.c                     |  23 +-
 hw/timer/Kconfig                    |   4 +
 hw/timer/k230_dwapb_timer.c         | 332 ++++++++++++++++++++++++++++
 hw/timer/meson.build                |   1 +
 hw/timer/trace-events               |   9 +
 include/hw/riscv/k230.h             |   8 +
 include/hw/timer/k230_dwapb_timer.h |  68 ++++++
 tests/qtest/k230-dwapb-timer-test.c | 224 +++++++++++++++++++
 tests/qtest/meson.build             |   2 +-
 11 files changed, 671 insertions(+), 4 deletions(-)
 create mode 100644 hw/timer/k230_dwapb_timer.c
 create mode 100644 include/hw/timer/k230_dwapb_timer.h
 create mode 100644 tests/qtest/k230-dwapb-timer-test.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 6171cc7494..ed2ba7c58a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1826,9 +1826,12 @@ S: Maintained
 F: docs/system/riscv/k230.rst
 F: hw/riscv/k230.c
 F: hw/watchdog/k230_wdt.c
+F: hw/timer/k230_dwapb_timer.c
 F: include/hw/riscv/k230.h
 F: include/hw/watchdog/k230_wdt.h
+F: include/hw/timer/k230_dwapb_timer.h
 F: tests/qtest/k230-wdt-test.c
+F: tests/qtest/k230-dwapb-timer-test.c
 
 RX Machines
 -----------
diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
index de37c08cae..c86968f759 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -162,3 +162,4 @@ config K230
     select SERIAL_MM
     select UNIMP
     select K230_WDT
+    select K230_TIMER
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 656f28190c..214fb3525e 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -24,6 +24,8 @@
 #include "target/riscv/cpu.h"
 #include "hw/core/loader.h"
 #include "hw/core/sysbus.h"
+#include "hw/core/clock.h"
+#include "hw/core/qdev-clock.h"
 #include "hw/riscv/k230.h"
 #include "hw/riscv/boot.h"
 #include "hw/riscv/machines-qom.h"
@@ -110,6 +112,12 @@ static void k230_soc_init(Object *obj)
     object_initialize_child(obj, "c908-cpu", cpu0, TYPE_RISCV_HART_ARRAY);
     object_initialize_child(obj, "k230-wdt0", &s->wdt[0], TYPE_K230_WDT);
     object_initialize_child(obj, "k230-wdt1", &s->wdt[1], TYPE_K230_WDT);
+    object_initialize_child(obj, "k230-dwapb-timer", &s->timer,
+                            TYPE_K230_TIMER);
+
+    Clock *timer_clk = clock_new(OBJECT(s), "timer-clk");
+    clock_set_hz(timer_clk, K230_APBTMR_DEFAULT_FREQ);
+    qdev_connect_clock_in(DEVICE(&s->timer), "pclk", timer_clk);
 
     qdev_prop_set_uint32(DEVICE(cpu0), "hartid-base", 0);
     qdev_prop_set_string(DEVICE(cpu0), "cpu-type", TYPE_RISCV_CPU_THEAD_C908);
@@ -191,6 +199,18 @@ static void k230_soc_realize(DeviceState *dev, Error 
**errp)
         k230_create_uart(sys_mem, DEVICE(s->c908_plic), i);
     }
 
+    /* Timer */
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->timer), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->timer), 0, memmap[K230_DEV_TIMER].base);
+
+    for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+        sysbus_connect_irq(SYS_BUS_DEVICE(&s->timer), i,
+                           qdev_get_gpio_in(DEVICE(s->c908_plic),
+                                            K230_TIMER0_IRQ + i));
+    }
+
     /* Watchdog */
     for (int i = 0; i < 2; i++) {
         if (!sysbus_realize(SYS_BUS_DEVICE(&s->wdt[i]), errp)) {
@@ -283,9 +303,6 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
     create_unimplemented_device("iomux", memmap[K230_DEV_IOMUX].base,
                                 memmap[K230_DEV_IOMUX].size);
 
-    create_unimplemented_device("timer", memmap[K230_DEV_TIMER].base,
-                                memmap[K230_DEV_TIMER].size);
-
     create_unimplemented_device("wdt0", memmap[K230_DEV_WDT0].base,
                                 memmap[K230_DEV_WDT0].size);
 
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..26578b32dd
--- /dev/null
+++ b/hw/timer/k230_dwapb_timer.c
@@ -0,0 +1,332 @@
+/*
+ * K230 DW APB timer
+ *
+ * Copyright (c) 2025 Chao Liu <[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/module.h"
+#include "qemu/bitops.h"
+#include "qapi/error.h"
+#include "trace.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 "migration/vmstate.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 size)
+{
+    K230TimerState *s = K230_TIMER(opaque);
+    uint32_t val = 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) {
+            return 0;
+        }
+
+        switch (reg) {
+        case K230_APBTMR_N_LOAD_COUNT:
+            val = s->timers[idx].load;
+            break;
+        case K230_APBTMR_N_CURRENT_VALUE:
+            if (s->timers[idx].control & K230_APBTMR_CONTROL_ENABLE) {
+                val = ptimer_get_count(s->timers[idx].ptimer);
+            } else {
+                val = 0;
+            }
+            break;
+        case K230_APBTMR_N_CONTROL:
+            val = 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);
+            val = 0;
+            break;
+        case K230_APBTMR_N_INT_STATUS:
+            if (s->timers[idx].control & K230_APBTMR_CONTROL_INT) {
+                val = 0;
+            } else {
+                val = s->timers[idx].int_status;
+            }
+            break;
+        }
+        goto out;
+    }
+
+    if (addr >= K230_APBTMR_N_LOAD2 &&
+        addr < K230_APBTMR_N_LOAD2 +
+               K230_APBTMR_NUM_TIMERS * K230_APBTMR_LOAD2_STRIDE) {
+        /* LoadCount2: R/W implemented. PWM output not generated. */
+        int idx = (addr - K230_APBTMR_N_LOAD2) / K230_APBTMR_LOAD2_STRIDE;
+        val = s->timers[idx].load2;
+        goto out;
+    }
+
+    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)) {
+                val |= 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);
+        }
+        val = 0;
+        break;
+    case K230_APBTMRS_RAW_INT_STATUS:
+        for (int i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+            val |= s->timers[i].int_status << i;
+        }
+        break;
+    case K230_APBTMRS_COMP_VERSION:
+        val = K230_APBTMR_COMP_VERSION_VAL;
+        break;
+    }
+
+out:
+    trace_k230_timer_read(addr, val);
+    return val;
+}
+
+static void k230_timer_write(void *opaque, hwaddr addr,
+                             uint64_t value, unsigned 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) {
+            return;
+        }
+
+        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;
+        }
+        goto out;
+    }
+
+    if (addr >= K230_APBTMR_N_LOAD2 &&
+        addr < K230_APBTMR_N_LOAD2 +
+               K230_APBTMR_NUM_TIMERS * K230_APBTMR_LOAD2_STRIDE) {
+        /* LoadCount2: R/W implemented. PWM output not generated. */
+        int idx = (addr - K230_APBTMR_N_LOAD2) / K230_APBTMR_LOAD2_STRIDE;
+        s->timers[idx].load2 = value;
+    }
+
+out:
+    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/riscv/k230.h b/include/hw/riscv/k230.h
index 592e1c26bf..3de3b7e765 100644
--- a/include/hw/riscv/k230.h
+++ b/include/hw/riscv/k230.h
@@ -18,6 +18,7 @@
 #include "hw/core/boards.h"
 #include "hw/riscv/riscv_hart.h"
 #include "hw/watchdog/k230_wdt.h"
+#include "hw/timer/k230_dwapb_timer.h"
 
 #define C908_CPU_HARTID   (0)
 
@@ -33,6 +34,7 @@ typedef struct K230SoCState {
     RISCVHartArrayState c908_cpu; /* Small core */
 
     K230WdtState wdt[2];
+    K230TimerState timer;
     MemoryRegion sram;
     MemoryRegion bootrom;
 
@@ -127,6 +129,12 @@ enum {
     K230_UART2_IRQ  = 18,
     K230_UART3_IRQ  = 19,
     K230_UART4_IRQ  = 20,
+    K230_TIMER0_IRQ = 101,
+    K230_TIMER1_IRQ = 102,
+    K230_TIMER2_IRQ = 103,
+    K230_TIMER3_IRQ = 104,
+    K230_TIMER4_IRQ = 105,
+    K230_TIMER5_IRQ = 106,
     K230_WDT0_IRQ   = 107,
     K230_WDT1_IRQ   = 108,
 };
diff --git a/include/hw/timer/k230_dwapb_timer.h 
b/include/hw/timer/k230_dwapb_timer.h
new file mode 100644
index 0000000000..0e3a8ece0e
--- /dev/null
+++ b/include/hw/timer/k230_dwapb_timer.h
@@ -0,0 +1,68 @@
+/*
+ * K230 DW APB timer
+ *
+ * Copyright (c) 2025 Chao Liu <[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 "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 {
+    SysBusDevice parent_obj;
+    MemoryRegion mmio;
+    K230Timer timers[K230_APBTMR_NUM_TIMERS];
+    Clock *pclk;
+};
+
+#endif
diff --git a/tests/qtest/k230-dwapb-timer-test.c 
b/tests/qtest/k230-dwapb-timer-test.c
new file mode 100644
index 0000000000..e995603a36
--- /dev/null
+++ b/tests/qtest/k230-dwapb-timer-test.c
@@ -0,0 +1,224 @@
+/*
+ * QTest testcase for K230 Timer
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#include "qemu/osdep.h"
+#include "qemu/timer.h"
+#include "libqtest.h"
+#include "hw/timer/k230_dwapb_timer.h"
+
+#define TIMER_BASE 0x91105800
+#define TIMER_REG(n, reg) (TIMER_BASE + (n) * K230_APBTMR_STRIDE + (reg))
+#define TIMER_TICK_NS (NANOSECONDS_PER_SECOND / K230_APBTMR_DEFAULT_FREQ)
+
+static void timer_load(QTestState *qts, int n, uint32_t value)
+{
+    qtest_writel(qts, TIMER_REG(n, K230_APBTMR_N_LOAD_COUNT), value);
+}
+
+static void timer_enable(QTestState *qts, int n, uint32_t ctrl)
+{
+    qtest_writel(qts, TIMER_REG(n, K230_APBTMR_N_CONTROL), ctrl);
+}
+
+static void timer_disable(QTestState *qts, int n)
+{
+    qtest_writel(qts, TIMER_REG(n, K230_APBTMR_N_CONTROL), 0);
+}
+
+static uint32_t timer_get_and_clear_irq(QTestState *qts, int n)
+{
+    uint32_t int_sts = qtest_readl(qts, TIMER_REG(n, 
K230_APBTMR_N_INT_STATUS));
+
+    if (int_sts) {
+        qtest_readl(qts, TIMER_REG(n, K230_APBTMR_N_EOI));
+    }
+
+    return int_sts;
+}
+
+static void test_timer_free_running(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    timer_load(qts, 0, 100);
+    timer_enable(qts, 0, K230_APBTMR_CONTROL_ENABLE);
+
+    qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+    g_assert_cmphex(timer_get_and_clear_irq(qts, 0), ==, 1);
+
+    qtest_clock_step(qts, 10ULL * TIMER_TICK_NS);
+    uint32_t cur = qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_CURRENT_VALUE));
+    g_assert_cmphex(cur, <, UINT32_MAX);
+    g_assert_cmphex(cur, >, 0);
+
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_INT_STATUS)), ==, 0);
+
+    qtest_quit(qts);
+}
+
+static void test_timer_periodic(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+    int i;
+
+    timer_load(qts, 0, 100);
+    timer_enable(qts, 0, K230_APBTMR_CONTROL_ENABLE |
+                        K230_APBTMR_CONTROL_MODE_PERIODIC);
+
+    for (i = 0; i < 3; i++) {
+        qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+        g_assert_cmphex(timer_get_and_clear_irq(qts, 0), ==, 1);
+        uint32_t cur = qtest_readl(qts,
+                          TIMER_REG(0, K230_APBTMR_N_CURRENT_VALUE));
+        g_assert_cmphex(cur, <=, 100);
+    }
+
+    qtest_quit(qts);
+}
+
+static void test_timer_int_mask(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    timer_load(qts, 0, 100);
+    timer_enable(qts, 0, K230_APBTMR_CONTROL_ENABLE |
+                        K230_APBTMR_CONTROL_MODE_PERIODIC |
+                        K230_APBTMR_CONTROL_INT);
+
+    qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_INT_STATUS)) & 1, ==, 0);
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_BASE + K230_APBTMRS_INT_STATUS), ==, 0);
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_BASE + K230_APBTMRS_RAW_INT_STATUS) & 1, ==, 1);
+
+    qtest_quit(qts);
+}
+
+static void test_timer_disable_clears_irq(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    timer_load(qts, 0, 100);
+    timer_enable(qts, 0, K230_APBTMR_CONTROL_ENABLE |
+                        K230_APBTMR_CONTROL_MODE_PERIODIC);
+
+    qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_INT_STATUS)) & 1, ==, 1);
+
+    timer_disable(qts, 0);
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_INT_STATUS)) & 1, ==, 0);
+
+    qtest_quit(qts);
+}
+
+static void test_timer_current_disabled(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+
+    /* TRM: "A '0' is always read back when the timer is not enabled" */
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_CURRENT_VALUE)), ==, 0);
+
+    timer_load(qts, 0, 9999);
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_CURRENT_VALUE)), ==, 0);
+
+    timer_enable(qts, 0, K230_APBTMR_CONTROL_ENABLE);
+    qtest_clock_step(qts, 10ULL * TIMER_TICK_NS);
+    uint32_t cur_enabled = qtest_readl(qts,
+                              TIMER_REG(0, K230_APBTMR_N_CURRENT_VALUE));
+    g_assert_cmphex(cur_enabled, >, 0);
+    g_assert_cmphex(cur_enabled, <, UINT32_MAX);
+
+    timer_disable(qts, 0);
+    g_assert_cmphex(qtest_readl(qts,
+                      TIMER_REG(0, K230_APBTMR_N_CURRENT_VALUE)), ==, 0);
+
+    qtest_quit(qts);
+}
+
+static void test_timer_dynamic_reload(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+    uint32_t cur;
+
+    timer_load(qts, 0, 1000);
+    timer_enable(qts, 0, K230_APBTMR_CONTROL_ENABLE |
+                        K230_APBTMR_CONTROL_MODE_PERIODIC);
+
+    qtest_clock_step(qts, 100ULL * TIMER_TICK_NS);
+
+    timer_load(qts, 0, 200);
+
+    qtest_clock_step(qts, 901ULL * TIMER_TICK_NS + 1);
+    g_assert_cmphex(timer_get_and_clear_irq(qts, 0), ==, 1);
+
+    qtest_clock_step(qts, 100ULL * TIMER_TICK_NS);
+    cur = qtest_readl(qts, TIMER_REG(0, K230_APBTMR_N_CURRENT_VALUE));
+    g_assert_cmphex(cur, ==, 99);
+
+    qtest_quit(qts);
+}
+
+static void test_timer_all_channels(void)
+{
+    QTestState *qts = qtest_init("-machine k230");
+    int i;
+
+    for (i = 0; i < K230_APBTMR_NUM_TIMERS; i++) {
+        timer_load(qts, i, 100 + i * 50);
+        timer_enable(qts, i, K230_APBTMR_CONTROL_ENABLE |
+                             K230_APBTMR_CONTROL_MODE_PERIODIC);
+    }
+
+    /* Step past timer 0 expiry (load=100) — only timer 0 should fire */
+    qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+    uint32_t sts = qtest_readl(qts, TIMER_BASE + K230_APBTMRS_INT_STATUS);
+    g_assert_cmphex(sts & 1, ==, 1);
+    g_assert_cmphex(sts & 0x3e, ==, 0);
+
+    /* Step to timer 1 expiry (another 50 ticks) */
+    qtest_clock_step(qts, 50ULL * TIMER_TICK_NS + 1);
+    sts = qtest_readl(qts, TIMER_BASE + K230_APBTMRS_INT_STATUS);
+    g_assert_cmphex(sts & 0x3, ==, 0x3);
+    g_assert_cmphex(sts & 0x3c, ==, 0);
+
+    /* Step to expiry of all remaining timers (another 200 ticks) */
+    qtest_clock_step(qts, 200ULL * TIMER_TICK_NS + 1);
+    sts = qtest_readl(qts, TIMER_BASE + K230_APBTMRS_INT_STATUS);
+    g_assert_cmphex(sts, ==, 0x3f);
+
+    /* EOI_ALL clears all */
+    qtest_readl(qts, TIMER_BASE + K230_APBTMRS_EOI);
+    sts = qtest_readl(qts, TIMER_BASE + K230_APBTMRS_INT_STATUS);
+    g_assert_cmphex(sts, ==, 0);
+
+    qtest_quit(qts);
+}
+
+int main(int argc, char *argv[])
+{
+    g_test_init(&argc, &argv, NULL);
+
+    qtest_add_func("/k230-dwapb-timer/free_running", test_timer_free_running);
+    qtest_add_func("/k230-dwapb-timer/periodic", test_timer_periodic);
+    qtest_add_func("/k230-dwapb-timer/int_mask", test_timer_int_mask);
+    qtest_add_func("/k230-dwapb-timer/disable_clears_irq",
+                   test_timer_disable_clears_irq);
+    qtest_add_func("/k230-dwapb-timer/current_disabled",
+                   test_timer_current_disabled);
+    qtest_add_func("/k230-dwapb-timer/dynamic_reload",
+                   test_timer_dynamic_reload);
+    qtest_add_func("/k230-dwapb-timer/all_channels", test_timer_all_channels);
+
+    return g_test_run();
+}
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 56ff860e21..db95099481 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -297,7 +297,7 @@ qtests_riscv64 = ['riscv-csr-test'] + \
   (config_all_devices.has_key('CONFIG_IOMMU_TESTDEV') and
    config_all_devices.has_key('CONFIG_RISCV_IOMMU') ?
    ['iommu-riscv-test'] : []) + \
-  (config_all_devices.has_key('CONFIG_K230') ? ['k230-wdt-test'] : [])
+   (config_all_devices.has_key('CONFIG_K230') ? ['k230-wdt-test', 
'k230-dwapb-timer-test'] : [])
 
 qtests_hexagon = ['boot-serial-test']
 
-- 
2.53.0


Reply via email to