[PATCH 10/16] hw/misc: add i.MX 95 watchdog

Kyle Fox <[email protected]>
Newsgroups org.nongnu.qemu-arm,org.nongnu.qemu-devel
Message-ID <[email protected]>
The i.MX 95 watchdog. A register-level model of the CS/CNT/TOVAL/WIN
registers and the unlock sequence, enough for the SM and Linux to
configure and refresh it. It has no live timer and does not reset the
machine on timeout; it exists so the watchdog node probes and is
programmable rather than faulting on first access.

Signed-off-by: Kyle Fox <[email protected]>
---
 hw/misc/Kconfig      |   3 +
 hw/misc/imx95_wdog.c | 198 +++++++++++++++++++++++++++++++++++++++++++
 hw/misc/meson.build  |   1 +
 hw/misc/trace-events |   2 +
 4 files changed, 204 insertions(+)
 create mode 100644 hw/misc/imx95_wdog.c

diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index ded60e21a6c..2bae76b4ec1 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -268,3 +268,6 @@ config IMX_MU
 config IMX95_ELE_SERVER
     bool
     select IMX_MU
+
+config IMX95_WDOG
+    bool
diff --git a/hw/misc/imx95_wdog.c b/hw/misc/imx95_wdog.c
new file mode 100644
index 00000000000..4fe91a92b6d
--- /dev/null
+++ b/hw/misc/imx95_wdog.c
@@ -0,0 +1,198 @@
+/*
+ * NXP i.MX 95 ULP Watchdog stub model
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Minimal stub of the ULP watchdog (compatible "fsl,imx93-wdt") used
+ * by U-Boot SPL's arch_cpu_init() to disable WDG3/4/5 before the
+ * console comes up. SPL's disable_wdog() reads CS @ 0x00 and either
+ * early-exits if the enable bit (0x80) is clear or runs an unlock +
+ * disable handshake that polls for CS.ULK (0x800) and CS.RCS (0x400).
+ *
+ * The stub leaves the watchdog disabled at reset, so the first CS
+ * read returns 0 and disable_wdog() returns immediately. The unlock
+ * sequence is implemented for the cases where the guest forces the
+ * unlock anyway: a write of 0xD928C520 to CNT @ 0x04 sets CS.ULK,
+ * any subsequent write to CS sets CS.RCS. No timer behaviour is
+ * modelled.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+#define TYPE_IMX95_WDOG "imx95.wdog"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95WDogState, IMX95_WDOG)
+
+#define IMX95_WDOG_REG_SIZE     0x10000
+
+/* Register offsets. */
+#define WDOG_CS                 0x00
+#define WDOG_CNT                0x04
+#define WDOG_TOVAL              0x08
+#define WDOG_WIN                0x0C
+
+/* CS bit fields exercised by U-Boot. */
+#define CS_EN                   0x00000080
+#define CS_RCS                  0x00000400  /* reconfig complete */
+#define CS_ULK                  0x00000800  /* unlocked */
+
+#define UNLOCK_WORD             0xD928C520
+
+struct IMX95WDogState {
+    SysBusDevice    parent_obj;
+
+    MemoryRegion    iomem;
+
+    uint32_t        cs;
+    uint32_t        cnt;
+    uint32_t        toval;
+    uint32_t        win;
+};
+
+static uint64_t imx95_wdog_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMX95WDogState *s = opaque;
+
+    switch (offset) {
+    case WDOG_CS:
+        return s->cs;
+    case WDOG_CNT:
+        return s->cnt;
+    case WDOG_TOVAL:
+        return s->toval;
+    case WDOG_WIN:
+        return s->win;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: bad read offset 0x%" HWADDR_PRIx "\n",
+                      __func__, offset);
+        return 0;
+    }
+}
+
+static void imx95_wdog_write(void *opaque, hwaddr offset,
+                             uint64_t value, unsigned size)
+{
+    IMX95WDogState *s = opaque;
+
+    switch (offset) {
+    case WDOG_CS:
+        /*
+         * SPL writes CS to update timeout + window and to clear EN.
+         * Acknowledge by setting RCS (reconfig complete) so the
+         * "wait for RCS" loop at the tail of disable_wdog() exits.
+         */
+        s->cs = (value & ~CS_RCS) | CS_RCS;
+        trace_imx95_wdog_config(s->cs);
+        break;
+
+    case WDOG_CNT:
+        s->cnt = value;
+        /*
+         * A 32-bit UNLOCK_WORD write to CNT puts the watchdog into
+         * the unlocked state. Any other value (e.g. REFRESH_WORD)
+         * is just a refresh ping; ignore.
+         */
+        if ((uint32_t)value == UNLOCK_WORD) {
+            s->cs |= CS_ULK;
+            trace_imx95_wdog_unlock();
+        }
+        break;
+
+    case WDOG_TOVAL:
+        s->toval = value;
+        break;
+
+    case WDOG_WIN:
+        s->win = value;
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: bad write offset 0x%" HWADDR_PRIx
+                      " value 0x%" PRIx64 "\n",
+                      __func__, offset, value);
+        break;
+    }
+}
+
+static const MemoryRegionOps imx95_wdog_ops = {
+    .read = imx95_wdog_read,
+    .write = imx95_wdog_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_wdog_reset_hold(Object *obj, ResetType type)
+{
+    IMX95WDogState *s = IMX95_WDOG(obj);
+
+    /* Watchdog disabled at reset: CS = 0 makes disable_wdog() early-exit. */
+    s->cs = 0;
+    s->cnt = 0;
+    s->toval = 0x400;
+    s->win = 0;
+}
+
+static void imx95_wdog_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMX95WDogState *s = IMX95_WDOG(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx95_wdog_ops, s,
+                          TYPE_IMX95_WDOG, IMX95_WDOG_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+}
+
+static const VMStateDescription vmstate_imx95_wdog = {
+    .name = TYPE_IMX95_WDOG,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(cs, IMX95WDogState),
+        VMSTATE_UINT32(cnt, IMX95WDogState),
+        VMSTATE_UINT32(toval, IMX95WDogState),
+        VMSTATE_UINT32(win, IMX95WDogState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx95_wdog_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx95_wdog;
+    rc->phases.hold = imx95_wdog_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX 95 ULP watchdog (stub)";
+}
+
+static const TypeInfo imx95_wdog_info = {
+    .name           = TYPE_IMX95_WDOG,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95WDogState),
+    .instance_init  = imx95_wdog_init,
+    .class_init     = imx95_wdog_class_init,
+};
+
+static void imx95_wdog_register_types(void)
+{
+    type_register_static(&imx95_wdog_info);
+}
+
+type_init(imx95_wdog_register_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index b0dade72c6c..3f8cd1d825e 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -173,3 +173,4 @@ system_ss.add(when: 'CONFIG_LASI', if_true: files('lasi.c'))
 system_ss.add(when: 'CONFIG_AXIADO_CLK', if_true: files('axiado_clk.c'))
 system_ss.add(when: 'CONFIG_IMX_MU', if_true: files('imx_mu.c'))
 system_ss.add(when: 'CONFIG_IMX95_ELE_SERVER', if_true: files('imx95_ele_server.c'))
+system_ss.add(when: 'CONFIG_IMX95_WDOG', if_true: files('imx95_wdog.c'))
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index 6b3cff74549..584c1f90963 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -448,3 +448,5 @@ imx_mu_doorbell(unsigned idx) "doorbell GIR channel %u"
 imx_mu_gip(unsigned idx) "GIP assert channel %u"
 imx95_ele_msg(uint8_t command, uint8_t tag, uint32_t size) "received cmd 0x%02x tag 0x%02x size %u"
 imx95_ele_response(uint8_t command) "response cmd 0x%02x"
+imx95_wdog_config(uint32_t cs) "CS <- 0x%08x"
+imx95_wdog_unlock(void) "unlock word written"
-- 
2.34.1
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.