From: raoyi <rao232328@gmail.com>
To: qemu-devel@nongnu.org
Cc: qemu-riscv@nongnu.org, alistair.francis@wdc.com,
palmer@dabbelt.com, liwei1518@gmail.com,
daniel.barboza@oss.qualcomm.com, zhiwei_liu@linux.alibaba.com,
chao.liu@processmission.com, bmeng.cn@gmail.com,
philmd@oss.qualcomm.com, pbonzini@redhat.com, lvivier@redhat.com,
farosas@suse.de, caojunze424@gmail.com,
raoyi <rao232328@gmail.com>
Subject: [PATCH v4 1/3] hw/timer: add DesignWare APB timer model
Date: Thu, 13 Aug 2026 21:36:41 +0800 [thread overview]
Message-ID: <20260813133643.20380-2-rao232328@gmail.com> (raw)
In-Reply-To: <20260813133643.20380-1-rao232328@gmail.com>
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
next prev parent reply other threads:[~2026-08-13 13:38 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-13 13:36 [PATCH v4 0/3] hw/timer: add DesignWare APB timer raoyi
2026-08-13 13:36 ` raoyi [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260813133643.20380-2-rao232328@gmail.com \
--to=rao232328@gmail.com \
--cc=alistair.francis@wdc.com \
--cc=bmeng.cn@gmail.com \
--cc=caojunze424@gmail.com \
--cc=chao.liu@processmission.com \
--cc=daniel.barboza@oss.qualcomm.com \
--cc=farosas@suse.de \
--cc=liwei1518@gmail.com \
--cc=lvivier@redhat.com \
--cc=palmer@dabbelt.com \
--cc=pbonzini@redhat.com \
--cc=philmd@oss.qualcomm.com \
--cc=qemu-devel@nongnu.org \
--cc=qemu-riscv@nongnu.org \
--cc=zhiwei_liu@linux.alibaba.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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.