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
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 <rao232328@gmail.com>
---
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 <chao.liu.zevorn@gmail.com>
+ *
+ * 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 <chao.liu.zevorn@gmail.com>
+ *
+ * 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
On Sun, 26 Jul 2026 22:26:39 +0800, raoyi <rao232328@gmail.com> wrote: > 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 <rao232328@gmail.com> Thank you very much for your contribution. I recommend splitting this patch into three separate patches.The plan consists of three patches: We want to keep the coding style as consistent as possible with the previous patch. 1. The implementation of the K230 DesignWare APB (DWAPB) timer model (k230_dwapb_timer.c). 2. The integration of peripheral models into the K230 board. 3. Testing and documentation. Additionally, this patch has a conflict with the current master branch. Please do a rebase when the maintainer asks for the merge. There's no need to do it right now. -- Junze Cao <caojunze424@gmail.com>
© 2016 - 2026 Red Hat, Inc.