* [PATCH v5 1/3] hw/timer: add DesignWare APB timer model
2026-08-19 13:18 [PATCH v5 0/3] hw/timer: add K230 DW APB timer support raoyi
@ 2026-08-19 13:18 ` raoyi
2026-08-19 13:18 ` [PATCH v5 2/3] hw/riscv: add DesignWare APB timer to K230 board raoyi
2026-08-19 13:18 ` [PATCH v5 3/3] tests/qtest: add k230 dwapb timer test raoyi
2 siblings, 0 replies; 4+ messages in thread
From: raoyi @ 2026-08-19 13:18 UTC (permalink / raw)
To: qemu-devel, qemu-riscv
Cc: Chao Liu, Alistair Francis, Palmer Dabbelt, Weiwei Li,
Daniel Henrique Barboza, Liu Zhiwei, caojunze424, Bin Meng,
Philippe Mathieu-Daudé, raoyi
Add generic DesignWare APB timer device model. The number
of timer channels is configurable via the num-timers property.
Add timer files to MAINTAINERS.
Signed-off-by: raoyi <rao232328@gmail.com>
---
MAINTAINERS | 2 +
hw/timer/Kconfig | 4 +
hw/timer/dw-apb-timer.c | 376 ++++++++++++++++++++++++++++++++
hw/timer/meson.build | 1 +
hw/timer/trace-events | 8 +
include/hw/timer/dw-apb-timer.h | 40 ++++
6 files changed, 431 insertions(+)
create mode 100644 hw/timer/dw-apb-timer.c
create mode 100644 include/hw/timer/dw-apb-timer.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 6171cc7494..cf689433ff 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1826,8 +1826,10 @@ S: Maintained
F: docs/system/riscv/k230.rst
F: hw/riscv/k230.c
F: hw/watchdog/k230_wdt.c
+F: hw/timer/dw-apb-timer.c
F: include/hw/riscv/k230.h
F: include/hw/watchdog/k230_wdt.h
+F: include/hw/timer/dw-apb-timer.h
F: tests/qtest/k230-wdt-test.c
RX Machines
diff --git a/hw/timer/Kconfig b/hw/timer/Kconfig
index b3d823ce2c..b533cf1ba9 100644
--- a/hw/timer/Kconfig
+++ b/hw/timer/Kconfig
@@ -65,3 +65,7 @@ config STELLARIS_GPTM
config AVR_TIMER16
bool
+
+config DW_APB_TIMER
+ bool
+ select PTIMER
diff --git a/hw/timer/dw-apb-timer.c b/hw/timer/dw-apb-timer.c
new file mode 100644
index 0000000000..86e1fac43f
--- /dev/null
+++ b/hw/timer/dw-apb-timer.c
@@ -0,0 +1,376 @@
+/*
+ * Synopsys DesignWare APB timer
+ *
+ * Copyright (c) 2026 raoyi <rao232328@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/bitops.h"
+#include "qemu/module.h"
+#include "qapi/error.h"
+#include "migration/vmstate.h"
+#include "hw/core/ptimer.h"
+#include "hw/core/qdev-clock.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/timer/dw-apb-timer.h"
+#include "trace.h"
+
+#define DW_APB_TIMER_STRIDE 0x14
+#define DW_APB_TIMER_MMIO_SIZE 0x100
+
+/* Per-timer register offsets */
+#define DW_APB_TIMER_N_LOAD_COUNT 0x00
+#define DW_APB_TIMER_N_CURRENT_VALUE 0x04
+#define DW_APB_TIMER_N_CONTROL 0x08
+#define DW_APB_TIMER_N_EOI 0x0c
+#define DW_APB_TIMER_N_INT_STATUS 0x10
+/* Global register offsets */
+#define DW_APB_TIMER_INT_STATUS 0xa0
+#define DW_APB_TIMER_EOI 0xa4
+#define DW_APB_TIMER_RAW_INT_STATUS 0xa8
+#define DW_APB_TIMER_COMP_VERSION 0xac
+
+/* Control register bits */
+#define DW_APB_TIMER_CONTROL_ENABLE BIT(0)
+/* 1: periodic, 0: free running. */
+#define DW_APB_TIMER_CONTROL_MODE_PERIODIC BIT(1)
+#define DW_APB_TIMER_CONTROL_INT_MASK BIT(2)
+#define DW_APB_TIMER_CONTROL_RW_MASK 0x7
+
+/*
+ * Component version of the DW_apb_timers IP, fixed in silicon.
+ * Decodes as the ASCII string "211*" (2.11a series); the model reports
+ * the version integrated in K230. Linux does not read this register.
+ */
+#define DW_APB_TIMER_COMP_VERSION_VAL 0x3231312A
+
+static void dw_apb_timer_update_irq(DWAPBTimerChannel *t)
+{
+ qemu_set_irq(t->irq, t->int_status &&
+ !(t->control & DW_APB_TIMER_CONTROL_INT_MASK));
+}
+
+static void dw_apb_timer_clk_update(void *opaque, ClockEvent event)
+{
+ DWAPBTimerChannel *t = opaque;
+
+ if (!t->ptimer) {
+ return;
+ }
+ ptimer_transaction_begin(t->ptimer);
+ ptimer_set_period_from_clock(t->ptimer, t->clk, 1);
+ ptimer_transaction_commit(t->ptimer);
+}
+
+static void dw_apb_timer_enable(DWAPBTimerChannel *t)
+{
+ trace_dw_apb_timer_enable(t->id, t->load);
+
+ 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);
+}
+
+static void dw_apb_timer_disable(DWAPBTimerChannel *t)
+{
+ trace_dw_apb_timer_disable(t->id);
+
+ ptimer_transaction_begin(t->ptimer);
+ ptimer_stop(t->ptimer);
+ ptimer_transaction_commit(t->ptimer);
+
+ t->int_status = 0;
+ dw_apb_timer_update_irq(t);
+}
+
+static void dw_apb_timer_tick(void *opaque)
+{
+ DWAPBTimerChannel *t = opaque;
+ uint32_t reload;
+
+ trace_dw_apb_timer_tick(t->id);
+
+ t->int_status = 1;
+ dw_apb_timer_update_irq(t);
+
+ if (t->control & DW_APB_TIMER_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 dw_apb_timer_read(void *opaque, hwaddr addr,
+ unsigned int size)
+{
+ DWAPBTimerState *s = DW_APB_TIMER(opaque);
+ uint32_t value = 0;
+
+ if (addr < DW_APB_TIMER_INT_STATUS) {
+ unsigned int idx = addr / DW_APB_TIMER_STRIDE;
+ hwaddr reg = addr % DW_APB_TIMER_STRIDE;
+
+ if (idx < s->num_timers) {
+ DWAPBTimerChannel *t = &s->timers[idx];
+
+ switch (reg) {
+ case DW_APB_TIMER_N_LOAD_COUNT:
+ value = t->load;
+ break;
+ case DW_APB_TIMER_N_CURRENT_VALUE:
+ if (t->control & DW_APB_TIMER_CONTROL_ENABLE) {
+ value = ptimer_get_count(t->ptimer);
+ }
+ break;
+ case DW_APB_TIMER_N_CONTROL:
+ value = t->control;
+ break;
+ case DW_APB_TIMER_N_EOI:
+ t->int_status = 0;
+ dw_apb_timer_update_irq(t);
+ trace_dw_apb_timer_irq_clear(t->id);
+ break;
+ case DW_APB_TIMER_N_INT_STATUS:
+ value = t->int_status &&
+ !(t->control & DW_APB_TIMER_CONTROL_INT_MASK);
+ break;
+ default:
+ break;
+ }
+ }
+ } else {
+ switch (addr) {
+ case DW_APB_TIMER_INT_STATUS:
+ for (int i = 0; i < s->num_timers; i++) {
+ DWAPBTimerChannel *t = &s->timers[i];
+
+ value |= (t->int_status &&
+ !(t->control & DW_APB_TIMER_CONTROL_INT_MASK)) << i;
+ }
+ break;
+ case DW_APB_TIMER_EOI:
+ for (int i = 0; i < s->num_timers; i++) {
+ DWAPBTimerChannel *t = &s->timers[i];
+
+ t->int_status = 0;
+ dw_apb_timer_update_irq(t);
+ }
+ break;
+ case DW_APB_TIMER_RAW_INT_STATUS:
+ for (int i = 0; i < s->num_timers; i++) {
+ value |= s->timers[i].int_status << i;
+ }
+ break;
+ case DW_APB_TIMER_COMP_VERSION:
+ value = DW_APB_TIMER_COMP_VERSION_VAL;
+ break;
+ default:
+ break;
+ }
+ }
+
+ trace_dw_apb_timer_read(addr, value);
+ return value;
+}
+
+static void dw_apb_timer_write(void *opaque, hwaddr addr,
+ uint64_t value, unsigned int size)
+{
+ DWAPBTimerState *s = DW_APB_TIMER(opaque);
+
+ if (addr < DW_APB_TIMER_INT_STATUS) {
+ unsigned int idx = addr / DW_APB_TIMER_STRIDE;
+ hwaddr reg = addr % DW_APB_TIMER_STRIDE;
+
+ if (idx < s->num_timers) {
+ DWAPBTimerChannel *t = &s->timers[idx];
+
+ switch (reg) {
+ case DW_APB_TIMER_N_LOAD_COUNT:
+ t->load = value;
+ break;
+ case DW_APB_TIMER_N_CONTROL: {
+ uint32_t old_control = t->control;
+ uint32_t new_control = value & DW_APB_TIMER_CONTROL_RW_MASK;
+
+ t->control = new_control;
+ if ((new_control ^ old_control) &
+ DW_APB_TIMER_CONTROL_ENABLE) {
+ if (new_control & DW_APB_TIMER_CONTROL_ENABLE) {
+ dw_apb_timer_enable(t);
+ } else {
+ dw_apb_timer_disable(t);
+ }
+ }
+ if ((new_control ^ old_control) &
+ DW_APB_TIMER_CONTROL_INT_MASK) {
+ dw_apb_timer_update_irq(t);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ trace_dw_apb_timer_write(addr, value);
+}
+
+static const MemoryRegionOps dw_apb_timer_ops = {
+ .read = dw_apb_timer_read,
+ .write = dw_apb_timer_write,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .impl = {
+ .min_access_size = 4,
+ .max_access_size = 4,
+ },
+};
+
+static void dw_apb_timer_reset_hold(Object *obj, ResetType type)
+{
+ DWAPBTimerState *s = DW_APB_TIMER(obj);
+
+ for (int i = 0; i < s->num_timers; i++) {
+ DWAPBTimerChannel *t = &s->timers[i];
+
+ ptimer_transaction_begin(t->ptimer);
+ ptimer_stop(t->ptimer);
+ ptimer_transaction_commit(t->ptimer);
+
+ t->load = 0;
+ t->control = 0;
+ t->int_status = 0;
+ dw_apb_timer_update_irq(t);
+ }
+}
+
+static const VMStateDescription vmstate_dw_apb_timer_channel = {
+ .name = "dw-apb-timer-channel",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (const VMStateField[]) {
+ VMSTATE_PTIMER(ptimer, DWAPBTimerChannel),
+ VMSTATE_CLOCK(clk, DWAPBTimerChannel),
+ VMSTATE_UINT32(load, DWAPBTimerChannel),
+ VMSTATE_UINT32(control, DWAPBTimerChannel),
+ VMSTATE_UINT32(int_status, DWAPBTimerChannel),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static const VMStateDescription vmstate_dw_apb_timer = {
+ .name = "dw-apb-timer",
+ .fields = (const VMStateField[]) {
+ VMSTATE_STRUCT_VARRAY_UINT32(timers, DWAPBTimerState,
+ num_timers, 0,
+ vmstate_dw_apb_timer_channel,
+ DWAPBTimerChannel),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static const Property dw_apb_timer_properties[] = {
+ DEFINE_PROP_UINT32("num-timers", DWAPBTimerState, num_timers, 1),
+};
+
+static void dw_apb_timer_init(Object *obj)
+{
+ DWAPBTimerState *s = DW_APB_TIMER(obj);
+
+ for (int i = 0; i < DW_APB_TIMER_MAX_TIMERS; i++) {
+ DWAPBTimerChannel *t = &s->timers[i];
+ g_autofree char *name = g_strdup_printf("timer[%d]", i);
+
+ t->id = i;
+ t->clk = qdev_init_clock_in(DEVICE(obj), name,
+ dw_apb_timer_clk_update, t,
+ ClockUpdate);
+ }
+}
+
+static void dw_apb_timer_realize(DeviceState *dev, Error **errp)
+{
+ DWAPBTimerState *s = DW_APB_TIMER(dev);
+ SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+
+ if (s->num_timers == 0 || s->num_timers > DW_APB_TIMER_MAX_TIMERS) {
+ error_setg(errp, "dw-apb-timer: num-timers must be between 1 and %u",
+ DW_APB_TIMER_MAX_TIMERS);
+ return;
+ }
+
+ for (int i = 0; i < s->num_timers; i++) {
+ DWAPBTimerChannel *t = &s->timers[i];
+
+ if (!clock_has_source(t->clk)) {
+ error_setg(errp, "dw-apb-timer: timer[%u] clock must be connected",
+ i);
+ return;
+ }
+ }
+
+ for (int i = 0; i < s->num_timers; i++) {
+ DWAPBTimerChannel *t = &s->timers[i];
+
+ t->ptimer = ptimer_init(dw_apb_timer_tick, t,
+ PTIMER_POLICY_NO_IMMEDIATE_TRIGGER |
+ PTIMER_POLICY_NO_IMMEDIATE_RELOAD |
+ PTIMER_POLICY_NO_COUNTER_ROUND_DOWN);
+ ptimer_transaction_begin(t->ptimer);
+ ptimer_set_limit(t->ptimer, UINT32_MAX, 1);
+ ptimer_transaction_commit(t->ptimer);
+ sysbus_init_irq(sbd, &t->irq);
+
+ /* The source may have been connected before the ptimer existed. */
+ dw_apb_timer_clk_update(t, ClockUpdate);
+ }
+
+ memory_region_init_io(&s->mmio, OBJECT(dev), &dw_apb_timer_ops,
+ s, TYPE_DW_APB_TIMER, DW_APB_TIMER_MMIO_SIZE);
+ sysbus_init_mmio(sbd, &s->mmio);
+}
+
+static void dw_apb_timer_unrealize(DeviceState *dev)
+{
+ DWAPBTimerState *s = DW_APB_TIMER(dev);
+
+ for (int i = 0; i < s->num_timers; i++) {
+ ptimer_free(s->timers[i].ptimer);
+ }
+}
+
+static void dw_apb_timer_class_init(ObjectClass *klass, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+ dc->realize = dw_apb_timer_realize;
+ dc->unrealize = dw_apb_timer_unrealize;
+ dc->vmsd = &vmstate_dw_apb_timer;
+ dc->desc = "Synopsys DesignWare APB timer";
+ rc->phases.hold = dw_apb_timer_reset_hold;
+ device_class_set_props(dc, dw_apb_timer_properties);
+}
+
+static const TypeInfo dw_apb_timer_info = {
+ .name = TYPE_DW_APB_TIMER,
+ .parent = TYPE_SYS_BUS_DEVICE,
+ .instance_size = sizeof(DWAPBTimerState),
+ .instance_init = dw_apb_timer_init,
+ .class_init = dw_apb_timer_class_init,
+};
+
+static void dw_apb_timer_register_type(void)
+{
+ type_register_static(&dw_apb_timer_info);
+}
+
+type_init(dw_apb_timer_register_type)
diff --git a/hw/timer/meson.build b/hw/timer/meson.build
index 201b5d8316..fccc36540c 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_DW_APB_TIMER', if_true: files('dw-apb-timer.c'))
diff --git a/hw/timer/trace-events b/hw/timer/trace-events
index 634ba1da27..7f592a96c8 100644
--- a/hw/timer/trace-events
+++ b/hw/timer/trace-events
@@ -1,5 +1,13 @@
# See docs/devel/tracing.rst for syntax documentation.
+# dw-apb-timer.c
+dw_apb_timer_read(uint64_t addr, uint32_t val) "DW APB timer read: [0x%" PRIx64 "] -> 0x%" PRIx32
+dw_apb_timer_write(uint64_t addr, uint64_t val) "DW APB timer write: [0x%" PRIx64 "] <- 0x%" PRIx64
+dw_apb_timer_tick(int idx) "DW APB timer %d tick"
+dw_apb_timer_irq_clear(int idx) "DW APB timer %d IRQ cleared"
+dw_apb_timer_enable(int idx, uint32_t load) "DW APB timer %d enabled load=0x%" PRIx32
+dw_apb_timer_disable(int idx) "DW APB 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/dw-apb-timer.h b/include/hw/timer/dw-apb-timer.h
new file mode 100644
index 0000000000..f6b83ae5ca
--- /dev/null
+++ b/include/hw/timer/dw-apb-timer.h
@@ -0,0 +1,40 @@
+/*
+ * Synopsys DesignWare APB timer
+ *
+ * Copyright (c) 2026 raoyi <rao232328@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef DW_APB_TIMER_H
+#define DW_APB_TIMER_H
+
+#include "hw/core/sysbus.h"
+#include "hw/core/irq.h"
+#include "hw/core/clock.h"
+#include "qom/object.h"
+
+#define TYPE_DW_APB_TIMER "dw-apb-timer"
+OBJECT_DECLARE_SIMPLE_TYPE(DWAPBTimerState, DW_APB_TIMER)
+
+#define DW_APB_TIMER_MAX_TIMERS 8
+
+typedef struct DWAPBTimerChannel {
+ struct ptimer_state *ptimer;
+ Clock *clk;
+ qemu_irq irq;
+ unsigned int id;
+ uint32_t load;
+ uint32_t control;
+ uint32_t int_status;
+} DWAPBTimerChannel;
+
+struct DWAPBTimerState {
+ SysBusDevice parent_obj;
+
+ MemoryRegion mmio;
+ uint32_t num_timers;
+ DWAPBTimerChannel timers[DW_APB_TIMER_MAX_TIMERS];
+};
+
+#endif
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH v5 2/3] hw/riscv: add DesignWare APB timer to K230 board
2026-08-19 13:18 [PATCH v5 0/3] hw/timer: add K230 DW APB timer support raoyi
2026-08-19 13:18 ` [PATCH v5 1/3] hw/timer: add DesignWare APB timer model raoyi
@ 2026-08-19 13:18 ` raoyi
2026-08-19 13:18 ` [PATCH v5 3/3] tests/qtest: add k230 dwapb timer test raoyi
2 siblings, 0 replies; 4+ messages in thread
From: raoyi @ 2026-08-19 13:18 UTC (permalink / raw)
To: qemu-devel, qemu-riscv
Cc: Chao Liu, Alistair Francis, Palmer Dabbelt, Weiwei Li,
Daniel Henrique Barboza, Liu Zhiwei, caojunze424, Bin Meng,
Philippe Mathieu-Daudé, raoyi
Wire up the DesignWare APB timer to the K230 SoC with
num-timers=6 and a 50 MHz clock. Connect timer IRQs
to the PLIC. Remove the previous unimplemented timer stub.
Signed-off-by: raoyi <rao232328@gmail.com>
Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
---
hw/riscv/Kconfig | 1 +
hw/riscv/k230.c | 33 ++++++++++++++++++++++++++++++---
include/hw/riscv/k230.h | 8 ++++++++
3 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
index de37c08cae..c2c01af6ee 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -162,3 +162,4 @@ config K230
select SERIAL_MM
select UNIMP
select K230_WDT
+ select DW_APB_TIMER
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 656f28190c..2e641ffec4 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"
@@ -37,6 +39,9 @@
#define K230_DIRECT_KERNEL_ADDR 0x8200000
#define K230_DIRECT_DTB_ADDR 0xa000000
+#define K230_TIMER_NUM_CHANNELS 6
+#define K230_TIMER_DEFAULT_FREQ 50000000
+
static const MemMapEntry memmap[] = {
[K230_DEV_DDRC] = { 0x00000000, 0x80000000 },
[K230_DEV_KPU_L2_CACHE] = { 0x80000000, 0x00200000 },
@@ -110,6 +115,19 @@ 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, "dw-apb-timer", &s->timer,
+ TYPE_DW_APB_TIMER);
+
+ qdev_prop_set_uint32(DEVICE(&s->timer), "num-timers",
+ K230_TIMER_NUM_CHANNELS);
+
+ Clock *timer_clk = clock_new(OBJECT(s), "timer-clk");
+ clock_set_hz(timer_clk, K230_TIMER_DEFAULT_FREQ);
+ for (int i = 0; i < K230_TIMER_NUM_CHANNELS; i++) {
+ g_autofree char *clock_name = g_strdup_printf("timer[%d]", i);
+
+ qdev_connect_clock_in(DEVICE(&s->timer), clock_name, 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 +209,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_TIMER_NUM_CHANNELS; 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 +313,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/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index 592e1c26bf..42a36664eb 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/dw-apb-timer.h"
#define C908_CPU_HARTID (0)
@@ -33,6 +34,7 @@ typedef struct K230SoCState {
RISCVHartArrayState c908_cpu; /* Small core */
K230WdtState wdt[2];
+ DWAPBTimerState 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,
};
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH v5 3/3] tests/qtest: add k230 dwapb timer test
2026-08-19 13:18 [PATCH v5 0/3] hw/timer: add K230 DW APB timer support raoyi
2026-08-19 13:18 ` [PATCH v5 1/3] hw/timer: add DesignWare APB timer model raoyi
2026-08-19 13:18 ` [PATCH v5 2/3] hw/riscv: add DesignWare APB timer to K230 board raoyi
@ 2026-08-19 13:18 ` raoyi
2 siblings, 0 replies; 4+ messages in thread
From: raoyi @ 2026-08-19 13:18 UTC (permalink / raw)
To: qemu-devel, qemu-riscv
Cc: Chao Liu, Alistair Francis, Palmer Dabbelt, Weiwei Li,
Daniel Henrique Barboza, Liu Zhiwei, caojunze424, Bin Meng,
Philippe Mathieu-Daudé, raoyi
Add qtest coverage for K230 DW APB timer, including free-running,
periodic, interrupt mask, disable behavior, current value, dynamic
reload, and multi-channel scenarios.
Add timer test file to MAINTAINERS.
Signed-off-by: raoyi <rao232328@gmail.com>
Reviewed-by: Bin Meng <bin.meng@processmission.com>
---
MAINTAINERS | 1 +
tests/qtest/k230-dwapb-timer-test.c | 274 ++++++++++++++++++++++++++++
tests/qtest/meson.build | 3 +-
3 files changed, 277 insertions(+), 1 deletion(-)
create mode 100644 tests/qtest/k230-dwapb-timer-test.c
diff --git a/MAINTAINERS b/MAINTAINERS
index cf689433ff..82e5643a93 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1830,6 +1830,7 @@ F: hw/timer/dw-apb-timer.c
F: include/hw/riscv/k230.h
F: include/hw/watchdog/k230_wdt.h
F: include/hw/timer/dw-apb-timer.h
+F: tests/qtest/k230-dwapb-timer-test.c
F: tests/qtest/k230-wdt-test.c
RX Machines
diff --git a/tests/qtest/k230-dwapb-timer-test.c b/tests/qtest/k230-dwapb-timer-test.c
new file mode 100644
index 0000000000..01bf03a8b6
--- /dev/null
+++ b/tests/qtest/k230-dwapb-timer-test.c
@@ -0,0 +1,274 @@
+/*
+ * QTest testcase for the DesignWare APB timer through the K230 machine
+ *
+ * Copyright (c) 2026 raoyi <rao232328@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#include "qemu/osdep.h"
+#include "qemu/timer.h"
+#include "libqtest.h"
+
+#define TIMER_BASE 0x91105800
+#define K230_TIMER_NUM_CHANNELS 6
+#define K230_TIMER_DEFAULT_FREQ 50000000
+
+#define DW_APB_TIMER_STRIDE 0x14
+#define DW_APB_TIMER_N_LOAD_COUNT 0x00
+#define DW_APB_TIMER_N_CURRENT_VALUE 0x04
+#define DW_APB_TIMER_N_CONTROL 0x08
+#define DW_APB_TIMER_N_EOI 0x0c
+#define DW_APB_TIMER_N_INT_STATUS 0x10
+#define DW_APB_TIMER_INT_STATUS 0xa0
+#define DW_APB_TIMER_EOI 0xa4
+#define DW_APB_TIMER_RAW_INT_STATUS 0xa8
+#define DW_APB_TIMER_CONTROL_ENABLE BIT(0)
+#define DW_APB_TIMER_CONTROL_MODE_PERIODIC BIT(1)
+#define DW_APB_TIMER_CONTROL_INT_MASK BIT(2)
+
+#define TIMER_REG(n, reg) (TIMER_BASE + (n) * DW_APB_TIMER_STRIDE + (reg))
+#define TIMER_TICK_NS (NANOSECONDS_PER_SECOND / K230_TIMER_DEFAULT_FREQ)
+
+static void timer_load(QTestState *qts, int n, uint32_t value)
+{
+ qtest_writel(qts, TIMER_REG(n, DW_APB_TIMER_N_LOAD_COUNT), value);
+}
+
+static void timer_enable(QTestState *qts, int n, uint32_t ctrl)
+{
+ qtest_writel(qts, TIMER_REG(n, DW_APB_TIMER_N_CONTROL), ctrl);
+}
+
+static void timer_disable(QTestState *qts, int n)
+{
+ qtest_writel(qts, TIMER_REG(n, DW_APB_TIMER_N_CONTROL), 0);
+}
+
+static uint32_t timer_get_and_clear_status(QTestState *qts, int n)
+{
+ uint32_t int_status = qtest_readl(qts,
+ TIMER_REG(n, DW_APB_TIMER_N_INT_STATUS));
+
+ if (int_status) {
+ qtest_readl(qts, TIMER_REG(n, DW_APB_TIMER_N_EOI));
+ }
+
+ return int_status;
+}
+
+static void test_timer_free_running(void)
+{
+ QTestState *qts = qtest_init("-machine k230");
+
+ timer_load(qts, 0, 100);
+ timer_enable(qts, 0, DW_APB_TIMER_CONTROL_ENABLE);
+
+ qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+ g_assert_cmphex(timer_get_and_clear_status(qts, 0), ==, 1);
+
+ qtest_clock_step(qts, 10ULL * TIMER_TICK_NS);
+ uint32_t cur = qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_CURRENT_VALUE));
+ g_assert_cmphex(cur, <, UINT32_MAX);
+ g_assert_cmphex(cur, >, 0);
+
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_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, DW_APB_TIMER_CONTROL_ENABLE |
+ DW_APB_TIMER_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_status(qts, 0), ==, 1);
+ uint32_t cur = qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_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, DW_APB_TIMER_CONTROL_ENABLE |
+ DW_APB_TIMER_CONTROL_MODE_PERIODIC |
+ DW_APB_TIMER_CONTROL_INT_MASK);
+
+ qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_INT_STATUS)) & 1, ==, 0);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_BASE + DW_APB_TIMER_INT_STATUS), ==, 0);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_BASE + DW_APB_TIMER_RAW_INT_STATUS) & 1, ==, 1);
+
+ /* Unmasking a pending interrupt must assert it immediately. */
+ timer_enable(qts, 0, DW_APB_TIMER_CONTROL_ENABLE |
+ DW_APB_TIMER_CONTROL_MODE_PERIODIC);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_INT_STATUS)), ==, 1);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_BASE + DW_APB_TIMER_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, DW_APB_TIMER_CONTROL_ENABLE |
+ DW_APB_TIMER_CONTROL_MODE_PERIODIC);
+
+ qtest_clock_step(qts, 101ULL * TIMER_TICK_NS + 1);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_INT_STATUS)) & 1, ==, 1);
+
+ timer_disable(qts, 0);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_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, DW_APB_TIMER_N_CURRENT_VALUE)), ==, 0);
+
+ timer_load(qts, 0, 9999);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_CURRENT_VALUE)), ==, 0);
+
+ timer_enable(qts, 0, DW_APB_TIMER_CONTROL_ENABLE);
+ qtest_clock_step(qts, 10ULL * TIMER_TICK_NS);
+ uint32_t cur_enabled = qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_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, DW_APB_TIMER_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, DW_APB_TIMER_CONTROL_ENABLE |
+ DW_APB_TIMER_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_status(qts, 0), ==, 1);
+
+ qtest_clock_step(qts, 100ULL * TIMER_TICK_NS);
+ cur = qtest_readl(qts, TIMER_REG(0, DW_APB_TIMER_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_TIMER_NUM_CHANNELS; i++) {
+ timer_load(qts, i, 100 + i * 50);
+ timer_enable(qts, i, DW_APB_TIMER_CONTROL_ENABLE |
+ DW_APB_TIMER_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 + DW_APB_TIMER_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 + DW_APB_TIMER_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 + DW_APB_TIMER_INT_STATUS);
+ g_assert_cmphex(sts, ==, 0x3f);
+
+ /* EOI_ALL clears all */
+ qtest_readl(qts, TIMER_BASE + DW_APB_TIMER_EOI);
+ sts = qtest_readl(qts, TIMER_BASE + DW_APB_TIMER_INT_STATUS);
+ g_assert_cmphex(sts, ==, 0);
+
+ qtest_quit(qts);
+}
+
+static void test_timer_reset(void)
+{
+ QTestState *qts = qtest_init("-machine k230");
+
+ timer_load(qts, 0, 100);
+ timer_enable(qts, 0, DW_APB_TIMER_CONTROL_ENABLE |
+ DW_APB_TIMER_CONTROL_MODE_PERIODIC);
+ qtest_clock_step(qts, 10ULL * TIMER_TICK_NS);
+
+ qtest_system_reset(qts);
+
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_LOAD_COUNT)), ==, 0);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_CONTROL)), ==, 0);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_REG(0, DW_APB_TIMER_N_CURRENT_VALUE)), ==, 0);
+ g_assert_cmphex(qtest_readl(qts,
+ TIMER_BASE + DW_APB_TIMER_RAW_INT_STATUS), ==, 0);
+
+ qtest_quit(qts);
+}
+
+int main(int argc, char *argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ qtest_add_func("/dw-apb-timer/free_running", test_timer_free_running);
+ qtest_add_func("/dw-apb-timer/periodic", test_timer_periodic);
+ qtest_add_func("/dw-apb-timer/int_mask", test_timer_int_mask);
+ qtest_add_func("/dw-apb-timer/disable_clears_irq",
+ test_timer_disable_clears_irq);
+ qtest_add_func("/dw-apb-timer/current_disabled",
+ test_timer_current_disabled);
+ qtest_add_func("/dw-apb-timer/dynamic_reload",
+ test_timer_dynamic_reload);
+ qtest_add_func("/dw-apb-timer/all_channels", test_timer_all_channels);
+ qtest_add_func("/dw-apb-timer/reset", test_timer_reset);
+
+ return g_test_run();
+}
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 56ff860e21..bacd8719b4 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -297,7 +297,8 @@ 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
^ permalink raw reply related [flat|nested] 4+ messages in thread