QEMU-Arm Archive on lore.kernel.org
 help / color / mirror / Atom feed
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



  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