[PATCH 12/16] hw/misc: add i.MX 95 PMIC (PF09/PF53/PCAL6408A) and xcache controllers

Kyle Fox <[email protected]>
Newsgroups org.nongnu.qemu-arm,org.nongnu.qemu-devel
Message-ID <[email protected]>
The i.MX 95 EVK power devices on LPI2C - the PF09 and PF53 regulators
and the PCAL6408A IO expander that the SM firmware sequences during
power-up - modelled as I2C-slave register files, plus the xcache (cache
controller) register block.

Signed-off-by: Kyle Fox <[email protected]>
---
 hw/misc/Kconfig        |   7 +
 hw/misc/imx95_pmic.c   | 357 +++++++++++++++++++++++++++++++++++++++++
 hw/misc/imx95_xcache.c | 185 +++++++++++++++++++++
 hw/misc/meson.build    |   2 +
 hw/misc/trace-events   |   8 +
 5 files changed, 559 insertions(+)
 create mode 100644 hw/misc/imx95_pmic.c
 create mode 100644 hw/misc/imx95_xcache.c

diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index 1e1cae92f6f..e1b8c390312 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -283,3 +283,10 @@ config IMX95_GPC
 
 config IMX95_SRC
     bool
+
+config IMX95_PMIC
+    bool
+    select I2C
+
+config IMX95_XCACHE
+    bool
diff --git a/hw/misc/imx95_pmic.c b/hw/misc/imx95_pmic.c
new file mode 100644
index 00000000000..e7101f1ed06
--- /dev/null
+++ b/hw/misc/imx95_pmic.c
@@ -0,0 +1,357 @@
+/*
+ * Minimal I2C device models for the i.MX 95 EVK System Manager:
+ *   - PF09 (PF0900) PMIC          (TYPE_PF09_PMIC,  addr 0x08, CRC'd)
+ *   - PCAL6408A 8-bit IO expander (TYPE_PCAL6408A,  addr 0x20)
+ *   - PF53 buck regulator         (TYPE_PF53_PMIC,  addr 0x2a/0x29)
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * The NXP SM firmware probes/configures both over LPI2C1 during board
+ * init (BRD_SM_SerialDevicesInit); a failed transfer aborts SM init.
+ * These are register-file models: writes store, reads return the stored
+ * byte. The PF09 driver protects each transfer with a J1850 CRC, so the
+ * PF09 read path appends the CRC the driver expects
+ * (CRC_J1850((devAddr<<1)|1, reg, data)); without it PF09_Init fails.
+ * No actual PMIC/regulator or GPIO behaviour is modelled.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/module.h"
+#include "hw/i2c/i2c.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+/* ---- J1850 CRC (matches imx-sm components/crc/crc.c CRC_J1850) ---- */
+#define CRC_J1850_POLY  0x1Du
+
+static uint8_t crc_j1850(const uint8_t *p, unsigned n)
+{
+    uint32_t crc = 0xffu;
+
+    for (unsigned i = 0; i < n; i++) {
+        crc ^= p[i];
+        for (unsigned b = 0; b < 8; b++) {
+            crc = (crc & 0x80u) ? ((crc << 1) ^ CRC_J1850_POLY)
+                                : ((crc << 1) & 0xffu);
+        }
+    }
+    return (uint8_t)crc;
+}
+
+/* ============================ PF09 PMIC ============================ */
+
+#define TYPE_PF09_PMIC "pf09-pmic"
+OBJECT_DECLARE_SIMPLE_TYPE(PF09State, PF09_PMIC)
+
+#define PF09_NUM_REG    256
+
+struct PF09State {
+    I2CSlave    parent_obj;
+
+    uint8_t     regs[PF09_NUM_REG];
+    uint8_t     cur_reg;
+    uint32_t    wcount;     /* bytes received since START(write) */
+    uint32_t    rcount;     /* bytes returned since START(read) */
+};
+
+static int pf09_event(I2CSlave *i2c, enum i2c_event event)
+{
+    PF09State *s = PF09_PMIC(i2c);
+
+    switch (event) {
+    case I2C_START_SEND:
+        s->wcount = 0;
+        break;
+    case I2C_START_RECV:
+        s->rcount = 0;
+        break;
+    default:
+        break;
+    }
+    return 0;   /* ACK */
+}
+
+static int pf09_send(I2CSlave *i2c, uint8_t data)
+{
+    PF09State *s = PF09_PMIC(i2c);
+
+    if (s->wcount == 0) {
+        s->cur_reg = data;          /* register address */
+    } else if (s->wcount == 1) {
+        s->regs[s->cur_reg] = data; /* data byte */
+        trace_pf09_reg_write(s->cur_reg, data);
+    }
+    /* wcount >= 2 is the trailing CRC byte: accept and ignore. */
+    s->wcount++;
+    return 0;   /* ACK */
+}
+
+static uint8_t pf09_recv(I2CSlave *i2c)
+{
+    PF09State *s = PF09_PMIC(i2c);
+    uint8_t val;
+
+    if (s->rcount == 0) {
+        val = s->regs[s->cur_reg];
+        trace_pf09_reg_read(s->cur_reg, val);
+    } else {
+        /* CRC over (devAddr<<1)|1, reg, data - what PF09_PmicRead checks. */
+        uint8_t buf[3] = {
+            (uint8_t)((i2c->address << 1) | 1u),
+            s->cur_reg,
+            s->regs[s->cur_reg],
+        };
+        val = crc_j1850(buf, 3);
+    }
+    s->rcount++;
+    return val;
+}
+
+static void pf09_reset_hold(Object *obj, ResetType type)
+{
+    PF09State *s = PF09_PMIC(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+    s->cur_reg = 0;
+    s->wcount = 0;
+    s->rcount = 0;
+    /* REV_ID >= 0x20 lets PF09_Init skip the STANDBY-monitor write. */
+    s->regs[0x00] = 0x20;
+}
+
+static const VMStateDescription vmstate_pf09 = {
+    .name = TYPE_PF09_PMIC,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_I2C_SLAVE(parent_obj, PF09State),
+        VMSTATE_UINT8_ARRAY(regs, PF09State, PF09_NUM_REG),
+        VMSTATE_UINT8(cur_reg, PF09State),
+        VMSTATE_UINT32(wcount, PF09State),
+        VMSTATE_UINT32(rcount, PF09State),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void pf09_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
+
+    k->event = pf09_event;
+    k->recv = pf09_recv;
+    k->send = pf09_send;
+    dc->vmsd = &vmstate_pf09;
+    rc->phases.hold = pf09_reset_hold;
+    dc->desc = "NXP PF09 PMIC (i.MX95 SM stub)";
+}
+
+static const TypeInfo pf09_info = {
+    .name          = TYPE_PF09_PMIC,
+    .parent        = TYPE_I2C_SLAVE,
+    .instance_size = sizeof(PF09State),
+    .class_init    = pf09_class_init,
+};
+
+/* ========================== PCAL6408A ============================= */
+
+#define TYPE_PCAL6408A "pcal6408a"
+OBJECT_DECLARE_SIMPLE_TYPE(PCAL6408AState, PCAL6408A)
+
+#define PCAL6408A_NUM_REG   256
+
+struct PCAL6408AState {
+    I2CSlave    parent_obj;
+
+    uint8_t     regs[PCAL6408A_NUM_REG];
+    uint8_t     cur_reg;
+    uint32_t    wcount;
+};
+
+static int pcal6408a_event(I2CSlave *i2c, enum i2c_event event)
+{
+    PCAL6408AState *s = PCAL6408A(i2c);
+
+    if (event == I2C_START_SEND) {
+        s->wcount = 0;
+    }
+    return 0;
+}
+
+static int pcal6408a_send(I2CSlave *i2c, uint8_t data)
+{
+    PCAL6408AState *s = PCAL6408A(i2c);
+
+    if (s->wcount == 0) {
+        s->cur_reg = data;
+    } else {
+        trace_pcal6408a_reg_write(s->cur_reg, data);
+        s->regs[s->cur_reg++] = data;
+    }
+    s->wcount++;
+    return 0;
+}
+
+static uint8_t pcal6408a_recv(I2CSlave *i2c)
+{
+    PCAL6408AState *s = PCAL6408A(i2c);
+    uint8_t val = s->regs[s->cur_reg];
+
+    trace_pcal6408a_reg_read(s->cur_reg, val);
+    s->cur_reg++;
+    return val;
+}
+
+static void pcal6408a_reset_hold(Object *obj, ResetType type)
+{
+    PCAL6408AState *s = PCAL6408A(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+    s->cur_reg = 0;
+    s->wcount = 0;
+}
+
+static const VMStateDescription vmstate_pcal6408a = {
+    .name = TYPE_PCAL6408A,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_I2C_SLAVE(parent_obj, PCAL6408AState),
+        VMSTATE_UINT8_ARRAY(regs, PCAL6408AState, PCAL6408A_NUM_REG),
+        VMSTATE_UINT8(cur_reg, PCAL6408AState),
+        VMSTATE_UINT32(wcount, PCAL6408AState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void pcal6408a_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
+
+    k->event = pcal6408a_event;
+    k->recv = pcal6408a_recv;
+    k->send = pcal6408a_send;
+    dc->vmsd = &vmstate_pcal6408a;
+    rc->phases.hold = pcal6408a_reset_hold;
+    dc->desc = "NXP PCAL6408A IO expander (i.MX95 SM stub)";
+}
+
+static const TypeInfo pcal6408a_info = {
+    .name          = TYPE_PCAL6408A,
+    .parent        = TYPE_I2C_SLAVE,
+    .instance_size = sizeof(PCAL6408AState),
+    .class_init    = pcal6408a_class_init,
+};
+
+/* ============================ PF53 PMIC ============================ */
+
+/*
+ * PF5301 / PF5302 (PF53-family) buck regulators on the same LPI2C bus.
+ * BRD_SM_SerialDevicesInit calls PF53_Init() on each, which only reads
+ * the DEV_ID register and checks that the transfer ACKs (it does not
+ * validate the value, and the SM configures crcEn=false for these). So a
+ * plain register file that ACKs is sufficient to get past SM init.
+ */
+#define TYPE_PF53_PMIC "pf53-pmic"
+OBJECT_DECLARE_SIMPLE_TYPE(PF53State, PF53_PMIC)
+
+#define PF53_NUM_REG    256
+
+struct PF53State {
+    I2CSlave    parent_obj;
+
+    uint8_t     regs[PF53_NUM_REG];
+    uint8_t     cur_reg;
+    uint32_t    wcount;
+};
+
+static int pf53_event(I2CSlave *i2c, enum i2c_event event)
+{
+    PF53State *s = PF53_PMIC(i2c);
+
+    if (event == I2C_START_SEND) {
+        s->wcount = 0;
+    }
+    return 0;
+}
+
+static int pf53_send(I2CSlave *i2c, uint8_t data)
+{
+    PF53State *s = PF53_PMIC(i2c);
+
+    if (s->wcount == 0) {
+        s->cur_reg = data;
+    } else {
+        trace_pf53_reg_write(s->cur_reg, data);
+        s->regs[s->cur_reg++] = data;
+    }
+    s->wcount++;
+    return 0;
+}
+
+static uint8_t pf53_recv(I2CSlave *i2c)
+{
+    PF53State *s = PF53_PMIC(i2c);
+    uint8_t val = s->regs[s->cur_reg];
+
+    trace_pf53_reg_read(s->cur_reg, val);
+    s->cur_reg++;
+    return val;
+}
+
+static void pf53_reset_hold(Object *obj, ResetType type)
+{
+    PF53State *s = PF53_PMIC(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+    s->cur_reg = 0;
+    s->wcount = 0;
+}
+
+static const VMStateDescription vmstate_pf53 = {
+    .name = TYPE_PF53_PMIC,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_I2C_SLAVE(parent_obj, PF53State),
+        VMSTATE_UINT8_ARRAY(regs, PF53State, PF53_NUM_REG),
+        VMSTATE_UINT8(cur_reg, PF53State),
+        VMSTATE_UINT32(wcount, PF53State),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void pf53_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
+
+    k->event = pf53_event;
+    k->recv = pf53_recv;
+    k->send = pf53_send;
+    dc->vmsd = &vmstate_pf53;
+    rc->phases.hold = pf53_reset_hold;
+    dc->desc = "NXP PF53 PMIC (i.MX95 SM stub)";
+}
+
+static const TypeInfo pf53_info = {
+    .name          = TYPE_PF53_PMIC,
+    .parent        = TYPE_I2C_SLAVE,
+    .instance_size = sizeof(PF53State),
+    .class_init    = pf53_class_init,
+};
+
+static void imx95_pmic_register_types(void)
+{
+    type_register_static(&pf09_info);
+    type_register_static(&pcal6408a_info);
+    type_register_static(&pf53_info);
+}
+
+type_init(imx95_pmic_register_types)
diff --git a/hw/misc/imx95_xcache.c b/hw/misc/imx95_xcache.c
new file mode 100644
index 00000000000..ef7752a6201
--- /dev/null
+++ b/hw/misc/imx95_xcache.c
@@ -0,0 +1,185 @@
+/*
+ * NXP i.MX 95 XCACHE controller stub model
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Minimal model of the M33 XCACHE controller (the two instances at
+ * 0x44400000 "PC" and 0x44400800 "PS"). The NXP System Manager firmware
+ * enables and invalidates its caches early in init by writing the cache
+ * control register (CCR): it sets ENCACHE plus the invalidate/push
+ * command bits and the GO bit, then polls.
+ *
+ * QEMU has no cache to model, so the only behaviour that matters is the
+ * self-clearing of the command bits: real hardware clears GO and the
+ * INVWn/PUSHWn bits once the (instantaneous, for us) operation completes,
+ * and leaves ENCACHE/config bits set. A plain RAM stub would leave GO set
+ * forever (the SM's "wait for GO to clear" loop would hang); a zero-return
+ * stub would leave ENCACHE clear (an "is cache enabled" check would hang).
+ * So CCR persists everything except the command bits, which read back 0.
+ * The line-maintenance registers (CLCR/CSAR/CCVR) are plain storage.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/core/sysbus.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+#define TYPE_IMX95_XCACHE "imx95.xcache"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95XCacheState, IMX95_XCACHE)
+
+/* Each instance owns 0x800 (the two M33 instances are 0x800 apart). */
+#define IMX95_XCACHE_REG_SIZE   0x800
+
+#define XCACHE_CCR              0x00
+#define XCACHE_CLCR             0x04
+#define XCACHE_CSAR             0x08
+#define XCACHE_CCVR             0x0C
+
+/* CCR bits that self-clear when the cache command completes. */
+#define CCR_GO                  0x80000000u
+#define CCR_PUSHW1              0x08000000u
+#define CCR_INVW1               0x04000000u
+#define CCR_PUSHW0              0x02000000u
+#define CCR_INVW0               0x01000000u
+#define CCR_CMD_BITS \
+    (CCR_GO | CCR_PUSHW1 | CCR_INVW1 | CCR_PUSHW0 | CCR_INVW0)
+
+struct IMX95XCacheState {
+    SysBusDevice    parent_obj;
+    MemoryRegion    iomem;
+
+    uint32_t        ccr;
+    uint32_t        clcr;
+    uint32_t        csar;
+    uint32_t        ccvr;
+};
+
+static uint64_t imx95_xcache_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMX95XCacheState *s = opaque;
+
+    trace_imx95_xcache_read(offset);
+
+    switch (offset) {
+    case XCACHE_CCR:
+        return s->ccr;
+    case XCACHE_CLCR:
+        return s->clcr;
+    case XCACHE_CSAR:
+        return s->csar;
+    case XCACHE_CCVR:
+        return s->ccvr;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: bad read offset 0x%" HWADDR_PRIx "\n",
+                      __func__, offset);
+        return 0;
+    }
+}
+
+static void imx95_xcache_write(void *opaque, hwaddr offset,
+                               uint64_t value, unsigned size)
+{
+    IMX95XCacheState *s = opaque;
+
+    trace_imx95_xcache_write(offset, value);
+
+    switch (offset) {
+    case XCACHE_CCR:
+        /* Command bits complete instantly: store everything else. */
+        s->ccr = (uint32_t)value & ~CCR_CMD_BITS;
+        break;
+    case XCACHE_CLCR:
+        s->clcr = value;
+        break;
+    case XCACHE_CSAR:
+        s->csar = value;
+        break;
+    case XCACHE_CCVR:
+        s->ccvr = 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_xcache_ops = {
+    .read = imx95_xcache_read,
+    .write = imx95_xcache_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_xcache_reset_hold(Object *obj, ResetType type)
+{
+    IMX95XCacheState *s = IMX95_XCACHE(obj);
+
+    s->ccr = 0;
+    s->clcr = 0;
+    s->csar = 0;
+    s->ccvr = 0;
+}
+
+static void imx95_xcache_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMX95XCacheState *s = IMX95_XCACHE(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx95_xcache_ops, s,
+                          TYPE_IMX95_XCACHE, IMX95_XCACHE_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+}
+
+static const VMStateDescription vmstate_imx95_xcache = {
+    .name = TYPE_IMX95_XCACHE,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(ccr, IMX95XCacheState),
+        VMSTATE_UINT32(clcr, IMX95XCacheState),
+        VMSTATE_UINT32(csar, IMX95XCacheState),
+        VMSTATE_UINT32(ccvr, IMX95XCacheState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx95_xcache_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx95_xcache;
+    rc->phases.hold = imx95_xcache_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX 95 XCACHE controller (stub)";
+}
+
+static const TypeInfo imx95_xcache_info = {
+    .name           = TYPE_IMX95_XCACHE,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95XCacheState),
+    .instance_init  = imx95_xcache_init,
+    .class_init     = imx95_xcache_class_init,
+};
+
+static void imx95_xcache_register_types(void)
+{
+    type_register_static(&imx95_xcache_info);
+}
+
+type_init(imx95_xcache_register_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 0b7f5e4382d..d7d283a191b 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -178,3 +178,5 @@ system_ss.add(when: 'CONFIG_IMX95_ANATOP', if_true: files('imx95_anatop.c'))
 system_ss.add(when: 'CONFIG_IMX95_AONMIX', if_true: files('imx95_aonmix.c'))
 system_ss.add(when: 'CONFIG_IMX95_GPC', if_true: files('imx95_gpc.c'))
 system_ss.add(when: 'CONFIG_IMX95_SRC', if_true: files('imx95_src.c'))
+system_ss.add(when: 'CONFIG_IMX95_PMIC', if_true: files('imx95_pmic.c'))
+system_ss.add(when: 'CONFIG_IMX95_XCACHE', if_true: files('imx95_xcache.c'))
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index 15fab0596c2..0555b15a1f3 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -458,3 +458,11 @@ imx95_aonmix_m7_gate(int run) "M7 wait gate -> run=%d"
 imx95_gpc_mode(uint64_t offset, uint32_t target) "CMC_MODE_CTRL off 0x%" PRIx64 " target %u"
 imx95_src_m7mix_release(void) "SRC_GEN.SCR M7MIX reset released"
 imx95_src_slice_ctrl(uint64_t offset, uint32_t value) "SLICE_SW_CTRL off 0x%" PRIx64 " <- 0x%08x"
+imx95_xcache_read(uint64_t offset) "read off 0x%" PRIx64
+imx95_xcache_write(uint64_t offset, uint64_t value) "write off 0x%" PRIx64 " <- 0x%08" PRIx64
+pf09_reg_read(uint8_t reg, uint8_t val) "PF09 reg 0x%02x -> 0x%02x"
+pf09_reg_write(uint8_t reg, uint8_t val) "PF09 reg 0x%02x <- 0x%02x"
+pcal6408a_reg_read(uint8_t reg, uint8_t val) "PCAL6408A reg 0x%02x -> 0x%02x"
+pcal6408a_reg_write(uint8_t reg, uint8_t val) "PCAL6408A reg 0x%02x <- 0x%02x"
+pf53_reg_read(uint8_t reg, uint8_t val) "PF53 reg 0x%02x -> 0x%02x"
+pf53_reg_write(uint8_t reg, uint8_t val) "PF53 reg 0x%02x <- 0x%02x"
-- 
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.