From: Kyle Fox <kylefoxaustin.github@gmail.com>
To: qemu-devel@nongnu.org
Cc: Kyle Fox <kylefoxaustin.github@gmail.com>,
Paolo Bonzini <pbonzini@redhat.com>,
qemu-arm@nongnu.org (open list:MCIMX95-19X19-EVK...)
Subject: [PATCH 09/16] hw/timer: add i.MX 95 system counter
Date: Wed, 19 Aug 2026 21:48:27 -0500 [thread overview]
Message-ID: <20260820024834.3286721-10-kylefoxaustin.github@gmail.com> (raw)
In-Reply-To: <20260820024834.3286721-1-kylefoxaustin.github@gmail.com>
The i.MX 95 System Counter: a 24 MHz free-running up-counter with a
compare-match interrupt. On this machine it is the Linux broadcast
clockevent. Models the counter read-back, compare-value programming and
the compare IRQ (backed by a QEMUTimer).
Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
hw/timer/Kconfig | 3 +
hw/timer/imx95_sysctr.c | 241 ++++++++++++++++++++++++++++++++++++++++
hw/timer/meson.build | 1 +
hw/timer/trace-events | 2 +
4 files changed, 247 insertions(+)
create mode 100644 hw/timer/imx95_sysctr.c
diff --git a/hw/timer/Kconfig b/hw/timer/Kconfig
index e1b751a54a7..1e8c5d50d7e 100644
--- a/hw/timer/Kconfig
+++ b/hw/timer/Kconfig
@@ -68,3 +68,6 @@ config AVR_TIMER16
config HEX_QTIMER
bool
+
+config IMX95_SYSCTR
+ bool
diff --git a/hw/timer/imx95_sysctr.c b/hw/timer/imx95_sysctr.c
new file mode 100644
index 00000000000..597bb851f23
--- /dev/null
+++ b/hw/timer/imx95_sysctr.c
@@ -0,0 +1,241 @@
+/*
+ * NXP i.MX 95 System Counter (sysctr) timer
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * The system counter is a free-running up-counter plus a compare block that
+ * raises an interrupt when the counter reaches a programmed value. Linux uses
+ * it as the tick BROADCAST clockevent: the imx95 idle state `cpu-pd-wait`
+ * carries `local-timer-stop`, so a core entering cpuidle shuts down its
+ * per-CPU arch timer and depends entirely on this counter's compare interrupt
+ * to be woken. Modelling it as plain RAM (the previous stub) left idle cores
+ * with no wake source -> RCU stalls -> the boot needed `cpuidle.off=1`. This
+ * model gives a live counter + working compare IRQ so the broadcast-timer
+ * wake path works (deep cpuidle still needs cpuidle.off=1 - see the docs).
+ *
+ * Register layout (from Linux drivers/clocksource/timer-imx-sysctr.c, the
+ * imx95 quirk path which QEMU always takes - the IMX_SIP_GET_SOC_INFO SiP
+ * SMC is unimplemented here, so the driver sets SYS_CTR_IMX95_QUIRK):
+ * - read frame @ 0x20000: CNTCV_LO 0x20008 / CNTCV_HI 0x2000c (RO counter)
+ * (also exposed at 0x8 / 0xc for the non-quirk read path)
+ * - cmp frame @ 0x10000: CMPCV_LO 0x10020 / CMPCV_HI 0x10024 (compare
+ * value, RW, read-back-verified by the driver) and CMPCR 0x1002c
+ * (control: EN = bit0; the driver acks the IRQ by clearing EN, which
+ * drops the status bit and negates the interrupt).
+ *
+ * Only the master compare channel + its single interrupt are modelled (the
+ * DT exposes one IRQ; the broadcast framework needs only one channel).
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/module.h"
+#include "qemu/timer.h"
+#include "qemu/host-utils.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/irq.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+#define TYPE_IMX95_SYSCTR "imx95.sysctr"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95SysctrState, IMX95_SYSCTR)
+
+#define IMX95_SYSCTR_REG_SIZE 0x30000
+
+/* The system counter reference is the 24 MHz oscillator (nxp,no-divider). */
+#define IMX95_SYSCTR_FREQ_HZ 24000000ULL
+
+/* Register offsets (see file header). */
+#define SYSCTR_CNTCV_LO 0x8
+#define SYSCTR_CNTCV_HI 0xc
+#define SYSCTR_CMPCV_LO 0x10020
+#define SYSCTR_CMPCV_HI 0x10024
+#define SYSCTR_CMPCR 0x1002c
+#define SYSCTR_CNTCV_LO_RD 0x20008
+#define SYSCTR_CNTCV_HI_RD 0x2000c
+
+#define SYSCTR_CMPCR_EN 0x1
+
+struct IMX95SysctrState {
+ SysBusDevice parent_obj;
+ MemoryRegion iomem;
+ qemu_irq irq;
+ QEMUTimer timer;
+
+ uint32_t cmpcv_lo;
+ uint32_t cmpcv_hi;
+ uint32_t cmpcr;
+};
+
+static uint64_t imx95_sysctr_count(void)
+{
+ return muldiv64(qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL),
+ IMX95_SYSCTR_FREQ_HZ, NANOSECONDS_PER_SECOND);
+}
+
+static uint64_t imx95_sysctr_cmpcv(IMX95SysctrState *s)
+{
+ return ((uint64_t)s->cmpcv_hi << 32) | s->cmpcv_lo;
+}
+
+/*
+ * (Re)arm or disarm the compare. When the compare is enabled, schedule the
+ * timer for the virtual-time instant the counter reaches CMPCV; a value
+ * already in the past makes timer_mod fire at the next opportunity. When
+ * disabled, cancel the timer and negate the interrupt (this is how the
+ * driver's ISR acks: it clears EN).
+ */
+static void imx95_sysctr_update(IMX95SysctrState *s)
+{
+ if (s->cmpcr & SYSCTR_CMPCR_EN) {
+ uint64_t deadline = muldiv64(imx95_sysctr_cmpcv(s),
+ NANOSECONDS_PER_SECOND,
+ IMX95_SYSCTR_FREQ_HZ);
+ trace_imx95_sysctr_cmp(imx95_sysctr_cmpcv(s));
+ timer_mod(&s->timer, deadline);
+ } else {
+ timer_del(&s->timer);
+ qemu_set_irq(s->irq, 0);
+ }
+}
+
+static void imx95_sysctr_timer_cb(void *opaque)
+{
+ IMX95SysctrState *s = opaque;
+
+ if (s->cmpcr & SYSCTR_CMPCR_EN) {
+ trace_imx95_sysctr_expire();
+ qemu_set_irq(s->irq, 1);
+ }
+}
+
+static uint64_t imx95_sysctr_read(void *opaque, hwaddr offset, unsigned size)
+{
+ IMX95SysctrState *s = opaque;
+ uint64_t cnt = imx95_sysctr_count();
+
+ switch (offset) {
+ case SYSCTR_CNTCV_LO:
+ case SYSCTR_CNTCV_LO_RD:
+ return cnt & 0xffffffff;
+ case SYSCTR_CNTCV_HI:
+ case SYSCTR_CNTCV_HI_RD:
+ return (cnt >> 32) & 0xffffffff;
+ case SYSCTR_CMPCV_LO:
+ return s->cmpcv_lo;
+ case SYSCTR_CMPCV_HI:
+ return s->cmpcv_hi;
+ case SYSCTR_CMPCR:
+ return s->cmpcr;
+ default:
+ return 0;
+ }
+}
+
+static void imx95_sysctr_write(void *opaque, hwaddr offset,
+ uint64_t value, unsigned size)
+{
+ IMX95SysctrState *s = opaque;
+
+ switch (offset) {
+ case SYSCTR_CMPCV_LO:
+ s->cmpcv_lo = value;
+ imx95_sysctr_update(s);
+ break;
+ case SYSCTR_CMPCV_HI:
+ s->cmpcv_hi = value;
+ imx95_sysctr_update(s);
+ break;
+ case SYSCTR_CMPCR:
+ s->cmpcr = value;
+ imx95_sysctr_update(s);
+ break;
+ default:
+ break;
+ }
+}
+
+static const MemoryRegionOps imx95_sysctr_ops = {
+ .read = imx95_sysctr_read,
+ .write = imx95_sysctr_write,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .impl = {
+ .min_access_size = 4,
+ .max_access_size = 4,
+ },
+ .valid = {
+ .min_access_size = 4,
+ .max_access_size = 4,
+ },
+};
+
+static void imx95_sysctr_reset_hold(Object *obj, ResetType type)
+{
+ IMX95SysctrState *s = IMX95_SYSCTR(obj);
+
+ s->cmpcv_lo = 0;
+ s->cmpcv_hi = 0;
+ s->cmpcr = 0;
+ timer_del(&s->timer);
+ qemu_set_irq(s->irq, 0);
+}
+
+static void imx95_sysctr_init(Object *obj)
+{
+ SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+ IMX95SysctrState *s = IMX95_SYSCTR(obj);
+
+ memory_region_init_io(&s->iomem, obj, &imx95_sysctr_ops, s,
+ TYPE_IMX95_SYSCTR, IMX95_SYSCTR_REG_SIZE);
+ sysbus_init_mmio(sbd, &s->iomem);
+ sysbus_init_irq(sbd, &s->irq);
+ timer_init_ns(&s->timer, QEMU_CLOCK_VIRTUAL, imx95_sysctr_timer_cb, s);
+}
+
+static int imx95_sysctr_post_load(void *opaque, int version_id)
+{
+ /* Re-arm the compare (and re-raise a past-due IRQ) from restored state. */
+ imx95_sysctr_update(opaque);
+ return 0;
+}
+
+static const VMStateDescription vmstate_imx95_sysctr = {
+ .name = TYPE_IMX95_SYSCTR,
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .post_load = imx95_sysctr_post_load,
+ .fields = (const VMStateField[]) {
+ VMSTATE_UINT32(cmpcv_lo, IMX95SysctrState),
+ VMSTATE_UINT32(cmpcv_hi, IMX95SysctrState),
+ VMSTATE_UINT32(cmpcr, IMX95SysctrState),
+ VMSTATE_TIMER(timer, IMX95SysctrState),
+ VMSTATE_END_OF_LIST()
+ },
+};
+
+static void imx95_sysctr_class_init(ObjectClass *klass, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+ dc->vmsd = &vmstate_imx95_sysctr;
+ rc->phases.hold = imx95_sysctr_reset_hold;
+ set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+ dc->desc = "NXP i.MX 95 system counter timer";
+}
+
+static const TypeInfo imx95_sysctr_info = {
+ .name = TYPE_IMX95_SYSCTR,
+ .parent = TYPE_SYS_BUS_DEVICE,
+ .instance_size = sizeof(IMX95SysctrState),
+ .instance_init = imx95_sysctr_init,
+ .class_init = imx95_sysctr_class_init,
+};
+
+static void imx95_sysctr_register_types(void)
+{
+ type_register_static(&imx95_sysctr_info);
+}
+
+type_init(imx95_sysctr_register_types)
diff --git a/hw/timer/meson.build b/hw/timer/meson.build
index 8323efaf46b..5ce27a6df0a 100644
--- a/hw/timer/meson.build
+++ b/hw/timer/meson.build
@@ -36,3 +36,4 @@ 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_HEX_QTIMER', if_true: files('qct-qtimer.c'))
+system_ss.add(when: 'CONFIG_IMX95_SYSCTR', if_true: files('imx95_sysctr.c'))
diff --git a/hw/timer/trace-events b/hw/timer/trace-events
index 636310f8caa..df457bbb967 100644
--- a/hw/timer/trace-events
+++ b/hw/timer/trace-events
@@ -133,3 +133,5 @@ imx_epit_cmp(uint32_t sr) "sr was %d"
qtimer_interrupt(void) "qtimer interrupt line updated"
qtimer_read(uint64_t offset) "offset 0x%" PRIx64
qtimer_write(uint64_t offset, uint64_t value) "offset 0x%" PRIx64 " value 0x%" PRIx64
+imx95_sysctr_cmp(uint64_t cmpcv) "compare value 0x%" PRIx64
+imx95_sysctr_expire(void) "compare fired"
--
2.34.1
next prev parent reply other threads:[~2026-08-20 2:51 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
[not found] <20260820024834.3286721-1-kylefoxaustin.github@gmail.com>
2026-08-20 2:48 ` [PATCH 02/16] hw/arm/boot: let a board preset initrd_start Kyle Fox
2026-08-20 2:48 ` [PATCH 03/16] target/arm: opt-in align-down for a misaligned PMSAv7 MPU RBAR Kyle Fox
2026-08-20 2:48 ` [PATCH 04/16] hw/arm/armv7m: forward pmsav7-rbar-align-down to the CPU Kyle Fox
2026-08-20 2:48 ` [PATCH 05/16] hw/char: add i.MX LPUART Kyle Fox
2026-08-20 2:48 ` [PATCH 06/16] hw/i2c: add i.MX LPI2C Kyle Fox
2026-08-20 2:48 ` [PATCH 07/16] hw/misc: add i.MX Messaging Unit (MU v2) Kyle Fox
2026-08-20 2:48 ` [PATCH 08/16] hw/misc: add NXP EdgeLock Enclave (ELE) responder Kyle Fox
2026-08-20 2:48 ` Kyle Fox [this message]
2026-08-20 2:48 ` [PATCH 10/16] hw/misc: add i.MX 95 watchdog Kyle Fox
2026-08-20 2:48 ` [PATCH 11/16] hw/misc: add i.MX 95 ANATOP/AONMIX/GPC/SRC power and clock blocks Kyle Fox
2026-08-20 2:48 ` [PATCH 12/16] hw/misc: add i.MX 95 PMIC (PF09/PF53/PCAL6408A) and xcache controllers Kyle Fox
2026-08-20 2:48 ` [PATCH 13/16] hw/misc: add i.MX 95 DPU command-sequencer stub (headless) Kyle Fox
2026-08-20 2:48 ` [PATCH 14/16] hw/arm: add i.MX 95 SoC container (fsl-imx95) Kyle Fox
2026-08-20 2:48 ` [PATCH 15/16] hw/arm: add i.MX 95 19x19 EVK board Kyle Fox
2026-08-20 2:48 ` [PATCH 16/16] docs, MAINTAINERS, tests/functional: add i.MX 95 EVK Kyle Fox
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=20260820024834.3286721-10-kylefoxaustin.github@gmail.com \
--to=kylefoxaustin.github@gmail.com \
--cc=pbonzini@redhat.com \
--cc=qemu-arm@nongnu.org \
--cc=qemu-devel@nongnu.org \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox