* [PATCH v4 0/3] hw/timer: add DesignWare APB timer
@ 2026-08-13 13:36 raoyi
2026-08-13 13:36 ` [PATCH v4 1/3] hw/timer: add DesignWare APB timer model raoyi
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: raoyi @ 2026-08-13 13:36 UTC (permalink / raw)
To: qemu-devel
Cc: qemu-riscv, alistair.francis, palmer, liwei1518, daniel.barboza,
zhiwei_liu, chao.liu, bmeng.cn, philmd, pbonzini, lvivier,
farosas, caojunze424, raoyi
Add a generic DesignWare APB timer device model, and wire it
up for the K230 machine with num-timers=6 and a 6.25 MHz clock.
Changes since v3 (address review comments from Philippe Mathieu-Daude):
- Drop Clock framework mention from the commit message
- Cover the device in MAINTAINERS (device files in patch 1/3,
test file in patch 3/3)
- Move trace calls to function entry
- Drop the addr mask; the MMIO region stays at 0x100 (the IP
register implementation range; the SoC aperture is board-level)
- Hard-code comp-version (0x3231312A, "211*", IP component version)
- Use the Resettable API (phases.hold) instead of legacy reset
- Add unrealize to free per-channel ptimers
- Keep register offsets and bit definitions in the C file
- Drop private/public comments in the header
- Move K230 timer macros into k230.c
raoyi (3):
hw/timer: add DesignWare APB timer model
hw/riscv: add DesignWare APB timer to K230 board
tests/qtest: add k230 dwapb timer test
MAINTAINERS | 3 +
hw/riscv/Kconfig | 1 +
hw/riscv/k230.c | 33 ++-
hw/timer/Kconfig | 4 +
hw/timer/dw-apb-timer.c | 376 ++++++++++++++++++++++++++++
hw/timer/meson.build | 1 +
hw/timer/trace-events | 8 +
include/hw/riscv/k230.h | 8 +
include/hw/timer/dw-apb-timer.h | 40 +++
tests/qtest/k230-dwapb-timer-test.c | 274 ++++++++++++++++++++
tests/qtest/meson.build | 4 +-
11 files changed, 748 insertions(+), 4 deletions(-)
create mode 100644 hw/timer/dw-apb-timer.c
create mode 100644 include/hw/timer/dw-apb-timer.h
create mode 100644 tests/qtest/k230-dwapb-timer-test.c
--
2.53.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v4 1/3] hw/timer: add DesignWare APB timer model
2026-08-13 13:36 [PATCH v4 0/3] hw/timer: add DesignWare APB timer raoyi
@ 2026-08-13 13:36 ` raoyi
2026-08-13 13:36 ` [PATCH v4 2/3] hw/riscv: add DesignWare APB timer to K230 board raoyi
2026-08-13 13:36 ` [PATCH v4 3/3] tests/qtest: add k230 dwapb timer test raoyi
2 siblings, 0 replies; 4+ messages in thread
From: raoyi @ 2026-08-13 13:36 UTC (permalink / raw)
To: qemu-devel
Cc: qemu-riscv, alistair.francis, palmer, liwei1518, daniel.barboza,
zhiwei_liu, chao.liu, bmeng.cn, philmd, pbonzini, lvivier,
farosas, caojunze424, 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..0b994e1570
--- /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 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));
+}
+
+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);
+ 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)) << 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) {
+ 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 v4 2/3] hw/riscv: add DesignWare APB timer to K230 board
2026-08-13 13:36 [PATCH v4 0/3] hw/timer: add DesignWare APB timer raoyi
2026-08-13 13:36 ` [PATCH v4 1/3] hw/timer: add DesignWare APB timer model raoyi
@ 2026-08-13 13:36 ` raoyi
2026-08-13 13:36 ` [PATCH v4 3/3] tests/qtest: add k230 dwapb timer test raoyi
2 siblings, 0 replies; 4+ messages in thread
From: raoyi @ 2026-08-13 13:36 UTC (permalink / raw)
To: qemu-devel
Cc: qemu-riscv, alistair.francis, palmer, liwei1518, daniel.barboza,
zhiwei_liu, chao.liu, bmeng.cn, philmd, pbonzini, lvivier,
farosas, caojunze424, raoyi
Wire up the DesignWare APB timer to the K230 SoC with
num-timers=6 and a 6.25 MHz clock. Connect timer IRQs
to the PLIC. Remove the previous unimplemented timer stub.
Signed-off-by: raoyi <rao232328@gmail.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..78a05492ad 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 6250000
+
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 v4 3/3] tests/qtest: add k230 dwapb timer test
2026-08-13 13:36 [PATCH v4 0/3] hw/timer: add DesignWare APB timer raoyi
2026-08-13 13:36 ` [PATCH v4 1/3] hw/timer: add DesignWare APB timer model raoyi
2026-08-13 13:36 ` [PATCH v4 2/3] hw/riscv: add DesignWare APB timer to K230 board raoyi
@ 2026-08-13 13:36 ` raoyi
2 siblings, 0 replies; 4+ messages in thread
From: raoyi @ 2026-08-13 13:36 UTC (permalink / raw)
To: qemu-devel
Cc: qemu-riscv, alistair.francis, palmer, liwei1518, daniel.barboza,
zhiwei_liu, chao.liu, bmeng.cn, philmd, pbonzini, lvivier,
farosas, caojunze424, 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>
---
MAINTAINERS | 1 +
tests/qtest/k230-dwapb-timer-test.c | 274 ++++++++++++++++++++++++++++
tests/qtest/meson.build | 4 +-
3 files changed, 278 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..a74f36999e
--- /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 6250000
+
+#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 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);
+
+ 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..295a724116 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -297,7 +297,9 @@ 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') and
+ config_all_devices.has_key('CONFIG_DW_APB_TIMER') ?
+ ['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
end of thread, other threads:[~2026-08-13 13:38 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-13 13:36 [PATCH v4 0/3] hw/timer: add DesignWare APB timer raoyi
2026-08-13 13:36 ` [PATCH v4 1/3] hw/timer: add DesignWare APB timer model raoyi
2026-08-13 13:36 ` [PATCH v4 2/3] hw/riscv: add DesignWare APB timer to K230 board raoyi
2026-08-13 13:36 ` [PATCH v4 3/3] tests/qtest: add k230 dwapb timer test raoyi
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.