[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