All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine
@ 2026-08-20  2:48 Kyle Fox
  2026-08-20  2:48 ` [PATCH 01/16] hw/sd/sdhci: add i.MX uSDHC SDCLK_AUTO_GATE and NO_SDMA_BOUNDARY quirks Kyle Fox
                   ` (15 more replies)
  0 siblings, 16 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox

This series adds a QEMU machine for the NXP i.MX 95 19x19 LPDDR5 EVK
(machine name "imx95-19x19-evk"). The i.MX 95 is a heterogeneous SoC;
the machine models a fixed topology of 6 Cortex-A55 application cores,
one Cortex-M33 that runs NXP's System Manager (SM) firmware, and one
Cortex-M7 real-time core that the SM boots and manages.

What it does
============

It boots an arm64 Linux kernel to a userspace shell on the A55 cluster
(both a mainline arm64-defconfig kernel and the NXP BSP kernel work).

The defining property of this SoC -- and the reason the machine is
shaped the way it is -- is that the System Manager firmware, running on
the M33, is the *only* SCMI provider. Linux's clock, power,
performance-domain, sensor and reset operations are all served by the SM
over a shared mailbox (MU2); there is no SCMI implementation inside QEMU.
The machine models the mailbox cross-connect and the SM's bring-up
peripherals (LPUART, MU, System Counter, ANATOP/SRC/GPC, ELE, LPI2C +
PMIC, uSDHC, ...) faithfully enough that the *real, unmodified* SM
firmware runs on the emulated M33 and answers Linux's SCMI traffic.

A consequence worth stating plainly up front: booting Linux on this
machine requires the SM firmware image as an input, loaded onto the M33
with "-device loader,file=m33_image.elf,cpu-num=6". Without it the M33
stays halted, nothing answers SCMI, and Linux hangs at arm-scmi probe.
The SM is open source (https://github.com/nxp-imx/imx-sm) and builds
with an arm-none-eabi toolchain; docs/system/arm/imx95-evk.rst walks
through obtaining every required artifact.

Scope (headless v1)
===================

This is a deliberately minimal first machine: it boots to a serial
console. It is headless -- the DPU (display controller) is a
probe-completing stub, and the GPU (Mali-G310), VPU (Amphion) and NPU
(Neutron) are MMIO stubs that Linux enumerates but that perform no
rendering, codec or inference. Real display + Wayland output, the NETC
networking complex, and the multimedia blocks are intended as follow-on
series on top of this base.

The blocks that are modelled are exactly those on the Linux+SM bring-up
critical path; everything else that the stock EVK device tree probes is
a logging stub so that enabling those nodes degrades gracefully rather
than faulting on the first access.

How to test
===========

  $ qemu-system-aarch64 -M imx95-19x19-evk -m 2G -display none \
      -kernel Image -dtb imx95-19x19-evk.dtb -initrd initramfs.cpio.gz \
      -device loader,file=m33_image.elf,cpu-num=6 \
      -append "earlycon=lpuart32,mmio32,0x44380010 console=ttyLP0,115200 \
               cpuidle.off=1" -serial mon:stdio

A functional test is included (tests/functional/aarch64/test_imx95_evk.py).
Because the SM firmware and a matching kernel/dtb are external inputs
that are not redistributable, the test is gated on QEMU_TEST_IMX95_*
environment variables and skips cleanly when they are unset -- so it
will SKIP in upstream CI as posted. I would appreciate guidance on the
preferred convention here: I am happy to host a redistributable
kernel+dtb (and exercise the SM path separately) as an Asset() so CI
actually runs the machine, if that is wanted.

Prerequisites (patches 01-04)
=============================

The first four patches are generic, non-i.MX95 changes that the machine
depends on. They are separate commits so they can be reviewed (and
taken) on their own merits:

  01  hw/sd/sdhci: two opt-in uSDHC quirk bits (SDCLK_AUTO_GATE,
      NO_SDMA_BOUNDARY). The i.MX (u)SDHC issues commands with SDCLK
      auto-gated and its SDMA does not honour the 512 KiB ADMA boundary
      the generic core assumes.
  02  hw/arm/boot: let a board preset info->initrd_start, so a board
      with a reserved-memory carveout can keep the initrd clear of it.
  03  target/arm: an opt-in "pmsav7-rbar-align-down" CPU property that
      aligns down a PMSAv7 MPU region base not aligned to its size,
      instead of the architectural UNPREDICTABLE drop. An earlier version
      of this fix changed that behaviour unconditionally and was rightly
      not taken; this reworks it as a property that defaults to false (so
      no existing behaviour changes) and is only registered on PMSAv7
      (v7-M / v7-R) CPUs. The NXP i.MX 95 Cortex-M7 SDK firmware relies on
      the align-down for one of its MPU regions.
  04  hw/arm/armv7m: forward that property to the CPU object, following
      the existing init-svtor / vfp / dsp forwarding, so an SoC can enable
      it on an affected core. (The i.MX 95 SoC enables it on the M7 only;
      the M33 is a v8-M / PMSAv8 core and needs no such quirk.)

Series layout
=============

  01-04  generic prerequisites (above)
  05     hw/char: i.MX LPUART
  06     hw/i2c:  i.MX LPI2C
  07     hw/misc: i.MX Messaging Unit (MU v2)
  08     hw/misc: NXP EdgeLock Enclave (ELE) responder
  09     hw/timer: i.MX 95 system counter
  10     hw/misc: i.MX 95 watchdog
  11     hw/misc: ANATOP/AONMIX/GPC/SRC power & clock blocks
  12     hw/misc: PMIC (PF09/PF53/PCAL6408A) + xcache
  13     hw/misc: DPU command-sequencer stub (headless)
  14     hw/arm:  i.MX 95 SoC container (fsl-imx95)
  15     hw/arm:  i.MX 95 19x19 EVK board
  16     docs, MAINTAINERS, tests/functional

Every patch builds on its own (the series is bisectable), device state
carries migration vmsd + reset, and checkpatch is clean.

  46 files changed, 6529 insertions(+), 13 deletions(-)

Thanks for reviewing.

Kyle Fox (16):
  hw/sd/sdhci: add i.MX uSDHC SDCLK_AUTO_GATE and NO_SDMA_BOUNDARY
    quirks
  hw/arm/boot: let a board preset initrd_start
  target/arm: opt-in align-down for a misaligned PMSAv7 MPU RBAR
  hw/arm/armv7m: forward pmsav7-rbar-align-down to the CPU
  hw/char: add i.MX LPUART
  hw/i2c: add i.MX LPI2C
  hw/misc: add i.MX Messaging Unit (MU v2)
  hw/misc: add NXP EdgeLock Enclave (ELE) responder
  hw/timer: add i.MX 95 system counter
  hw/misc: add i.MX 95 watchdog
  hw/misc: add i.MX 95 ANATOP/AONMIX/GPC/SRC power and clock blocks
  hw/misc: add i.MX 95 PMIC (PF09/PF53/PCAL6408A) and xcache controllers
  hw/misc: add i.MX 95 DPU command-sequencer stub (headless)
  hw/arm: add i.MX 95 SoC container (fsl-imx95)
  hw/arm: add i.MX 95 19x19 EVK board
  docs, MAINTAINERS, tests/functional: add i.MX 95 EVK

 MAINTAINERS                                |   18 +
 docs/system/arm/imx95-evk.rst              |  242 +++
 docs/system/target-arm.rst                 |    1 +
 hw/arm/Kconfig                             |   26 +
 hw/arm/armv7m.c                            |    8 +
 hw/arm/boot.c                              |   12 +-
 hw/arm/fsl-imx95.c                         | 1646 ++++++++++++++++++++
 hw/arm/imx95-evk.c                         |  136 ++
 hw/arm/meson.build                         |    2 +
 hw/char/Kconfig                            |    3 +
 hw/char/imx_lpuart.c                       |  382 +++++
 hw/char/meson.build                        |    1 +
 hw/char/trace-events                       |    3 +
 hw/i2c/Kconfig                             |    4 +
 hw/i2c/imx_lpi2c.c                         |  296 ++++
 hw/i2c/meson.build                         |    1 +
 hw/i2c/trace-events                        |    4 +
 hw/misc/Kconfig                            |   32 +
 hw/misc/imx95_anatop.c                     |  201 +++
 hw/misc/imx95_aonmix.c                     |  160 ++
 hw/misc/imx95_dpu.c                        |  117 ++
 hw/misc/imx95_ele_server.c                 |  284 ++++
 hw/misc/imx95_gpc.c                        |  160 ++
 hw/misc/imx95_pmic.c                       |  357 +++++
 hw/misc/imx95_src.c                        |  228 +++
 hw/misc/imx95_wdog.c                       |  198 +++
 hw/misc/imx95_xcache.c                     |  185 +++
 hw/misc/imx_mu.c                           |  454 ++++++
 hw/misc/meson.build                        |   10 +
 hw/misc/trace-events                       |   25 +
 hw/sd/sdhci.c                              |   67 +-
 hw/timer/Kconfig                           |    3 +
 hw/timer/imx95_sysctr.c                    |  241 +++
 hw/timer/meson.build                       |    1 +
 hw/timer/trace-events                      |    2 +
 include/hw/arm/armv7m.h                    |    4 +
 include/hw/arm/fsl-imx95.h                 |  481 ++++++
 include/hw/char/imx_lpuart.h               |  123 ++
 include/hw/misc/imx95_ele_server.h         |   47 +
 include/hw/misc/imx_mu.h                   |  144 ++
 include/hw/sd/sdhci.h                      |   21 +
 target/arm/cpu.c                           |   19 +
 target/arm/cpu.h                           |    6 +
 target/arm/ptw.c                           |   28 +-
 tests/functional/aarch64/meson.build       |    2 +
 tests/functional/aarch64/test_imx95_evk.py |  157 ++
 46 files changed, 6529 insertions(+), 13 deletions(-)
 create mode 100644 docs/system/arm/imx95-evk.rst
 create mode 100644 hw/arm/fsl-imx95.c
 create mode 100644 hw/arm/imx95-evk.c
 create mode 100644 hw/char/imx_lpuart.c
 create mode 100644 hw/i2c/imx_lpi2c.c
 create mode 100644 hw/misc/imx95_anatop.c
 create mode 100644 hw/misc/imx95_aonmix.c
 create mode 100644 hw/misc/imx95_dpu.c
 create mode 100644 hw/misc/imx95_ele_server.c
 create mode 100644 hw/misc/imx95_gpc.c
 create mode 100644 hw/misc/imx95_pmic.c
 create mode 100644 hw/misc/imx95_src.c
 create mode 100644 hw/misc/imx95_wdog.c
 create mode 100644 hw/misc/imx95_xcache.c
 create mode 100644 hw/misc/imx_mu.c
 create mode 100644 hw/timer/imx95_sysctr.c
 create mode 100644 include/hw/arm/fsl-imx95.h
 create mode 100644 include/hw/char/imx_lpuart.h
 create mode 100644 include/hw/misc/imx95_ele_server.h
 create mode 100644 include/hw/misc/imx_mu.h
 create mode 100644 tests/functional/aarch64/test_imx95_evk.py


base-commit: ae4f3443209ab154b48b706a146e5f557ab147cb
-- 
2.34.1



^ permalink raw reply	[flat|nested] 17+ messages in thread

* [PATCH 01/16] hw/sd/sdhci: add i.MX uSDHC SDCLK_AUTO_GATE and NO_SDMA_BOUNDARY quirks
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
@ 2026-08-20  2:48 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 02/16] hw/arm/boot: let a board preset initrd_start Kyle Fox
                   ` (14 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kyle Fox, Philippe Mathieu-Daudé, Bin Meng,
	open list:SD (Secure Card)

The i.MX uSDHC issues commands with SDCLK auto-gated (its driver never
sets SDCLK_EN), and its SDMA does not honour the 512 KiB ADMA buffer
boundary the generic core assumes. Add two opt-in quirk bits for this:

  - SDCLK_AUTO_GATE: treat the clock as running once the internal clock
    is enabled and stable, instead of also requiring SDCLK_EN.
  - NO_SDMA_BOUNDARY: skip the buffer-boundary break in multi-block SDMA.

Both default off and are a no-op when the bit is clear, so no existing
user of the SDHCI models changes behaviour; a machine opts in by setting
the bits on its own uSDHC instances (the i.MX 95 SoC does so).

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/sd/sdhci.c         | 67 +++++++++++++++++++++++++++++++++++++++----
 include/hw/sd/sdhci.h | 21 ++++++++++++++
 2 files changed, 82 insertions(+), 6 deletions(-)

diff --git a/hw/sd/sdhci.c b/hw/sd/sdhci.c
index e58a6103970..327e9a7cb74 100644
--- a/hw/sd/sdhci.c
+++ b/hw/sd/sdhci.c
@@ -611,12 +611,27 @@ static void sdhci_sdma_transfer_multi_blocks(SDHCIState *s)
     }
 
     /*
-     * XXX: Some sd/mmc drivers (for example, u-boot-slp) do not account for
-     * possible stop at page boundary if initial address is not page aligned,
-     * allow them to work properly
+     * The i.MX/FSL uSDHC silicon does not implement the SDHCI "Host SDMA
+     * Buffer Boundary" mechanism; it transfers all blocks back-to-back
+     * regardless of buffer-boundary address alignment. The blksize-quirk
+     * in esdhc_write() forces bits 14:12 to 0b111 (= 512 KiB) so Linux's
+     * "zero this field out" pattern doesn't trip a 4 KiB boundary break,
+     * but the real hardware never pauses for the boundary at all, so
+     * neither should we. Skipping the page-aligned path makes the SDMA
+     * transfer run to completion (blkcnt -> 0) and fire transfer-complete,
+     * which is what the FSL U-Boot SPL driver expects (it doesn't ack
+     * SDMA-boundary IRQs by re-writing SYSAD). Controllers with this
+     * behaviour set SDHCI_QUIRK_NO_SDMA_BOUNDARY.
      */
-    if ((s->sdmasysad % boundary_chk) == 0) {
-        page_aligned = true;
+    if (!(s->quirks & SDHCI_QUIRK_NO_SDMA_BOUNDARY)) {
+        /*
+         * XXX: Some sd/mmc drivers (for example, u-boot-slp) do not
+         * account for possible stop at page boundary if initial address
+         * is not page aligned, allow them to work properly
+         */
+        if ((s->sdmasysad % boundary_chk) == 0) {
+            page_aligned = true;
+        }
     }
 
     s->prnsts |= SDHC_DATA_INHIBIT | SDHC_DAT_LINE_ACTIVE;
@@ -992,9 +1007,26 @@ static void sdhci_data_transfer(void *opaque)
     }
 }
 
+static bool sdhci_clock_is_on(SDHCIState *s)
+{
+    /*
+     * The i.MX (u)SDHC has no software SD-clock-enable bit (the card clock
+     * is auto-gated by hardware), so its driver never sets
+     * SDHC_CLOCK_SDCLK_EN. For such a controller, treat the clock as
+     * running once the internal clock is enabled and stable; otherwise
+     * every command would be silently dropped (no completion/timeout IRQ),
+     * stalling the guest on 10s host-side timeouts.
+     */
+    if (s->quirks & SDHCI_QUIRK_SDCLK_AUTO_GATE) {
+        return (s->clkcon & (SDHC_CLOCK_INT_EN | SDHC_CLOCK_INT_STABLE)) ==
+               (SDHC_CLOCK_INT_EN | SDHC_CLOCK_INT_STABLE);
+    }
+    return SDHC_CLOCK_IS_ON(s->clkcon);
+}
+
 static bool sdhci_can_issue_command(SDHCIState *s)
 {
-    if (!SDHC_CLOCK_IS_ON(s->clkcon) ||
+    if (!sdhci_clock_is_on(s) ||
         (((s->prnsts & SDHC_DATA_INHIBIT) || s->stopped_state) &&
         ((s->cmdreg & SDHC_CMD_DATA_PRESENT) ||
         ((s->cmdreg & SDHC_CMD_RESPONSE) == SDHC_CMD_RSP_WITH_BUSY &&
@@ -1613,6 +1645,9 @@ static void sdhci_bus_class_init(ObjectClass *klass, const void *data)
 
 #define ESDHC_VENDOR_SPEC               0xc0
 #define ESDHC_FRC_SDCLK_ON              (1 << 8)
+#define ESDHC_VENDORSPEC_IPGEN          (1 << 11)
+#define ESDHC_VENDORSPEC_HCKEN          (1 << 12)
+#define ESDHC_VENDORSPEC_CKEN           (1 << 14)
 
 #define ESDHC_DLL_CTRL                  0x60
 
@@ -1707,6 +1742,26 @@ esdhc_write(void *opaque, hwaddr offset, uint64_t val, unsigned size)
         } else {
             s->prnsts |= ESDHC_PRNSTS_CLOCK_GATE_OFF;
         }
+        /*
+         * An SDCLK_AUTO_GATE i.MX uSDHC drives the SD clock through
+         * VENDORSPEC, not through the SDHCI CLKCON. For those instances
+         * mirror the VENDORSPEC clock-enable bits into clkcon so the
+         * standard SDHCI paths (PRNSTS.SDSTB derived from CLOCK_INT_STABLE;
+         * sdhci_can_issue_command gating on CLOCK_IS_ON) see the clock as
+         * enabled; otherwise the U-Boot fsl_esdhc_imx driver waits forever
+         * for SDSTB. Consumers without the quirk keep the upstream behaviour
+         * (a VENDORSPEC write touches only vendor_spec/prnsts).
+         */
+        if (s->quirks & SDHCI_QUIRK_SDCLK_AUTO_GATE) {
+            if (value & (ESDHC_VENDORSPEC_HCKEN | ESDHC_VENDORSPEC_IPGEN)) {
+                s->clkcon |= SDHC_CLOCK_INT_EN | SDHC_CLOCK_INT_STABLE;
+            }
+            if (value & ESDHC_VENDORSPEC_CKEN) {
+                s->clkcon |= SDHC_CLOCK_SDCLK_EN;
+            } else {
+                s->clkcon &= ~SDHC_CLOCK_SDCLK_EN;
+            }
+        }
         break;
 
     case SDHC_HOSTCTL:
diff --git a/include/hw/sd/sdhci.h b/include/hw/sd/sdhci.h
index a9da6203fcb..e6940b62562 100644
--- a/include/hw/sd/sdhci.h
+++ b/include/hw/sd/sdhci.h
@@ -115,6 +115,27 @@ typedef struct SDHCIState SDHCIState;
  */
 #define SDHCI_QUIRK_NO_BUSY_IRQ    BIT(14)
 
+/*
+ * The i.MX (u)SDHC gates the SD card clock automatically and has no
+ * software-visible "SD Clock Enable" bit: its driver programs only the
+ * divider and the internal-clock enable, never SDHC_CLOCK_SDCLK_EN. With
+ * this quirk the controller is considered ready to issue commands once the
+ * internal clock is enabled and stable, instead of also requiring
+ * SDHC_CLOCK_SDCLK_EN (which such a driver never sets). QEMU-internal bit,
+ * taken from the top of the word to avoid the Linux quirk numbering.
+ */
+#define SDHCI_QUIRK_SDCLK_AUTO_GATE    BIT(31)
+
+/*
+ * The i.MX uSDHC silicon does not implement the SDHCI "Host SDMA Buffer
+ * Boundary" mechanism; it streams all blocks back-to-back regardless of the
+ * buffer-boundary alignment, never pausing at the boundary. With this quirk
+ * the SDMA engine skips the page-aligned boundary-break path so the transfer
+ * runs to completion. QEMU-internal bit, taken from the top of the word to
+ * avoid the Linux quirk numbering.
+ */
+#define SDHCI_QUIRK_NO_SDMA_BOUNDARY    BIT(30)
+
 #define TYPE_PCI_SDHCI "sdhci-pci"
 DECLARE_INSTANCE_CHECKER(SDHCIState, PCI_SDHCI,
                          TYPE_PCI_SDHCI)
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 02/16] hw/arm/boot: let a board preset initrd_start
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
  2026-08-20  2:48 ` [PATCH 01/16] hw/sd/sdhci: add i.MX uSDHC SDCLK_AUTO_GATE and NO_SDMA_BOUNDARY quirks Kyle Fox
@ 2026-08-20  2:48 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 03/16] target/arm: opt-in align-down for a misaligned PMSAv7 MPU RBAR Kyle Fox
                   ` (13 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Peter Maydell, open list:ARM TCG CPUs

Allow a board to place the initrd itself (honour a pre-set
info->initrd_start) so a board with a reserved-memory carveout can keep
the initrd clear of it.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/arm/boot.c | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/hw/arm/boot.c b/hw/arm/boot.c
index 9b7553dde5e..4bfb0d8264b 100644
--- a/hw/arm/boot.c
+++ b/hw/arm/boot.c
@@ -997,8 +997,16 @@ static void arm_setup_direct_kernel_boot(ARMCPU *cpu,
      * don't tell us their exact size (eg self-decompressing 32-bit kernels)
      * we might still make a bad choice here.
      */
-    info->initrd_start = info->loader_start +
-        MIN(info->ram_size / 2, 128 * MiB);
+    /*
+     * Boards may preset info->initrd_start to force a placement (e.g. to
+     * dodge a guest reserved-memory carveout that happens to sit where the
+     * default heuristic would land the initrd). Only compute a default when
+     * the board left it unset.
+     */
+    if (!info->initrd_start) {
+        info->initrd_start = info->loader_start +
+            MIN(info->ram_size / 2, 128 * MiB);
+    }
     if (image_high_addr) {
         info->initrd_start = MAX(info->initrd_start, image_high_addr);
     }
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 03/16] target/arm: opt-in align-down for a misaligned PMSAv7 MPU RBAR
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
  2026-08-20  2:48 ` [PATCH 01/16] hw/sd/sdhci: add i.MX uSDHC SDCLK_AUTO_GATE and NO_SDMA_BOUNDARY quirks Kyle Fox
  2026-08-20  2:48 ` [PATCH 02/16] hw/arm/boot: let a board preset initrd_start Kyle Fox
@ 2026-08-20  2:48 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 04/16] hw/arm/armv7m: forward pmsav7-rbar-align-down to the CPU Kyle Fox
                   ` (12 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Peter Maydell, open list:ARM TCG CPUs

A PMSAv7 MPU region base that is not aligned to its region size is
UNPREDICTABLE per the architecture, and QEMU disables such a region.
Some PMSAv7 implementations instead ignore the sub-size low bits of
RBAR.ADDR and match against the aligned-down base. The NXP i.MX 95 RT
core (Cortex-M7) SDK firmware relies on this for a peripheral region
(DRBAR 0x4c800000 programmed with a 512MB size, intended as 0x40000000).

Add an opt-in "pmsav7-rbar-align-down" property that aligns such a base
down instead of dropping the region. It defaults to false, so existing
behaviour is unchanged, and is only registered on PMSAv7 CPUs (v7-M and
v7-R, i.e. v7 without v8), where get_phys_addr_pmsav7() runs.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 target/arm/cpu.c | 19 +++++++++++++++++++
 target/arm/cpu.h |  6 ++++++
 target/arm/ptw.c | 28 +++++++++++++++++++++++-----
 3 files changed, 48 insertions(+), 5 deletions(-)

diff --git a/target/arm/cpu.c b/target/arm/cpu.c
index 77aa78f00e2..22e84c1ced2 100644
--- a/target/arm/cpu.c
+++ b/target/arm/cpu.c
@@ -1349,6 +1349,16 @@ static const Property arm_cpu_pmsav7_dregion_property =
             DEFINE_PROP_UNSIGNED_NODEFAULT("pmsav7-dregion", ARMCPU,
                                            pmsav7_dregion,
                                            qdev_prop_uint32, uint32_t);
+
+/*
+ * Opt-in: treat a PMSAv7 MPU region base that is not aligned to its region
+ * size as aligned-down instead of UNPREDICTABLE (see get_phys_addr_pmsav7()).
+ * Only registered on PMSAv7 (v7-M / v7-R) CPUs, and only enabled by an
+ * integrator whose firmware relies on it (e.g. the i.MX 95 Cortex-M7).
+ */
+static const Property arm_cpu_pmsav7_rbar_align_down_property =
+            DEFINE_PROP_BOOL("pmsav7-rbar-align-down", ARMCPU,
+                             pmsav7_rbar_align_down, false);
 #endif
 
 static bool arm_get_pmu(Object *obj, Error **errp)
@@ -1659,6 +1669,15 @@ static void arm_cpu_post_init(Object *obj)
         if (arm_feature(&cpu->env, ARM_FEATURE_V7)) {
             qdev_property_add_static(DEVICE(obj),
                                      &arm_cpu_pmsav7_dregion_property);
+            /*
+             * get_phys_addr_pmsav7() only runs on PMSAv7 CPUs, i.e. v7
+             * without v8 (a v8 M/R core uses the PMSAv8 MPU, which has no
+             * such alignment rule), so only offer the property there.
+             */
+            if (!arm_feature(&cpu->env, ARM_FEATURE_V8)) {
+                qdev_property_add_static(DEVICE(obj),
+                        &arm_cpu_pmsav7_rbar_align_down_property);
+            }
         }
     }
 
diff --git a/target/arm/cpu.h b/target/arm/cpu.h
index c0492c8dfbc..03ead3cda15 100644
--- a/target/arm/cpu.h
+++ b/target/arm/cpu.h
@@ -1145,6 +1145,12 @@ struct ArchCPU {
      * architecture version.
      */
     bool cfgend;
+    /*
+     * If set, a PMSAv7 MPU region base that is not aligned to its region
+     * size is treated as aligned-down (matching Cortex-M silicon such as the
+     * i.MX 95 M-cores) instead of the architectural UNPREDICTABLE drop.
+     */
+    bool pmsav7_rbar_align_down;
 
     QLIST_HEAD(, ARMELChangeHook) pre_el_change_hooks;
     QLIST_HEAD(, ARMELChangeHook) el_change_hooks;
diff --git a/target/arm/ptw.c b/target/arm/ptw.c
index a29de0385f4..51805c05226 100644
--- a/target/arm/ptw.c
+++ b/target/arm/ptw.c
@@ -2757,11 +2757,29 @@ static bool get_phys_addr_pmsav7(CPUARMState *env,
             rmask = (1ull << rsize) - 1;
 
             if (base & rmask) {
-                qemu_log_mask(LOG_GUEST_ERROR,
-                              "DRBAR[%d]: 0x%" PRIx32 " misaligned "
-                              "to DRSR region size, mask = 0x%" PRIx32 "\n",
-                              n, base, rmask);
-                continue;
+                if (cpu->pmsav7_rbar_align_down) {
+                    /*
+                     * Opt-in behaviour for Cortex-M silicon such as the
+                     * i.MX 95 M-cores: a region base not aligned to its size
+                     * is treated as aligned-down (RBAR.ADDR is only
+                     * [31:log2(size)]), matching that hardware, whose SM/RT
+                     * firmware programs e.g. DRBAR 0x4c800000 with a 512MB
+                     * size intended as 0x40000000. Off by default because the
+                     * architecture calls a misaligned base UNPREDICTABLE.
+                     */
+                    qemu_log_mask(LOG_GUEST_ERROR,
+                                  "DRBAR[%d]: 0x%" PRIx32 " not aligned to "
+                                  "DRSR region size (mask 0x%" PRIx32 "); "
+                                  "aligning down to 0x%" PRIx32 "\n",
+                                  n, base, rmask, base & ~rmask);
+                    base &= ~rmask;
+                } else {
+                    qemu_log_mask(LOG_GUEST_ERROR,
+                                  "DRBAR[%d]: 0x%" PRIx32 " misaligned "
+                                  "to DRSR region size, mask = 0x%" PRIx32 "\n",
+                                  n, base, rmask);
+                    continue;
+                }
             }
 
             if (address < base || address > base + rmask) {
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 04/16] hw/arm/armv7m: forward pmsav7-rbar-align-down to the CPU
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (2 preceding siblings ...)
  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 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 05/16] hw/char: add i.MX LPUART Kyle Fox
                   ` (11 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Peter Maydell, open list:ARM TCG CPUs

Forward the opt-in "pmsav7-rbar-align-down" property to the CPU object,
following the existing init-svtor / vfp / dsp forwarding pattern, so an
SoC integrating a PMSAv7 core whose firmware relies on the align-down
behaviour can enable it.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/arm/armv7m.c         | 8 ++++++++
 include/hw/arm/armv7m.h | 4 ++++
 2 files changed, 12 insertions(+)

diff --git a/hw/arm/armv7m.c b/hw/arm/armv7m.c
index 68a1cbd6316..bb0f95d423d 100644
--- a/hw/arm/armv7m.c
+++ b/hw/arm/armv7m.c
@@ -332,6 +332,12 @@ static void armv7m_realize(DeviceState *dev, Error **errp)
             return;
         }
     }
+    if (object_property_find(OBJECT(s->cpu), "pmsav7-rbar-align-down")) {
+        if (!object_property_set_bool(OBJECT(s->cpu), "pmsav7-rbar-align-down",
+                                      s->pmsav7_rbar_align_down, errp)) {
+            return;
+        }
+    }
     object_property_set_bool(OBJECT(s->cpu), "start-powered-off",
                              s->start_powered_off, &error_abort);
 
@@ -562,6 +568,8 @@ static const Property armv7m_properties[] = {
                      false),
     DEFINE_PROP_BOOL("vfp", ARMv7MState, vfp, true),
     DEFINE_PROP_BOOL("dsp", ARMv7MState, dsp, true),
+    DEFINE_PROP_BOOL("pmsav7-rbar-align-down", ARMv7MState,
+                     pmsav7_rbar_align_down, false),
     DEFINE_PROP_UINT32("mpu-ns-regions", ARMv7MState, mpu_ns_regions, UINT_MAX),
     DEFINE_PROP_UINT32("mpu-s-regions", ARMv7MState, mpu_s_regions, UINT_MAX),
 };
diff --git a/include/hw/arm/armv7m.h b/include/hw/arm/armv7m.h
index 70555962bb9..23cfed222c1 100644
--- a/include/hw/arm/armv7m.h
+++ b/include/hw/arm/armv7m.h
@@ -52,6 +52,9 @@ OBJECT_DECLARE_SIMPLE_TYPE(ARMv7MState, ARMV7M)
  * + Property "init-nsvtor": non-secure VTOR reset value (forwarded to CPU object)
  * + Property "vfp": enable VFP (forwarded to CPU object)
  * + Property "dsp": enable DSP (forwarded to CPU object)
+ * + Property "pmsav7-rbar-align-down": align down a misaligned PMSAv7 MPU
+ *   region base rather than treating it as UNPREDICTABLE (forwarded to the
+ *   CPU object, which only offers it on PMSAv7 cores)
  * + Property "enable-bitband": expose bitbanded IO
  * + Property "mpu-ns-regions": number of Non-Secure MPU regions (forwarded
  *   to CPU object pmsav7-dregion property; default is whatever the default
@@ -108,6 +111,7 @@ struct ARMv7MState {
     bool start_powered_off;
     bool vfp;
     bool dsp;
+    bool pmsav7_rbar_align_down;
 };
 
 #endif
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 05/16] hw/char: add i.MX LPUART
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (3 preceding siblings ...)
  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 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 06/16] hw/i2c: add i.MX LPI2C Kyle Fox
                   ` (10 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kyle Fox, Paolo Bonzini, Marc-André Lureau, Peter Maydell,
	open list:i.MX31 (kzm)

The i.MX Low-Power UART. Models the register subset the Linux fsl-lpuart
driver and the SM firmware drive (BAUD/CTRL/STAT/DATA plus the TX/RX
FIFOs) as an interrupt- and poll-capable serial device. On this machine
LPUART1 is the Linux console (ttyLP0), LPUART2 the SM debug monitor and
LPUART3 the M7 console.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/char/Kconfig              |   3 +
 hw/char/imx_lpuart.c         | 382 +++++++++++++++++++++++++++++++++++
 hw/char/meson.build          |   1 +
 hw/char/trace-events         |   3 +
 include/hw/char/imx_lpuart.h | 123 +++++++++++
 5 files changed, 512 insertions(+)
 create mode 100644 hw/char/imx_lpuart.c
 create mode 100644 include/hw/char/imx_lpuart.h

diff --git a/hw/char/Kconfig b/hw/char/Kconfig
index 020c0a84bb6..51b4c3e00ce 100644
--- a/hw/char/Kconfig
+++ b/hw/char/Kconfig
@@ -95,3 +95,6 @@ config IP_OCTAL_232
     bool
     default y
     depends on IPACK
+
+config IMX_LPUART
+    bool
diff --git a/hw/char/imx_lpuart.c b/hw/char/imx_lpuart.c
new file mode 100644
index 00000000000..714bc3e52a4
--- /dev/null
+++ b/hw/char/imx_lpuart.c
@@ -0,0 +1,382 @@
+/*
+ * NXP i.MX LPUART (Low-Power UART) device model
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Modelled on hw/char/imx_serial.c for structural conventions but with
+ * the LPUART register set and semantics. TX bytes are pushed through the
+ * chardev backend synchronously; RX bytes are buffered one-deep and
+ * surface in DATA, with an interrupt raised when CTRL.RIE is set.
+ * FIFO depth is reported as 1; no DMA, no baud-rate timing, no flow
+ * control are modelled.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/char/imx_lpuart.h"
+#include "hw/core/irq.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/qdev-properties-system.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+static void imx_lpuart_update_irq(IMXLPUARTState *s);
+
+static int imx_lpuart_post_load(void *opaque, int version_id)
+{
+    /* Recompute the IRQ line level from the restored register state. */
+    imx_lpuart_update_irq(opaque);
+    return 0;
+}
+
+static const VMStateDescription vmstate_imx_lpuart = {
+    .name = TYPE_IMX_LPUART,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .post_load = imx_lpuart_post_load,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(baud, IMXLPUARTState),
+        VMSTATE_UINT32(stat, IMXLPUARTState),
+        VMSTATE_UINT32(ctrl, IMXLPUARTState),
+        VMSTATE_UINT32(match, IMXLPUARTState),
+        VMSTATE_UINT32(modir, IMXLPUARTState),
+        VMSTATE_UINT32(fifo, IMXLPUARTState),
+        VMSTATE_UINT32(water, IMXLPUARTState),
+        VMSTATE_UINT32(pincfg, IMXLPUARTState),
+        VMSTATE_UINT8(rx_byte, IMXLPUARTState),
+        VMSTATE_BOOL(rx_full, IMXLPUARTState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+/* Recompute the IRQ line from STAT & CTRL interrupt-enable bits. */
+static void imx_lpuart_update_irq(IMXLPUARTState *s)
+{
+    bool level =
+        ((s->stat & LPUART_STAT_RDRF) && (s->ctrl & LPUART_CTRL_RIE)) ||
+        ((s->stat & LPUART_STAT_TDRE) && (s->ctrl & LPUART_CTRL_TIE)) ||
+        ((s->stat & LPUART_STAT_TC)   && (s->ctrl & LPUART_CTRL_TCIE)) ||
+        ((s->stat & LPUART_STAT_IDLE) && (s->ctrl & LPUART_CTRL_ILIE));
+
+    qemu_set_irq(s->irq, level);
+}
+
+static void imx_lpuart_reset(IMXLPUARTState *s)
+{
+    /*
+     * Power-on reset values per the i.MX LPUART chapter. STAT comes up
+     * with TDRE and TC set because the FIFO is empty.
+     */
+    s->baud   = 0x0F000004;     /* default OSR = 4 + SBR field */
+    s->stat   = LPUART_STAT_TDRE | LPUART_STAT_TC;
+    s->ctrl   = 0;
+    s->match  = 0;
+    s->modir  = 0;
+    s->fifo   = 0;
+    s->water  = 0;
+    s->pincfg = 0;
+    s->rx_byte = 0;
+    s->rx_full = false;
+}
+
+static void imx_lpuart_reset_at_boot_hold(Object *obj, ResetType type)
+{
+    IMXLPUARTState *s = IMX_LPUART(obj);
+
+    imx_lpuart_reset(s);
+    imx_lpuart_update_irq(s);
+}
+
+static uint64_t imx_lpuart_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMXLPUARTState *s = opaque;
+    uint32_t value = 0;
+    uint32_t c;
+
+    switch (offset) {
+    case LPUART_VERID:
+        value = LPUART_VERID_VALUE;
+        break;
+
+    case LPUART_PARAM:
+        /* TXFIFO and RXFIFO size fields both zero -> depth = 1. */
+        value = 0;
+        break;
+
+    case LPUART_GLOBAL:
+        value = 0;
+        break;
+
+    case LPUART_PINCFG:
+        value = s->pincfg;
+        break;
+
+    case LPUART_BAUD:
+        value = s->baud;
+        break;
+
+    case LPUART_STAT:
+        value = s->stat;
+        break;
+
+    case LPUART_CTRL:
+        value = s->ctrl;
+        break;
+
+    case LPUART_DATA:
+        if (s->rx_full) {
+            c = s->rx_byte;
+            s->rx_full = false;
+            s->stat &= ~LPUART_STAT_RDRF;
+            value = c & LPUART_DATA_MASK;
+            imx_lpuart_update_irq(s);
+            qemu_chr_fe_accept_input(&s->chr);
+        } else {
+            value = LPUART_DATA_RXEMPT;
+        }
+        break;
+
+    case LPUART_MATCH:
+        value = s->match;
+        break;
+
+    case LPUART_MODIR:
+        value = s->modir;
+        break;
+
+    case LPUART_FIFO:
+        /*
+         * Report the FIFO control bits the guest set, plus the read-only
+         * TXEMPT/RXEMPT status computed from our 1-deep model. The status
+         * bits must be derived fresh, never returned from s->fifo: the Linux
+         * driver read-modify-writes UARTFIFO during setup, so a stored RXEMPT
+         * would stick set and make its RX drain loop (while !(FIFO & RXEMPT))
+         * believe the RX FIFO is always empty - it would never read DATA, RDRF
+         * would never clear, and the RX interrupt would storm.
+         */
+        value = s->fifo & ~(LPUART_FIFO_TXEMPT | LPUART_FIFO_RXEMPT);
+        value |= LPUART_FIFO_TXEMPT;  /* we drain TX instantly */
+        if (!s->rx_full) {
+            value |= LPUART_FIFO_RXEMPT;
+        }
+        break;
+
+    case LPUART_WATER:
+        /*
+         * Reflect the current fifo state in the RX/TX count fields
+         * (bits[31:24] RXCOUNT, bits[15:8] TXCOUNT). U-Boot's
+         * _lpuart32_serial_tstc() polls (water >> 24) for incoming-
+         * char detection, so RXCOUNT must update when imx_lpuart_receive
+         * stashes a byte. We are 1-deep: 0 or 1 in either slot.
+         * Preserve the guest-written watermark bits unchanged.
+         */
+        value = (s->water & 0x00ff00ffu)
+              | ((uint32_t)(s->rx_full ? 1 : 0) << 24)
+              | 0u;  /* TXCOUNT always 0 - we drain TR writes instantly */
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: bad read offset 0x%" HWADDR_PRIx "\n",
+                      __func__, offset);
+        break;
+    }
+
+    return value;
+}
+
+static void imx_lpuart_write(void *opaque, hwaddr offset,
+                             uint64_t value, unsigned size)
+{
+    IMXLPUARTState *s = opaque;
+    uint8_t ch;
+
+    switch (offset) {
+    case LPUART_VERID:
+    case LPUART_PARAM:
+        /* Read-only registers; silently ignore writes (HW behavior). */
+        break;
+
+    case LPUART_GLOBAL:
+        if (value & LPUART_GLOBAL_RST) {
+            imx_lpuart_reset(s);
+            imx_lpuart_update_irq(s);
+        }
+        break;
+
+    case LPUART_PINCFG:
+        s->pincfg = value;
+        break;
+
+    case LPUART_BAUD:
+        /*
+         * Baud-rate timing isn't modelled, but the driver reads back what
+         * it wrote. Preserve DMA-enable and other configuration bits.
+         */
+        s->baud = value;
+        trace_imx_lpuart_baud(s->baud);
+        break;
+
+    case LPUART_STAT:
+        /*
+         * STAT bits IDLE and OR are W1C; the rest are read-only status
+         * managed by the model.
+         */
+        s->stat &= ~(value & LPUART_STAT_W1C_MASK);
+        imx_lpuart_update_irq(s);
+        break;
+
+    case LPUART_CTRL:
+        s->ctrl = value;
+        imx_lpuart_update_irq(s);
+        break;
+
+    case LPUART_DATA:
+        ch = value & LPUART_DATA_MASK;
+        /*
+         * Always push the byte through the chardev backend, even if
+         * CTRL_TE is currently 0. Real silicon would queue or drop the
+         * byte until TE is enabled; in emulation we don't model that
+         * gating because some consumers (notably the Linux earlycon
+         * path under -kernel direct boot, which assumes firmware
+         * already set TE before the kernel runs) write to DATA without
+         * first enabling TE themselves. Without this, kernel console
+         * output vanishes silently and earlycon appears broken.
+         */
+        trace_imx_lpuart_tx(ch);
+        qemu_chr_fe_write_all(&s->chr, &ch, 1);
+        /*
+         * TDRE and TC are pinned high; the byte goes out instantly. A
+         * future model would clear them for one host tick, then set TC
+         * when the line goes idle.
+         */
+        s->stat |= LPUART_STAT_TDRE | LPUART_STAT_TC;
+        imx_lpuart_update_irq(s);
+        break;
+
+    case LPUART_MATCH:
+        s->match = value;
+        break;
+
+    case LPUART_MODIR:
+        s->modir = value;
+        break;
+
+    case LPUART_FIFO:
+        /*
+         * Store only the writable configuration bits. The read-only status
+         * bits (TXEMPT/RXEMPT/TXOF/RXUF) and the self-clearing flush commands
+         * (TXFLUSH/RXFLUSH) must NOT be latched: the driver read-modify-writes
+         * UARTFIFO, so latching RXEMPT would make it stick set and break the
+         * RX drain loop (see the FIFO read).
+         */
+        s->fifo = value & ~LPUART_FIFO_RO_MASK;
+        if (value & LPUART_FIFO_RXFLUSH) {
+            s->rx_full = false;
+            s->stat &= ~LPUART_STAT_RDRF;
+        }
+        imx_lpuart_update_irq(s);
+        break;
+
+    case LPUART_WATER:
+        s->water = 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 int imx_lpuart_can_receive(void *opaque)
+{
+    IMXLPUARTState *s = opaque;
+
+    return ((s->ctrl & LPUART_CTRL_RE) && !s->rx_full) ? 1 : 0;
+}
+
+static void imx_lpuart_receive(void *opaque, const uint8_t *buf, int size)
+{
+    IMXLPUARTState *s = opaque;
+
+    if (size <= 0 || s->rx_full) {
+        return;
+    }
+
+    s->rx_byte = buf[0];
+    s->rx_full = true;
+    trace_imx_lpuart_rx(s->rx_byte);
+    s->stat |= LPUART_STAT_RDRF;
+    imx_lpuart_update_irq(s);
+}
+
+static const MemoryRegionOps imx_lpuart_ops = {
+    .read = imx_lpuart_read,
+    .write = imx_lpuart_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 imx_lpuart_realize(DeviceState *dev, Error **errp)
+{
+    IMXLPUARTState *s = IMX_LPUART(dev);
+
+    qemu_chr_fe_set_handlers(&s->chr, imx_lpuart_can_receive,
+                             imx_lpuart_receive, NULL, NULL, s, NULL, true);
+}
+
+static void imx_lpuart_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMXLPUARTState *s = IMX_LPUART(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx_lpuart_ops, s,
+                          TYPE_IMX_LPUART, IMX_LPUART_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+    sysbus_init_irq(sbd, &s->irq);
+}
+
+static const Property imx_lpuart_properties[] = {
+    DEFINE_PROP_CHR("chardev", IMXLPUARTState, chr),
+};
+
+static void imx_lpuart_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->realize = imx_lpuart_realize;
+    dc->vmsd = &vmstate_imx_lpuart;
+    rc->phases.hold = imx_lpuart_reset_at_boot_hold;
+    set_bit(DEVICE_CATEGORY_INPUT, dc->categories);
+    dc->desc = "NXP i.MX LPUART";
+    device_class_set_props(dc, imx_lpuart_properties);
+}
+
+static const TypeInfo imx_lpuart_info = {
+    .name           = TYPE_IMX_LPUART,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMXLPUARTState),
+    .instance_init  = imx_lpuart_init,
+    .class_init     = imx_lpuart_class_init,
+};
+
+static void imx_lpuart_register_types(void)
+{
+    type_register_static(&imx_lpuart_info);
+}
+
+type_init(imx_lpuart_register_types)
diff --git a/hw/char/meson.build b/hw/char/meson.build
index fc3d7ee506f..440142ae803 100644
--- a/hw/char/meson.build
+++ b/hw/char/meson.build
@@ -41,3 +41,4 @@ system_ss.add(when: 'CONFIG_GOLDFISH_TTY', if_true: files('goldfish_tty.c'))
 
 specific_ss.add(when: 'CONFIG_TERMINAL3270', if_true: files('terminal3270.c'))
 specific_ss.add(when: 'CONFIG_PSERIES', if_true: files('spapr_vty.c'))
+system_ss.add(when: 'CONFIG_IMX_LPUART', if_true: files('imx_lpuart.c'))
diff --git a/hw/char/trace-events b/hw/char/trace-events
index a3fcc772877..6d9b71961e5 100644
--- a/hw/char/trace-events
+++ b/hw/char/trace-events
@@ -141,3 +141,6 @@ stm32f2xx_usart_receive(char *id, uint8_t chr) " %s receiving '%c'"
 # riscv_htif.c
 htif_uart_write_to_host(uint8_t device, uint8_t cmd, uint64_t payload) "device: %u cmd: %02u payload: %016" PRIx64
 htif_uart_unknown_device_command(uint8_t device, uint8_t cmd, uint64_t payload) "device: %u cmd: %02u payload: %016" PRIx64
+imx_lpuart_tx(uint8_t ch) "tx 0x%02x"
+imx_lpuart_rx(uint8_t ch) "rx 0x%02x"
+imx_lpuart_baud(uint32_t baud) "baud <- 0x%08x"
diff --git a/include/hw/char/imx_lpuart.h b/include/hw/char/imx_lpuart.h
new file mode 100644
index 00000000000..a18809c432f
--- /dev/null
+++ b/include/hw/char/imx_lpuart.h
@@ -0,0 +1,123 @@
+/*
+ * NXP i.MX LPUART (Low-Power UART) device model
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Models the LPUART IP found in i.MX 7ULP / 8ULP / 8QXP / 95 and other
+ * recent NXP SoCs. Register layout matches the
+ * "fsl,imx95-lpuart" / "fsl,imx8ulp-lpuart" / "fsl,imx7ulp-lpuart"
+ * compatible string in the Linux fsl_lpuart driver.
+ */
+
+#ifndef IMX_LPUART_H
+#define IMX_LPUART_H
+
+#include "hw/core/sysbus.h"
+#include "chardev/char-fe.h"
+#include "qom/object.h"
+
+#define TYPE_IMX_LPUART "imx.lpuart"
+OBJECT_DECLARE_SIMPLE_TYPE(IMXLPUARTState, IMX_LPUART)
+
+/* MMIO size of one LPUART instance (from the device tree). */
+#define IMX_LPUART_REG_SIZE     0x1000
+
+/*
+ * Register offsets. The first four 32-bit words (VERID/PARAM/GLOBAL/
+ * PINCFG) sit before the UART register block proper, which starts at
+ * BAUD = 0x10. The Linux driver compensates with sport->reg_off = 0x10
+ * on i.MX variants; the model exposes the absolute offsets directly.
+ */
+#define LPUART_VERID            0x00
+#define LPUART_PARAM            0x04
+#define LPUART_GLOBAL           0x08
+#define LPUART_PINCFG           0x0C
+#define LPUART_BAUD             0x10
+#define LPUART_STAT             0x14
+#define LPUART_CTRL             0x18
+#define LPUART_DATA             0x1C
+#define LPUART_MATCH            0x20
+#define LPUART_MODIR            0x24
+#define LPUART_FIFO             0x28
+#define LPUART_WATER            0x2C
+
+/* GLOBAL: write GLOBAL_RST to trigger a software reset of the IP. */
+#define LPUART_GLOBAL_RST       0x00000002
+
+/* STAT bits the model implements. */
+#define LPUART_STAT_TDRE        0x00800000  /* TX data register empty */
+#define LPUART_STAT_TC          0x00400000  /* TX complete */
+#define LPUART_STAT_RDRF        0x00200000  /* RX data register full */
+#define LPUART_STAT_IDLE        0x00100000  /* RX line idle */
+#define LPUART_STAT_OR          0x00080000  /* RX overrun */
+/* RW1C mask: writing 1 to these bits clears them. */
+#define LPUART_STAT_W1C_MASK    (LPUART_STAT_IDLE | LPUART_STAT_OR)
+
+/* CTRL bits the model implements. */
+#define LPUART_CTRL_TIE         0x00800000  /* TDRE IRQ enable */
+#define LPUART_CTRL_TCIE        0x00400000  /* TC IRQ enable */
+#define LPUART_CTRL_RIE         0x00200000  /* RDRF IRQ enable */
+#define LPUART_CTRL_ILIE        0x00100000  /* IDLE IRQ enable */
+#define LPUART_CTRL_TE          0x00080000  /* Transmitter enable */
+#define LPUART_CTRL_RE          0x00040000  /* Receiver enable */
+
+/* DATA: bits [7:0] carry the actual character; upper bits are status. */
+#define LPUART_DATA_MASK        0x000000FF
+#define LPUART_DATA_RXEMPT      0x00001000  /* RX buffer was empty */
+
+/*
+ * FIFO bits. TXEMPT/RXEMPT/TXOF/RXUF are read-only (status) and must never be
+ * latched from a guest write - the Linux driver read-modify-writes UARTFIFO,
+ * so a stored RXEMPT would stick and break its RX drain loop. TXFLUSH/RXFLUSH
+ * are write-1 self-clearing commands.
+ */
+#define LPUART_FIFO_TXEMPT      0x00800000
+#define LPUART_FIFO_RXEMPT      0x00400000
+#define LPUART_FIFO_TXOF        0x00020000  /* TX overflow (W1C) */
+#define LPUART_FIFO_RXUF        0x00010000  /* RX underflow (W1C) */
+#define LPUART_FIFO_TXFLUSH     0x00008000  /* self-clearing */
+#define LPUART_FIFO_RXFLUSH     0x00004000  /* self-clearing */
+#define LPUART_FIFO_RO_MASK     (LPUART_FIFO_TXEMPT | LPUART_FIFO_RXEMPT | \
+                                 LPUART_FIFO_TXOF | LPUART_FIFO_RXUF | \
+                                 LPUART_FIFO_TXFLUSH | LPUART_FIFO_RXFLUSH)
+
+/*
+ * PARAM: FIFO depth is encoded as 1 << (field + 1) when nonzero, else 1.
+ * The model implements a single-byte TX path and a 1-deep RX buffer, so
+ * both TX and RX fields read back zero (depth = 1).
+ */
+#define LPUART_PARAM_TXFIFO_SHIFT   0
+#define LPUART_PARAM_RXFIFO_SHIFT   8
+
+/*
+ * VERID: report an i.MX 95-style version. The Linux driver
+ * does not branch on this field for any of the imx*ulp variants, so the
+ * exact value is not load-bearing.
+ */
+#define LPUART_VERID_VALUE      0x04040007
+
+struct IMXLPUARTState {
+    SysBusDevice    parent_obj;
+
+    MemoryRegion    iomem;
+    CharFrontend    chr;
+    qemu_irq        irq;
+
+    /* Register state. Only the writable bits are tracked. */
+    uint32_t        baud;
+    uint32_t        stat;
+    uint32_t        ctrl;
+    uint32_t        match;
+    uint32_t        modir;
+    uint32_t        fifo;
+    uint32_t        water;
+    uint32_t        pincfg;
+
+    /* Single-byte RX buffer (1-deep). */
+    uint8_t         rx_byte;
+    bool            rx_full;
+};
+
+#endif /* IMX_LPUART_H */
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 06/16] hw/i2c: add i.MX LPI2C
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (4 preceding siblings ...)
  2026-08-20  2:48 ` [PATCH 05/16] hw/char: add i.MX LPUART Kyle Fox
@ 2026-08-20  2:48 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 07/16] hw/misc: add i.MX Messaging Unit (MU v2) Kyle Fox
                   ` (9 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kyle Fox, Paolo Bonzini, Peter Maydell,
	open list:MCIMX95-19X19-EVK...

The i.MX Low-Power I2C master. Models the polled master transfer path
(MCR/MSR, the MTDR command FIFO and the MRDR receive FIFO) that the
System Manager firmware uses to bring up the PMIC and IO expander. No
interrupts are modelled (MIER reads back 0). Slave devices attach on the
QEMU I2C bus.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/i2c/Kconfig      |   4 +
 hw/i2c/imx_lpi2c.c  | 296 ++++++++++++++++++++++++++++++++++++++++++++
 hw/i2c/meson.build  |   1 +
 hw/i2c/trace-events |   4 +
 4 files changed, 305 insertions(+)
 create mode 100644 hw/i2c/imx_lpi2c.c

diff --git a/hw/i2c/Kconfig b/hw/i2c/Kconfig
index 0766130b596..ee31513529f 100644
--- a/hw/i2c/Kconfig
+++ b/hw/i2c/Kconfig
@@ -54,3 +54,7 @@ config PMBUS
 config BCM2835_I2C
     bool
     select I2C
+
+config IMX_LPI2C
+    bool
+    select I2C
diff --git a/hw/i2c/imx_lpi2c.c b/hw/i2c/imx_lpi2c.c
new file mode 100644
index 00000000000..96180c02c8f
--- /dev/null
+++ b/hw/i2c/imx_lpi2c.c
@@ -0,0 +1,296 @@
+/*
+ * NXP i.MX LPI2C master controller
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Models the master side of the i.MX LPI2C used by the System Manager
+ * firmware to talk to the board PMIC / IO-expander. Only the polled
+ * blocking-transfer path the SM driver uses is modelled:
+ *
+ *   - MTDR is a command FIFO: the agent writes START (with address),
+ *     TX-data, RX-data (count) and STOP commands.
+ *   - Each command is executed synchronously against the QEMU I2C bus
+ *     (i2c_start_transfer / i2c_send / i2c_recv / i2c_end_transfer).
+ *   - MSR exposes the status flags the driver polls: TDF (tx ready,
+ *     always set since we drain the FIFO immediately), RDF (rx data
+ *     available), SDF (stop detected), NDF (NACK), MBF/BBF (busy).
+ *   - MRDR pops received bytes (RXEMPTY when the rx FIFO is empty).
+ *
+ * No interrupts, DMA, slave mode, clocking or timing are modelled.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/core/sysbus.h"
+#include "hw/i2c/i2c.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+#define TYPE_IMX_LPI2C "imx.lpi2c"
+OBJECT_DECLARE_SIMPLE_TYPE(IMXLPI2CState, IMX_LPI2C)
+
+#define IMX_LPI2C_REG_SIZE      0x10000
+
+/* Register offsets. */
+#define LPI2C_VERID             0x00
+#define LPI2C_PARAM             0x04
+#define LPI2C_MCR               0x10
+#define LPI2C_MSR               0x14
+#define LPI2C_MIER              0x18
+#define LPI2C_MCFGR1            0x24
+#define LPI2C_MFSR              0x5C
+#define LPI2C_MTDR              0x60
+#define LPI2C_MRDR              0x70
+
+/* MCR bits. */
+#define MCR_MEN                 (1u << 0)
+#define MCR_RST                 (1u << 1)
+#define MCR_RTF                 (1u << 8)   /* reset tx FIFO (self-clearing) */
+#define MCR_RRF                 (1u << 9)   /* reset rx FIFO (self-clearing) */
+
+/* MSR bits. */
+#define MSR_TDF                 (1u << 0)
+#define MSR_RDF                 (1u << 1)
+#define MSR_EPF                 (1u << 8)
+#define MSR_SDF                 (1u << 9)
+#define MSR_NDF                 (1u << 10)
+#define MSR_MBF                 (1u << 24)
+#define MSR_BBF                 (1u << 25)
+/* Write-1-to-clear status bits. */
+#define MSR_W1C                 (MSR_EPF | MSR_SDF | MSR_NDF | (0xfu << 11))
+
+/* MTDR command field [10:8]. */
+#define MTDR_CMD_TXDATA         0x0
+#define MTDR_CMD_RXDATA         0x1
+#define MTDR_CMD_STOP           0x2
+#define MTDR_CMD_START          0x4
+
+/* MRDR bits. */
+#define MRDR_RXEMPTY            (1u << 14)
+
+#define RXFIFO_LEN              256
+
+struct IMXLPI2CState {
+    SysBusDevice    parent_obj;
+
+    MemoryRegion    iomem;
+    I2CBus         *bus;
+
+    uint32_t        mcr;
+    uint32_t        msr;       /* sticky status bits (SDF/NDF/MBF/BBF/...) */
+    uint32_t        mcfgr1;
+
+    uint8_t         rxfifo[RXFIFO_LEN];
+    uint32_t        rx_head;
+    uint32_t        rx_count;
+};
+
+static void imx_lpi2c_rx_push(IMXLPI2CState *s, uint8_t b)
+{
+    if (s->rx_count < RXFIFO_LEN) {
+        unsigned tail = (s->rx_head + s->rx_count) % RXFIFO_LEN;
+        s->rxfifo[tail] = b;
+        s->rx_count++;
+    }
+}
+
+static void imx_lpi2c_do_command(IMXLPI2CState *s, uint32_t val)
+{
+    uint32_t cmd = (val >> 8) & 0x7;
+    uint8_t data = val & 0xff;
+
+    switch (cmd) {
+    case MTDR_CMD_START: {
+        /* DATA = (addr << 1) | rw. */
+        uint8_t addr = data >> 1;
+        bool recv = data & 1;
+        trace_imx_lpi2c_start(addr, recv);
+        if (i2c_start_transfer(s->bus, addr, recv) != 0) {
+            s->msr |= MSR_NDF;     /* no slave acked the address */
+        } else {
+            s->msr |= MSR_MBF | MSR_BBF;
+        }
+        break;
+    }
+    case MTDR_CMD_TXDATA:
+        trace_imx_lpi2c_send(data);
+        if (i2c_send(s->bus, data) != 0) {
+            s->msr |= MSR_NDF;
+        }
+        break;
+    case MTDR_CMD_RXDATA: {
+        /* Receive (DATA + 1) bytes. */
+        unsigned n = (unsigned)data + 1;
+        for (unsigned i = 0; i < n; i++) {
+            uint8_t b = i2c_recv(s->bus);
+            trace_imx_lpi2c_recv(b);
+            imx_lpi2c_rx_push(s, b);
+        }
+        break;
+    }
+    case MTDR_CMD_STOP:
+        trace_imx_lpi2c_stop();
+        i2c_end_transfer(s->bus);
+        s->msr |= MSR_SDF | MSR_EPF;
+        s->msr &= ~(MSR_MBF | MSR_BBF);
+        break;
+    default:
+        qemu_log_mask(LOG_UNIMP, "%s: unhandled MTDR cmd %u\n", __func__, cmd);
+        break;
+    }
+}
+
+static uint64_t imx_lpi2c_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMXLPI2CState *s = opaque;
+
+    switch (offset) {
+    case LPI2C_VERID:
+        return 0x01000003;     /* plausible LPI2C version */
+    case LPI2C_PARAM:
+        return 0x00000303;     /* 8-deep tx/rx FIFOs (not load-bearing) */
+    case LPI2C_MCR:
+        return s->mcr;
+    case LPI2C_MSR: {
+        /*
+         * TDF is always ready (we drain the tx command immediately);
+         * RDF reflects the rx FIFO.
+         */
+        uint32_t v = s->msr | MSR_TDF;
+        if (s->rx_count) {
+            v |= MSR_RDF;
+        }
+        return v;
+    }
+    case LPI2C_MIER:
+        return 0;
+    case LPI2C_MCFGR1:
+        return s->mcfgr1;
+    case LPI2C_MFSR:
+        /* rx FIFO count in [22:16]; tx always empty. */
+        return (s->rx_count & 0x7f) << 16;
+    case LPI2C_MRDR:
+        if (s->rx_count == 0) {
+            return MRDR_RXEMPTY;
+        } else {
+            uint8_t b = s->rxfifo[s->rx_head];
+            s->rx_head = (s->rx_head + 1) % RXFIFO_LEN;
+            s->rx_count--;
+            return b;
+        }
+    default:
+        return 0;
+    }
+}
+
+static void imx_lpi2c_write(void *opaque, hwaddr offset,
+                            uint64_t value, unsigned size)
+{
+    IMXLPI2CState *s = opaque;
+
+    switch (offset) {
+    case LPI2C_MCR:
+        if (value & MCR_RRF) {
+            s->rx_head = s->rx_count = 0;
+        }
+        /* RST / RTF / RRF are momentary; don't persist them. */
+        s->mcr = value & ~(MCR_RST | MCR_RTF | MCR_RRF);
+        break;
+    case LPI2C_MSR:
+        /* Write-1-to-clear the status flags. */
+        s->msr &= ~((uint32_t)value & MSR_W1C);
+        break;
+    case LPI2C_MIER:
+    case LPI2C_MCFGR1:
+        if (offset == LPI2C_MCFGR1) {
+            s->mcfgr1 = value;
+        }
+        break;
+    case LPI2C_MTDR:
+        if (s->mcr & MCR_MEN) {
+            imx_lpi2c_do_command(s, value);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static const MemoryRegionOps imx_lpi2c_ops = {
+    .read = imx_lpi2c_read,
+    .write = imx_lpi2c_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 imx_lpi2c_reset_hold(Object *obj, ResetType type)
+{
+    IMXLPI2CState *s = IMX_LPI2C(obj);
+
+    s->mcr = 0;
+    s->msr = 0;
+    s->mcfgr1 = 0;
+    s->rx_head = s->rx_count = 0;
+}
+
+static void imx_lpi2c_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMXLPI2CState *s = IMX_LPI2C(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx_lpi2c_ops, s,
+                          TYPE_IMX_LPI2C, IMX_LPI2C_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+    s->bus = i2c_init_bus(DEVICE(obj), "i2c");
+}
+
+static const VMStateDescription vmstate_imx_lpi2c = {
+    .name = TYPE_IMX_LPI2C,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(mcr, IMXLPI2CState),
+        VMSTATE_UINT32(msr, IMXLPI2CState),
+        VMSTATE_UINT32(mcfgr1, IMXLPI2CState),
+        VMSTATE_UINT8_ARRAY(rxfifo, IMXLPI2CState, RXFIFO_LEN),
+        VMSTATE_UINT32(rx_head, IMXLPI2CState),
+        VMSTATE_UINT32(rx_count, IMXLPI2CState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx_lpi2c_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx_lpi2c;
+    rc->phases.hold = imx_lpi2c_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX LPI2C master";
+}
+
+static const TypeInfo imx_lpi2c_info = {
+    .name           = TYPE_IMX_LPI2C,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMXLPI2CState),
+    .instance_init  = imx_lpi2c_init,
+    .class_init     = imx_lpi2c_class_init,
+};
+
+static void imx_lpi2c_register_types(void)
+{
+    type_register_static(&imx_lpi2c_info);
+}
+
+type_init(imx_lpi2c_register_types)
diff --git a/hw/i2c/meson.build b/hw/i2c/meson.build
index 88aea35662d..ad6738f820e 100644
--- a/hw/i2c/meson.build
+++ b/hw/i2c/meson.build
@@ -19,4 +19,5 @@ i2c_ss.add(when: 'CONFIG_PPC4XX', if_true: files('ppc4xx_i2c.c'))
 i2c_ss.add(when: 'CONFIG_PCA954X', if_true: files('i2c_mux_pca954x.c'))
 i2c_ss.add(when: 'CONFIG_PMBUS', if_true: files('pmbus_device.c'))
 i2c_ss.add(when: 'CONFIG_BCM2835_I2C', if_true: files('bcm2835_i2c.c'))
+i2c_ss.add(when: 'CONFIG_IMX_LPI2C', if_true: files('imx_lpi2c.c'))
 system_ss.add_all(when: 'CONFIG_I2C', if_true: i2c_ss)
diff --git a/hw/i2c/trace-events b/hw/i2c/trace-events
index 1ad0e95c0e6..88078e1c590 100644
--- a/hw/i2c/trace-events
+++ b/hw/i2c/trace-events
@@ -61,3 +61,7 @@ pca954x_read_data(uint8_t value) "PCA954X read data: 0x%02x"
 
 imx_i2c_read(const char *id, const char *reg, uint64_t ofs, uint64_t value) "%s:[%s (0x%" PRIx64 ")] -> 0x%02" PRIx64
 imx_i2c_write(const char *id, const char *reg, uint64_t ofs, uint64_t value) "%s:[%s (0x%" PRIx64 ")] <- 0x%02" PRIx64
+imx_lpi2c_start(uint8_t addr, int recv) "start addr 0x%02x recv %d"
+imx_lpi2c_send(uint8_t data) "tx 0x%02x"
+imx_lpi2c_recv(uint8_t data) "rx 0x%02x"
+imx_lpi2c_stop(void) "stop"
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 07/16] hw/misc: add i.MX Messaging Unit (MU v2)
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (5 preceding siblings ...)
  2026-08-20  2:48 ` [PATCH 06/16] hw/i2c: add i.MX LPI2C Kyle Fox
@ 2026-08-20  2:48 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 08/16] hw/misc: add NXP EdgeLock Enclave (ELE) responder Kyle Fox
                   ` (8 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, Peter Maydell, open list:i.MX31 (kzm)

The i.MX Messaging Unit (V2 register layout): the mailbox that carries
SCMI between the A55 cluster and the M33 System Manager. Models
CR/SR/GCR/GSR/GIER, the TR/RR data registers and the GCR.GIRn/GSR.GIPn
doorbell. Two endpoints pair through a peer QOM link: a TR write is
delivered into the peer's RR and the TX handshake completes when the
peer reads RR, so the real, unmodified SM firmware answers Linux's SCMI
traffic over the emulated mailbox.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/misc/Kconfig          |   3 +
 hw/misc/imx_mu.c         | 454 +++++++++++++++++++++++++++++++++++++++
 hw/misc/meson.build      |   1 +
 hw/misc/trace-events     |   4 +
 include/hw/misc/imx_mu.h | 144 +++++++++++++
 5 files changed, 606 insertions(+)
 create mode 100644 hw/misc/imx_mu.c
 create mode 100644 include/hw/misc/imx_mu.h

diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index b8860dd3e77..8645ca11f3f 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -261,3 +261,6 @@ config AXIADO_CLK
     bool
 
 source macio/Kconfig
+
+config IMX_MU
+    bool
diff --git a/hw/misc/imx_mu.c b/hw/misc/imx_mu.c
new file mode 100644
index 00000000000..c9f372a8726
--- /dev/null
+++ b/hw/misc/imx_mu.c
@@ -0,0 +1,454 @@
+/*
+ * NXP i.MX Messaging Unit (MU) device model - V2 register layout
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Implements the subset of the V2 MU register set exercised by U-Boot
+ * SPL's imx_mu_init_generic() probe path and by SCMI mailbox traffic:
+ * CR/SR/GCR/GSR/GIER/TCR/TSR/RCR/RSR + 4 TR/RR data registers, plus
+ * a doorbell mechanism via GCR.GIRn / GSR.GIPn.
+ *
+ * Models enough for the SPL probe to succeed and for the SM to
+ * handshake on doorbells. No
+ * cross-domain interrupt routing, no flag-bit semantics beyond
+ * read/write-back, no per-channel TX/RX FIFO depth modelling.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/misc/imx_mu.h"
+#include "hw/core/irq.h"
+#include "hw/core/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+static void imx_mu_update_irq(IMXMUState *s);
+
+static int imx_mu_post_load(void *opaque, int version_id)
+{
+    /* Recompute the IRQ line level from the restored register state. */
+    imx_mu_update_irq(opaque);
+    return 0;
+}
+
+static const VMStateDescription vmstate_imx_mu = {
+    .name = TYPE_IMX_MU,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .post_load = imx_mu_post_load,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(cr, IMXMUState),
+        VMSTATE_UINT32(sr, IMXMUState),
+        VMSTATE_UINT32(fcr, IMXMUState),
+        VMSTATE_UINT32(fsr, IMXMUState),
+        VMSTATE_UINT32(gier, IMXMUState),
+        VMSTATE_UINT32(gcr, IMXMUState),
+        VMSTATE_UINT32(gsr, IMXMUState),
+        VMSTATE_UINT32(tcr, IMXMUState),
+        VMSTATE_UINT32(tsr, IMXMUState),
+        VMSTATE_UINT32(rcr, IMXMUState),
+        VMSTATE_UINT32(rsr, IMXMUState),
+        VMSTATE_UINT32_ARRAY(tr, IMXMUState, IMX_MU_NUM_CHANNELS),
+        VMSTATE_UINT32_ARRAY(rr, IMXMUState, IMX_MU_NUM_CHANNELS),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+/*
+ * Recompute the IRQ line. V2 layout: each xSR pending bit ANDed with
+ * the matching xCR enable bit; any one of those produces an interrupt.
+ * GP-IRQ comes from (GSR.GIPn & GIER.GIEn) across the channels in use.
+ */
+static void imx_mu_update_irq(IMXMUState *s)
+{
+    uint32_t mask = (1u << IMX_MU_NUM_CHANNELS) - 1u;
+    bool level =
+        ((s->gsr & s->gier & mask) != 0) ||
+        ((s->tsr & s->tcr  & mask) != 0) ||
+        ((s->rsr & s->rcr  & mask) != 0);
+
+    qemu_set_irq(s->irq, level);
+}
+
+static void imx_mu_reset_state(IMXMUState *s)
+{
+    s->cr   = 0;
+    s->sr   = 0;
+    s->fcr  = 0;
+    s->fsr  = 0;
+    s->gier = 0;
+    s->gcr  = 0;
+    s->gsr  = 0;
+    s->tcr  = 0;
+    /* All TX slots come up empty - TEn bits set. */
+    s->tsr  = (1u << IMX_MU_NUM_CHANNELS) - 1u;
+    s->rcr  = 0;
+    s->rsr  = 0;
+    memset(s->tr, 0, sizeof(s->tr));
+    memset(s->rr, 0, sizeof(s->rr));
+}
+
+static void imx_mu_reset_at_boot_hold(Object *obj, ResetType type)
+{
+    IMXMUState *s = IMX_MU(obj);
+
+    imx_mu_reset_state(s);
+    imx_mu_update_irq(s);
+}
+
+void imx_mu_assert_gip(IMXMUState *s, unsigned int idx)
+{
+    if (idx >= IMX_MU_NUM_CHANNELS) {
+        return;
+    }
+    trace_imx_mu_gip(idx);
+    s->gsr |= IMX_MU_V2_BIT(idx);
+    imx_mu_update_irq(s);
+}
+
+void imx_mu_set_doorbell_handler(IMXMUState *s,
+                                 IMXMUDoorbellHandler handler,
+                                 void *opaque)
+{
+    s->doorbell_handler = handler;
+    s->doorbell_opaque  = opaque;
+}
+
+void imx_mu_set_tr_write_handler(IMXMUState *s,
+                                 IMXMUTRWriteHandler handler,
+                                 void *opaque)
+{
+    s->tr_write_handler = handler;
+    s->tr_write_opaque  = opaque;
+}
+
+void imx_mu_deliver_rr(IMXMUState *s, unsigned int idx, uint32_t value)
+{
+    if (idx >= IMX_MU_NUM_CHANNELS) {
+        return;
+    }
+    trace_imx_mu_rr_deliver(idx, value);
+    s->rr[idx]  = value;
+    s->rsr     |= IMX_MU_V2_BIT(idx);
+    imx_mu_update_irq(s);
+}
+
+static uint64_t imx_mu_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMXMUState *s = opaque;
+    uint64_t value = 0;
+
+    if (offset >= IMX_MU_TR_BASE &&
+        offset < IMX_MU_TR_BASE + IMX_MU_NUM_CHANNELS * 4) {
+        value = s->tr[(offset - IMX_MU_TR_BASE) / 4];
+        return value;
+    }
+    if (offset >= IMX_MU_RR_BASE &&
+        offset < IMX_MU_RR_BASE + IMX_MU_NUM_CHANNELS * 4) {
+        unsigned idx = (offset - IMX_MU_RR_BASE) / 4;
+        value = s->rr[idx];
+        /*
+         * Reading RR[n] clears RSR.RFn (the receive-full status bit)
+         * so the next sender can re-queue. The SCMI transport doesn't
+         * rely on this, but it matches the documented HW
+         * behaviour and lets the U-Boot probe drain pending data.
+         *
+         * For a peer-linked MU this completes the TX handshake: the word
+         * was delivered into RR[n] by the peer's TR[n] write (which left
+         * the peer's TSR.TEn clear), so draining it re-asserts the peer's
+         * TX-empty and lets a blocked mbox send finish.
+         */
+        s->rsr &= ~IMX_MU_V2_BIT(idx);
+        if (s->peer) {
+            s->peer->tsr |= IMX_MU_V2_BIT(idx);
+            imx_mu_update_irq(s->peer);
+        }
+        imx_mu_update_irq(s);
+        return value;
+    }
+
+    switch (offset) {
+    case IMX_MU_VER:
+        /* Plausible V2 version. Linux/U-Boot do not branch on this. */
+        value = 0x00020000;
+        break;
+    case IMX_MU_PAR:
+        /* TR count in [7:0], RR count in [15:8] (V2 encoding). */
+        value = ((uint32_t)IMX_MU_NUM_CHANNELS) |
+                ((uint32_t)IMX_MU_NUM_CHANNELS << 8);
+        break;
+    case IMX_MU_CR:
+        value = s->cr;
+        break;
+    case IMX_MU_SR:
+        value = s->sr;
+        break;
+    case IMX_MU_FCR:
+        value = s->fcr;
+        break;
+    case IMX_MU_FSR:
+        value = s->fsr;
+        break;
+    case IMX_MU_GIER:
+        value = s->gier;
+        break;
+    case IMX_MU_GCR:
+        value = s->gcr;
+        break;
+    case IMX_MU_GSR:
+        value = s->gsr;
+        break;
+    case IMX_MU_TCR:
+        value = s->tcr;
+        break;
+    case IMX_MU_TSR:
+        value = s->tsr;
+        break;
+    case IMX_MU_RCR:
+        value = s->rcr;
+        break;
+    case IMX_MU_RSR:
+        value = s->rsr;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: bad read offset 0x%" HWADDR_PRIx "\n",
+                      __func__, offset);
+        break;
+    }
+    return value;
+}
+
+static void imx_mu_write(void *opaque, hwaddr offset,
+                         uint64_t value, unsigned size)
+{
+    IMXMUState *s = opaque;
+
+    if (offset >= IMX_MU_TR_BASE &&
+        offset < IMX_MU_TR_BASE + IMX_MU_NUM_CHANNELS * 4) {
+        unsigned idx = (offset - IMX_MU_TR_BASE) / 4;
+        s->tr[idx] = value;
+        trace_imx_mu_tr_write(idx, value);
+        /*
+         * Writing TR[n] clears TSR.TEn (TX empty). Three delivery modes:
+         *  - TR-write handler (a responder, e.g. the ELE server): consumes
+         *    synchronously and we re-set TEn so the next write succeeds
+         *    without polling.
+         *  - peer linked (the other side of a real MU, e.g. the A55<->M7
+         *    rpmsg channel on MU7): deliver the word into the peer's RR[n]
+         *    and raise its RX interrupt. TEn stays clear until the peer
+         *    reads RR[n] (see the RR read path), which is the real MU TX
+         *    full/empty handshake the imx-mailbox driver waits on.
+         *  - neither (unattached MU): the slot stays "full" forever, which
+         *    is correct for a mailbox with nothing on the far side.
+         */
+        s->tsr &= ~IMX_MU_V2_BIT(idx);
+        if (s->tr_write_handler) {
+            s->tr_write_handler(s->tr_write_opaque, idx, value);
+            s->tsr |= IMX_MU_V2_BIT(idx);
+        } else if (s->peer) {
+            imx_mu_deliver_rr(s->peer, idx, (uint32_t)value);
+        }
+        imx_mu_update_irq(s);
+        return;
+    }
+    if (offset >= IMX_MU_RR_BASE &&
+        offset < IMX_MU_RR_BASE + IMX_MU_NUM_CHANNELS * 4) {
+        /* RR is RO from the guest's perspective. */
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: write to read-only RR at 0x%" HWADDR_PRIx "\n",
+                      __func__, offset);
+        return;
+    }
+
+    switch (offset) {
+    case IMX_MU_VER:
+    case IMX_MU_PAR:
+        /* Read-only. Silent ignore matches HW. */
+        break;
+
+    case IMX_MU_CR:
+        if (value & IMX_MU_CR_RST) {
+            imx_mu_reset_state(s);
+            imx_mu_update_irq(s);
+            return;
+        }
+        s->cr = value;
+        break;
+
+    case IMX_MU_SR:
+        /*
+         * Per the kernel/U-Boot drivers, SR bits in V2 are read-only
+         * status that the model manages. Writing has no effect.
+         */
+        break;
+
+    case IMX_MU_FCR:
+        /*
+         * Flag-update register. On hardware a written flag bit is reflected
+         * to the peer's FSR; the SCMI transport does not use these flags, so
+         * we just store the value (no peer reflection or interrupt modelled).
+         */
+        s->fcr = value;
+        break;
+
+    case IMX_MU_FSR:
+        /* Write-1-to-clear of latched flag-status bits. */
+        s->fsr &= ~value;
+        imx_mu_update_irq(s);
+        break;
+
+    case IMX_MU_GIER:
+        s->gier = value;
+        imx_mu_update_irq(s);
+        break;
+
+    case IMX_MU_GCR: {
+        /*
+         * GCR.GIRn writes are doorbell triggers. Detect 0->1 transitions
+         * and, for each newly-asserted channel, deliver the doorbell. The
+         * bit is auto-cleared afterwards (write-1-to-trigger pulse),
+         * mirroring real HW where the request clears once the peer ACKs.
+         *
+         * Two delivery modes:
+         *  - peer linked (the other side of a real MU): latch the matching
+         *    GSR.GIPn on the peer and recompute the peer's IRQ. GIPn latches
+         *    regardless of the peer's GIER, so a doorbell that arrives before
+         *    the peer enables GIER.GIEn fires the moment it does (the peer's
+         *    GIER write recomputes the IRQ) - real pending-vs-enable HW.
+         *  - doorbell handler (a responder, e.g. the ELE server): invoke it
+         *    synchronously to consume the request.
+         */
+        uint32_t mask  = (1u << IMX_MU_NUM_CHANNELS) - 1u;
+        uint32_t newly = (value & ~s->gcr) & mask;
+        s->gcr = value;
+        for (unsigned i = 0; i < IMX_MU_NUM_CHANNELS; i++) {
+            if (!(newly & IMX_MU_V2_BIT(i))) {
+                continue;
+            }
+            trace_imx_mu_doorbell(i);
+            if (s->peer) {
+                s->peer->gsr |= IMX_MU_V2_BIT(i);
+                imx_mu_update_irq(s->peer);
+                s->gcr &= ~IMX_MU_V2_BIT(i);
+            } else if (s->doorbell_handler) {
+                s->doorbell_handler(s->doorbell_opaque, i);
+                s->gcr &= ~IMX_MU_V2_BIT(i);
+            }
+        }
+        imx_mu_update_irq(s);
+        break;
+    }
+
+    case IMX_MU_GSR:
+        /* Write-1-to-clear of GIP bits. */
+        s->gsr &= ~value;
+        imx_mu_update_irq(s);
+        break;
+
+    case IMX_MU_TCR:
+        s->tcr = value;
+        imx_mu_update_irq(s);
+        break;
+
+    case IMX_MU_TSR:
+        /* W1C of TX status. */
+        s->tsr &= ~value;
+        imx_mu_update_irq(s);
+        break;
+
+    case IMX_MU_RCR:
+        s->rcr = value;
+        imx_mu_update_irq(s);
+        break;
+
+    case IMX_MU_RSR:
+        /* W1C of RX status. */
+        s->rsr &= ~value;
+        imx_mu_update_irq(s);
+        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 imx_mu_ops = {
+    .read = imx_mu_read,
+    .write = imx_mu_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .impl = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+    .valid = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+    },
+};
+
+/*
+ * "peer" accepts any TYPE_IMX_MU and is settable at any time: the SM/M7 MUB
+ * endpoint is only created (and hence linkable) once its M-core is realized,
+ * so the SoC establishes the link after realize. A non-NULL check is what
+ * makes the link writable; the type is already enforced by the link itself.
+ */
+static void imx_mu_peer_check(const Object *obj, const char *name,
+                              Object *val, Error **errp)
+{
+}
+
+static void imx_mu_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMXMUState *s = IMX_MU(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx_mu_ops, s,
+                          TYPE_IMX_MU, IMX_MU_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+    sysbus_init_irq(sbd, &s->irq);
+
+    /*
+     * "peer" links the two endpoints (MUA/MUB) of one physical MU. It is a
+     * weak link - both endpoints are owned by the SoC container, so neither
+     * refs the other - and is settable after realize, because the SM/M7 MUB
+     * side is only created once the M-core it faces exists. That is why this
+     * uses object_property_add_link() rather than a static DEFINE_PROP_LINK
+     * (which forbids setting a link once the device is realized).
+     */
+    object_property_add_link(obj, "peer", TYPE_IMX_MU,
+                             (Object **)&s->peer, imx_mu_peer_check, 0);
+}
+
+static void imx_mu_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx_mu;
+    rc->phases.hold = imx_mu_reset_at_boot_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX Messaging Unit (V2)";
+}
+
+static const TypeInfo imx_mu_info = {
+    .name           = TYPE_IMX_MU,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMXMUState),
+    .instance_init  = imx_mu_init,
+    .class_init     = imx_mu_class_init,
+};
+
+static void imx_mu_register_types(void)
+{
+    type_register_static(&imx_mu_info);
+}
+
+type_init(imx_mu_register_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index e86d9ad6b39..9d0e3ed220d 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -171,3 +171,4 @@ system_ss.add(when: 'CONFIG_SBSA_REF', if_true: files('sbsa_ec.c'))
 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'))
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index c9a868b3efb..01b0b275f7f 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -442,3 +442,7 @@ iommu_testdev_dma_read(uint64_t gva, uint32_t len) "gva=0x%" PRIx64 " len=%u"
 iommu_testdev_dma_verify(uint32_t expected, uint32_t actual) "expected=0x%x actual=0x%x"
 iommu_testdev_dma_result(uint32_t result) "DMA completed result=0x%x"
 iommu_testdev_dma_armed(bool armed) "armed=%d"
+imx_mu_tr_write(unsigned idx, uint32_t val) "TR[%u] <- 0x%08x"
+imx_mu_rr_deliver(unsigned idx, uint32_t val) "RR[%u] <- 0x%08x"
+imx_mu_doorbell(unsigned idx) "doorbell GIR channel %u"
+imx_mu_gip(unsigned idx) "GIP assert channel %u"
diff --git a/include/hw/misc/imx_mu.h b/include/hw/misc/imx_mu.h
new file mode 100644
index 00000000000..e308ebf70ea
--- /dev/null
+++ b/include/hw/misc/imx_mu.h
@@ -0,0 +1,144 @@
+/*
+ * NXP i.MX Messaging Unit (MU) device model - V2 register layout
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Models the V2 MU register layout used by i.MX 95 (compatible
+ * "fsl,imx95-mu") and other recent NXP SoCs. The MU is the
+ * messaging channel between the Cortex-A55 cluster and the Cortex-M33
+ * System Manager (and ELE, V2X, camera-mix, etc., depending on which
+ * instance); it carries SCMI traffic for clock / pinctrl / power-
+ * domain operations.
+ */
+
+#ifndef IMX_MU_H
+#define IMX_MU_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+#define TYPE_IMX_MU "imx.mu"
+OBJECT_DECLARE_SIMPLE_TYPE(IMXMUState, IMX_MU)
+
+#define IMX_MU_REG_SIZE         0x1000
+#define IMX_MU_NUM_CHANNELS     4       /* TR[0..3] / RR[0..3] */
+
+/*
+ * V2 register offsets. Source: NXP U-Boot imx_mu_cfg_imx95 in
+ * U-Boot drivers/mailbox/imx-mailbox.c.
+ */
+#define IMX_MU_VER              0x000   /* version (RO) */
+#define IMX_MU_PAR              0x004   /* parameter (RO; TR/RR counts) */
+#define IMX_MU_CR               0x008   /* control */
+#define IMX_MU_SR               0x00C   /* status */
+#define IMX_MU_FCR              0x100   /* flag control */
+#define IMX_MU_FSR              0x104   /* flag status */
+#define IMX_MU_GIER             0x110   /* general-purpose IRQ enable */
+#define IMX_MU_GCR              0x114   /* general-purpose control */
+#define IMX_MU_GSR              0x118   /* general-purpose status (GIP bits) */
+#define IMX_MU_TCR              0x120   /* TX IRQ enable */
+#define IMX_MU_TSR              0x124   /* TX status (TEn = TX empty) */
+#define IMX_MU_RCR              0x128   /* RX IRQ enable */
+#define IMX_MU_RSR              0x12C   /* RX status (RFn = RX full) */
+#define IMX_MU_TR_BASE          0x200   /* TR[0..15] (TX data) */
+#define IMX_MU_RR_BASE          0x280   /* RR[0..15] (RX data) */
+
+/* Helpers for V2 bit numbering (one bit per channel index, starting at 0). */
+#define IMX_MU_V2_BIT(idx)      (1u << (idx))
+
+/* CR.RST clears all state when written (V2 bit 0). */
+#define IMX_MU_CR_RST           BIT(0)
+
+/*
+ * Optional callback invoked when the guest writes a 0->1 transition
+ * on any GCR.GIRn bit (a doorbell trigger from the agent side). A
+ * responder (e.g. the ELE server) registers itself here to process
+ * inbound messages. Unset by default; the model functions as a plain
+ * register file when no handler is registered.
+ */
+typedef void (*IMXMUDoorbellHandler)(void *opaque, unsigned int idx);
+
+/*
+ * Optional callback invoked when the guest writes to a TR[idx] register.
+ * The ELE responder stub uses this to accumulate incoming ELE-protocol
+ * message words and react when a full message has arrived. Unset by
+ * default; the model is a plain register file with no consumer.
+ */
+typedef void (*IMXMUTRWriteHandler)(void *opaque, unsigned int idx,
+                                    uint32_t value);
+
+struct IMXMUState {
+    SysBusDevice    parent_obj;
+
+    MemoryRegion    iomem;
+    qemu_irq        irq;
+
+    /*
+     * Writable register state. Read-only registers (VER, PAR) are
+     * computed at read time from the configured channel count.
+     */
+    uint32_t        cr;
+    uint32_t        sr;
+    uint32_t        fcr;
+    uint32_t        fsr;
+    uint32_t        gier;
+    uint32_t        gcr;
+    uint32_t        gsr;
+    uint32_t        tcr;
+    uint32_t        tsr;
+    uint32_t        rcr;
+    uint32_t        rsr;
+    uint32_t        tr[IMX_MU_NUM_CHANNELS];
+    uint32_t        rr[IMX_MU_NUM_CHANNELS];
+
+    /* Doorbell forwarding (see typedef above). */
+    IMXMUDoorbellHandler doorbell_handler;
+    void                *doorbell_opaque;
+
+    /* TR-write forwarding (see typedef above). */
+    IMXMUTRWriteHandler  tr_write_handler;
+    void                *tr_write_opaque;
+
+    /*
+     * Optional peer MU endpoint (the other side of a real hardware MU).
+     * When set, a GCR.GIRn doorbell trigger on this side latches the
+     * matching GSR.GIPn on the peer (raising the peer's IRQ once the peer
+     * enables GIER.GIEn), instead of invoking doorbell_handler. This models
+     * the A55-side (MUA) <-> M33-side (MUB) cross-connect used to let the
+     * real SM firmware service the A55's SCMI traffic. Set via the "peer"
+     * QOM link property (registered in imx_mu_init()).
+     */
+    IMXMUState          *peer;
+};
+
+/*
+ * Register a doorbell handler. Replaces any previously registered
+ * handler. Call with handler = NULL to deregister.
+ */
+void imx_mu_set_doorbell_handler(IMXMUState *s,
+                                 IMXMUDoorbellHandler handler,
+                                 void *opaque);
+
+/* Register a TR-write handler. NULL to deregister. */
+void imx_mu_set_tr_write_handler(IMXMUState *s,
+                                 IMXMUTRWriteHandler handler,
+                                 void *opaque);
+
+/*
+ * Deliver a response word into RR[idx] and assert RSR.RFn so the
+ * agent's mu_hal_receivemsg() poll loop exits. Used by responder
+ * stubs (e.g. ELE) to push a message back to the agent.
+ */
+void imx_mu_deliver_rr(IMXMUState *s, unsigned int idx, uint32_t value);
+
+/*
+ * External hook: peripheral code (e.g. a responder) can call
+ * this to assert a GP-interrupt-pending bit on a particular channel,
+ * which surfaces as GSR.GIPn and (if GIER.GIEn is set) raises the IRQ.
+ * Used by the response side of a doorbell handshake.
+ */
+void imx_mu_assert_gip(IMXMUState *s, unsigned int idx);
+
+#endif /* IMX_MU_H */
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 08/16] hw/misc: add NXP EdgeLock Enclave (ELE) responder
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (6 preceding siblings ...)
  2026-08-20  2:48 ` [PATCH 07/16] hw/misc: add i.MX Messaging Unit (MU v2) Kyle Fox
@ 2026-08-20  2:48 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 09/16] hw/timer: add i.MX 95 system counter Kyle Fox
                   ` (7 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, open list:MCIMX95-19X19-EVK...

The NXP EdgeLock Enclave (ELE) responder. U-Boot and the SM firmware
issue ELE service requests (e.g. GET_INFO, get-random) over a dedicated
MU; this models the responder that consumes the request words and
returns a response so the secure bring-up sequence proceeds. It is
linked to its MU through a QOM link property.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/misc/Kconfig                    |   4 +
 hw/misc/imx95_ele_server.c         | 284 +++++++++++++++++++++++++++++
 hw/misc/meson.build                |   1 +
 hw/misc/trace-events               |   2 +
 include/hw/misc/imx95_ele_server.h |  47 +++++
 5 files changed, 338 insertions(+)
 create mode 100644 hw/misc/imx95_ele_server.c
 create mode 100644 include/hw/misc/imx95_ele_server.h

diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index 8645ca11f3f..ded60e21a6c 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -264,3 +264,7 @@ source macio/Kconfig
 
 config IMX_MU
     bool
+
+config IMX95_ELE_SERVER
+    bool
+    select IMX_MU
diff --git a/hw/misc/imx95_ele_server.c b/hw/misc/imx95_ele_server.c
new file mode 100644
index 00000000000..b8ee3f78b61
--- /dev/null
+++ b/hw/misc/imx95_ele_server.c
@@ -0,0 +1,284 @@
+/*
+ * Minimal NXP EdgeLock Enclave (ELE) responder stub
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Watches TR-register writes on an i.MX MU, accumulates ELE-protocol
+ * command words until the per-message size is reached, dispatches to a
+ * command handler, and writes the response into RR registers + asserts
+ * RSR.RFn so the agent's mu_hal_receivemsg() poll exits.
+ *
+ * ELE message format (per ele_api.h):
+ *
+ *   word 0 (header): version[7:0] | size[15:8] | command[23:16] | tag[31:24]
+ *   word 1..N-1:     payload
+ *
+ * size = total words including header. tag = ELE_CMD_TAG (0x17) for
+ * commands, ELE_RESP_TAG (0xE1) for responses. Responses also have a
+ * status byte (ELE_SUCCESS_IND = 0xD6 for success) somewhere in the
+ * payload depending on command.
+ *
+ * Only ELE_GET_INFO_REQ is implemented in detail (the
+ * only ELE call U-Boot SPL's imx9_probe_mu() makes pre-relocation,
+ * via ele_get_info()). Other commands get a generic SUCCESS response.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/main-loop.h"
+#include "qemu/module.h"
+#include "qapi/error.h"
+#include "hw/misc/imx95_ele_server.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/qdev-properties-system.h"
+#include "system/dma.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+/*
+ * struct ele_get_info_data layout (u32 word offsets), pinned to the
+ * U-Boot ele_api.h definition at
+ * U-Boot arch/arm/include/asm/mach-imx/ele_api.h:168.
+ * Using named offsets here so a future U-Boot rev that reorders or
+ * adds fields trips a build/runtime mismatch instead of silently
+ * writing into the wrong slot.
+ *
+ * Reference layout:
+ *   u32 hdr;                  // word 0
+ *   u32 soc;                  // word 1
+ *   u32 lc;                   // word 2
+ *   u32 uid[4];               // words 3..6
+ *   u32 sha256_rom_patch[8];  // words 7..14
+ *   u32 sha_fw[8];            // words 15..22
+ *   u32 oem_srkh[16];         // words 23..38
+ *   u32 state;                // word 39
+ *   u32 oem_pqc_srkh[16];     // words 40..55
+ *   u32 reserved[8];          // words 56..63
+ * Total: 64 u32 = 256 bytes.
+ */
+#define ELE_INFO_OFFSET_HDR             0
+#define ELE_INFO_OFFSET_SOC             1
+#define ELE_INFO_OFFSET_LC              2
+#define ELE_INFO_OFFSET_UID             3
+#define ELE_INFO_OFFSET_SHA256_ROM      7
+#define ELE_INFO_OFFSET_SHA_FW          15
+#define ELE_INFO_OFFSET_OEM_SRKH        23
+#define ELE_INFO_OFFSET_STATE           39
+#define ELE_INFO_OFFSET_OEM_PQC_SRKH    40
+#define ELE_INFO_OFFSET_RESERVED        56
+#define ELE_INFO_SIZE_WORDS             64
+#define ELE_INFO_SIZE_BYTES             (ELE_INFO_SIZE_WORDS * 4)
+
+/* Header field packing. */
+static inline uint32_t ele_make_header(uint8_t version, uint8_t size,
+                                       uint8_t command, uint8_t tag)
+{
+    return (uint32_t)version |
+           ((uint32_t)size    << 8) |
+           ((uint32_t)command << 16) |
+           ((uint32_t)tag     << 24);
+}
+
+/* Header field extraction. */
+#define ELE_HDR_VERSION(h)  ((uint8_t)((h) & 0xFF))
+#define ELE_HDR_SIZE(h)     ((uint8_t)(((h) >> 8) & 0xFF))
+#define ELE_HDR_COMMAND(h)  ((uint8_t)(((h) >> 16) & 0xFF))
+#define ELE_HDR_TAG(h)      ((uint8_t)(((h) >> 24) & 0xFF))
+
+/*
+ * ELE_GET_INFO_REQ handler.
+ *
+ * Request layout (4 words):
+ *   [0] header (command = ELE_GET_INFO_REQ)
+ *   [1] info_addr_hi (upper 32 bits of guest pointer)
+ *   [2] info_addr_lo (lower 32 bits of guest pointer)
+ *   [3] size of ele_get_info_data
+ *
+ * Real ELE writes a struct ele_get_info_data to the guest address.
+ * We fake plausible values: rev 0xA1 i.MX 95, OEM-open lifecycle,
+ * UID zero-filled. The Linux/U-Boot drivers do not validate beyond
+ * memcpy into gd->arch.
+ *
+ * Response: 2 words. Status word ELE_SUCCESS_IND in data[0].
+ */
+static void ele_handle_get_info(IMX95ELEServerState *s)
+{
+    if (s->msg_count < 4) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: GET_INFO with only %u words received\n",
+                      __func__, s->msg_count);
+        return;
+    }
+
+    uint64_t info_addr = ((uint64_t)s->msg_buf[1] << 32) |
+                          (uint64_t)s->msg_buf[2];
+
+    /*
+     * Zero-fill the full 256-byte struct, then set the fields U-Boot's
+     * set_cpu_info() reads (soc, lc). Named offsets above keep the
+     * mapping legible; if U-Boot ever shifts the struct layout, the
+     * build check below + a re-read of ele_api.h are how this gets
+     * detected.
+     */
+    uint32_t info_data[ELE_INFO_SIZE_WORDS] = {0};
+    QEMU_BUILD_BUG_ON(sizeof(info_data) != ELE_INFO_SIZE_BYTES);
+
+    info_data[ELE_INFO_OFFSET_SOC] = 0xA1009500;  /* SoC rev 0xA1, type 0x95 */
+    info_data[ELE_INFO_OFFSET_LC]  = 0x00000080;  /* lifecycle = OEM open */
+
+    dma_memory_write(&address_space_memory, info_addr,
+                     info_data, sizeof(info_data),
+                     MEMTXATTRS_UNSPECIFIED);
+
+    /* Response: header + status. */
+    uint32_t resp_hdr = ele_make_header(ELE_VERSION, 2,
+                                        ELE_GET_INFO_REQ, ELE_RESP_TAG);
+    trace_imx95_ele_response(ELE_GET_INFO_REQ);
+    imx_mu_deliver_rr(s->mu, 0, resp_hdr);
+    imx_mu_deliver_rr(s->mu, 1, ELE_SUCCESS_IND);
+}
+
+/*
+ * Generic SUCCESS response for any command we do not specifically
+ * handle. 2-word response: header + ELE_SUCCESS_IND.
+ */
+static void ele_handle_generic_ok(IMX95ELEServerState *s, uint8_t command)
+{
+    uint32_t resp_hdr = ele_make_header(ELE_VERSION, 2,
+                                        command, ELE_RESP_TAG);
+    trace_imx95_ele_response(command);
+    imx_mu_deliver_rr(s->mu, 0, resp_hdr);
+    imx_mu_deliver_rr(s->mu, 1, ELE_SUCCESS_IND);
+}
+
+static void ele_dispatch(IMX95ELEServerState *s)
+{
+    uint32_t header  = s->msg_buf[0];
+    uint8_t  command = ELE_HDR_COMMAND(header);
+    uint8_t  tag     = ELE_HDR_TAG(header);
+
+    trace_imx95_ele_msg(command, tag, s->msg_size);
+
+    switch (command) {
+    case ELE_GET_INFO_REQ:
+        ele_handle_get_info(s);
+        return;
+    default:
+        qemu_log_mask(LOG_UNIMP,
+                      "%s: cmd 0x%02x not handled in detail; responding OK\n",
+                      __func__, command);
+        ele_handle_generic_ok(s, command);
+        return;
+    }
+}
+
+/*
+ * TR-write callback. Invoked synchronously by the MU model from inside
+ * the guest's MMIO write to TR[idx]. The MU itself re-sets TSR.TEn
+ * after this returns so the next TR write succeeds without polling.
+ */
+static void ele_on_tr_write(void *opaque, unsigned int idx, uint32_t value)
+{
+    IMX95ELEServerState *s = opaque;
+
+    /* Invoked from the MU MMIO write handler, which runs under the BQL. */
+    assert(bql_locked());
+
+    if (s->msg_count >= IMX95_ELE_MAX_WORDS) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: too many words; resetting accumulator\n",
+                      __func__);
+        s->msg_count = 0;
+        s->msg_size  = 0;
+        return;
+    }
+
+    s->msg_buf[s->msg_count++] = value;
+
+    if (s->msg_count == 1) {
+        /* First word is the header; pick out size. */
+        s->msg_size = ELE_HDR_SIZE(value);
+        if (s->msg_size == 0 || s->msg_size > IMX95_ELE_MAX_WORDS) {
+            qemu_log_mask(LOG_GUEST_ERROR,
+                          "%s: bad size %u in header 0x%08x\n",
+                          __func__, s->msg_size, value);
+            s->msg_count = 0;
+            s->msg_size  = 0;
+            return;
+        }
+    }
+
+    if (s->msg_count == s->msg_size) {
+        ele_dispatch(s);
+        s->msg_count = 0;
+        s->msg_size  = 0;
+    }
+}
+
+static void imx95_ele_server_reset_hold(Object *obj, ResetType type)
+{
+    IMX95ELEServerState *s = IMX95_ELE_SERVER(obj);
+
+    s->msg_count = 0;
+    s->msg_size  = 0;
+    memset(s->msg_buf, 0, sizeof(s->msg_buf));
+}
+
+static void imx95_ele_server_realize(DeviceState *dev, Error **errp)
+{
+    IMX95ELEServerState *s = IMX95_ELE_SERVER(dev);
+
+    if (!s->mu) {
+        error_setg(errp, "%s: 'mu' link property must be set",
+                   TYPE_IMX95_ELE_SERVER);
+        return;
+    }
+    imx_mu_set_tr_write_handler(s->mu, ele_on_tr_write, s);
+}
+
+static const Property imx95_ele_server_properties[] = {
+    DEFINE_PROP_LINK("mu", IMX95ELEServerState, mu,
+                     TYPE_IMX_MU, IMXMUState *),
+};
+
+static const VMStateDescription vmstate_imx95_ele_server = {
+    .name = TYPE_IMX95_ELE_SERVER,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32(msg_count, IMX95ELEServerState),
+        VMSTATE_UINT32(msg_size, IMX95ELEServerState),
+        VMSTATE_UINT32_ARRAY(msg_buf, IMX95ELEServerState,
+                             IMX95_ELE_MAX_WORDS),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx95_ele_server_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->realize = imx95_ele_server_realize;
+    dc->vmsd = &vmstate_imx95_ele_server;
+    rc->phases.hold = imx95_ele_server_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX 95 ELE responder (get_info)";
+    device_class_set_props(dc, imx95_ele_server_properties);
+}
+
+static const TypeInfo imx95_ele_server_info = {
+    .name           = TYPE_IMX95_ELE_SERVER,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95ELEServerState),
+    .class_init     = imx95_ele_server_class_init,
+};
+
+static void imx95_ele_server_register_types(void)
+{
+    type_register_static(&imx95_ele_server_info);
+}
+
+type_init(imx95_ele_server_register_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 9d0e3ed220d..b0dade72c6c 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -172,3 +172,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'))
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index 01b0b275f7f..6b3cff74549 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -446,3 +446,5 @@ imx_mu_tr_write(unsigned idx, uint32_t val) "TR[%u] <- 0x%08x"
 imx_mu_rr_deliver(unsigned idx, uint32_t val) "RR[%u] <- 0x%08x"
 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"
diff --git a/include/hw/misc/imx95_ele_server.h b/include/hw/misc/imx95_ele_server.h
new file mode 100644
index 00000000000..75e6dfa4e25
--- /dev/null
+++ b/include/hw/misc/imx95_ele_server.h
@@ -0,0 +1,47 @@
+/*
+ * Minimal NXP EdgeLock Enclave (ELE) responder stub
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Watches an i.MX MU for ELE-protocol command words, dispatches them
+ * to handlers, and writes responses back through the MU's RR
+ * registers. Implements only ele_get_info() (the one ELE call
+ * U-Boot SPL's imx9_probe_mu() makes pre-relocation). Everything
+ * else returns ELE_OK with a stub response so SPL doesn't panic.
+ */
+
+#ifndef IMX95_ELE_SERVER_H
+#define IMX95_ELE_SERVER_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+#include "hw/misc/imx_mu.h"
+
+#define TYPE_IMX95_ELE_SERVER "imx95.ele-server"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95ELEServerState, IMX95_ELE_SERVER)
+
+/* ELE protocol constants from arch/arm/include/asm/mach-imx/ele_api.h. */
+#define ELE_VERSION                 0x06
+#define ELE_CMD_TAG                 0x17
+#define ELE_RESP_TAG                0xE1
+#define ELE_GET_INFO_REQ            0xDA
+#define ELE_SUCCESS_IND             0xD6
+
+/* Maximum words in a single ELE message (per ELE_MAX_MSG in U-Boot). */
+#define IMX95_ELE_MAX_WORDS         32
+
+struct IMX95ELEServerState {
+    SysBusDevice    parent_obj;
+
+    /* Link to the MU used as ELE transport (elemu1). */
+    IMXMUState     *mu;
+
+    /* Accumulator for incoming command words. */
+    uint32_t        msg_buf[IMX95_ELE_MAX_WORDS];
+    uint32_t        msg_count;
+    uint32_t        msg_size;
+};
+
+#endif /* IMX95_ELE_SERVER_H */
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 09/16] hw/timer: add i.MX 95 system counter
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (7 preceding siblings ...)
  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
  2026-08-20  2:48 ` [PATCH 10/16] hw/misc: add i.MX 95 watchdog Kyle Fox
                   ` (6 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, open list:MCIMX95-19X19-EVK...

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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 10/16] hw/misc: add i.MX 95 watchdog
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (8 preceding siblings ...)
  2026-08-20  2:48 ` [PATCH 09/16] hw/timer: add i.MX 95 system counter Kyle Fox
@ 2026-08-20  2:48 ` 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
                   ` (5 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, open list:MCIMX95-19X19-EVK...

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 <kylefoxaustin.github@gmail.com>
---
 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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 11/16] hw/misc: add i.MX 95 ANATOP/AONMIX/GPC/SRC power and clock blocks
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (9 preceding siblings ...)
  2026-08-20  2:48 ` [PATCH 10/16] hw/misc: add i.MX 95 watchdog Kyle Fox
@ 2026-08-20  2:48 ` 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
                   ` (4 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, open list:MCIMX95-19X19-EVK...

The power and clock bring-up blocks that the System Manager firmware
programs before Linux starts: ANATOP (PLL lock / DFS status), SRC
(system reset controller, including the M7 mix-slice release), GPC
(general power controller mode requests) and the AONMIX block-control M7
CPU-WAIT gate (the SM's M7 hold/run control). These are register-level
models that return the lock, ack and status bits the firmware polls.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/misc/Kconfig        |  12 +++
 hw/misc/imx95_anatop.c | 201 ++++++++++++++++++++++++++++++++++++
 hw/misc/imx95_aonmix.c | 160 +++++++++++++++++++++++++++++
 hw/misc/imx95_gpc.c    | 160 +++++++++++++++++++++++++++++
 hw/misc/imx95_src.c    | 228 +++++++++++++++++++++++++++++++++++++++++
 hw/misc/meson.build    |   4 +
 hw/misc/trace-events   |   8 ++
 7 files changed, 773 insertions(+)
 create mode 100644 hw/misc/imx95_anatop.c
 create mode 100644 hw/misc/imx95_aonmix.c
 create mode 100644 hw/misc/imx95_gpc.c
 create mode 100644 hw/misc/imx95_src.c

diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index 2bae76b4ec1..1e1cae92f6f 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -271,3 +271,15 @@ config IMX95_ELE_SERVER
 
 config IMX95_WDOG
     bool
+
+config IMX95_ANATOP
+    bool
+
+config IMX95_AONMIX
+    bool
+
+config IMX95_GPC
+    bool
+
+config IMX95_SRC
+    bool
diff --git a/hw/misc/imx95_anatop.c b/hw/misc/imx95_anatop.c
new file mode 100644
index 00000000000..b8b18d405d0
--- /dev/null
+++ b/hw/misc/imx95_anatop.c
@@ -0,0 +1,201 @@
+/*
+ * NXP i.MX 95 ANATOP / PLL stub model
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Covers the ANATOP block at 0x44480000. Its PLL instances live at
+ * 0x44481000..0x444819ff, one every 0x100 (SYS_PLL1, AUDIO_PLL1/2,
+ * VIDEO_PLL1, ARM_PLL, DRAM_PLL, HSIO_PLL, LDB_PLL). Each PLL register
+ * uses the i.MX RW/SET/CLR/TOG quad layout (a 16-byte group: write +0 =
+ * assign, +4 = set bits, +8 = clear bits, +C = toggle; reads return the
+ * accumulated value), and each PLL has two read-only status words:
+ * PLL_STATUS (+0xf0, bit0 = PLL_LOCK) and DFS_STATUS (+0xf4, bits[3:0] =
+ * per-DFS clock-ok).
+ *
+ * The System Manager's DVFS path (DEV_SM_PerfA55FreqUpdate via
+ * FRACTPLL_UpdateRate / FRACTPLL_UpdateDfsRate) powers a PLL up by setting
+ * CTRL.POWERUP and then polls PLL_STATUS.PLL_LOCK; it enables a DFS and
+ * polls DFS_STATUS.DFS_OK (an unbounded wait). QEMU has no analog PLLs, so
+ * the transition is instantaneous: PLL_LOCK mirrors CTRL.POWERUP and a
+ * DFS's DFS_OK bit mirrors its DFS_CTRL.ENABLE. Everything else is plain
+ * storage. Same status-mirrors-control idea as the GPC/SRC models.
+ *
+ * Non-PLL parts of ANATOP (e.g. TMPSNS at +0x2000) are plain RAM here -
+ * the quad/status behaviour is confined to the PLL window.
+ */
+
+#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_ANATOP "imx95.anatop"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95AnatopState, IMX95_ANATOP)
+
+#define IMX95_ANATOP_REG_SIZE   0x10000
+#define IMX95_ANATOP_NUM_WORDS  (IMX95_ANATOP_REG_SIZE / 4)
+
+/* PLL window within ANATOP: [0x1000, 0x1a00), one PLL every 0x100. */
+#define ANATOP_PLL_START        0x1000
+#define ANATOP_PLL_END          0x1a00
+#define PLL_BLOCK_SIZE          0x100
+
+/* Per-PLL register offsets. */
+#define PLL_CTRL_OFF            0x00    /* RW/SET/CLR/TOG, POWERUP in bit 0 */
+#define PLL_DFS0_OFF            0x70    /* DFS[0] DFS_CTRL; step 0x20 */
+#define PLL_DFS_STEP            0x20
+#define PLL_NUM_DFS             4
+#define PLL_STATUS_OFF          0xf0    /* RO: PLL_LOCK in bit 0 */
+#define PLL_DFS_STATUS_OFF      0xf4    /* RO: DFS_OK in bits [3:0] */
+
+#define PLL_CTRL_POWERUP        0x00000001u
+#define PLL_STATUS_LOCK         0x00000001u
+#define PLL_DFS_CTRL_ENABLE     0x80000000u
+
+struct IMX95AnatopState {
+    SysBusDevice    parent_obj;
+    MemoryRegion    iomem;
+    uint32_t        regs[IMX95_ANATOP_NUM_WORDS];
+};
+
+static bool anatop_in_pll_window(hwaddr offset)
+{
+    return offset >= ANATOP_PLL_START && offset < ANATOP_PLL_END;
+}
+
+static uint64_t imx95_anatop_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMX95AnatopState *s = opaque;
+
+    trace_imx95_anatop_read(offset);
+
+    if (anatop_in_pll_window(offset)) {
+        hwaddr blk = offset & ~(hwaddr)(PLL_BLOCK_SIZE - 1);
+        uint32_t reg = offset & (PLL_BLOCK_SIZE - 1);
+
+        if (reg == PLL_STATUS_OFF) {
+            /* PLL_LOCK mirrors CTRL.POWERUP (lock is instantaneous here). */
+            uint32_t ctrl = s->regs[(blk + PLL_CTRL_OFF) / 4];
+            return (ctrl & PLL_CTRL_POWERUP) ? PLL_STATUS_LOCK : 0;
+        }
+        if (reg == PLL_DFS_STATUS_OFF) {
+            /* Each DFS_OK bit mirrors that DFS's DFS_CTRL.ENABLE. */
+            uint32_t ok = 0;
+            for (unsigned n = 0; n < PLL_NUM_DFS; n++) {
+                hwaddr dfs = blk + PLL_DFS0_OFF + n * PLL_DFS_STEP;
+                if (s->regs[dfs / 4] & PLL_DFS_CTRL_ENABLE) {
+                    ok |= (1u << n);
+                }
+            }
+            return ok;
+        }
+    }
+    return s->regs[offset / 4];
+}
+
+static void imx95_anatop_write(void *opaque, hwaddr offset,
+                               uint64_t value, unsigned size)
+{
+    IMX95AnatopState *s = opaque;
+
+    trace_imx95_anatop_write(offset, value);
+
+    if (anatop_in_pll_window(offset)) {
+        uint32_t reg = offset & (PLL_BLOCK_SIZE - 1);
+
+        /* PLL_STATUS / DFS_STATUS are read-only. */
+        if (reg == PLL_STATUS_OFF || reg == PLL_DFS_STATUS_OFF) {
+            return;
+        }
+
+        /* RW/SET/CLR/TOG aliases of one logical register (16-byte group). */
+        hwaddr base = offset & ~(hwaddr)0xc;
+        switch (offset & 0xc) {
+        case 0x0:
+            s->regs[offset / 4] = value;
+            break;
+        case 0x4:
+            s->regs[base / 4] |= value;
+            break;
+        case 0x8:
+            s->regs[base / 4] &= ~(uint32_t)value;
+            break;
+        case 0xc:
+            s->regs[base / 4] ^= value;
+            break;
+        }
+        return;
+    }
+    s->regs[offset / 4] = value;
+}
+
+static const MemoryRegionOps imx95_anatop_ops = {
+    .read = imx95_anatop_read,
+    .write = imx95_anatop_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_anatop_reset_hold(Object *obj, ResetType type)
+{
+    IMX95AnatopState *s = IMX95_ANATOP(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+}
+
+static void imx95_anatop_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMX95AnatopState *s = IMX95_ANATOP(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx95_anatop_ops, s,
+                          TYPE_IMX95_ANATOP, IMX95_ANATOP_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+}
+
+static const VMStateDescription vmstate_imx95_anatop = {
+    .name = TYPE_IMX95_ANATOP,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, IMX95AnatopState, IMX95_ANATOP_NUM_WORDS),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx95_anatop_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx95_anatop;
+    rc->phases.hold = imx95_anatop_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX 95 ANATOP/PLL (stub)";
+}
+
+static const TypeInfo imx95_anatop_info = {
+    .name           = TYPE_IMX95_ANATOP,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95AnatopState),
+    .instance_init  = imx95_anatop_init,
+    .class_init     = imx95_anatop_class_init,
+};
+
+static void imx95_anatop_register_types(void)
+{
+    type_register_static(&imx95_anatop_info);
+}
+
+type_init(imx95_anatop_register_types)
diff --git a/hw/misc/imx95_aonmix.c b/hw/misc/imx95_aonmix.c
new file mode 100644
index 00000000000..d794440170e
--- /dev/null
+++ b/hw/misc/imx95_aonmix.c
@@ -0,0 +1,160 @@
+/*
+ * NXP i.MX 95 BLK_CTRL_S_AONMIX - minimal model for the M7 CPU-WAIT gate
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Covers BLK_CTRL_S_AONMIX at 0x444f0000. The block is mostly plain
+ * configuration storage, modelled here as a RAM-backed register file (an
+ * improvement over the previous logging stub, which read back as zero).
+ *
+ * The one load-bearing register is M7_CFG (offset 0x124). Its WAIT bit
+ * (bit 4) is the Cortex-M7 hold/run gate the System Manager uses to manage
+ * the M7's lifecycle: CPU_RunModeGet reads it (WAIT set => HOLD, clear =>
+ * START) and CPU_WaitSet sets/clears it. At reset WAIT is set, so the SM
+ * sees the M7 held and runs its full DEV_SM_CpuStart sequence - releasing
+ * CPUWAIT (which we surface as the m7-run gpio) and, crucially, enabling
+ * the CM7_SYSRESETREQ fault IRQ so the SM can later cold-reset the M7 LM.
+ * A WAIT 1->0 transition releases the M7; 0->1 holds it.
+ *
+ * INITVTOR (0x108) - the M7 boot vector the SM programs - is stored but
+ * unused: our M7 boots from its ITCM reset vector (init-svtor = 0).
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/irq.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+#define TYPE_IMX95_AONMIX "imx95.aonmix"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95AonmixState, IMX95_AONMIX)
+
+#define IMX95_AONMIX_REG_SIZE   0x10000
+#define IMX95_AONMIX_NUM_WORDS  (IMX95_AONMIX_REG_SIZE / 4)
+
+#define AONMIX_M7_CFG           0x124       /* M7 configure register */
+#define AONMIX_M7_CFG_WAIT      0x10        /* bit 4: M7 CPU-WAIT (hold) */
+
+struct IMX95AonmixState {
+    SysBusDevice    parent_obj;
+    MemoryRegion    iomem;
+    uint32_t        regs[IMX95_AONMIX_NUM_WORDS];
+
+    /*
+     * M7 run gate, driven by M7_CFG.WAIT (level: 1 = released/run,
+     * 0 = held). The machine wires this to a handler that resets+resumes
+     * the M7 on release and halts it on hold.
+     */
+    qemu_irq        m7_run;
+};
+
+static uint64_t imx95_aonmix_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMX95AonmixState *s = opaque;
+
+    trace_imx95_aonmix_read(offset);
+    return s->regs[offset / 4];
+}
+
+static void imx95_aonmix_write(void *opaque, hwaddr offset,
+                               uint64_t value, unsigned size)
+{
+    IMX95AonmixState *s = opaque;
+
+    trace_imx95_aonmix_write(offset, value);
+
+    /*
+     * M7_CFG.WAIT toggling is the SM holding (set) or releasing (clear) the
+     * M7. Surface the released state on the m7-run line so the machine
+     * cycles the core. Only fires on an actual WAIT transition.
+     */
+    if (offset == AONMIX_M7_CFG) {
+        uint32_t old = s->regs[offset / 4];
+        uint32_t new = (uint32_t)value;
+
+        s->regs[offset / 4] = new;
+        if ((old ^ new) & AONMIX_M7_CFG_WAIT) {
+            int run = (new & AONMIX_M7_CFG_WAIT) ? 0 : 1;
+            trace_imx95_aonmix_m7_gate(run);
+            qemu_set_irq(s->m7_run, run);
+        }
+        return;
+    }
+
+    s->regs[offset / 4] = value;
+}
+
+static const MemoryRegionOps imx95_aonmix_ops = {
+    .read = imx95_aonmix_read,
+    .write = imx95_aonmix_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_aonmix_reset_hold(Object *obj, ResetType type)
+{
+    IMX95AonmixState *s = IMX95_AONMIX(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+    /* M7 held at reset: the SM sees HOLD and runs its full CpuStart. */
+    s->regs[AONMIX_M7_CFG / 4] = AONMIX_M7_CFG_WAIT;
+}
+
+static void imx95_aonmix_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMX95AonmixState *s = IMX95_AONMIX(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx95_aonmix_ops, s,
+                          TYPE_IMX95_AONMIX, IMX95_AONMIX_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+
+    qdev_init_gpio_out_named(DEVICE(obj), &s->m7_run, "m7-run", 1);
+}
+
+static const VMStateDescription vmstate_imx95_aonmix = {
+    .name = TYPE_IMX95_AONMIX,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, IMX95AonmixState, IMX95_AONMIX_NUM_WORDS),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx95_aonmix_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx95_aonmix;
+    rc->phases.hold = imx95_aonmix_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX 95 BLK_CTRL_S_AONMIX (M7 CPU-WAIT gate)";
+}
+
+static const TypeInfo imx95_aonmix_info = {
+    .name           = TYPE_IMX95_AONMIX,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95AonmixState),
+    .instance_init  = imx95_aonmix_init,
+    .class_init     = imx95_aonmix_class_init,
+};
+
+static void imx95_aonmix_register_types(void)
+{
+    type_register_static(&imx95_aonmix_info);
+}
+
+type_init(imx95_aonmix_register_types)
diff --git a/hw/misc/imx95_gpc.c b/hw/misc/imx95_gpc.c
new file mode 100644
index 00000000000..d18b6da13c0
--- /dev/null
+++ b/hw/misc/imx95_gpc.c
@@ -0,0 +1,160 @@
+/*
+ * NXP i.MX 95 GPC (General Power Controller) stub model
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Covers the GPC block at 0x44470000: the per-domain GPC_CPU_CTRL
+ * instances (CM33 @+0x0000, CM7 @+0x0800, CA55_0..5 @+0x1000..+0x3800,
+ * CA55_CLUSTER @+0x4000) and GPC_GLOBAL @+0x4800.
+ *
+ * The System Manager drives CPU power modes by writing CMC_MODE_CTRL
+ * (CPU_MODE_TARGET, offset 0x10 in a CPU_CTRL block) and polling
+ * CMC_MODE_STAT (CPU_MODE_CURRENT, offset 0x14) until the mode settles.
+ * It also uses the CMC_SLEEP_*_CTRL / *_STAT handshake pairs (a *_STAT
+ * at *_CTRL + 4). QEMU has no power hardware, so transitions are
+ * instantaneous: every status register reflects the value last written
+ * to its paired control register. Everything else is plain storage.
+ *
+ * This is the register-class triage used for XCACHE applied to a
+ * control/status block: control bits persist (RAM), status registers
+ * mirror their control so "request X, wait for X" loops converge.
+ */
+
+#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_GPC "imx95.gpc"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95GPCState, IMX95_GPC)
+
+#define IMX95_GPC_REG_SIZE      0x10000
+#define IMX95_GPC_NUM_WORDS     (IMX95_GPC_REG_SIZE / 4)
+
+/* GPC_CPU_CTRL per-domain block layout (the GLOBAL block lives at +0x4800). */
+#define GPC_GLOBAL_OFF          0x4800
+#define CPU_CTRL_BLOCK_SIZE     0x800
+#define CMC_MODE_CTRL           0x10    /* CPU_MODE_TARGET in [1:0] */
+#define CMC_MODE_STAT           0x14    /* CPU_MODE_CURRENT in [1:0] */
+
+struct IMX95GPCState {
+    SysBusDevice    parent_obj;
+    MemoryRegion    iomem;
+    uint32_t        regs[IMX95_GPC_NUM_WORDS];
+};
+
+/*
+ * Is `reg` (offset within a CPU_CTRL block) a status register that mirrors
+ * the control register 4 bytes below it? Covers CMC_MODE_STAT (0x14) and
+ * the CMC_SLEEP_*_STAT handshake registers (0x204, 0x20c, ... 0x24c), all
+ * of which sit at their paired *_CTRL + 4.
+ */
+static bool gpc_is_mirror_stat(uint32_t reg)
+{
+    if (reg == CMC_MODE_STAT) {
+        return true;
+    }
+    /* SLEEP/WAKEUP handshake STATs: odd-word in the 0x200..0x2ff range. */
+    if (reg >= 0x204 && reg <= 0x2fc && ((reg & 0x4) != 0)) {
+        return true;
+    }
+    return false;
+}
+
+static uint64_t imx95_gpc_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMX95GPCState *s = opaque;
+
+    if (offset < GPC_GLOBAL_OFF) {
+        uint32_t reg = offset & (CPU_CTRL_BLOCK_SIZE - 1);
+        if (gpc_is_mirror_stat(reg)) {
+            /* Status mirrors the paired control (transition is instant). */
+            return s->regs[(offset - 4) / 4];
+        }
+    }
+    return s->regs[offset / 4];
+}
+
+static void imx95_gpc_write(void *opaque, hwaddr offset,
+                            uint64_t value, unsigned size)
+{
+    IMX95GPCState *s = opaque;
+
+    if (offset < GPC_GLOBAL_OFF &&
+        (offset & (CPU_CTRL_BLOCK_SIZE - 1)) == CMC_MODE_CTRL) {
+        trace_imx95_gpc_mode(offset, (uint32_t)value & 0x3);
+    }
+
+    s->regs[offset / 4] = value;
+}
+
+static const MemoryRegionOps imx95_gpc_ops = {
+    .read = imx95_gpc_read,
+    .write = imx95_gpc_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_gpc_reset_hold(Object *obj, ResetType type)
+{
+    IMX95GPCState *s = IMX95_GPC(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+}
+
+static void imx95_gpc_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMX95GPCState *s = IMX95_GPC(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx95_gpc_ops, s,
+                          TYPE_IMX95_GPC, IMX95_GPC_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+}
+
+static const VMStateDescription vmstate_imx95_gpc = {
+    .name = TYPE_IMX95_GPC,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, IMX95GPCState, IMX95_GPC_NUM_WORDS),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx95_gpc_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx95_gpc;
+    rc->phases.hold = imx95_gpc_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX 95 GPC (stub)";
+}
+
+static const TypeInfo imx95_gpc_info = {
+    .name           = TYPE_IMX95_GPC,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95GPCState),
+    .instance_init  = imx95_gpc_init,
+    .class_init     = imx95_gpc_class_init,
+};
+
+static void imx95_gpc_register_types(void)
+{
+    type_register_static(&imx95_gpc_info);
+}
+
+type_init(imx95_gpc_register_types)
diff --git a/hw/misc/imx95_src.c b/hw/misc/imx95_src.c
new file mode 100644
index 00000000000..8aa1d7d56b4
--- /dev/null
+++ b/hw/misc/imx95_src.c
@@ -0,0 +1,228 @@
+/*
+ * NXP i.MX 95 SRC (System Reset Controller) mix-slice stub model
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Covers the SRC block at 0x44460000: SRC_GEN at offset 0, then the
+ * per-power-domain "mix slice" register blocks (SRC_XSPR) starting at
+ * offset 0x400, one every 0x400 (ANAMIX, AONMIX, ..., CCMSRCGPCMIX, ...).
+ *
+ * The System Manager powers a mix down by setting SLICE_SW_CTRL.PDN_SOFT
+ * (bit 31, slice offset 0x20) and powers it up by clearing it, then polls
+ * the read-only FUNC_STAT (slice offset 0xb4) until the power state
+ * settles. QEMU has no power switches, so the transition is instantaneous:
+ * FUNC_STAT is derived from the slice's SLICE_SW_CTRL.PDN_SOFT -
+ *   powered up   -> RST_STAT released                  (0x00000004)
+ *   powered down -> PSW off, ISO on, handshakes done    (0x00005511)
+ * matching PWR_MIX_FUNC_STAT_PUP / _PDN in the SM's fsl_power.h. Everything
+ * else is plain storage. Same status-mirrors-control idea as the GPC model.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "hw/core/sysbus.h"
+#include "hw/core/irq.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+#define TYPE_IMX95_SRC "imx95.src"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95SRCState, IMX95_SRC)
+
+#define IMX95_SRC_REG_SIZE      0x10000
+#define IMX95_SRC_NUM_WORDS     (IMX95_SRC_REG_SIZE / 4)
+
+/* Mix-slice (SRC_XSPR) layout. SRC_GEN occupies the first slice-sized block. */
+#define SRC_SLICE_STRIDE        0x400
+#define SRC_SLICE_SW_CTRL       0x20    /* PDN_SOFT in bit 31 */
+#define SRC_FUNC_STAT           0xb4    /* read-only power/reset status */
+#define SRC_SLICE_SW_CTRL_PDN_SOFT  0x80000000u
+
+/* FUNC_STAT values for the fully-up / fully-down states (SM fsl_power.h). */
+#define SRC_FUNC_STAT_PUP       0x00000004u
+#define SRC_FUNC_STAT_PDN       0x00005511u
+
+/*
+ * SRC_GEN.SCR - the boot-reset-release latch register at offset 0x10
+ * within SRC_GEN (which sits at the SRC block base). Bit 12 is the
+ * M7MIX release; per the i.MX 95 reference manual it is sticky:
+ * "M7MIX will be held under reset until boot core writes this bit to 1.
+ * Once this bit is set to 1, it will be locked." The SM's
+ * DEV_SM_CpuStart(M7) path writes this bit during the LMM_Boot phase;
+ * we use the 0->1 transition as a rising edge on the m7mix_release
+ * gpio-out so the machine wrapper can release the M7 CPU from
+ * start-powered-off (silicon-faithful M7 release).
+ */
+#define SRC_GEN_SCR_OFFSET                      0x10u
+#define SRC_GEN_SCR_BOOT_RESET_RELEASE_M7MIX    (1u << 12)
+
+/*
+ * Per-slice reset line. SLICE_SW_CTRL.RST_RSTR_0 (bit 20) asserts the
+ * slice's core reset; the read-only RSTR_STAT.RSTR_0_RST_STAT (bit 0, slice
+ * offset 0xb8) reports it back. The SM's SRC_MixSetResetLine writes RST_RSTR_0
+ * and SRC_MixGetResetLine polls RSTR_STAT until it matches, so - like
+ * FUNC_STAT mirroring PDN_SOFT - we derive RSTR_STAT from RST_RSTR_0 so the
+ * SM's assert/deassert wait loops converge.
+ */
+#define SRC_RSTR_STAT                           0xb8
+/*
+ * SLICE_SW_CTRL.RST_RSTR[3:0] live in bits [23:20]; RSTR_STAT.RST_STAT[3:0] in
+ * bits [3:0]. The reset lines (M7MIX + sub-resets) each use one of these.
+ */
+#define SRC_SLICE_SW_CTRL_RST_RSTR_SHIFT        20
+
+struct IMX95SRCState {
+    SysBusDevice    parent_obj;
+    MemoryRegion    iomem;
+    uint32_t        regs[IMX95_SRC_NUM_WORDS];
+
+    /*
+     * Rising edge when SRC_GEN.SCR.BOOT_RESET_RELEASE_M7MIX goes 0 -> 1.
+     * Wired by the machine to a handler that releases the M7 CPU,
+     * complementing the existing reset-time release path (which
+     * releases M7 when -device loader has staged firmware into ITCM,
+     * used by tests that boot the M7 standalone without the SM).
+     */
+    qemu_irq        m7mix_release;
+};
+
+static uint64_t imx95_src_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMX95SRCState *s = opaque;
+
+    /* FUNC_STAT in a mix slice mirrors that slice's SLICE_SW_CTRL.PDN_SOFT. */
+    if (offset >= SRC_SLICE_STRIDE &&
+        (offset & (SRC_SLICE_STRIDE - 1)) == SRC_FUNC_STAT) {
+        hwaddr slice = offset & ~(hwaddr)(SRC_SLICE_STRIDE - 1);
+        uint32_t ctrl = s->regs[(slice + SRC_SLICE_SW_CTRL) / 4];
+
+        return (ctrl & SRC_SLICE_SW_CTRL_PDN_SOFT) ? SRC_FUNC_STAT_PDN
+                                                   : SRC_FUNC_STAT_PUP;
+    }
+
+    /*
+     * RSTR_STAT mirrors that slice's SLICE_SW_CTRL reset lines: the SM
+     * asserts one of RST_RSTR[3:0] (SLICE_SW_CTRL bits [23:20]) and polls the
+     * matching RST_STAT[3:0] (RSTR_STAT bits [3:0]) until it tracks. The M7 LM
+     * reset walks several reset lines (M7MIX + associated sub-resets), each on
+     * a different RST_RSTR bit, so mirror all four.
+     */
+    if (offset >= SRC_SLICE_STRIDE &&
+        (offset & (SRC_SLICE_STRIDE - 1)) == SRC_RSTR_STAT) {
+        hwaddr slice = offset & ~(hwaddr)(SRC_SLICE_STRIDE - 1);
+        uint32_t ctrl = s->regs[(slice + SRC_SLICE_SW_CTRL) / 4];
+
+        return (ctrl >> SRC_SLICE_SW_CTRL_RST_RSTR_SHIFT) & 0xf;
+    }
+    return s->regs[offset / 4];
+}
+
+static void imx95_src_write(void *opaque, hwaddr offset,
+                            uint64_t value, unsigned size)
+{
+    IMX95SRCState *s = opaque;
+
+    /*
+     * SRC_GEN.SCR: bit 12 (M7MIX release) is sticky/locked once set per
+     * the RM. Track the 0->1 transition and pulse the m7mix_release out
+     * so the machine releases the M7 CPU.
+     */
+    if (offset == SRC_GEN_SCR_OFFSET) {
+        uint32_t old = s->regs[offset / 4];
+        uint32_t new = (uint32_t)value;
+
+        /* Lock any bit that was already 1: hardware-sticky behaviour. */
+        new |= old & SRC_GEN_SCR_BOOT_RESET_RELEASE_M7MIX;
+        s->regs[offset / 4] = new;
+
+        if (!(old & SRC_GEN_SCR_BOOT_RESET_RELEASE_M7MIX) &&
+             (new & SRC_GEN_SCR_BOOT_RESET_RELEASE_M7MIX)) {
+            trace_imx95_src_m7mix_release();
+            qemu_irq_raise(s->m7mix_release);
+        }
+        return;
+    }
+
+    if (offset >= SRC_SLICE_STRIDE &&
+        (offset & (SRC_SLICE_STRIDE - 1)) == SRC_SLICE_SW_CTRL) {
+        trace_imx95_src_slice_ctrl(offset, (uint32_t)value);
+    }
+
+    s->regs[offset / 4] = value;
+}
+
+static const MemoryRegionOps imx95_src_ops = {
+    .read = imx95_src_read,
+    .write = imx95_src_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_src_reset_hold(Object *obj, ResetType type)
+{
+    IMX95SRCState *s = IMX95_SRC(obj);
+
+    memset(s->regs, 0, sizeof(s->regs));
+}
+
+static void imx95_src_init(Object *obj)
+{
+    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
+    IMX95SRCState *s = IMX95_SRC(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx95_src_ops, s,
+                          TYPE_IMX95_SRC, IMX95_SRC_REG_SIZE);
+    sysbus_init_mmio(sbd, &s->iomem);
+
+    /*
+     * Named gpio-out for SRC_GEN.SCR.M7MIX rising edge. Connected by
+     * the machine wrapper to a handler that releases the M7 CPU.
+     */
+    qdev_init_gpio_out_named(DEVICE(obj), &s->m7mix_release,
+                             "m7mix-release", 1);
+}
+
+static const VMStateDescription vmstate_imx95_src = {
+    .name = TYPE_IMX95_SRC,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, IMX95SRCState, IMX95_SRC_NUM_WORDS),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx95_src_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    dc->vmsd = &vmstate_imx95_src;
+    rc->phases.hold = imx95_src_reset_hold;
+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
+    dc->desc = "NXP i.MX 95 SRC (stub)";
+}
+
+static const TypeInfo imx95_src_info = {
+    .name           = TYPE_IMX95_SRC,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95SRCState),
+    .instance_init  = imx95_src_init,
+    .class_init     = imx95_src_class_init,
+};
+
+static void imx95_src_register_types(void)
+{
+    type_register_static(&imx95_src_info);
+}
+
+type_init(imx95_src_register_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 3f8cd1d825e..0b7f5e4382d 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -174,3 +174,7 @@ 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'))
+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'))
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index 584c1f90963..15fab0596c2 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -450,3 +450,11 @@ imx95_ele_msg(uint8_t command, uint8_t tag, uint32_t size) "received cmd 0x%02x
 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"
+imx95_anatop_read(uint64_t offset) "read off 0x%" PRIx64
+imx95_anatop_write(uint64_t offset, uint64_t value) "write off 0x%" PRIx64 " <- 0x%08" PRIx64
+imx95_aonmix_read(uint64_t offset) "read off 0x%" PRIx64
+imx95_aonmix_write(uint64_t offset, uint64_t value) "write off 0x%" PRIx64 " <- 0x%08" PRIx64
+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"
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 12/16] hw/misc: add i.MX 95 PMIC (PF09/PF53/PCAL6408A) and xcache controllers
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (10 preceding siblings ...)
  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 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 13/16] hw/misc: add i.MX 95 DPU command-sequencer stub (headless) Kyle Fox
                   ` (3 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, open list:MCIMX95-19X19-EVK...

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 <kylefoxaustin.github@gmail.com>
---
 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



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 13/16] hw/misc: add i.MX 95 DPU command-sequencer stub (headless)
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (11 preceding siblings ...)
  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 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 14/16] hw/arm: add i.MX 95 SoC container (fsl-imx95) Kyle Fox
                   ` (2 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, open list:MCIMX95-19X19-EVK...

A minimal stub for the i.MX 95 DPU (display controller) command
sequencer. This machine is headless, but Linux's dpu95 driver busy-waits
with no timeout on the sequencer status register during probe, so a
reads-as-zero stub would hang userspace bring-up. The stub reports the
sequencer idle with FIFO space so the probe completes. Real display
scanout is left to a follow-on series.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/misc/Kconfig      |   3 ++
 hw/misc/imx95_dpu.c  | 117 +++++++++++++++++++++++++++++++++++++++++++
 hw/misc/meson.build  |   1 +
 hw/misc/trace-events |   1 +
 4 files changed, 122 insertions(+)
 create mode 100644 hw/misc/imx95_dpu.c

diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index e1b8c390312..59c55cb5b83 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -290,3 +290,6 @@ config IMX95_PMIC
 
 config IMX95_XCACHE
     bool
+
+config IMX95_DPU
+    bool
diff --git a/hw/misc/imx95_dpu.c b/hw/misc/imx95_dpu.c
new file mode 100644
index 00000000000..4e09b399d85
--- /dev/null
+++ b/hw/misc/imx95_dpu.c
@@ -0,0 +1,117 @@
+/*
+ * NXP i.MX 95 DPU (Display Processing Unit) command-sequencer status stub
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * The DPU is not modelled: this base machine is headless, and real display
+ * scanout is a follow-on series. With the real System Manager powering the
+ * display mix, however, Linux's dpu95 driver stops deferring and probes - and
+ * its blit-engine bring-up busy-waits with NO
+ * timeout on the command sequencer status register:
+ *   dpu95_cs_wait_idle()       spins until CMDSEQ_STATUS.IDLE      is set
+ *   dpu95_cs_wait_fifo_space() spins until CMDSEQ_STATUS.FIFOSPACE >= 192
+ * A plain zero-returning stub never satisfies either, so the probe kthread
+ * hangs forever, which in turn wedges wait_for_device_probe() in
+ * kernel_init() and userspace never starts.
+ *
+ * This stub returns CMDSEQ_STATUS with IDLE set and a full FIFOSPACE so both
+ * polls pass instantly; every other register reads back zero and writes are
+ * dropped. That is enough to let the probe complete (the actual command
+ * submission / fence path runs only at runtime, which we never reach). A
+ * follow-on series that models the DPU for real (adding display scanout and
+ * Wayland output) removes this stub.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/module.h"
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+#include "trace.h"
+
+#define TYPE_IMX95_DPU "imx95.dpu"
+OBJECT_DECLARE_SIMPLE_TYPE(IMX95DPUState, IMX95_DPU)
+
+#define IMX95_DPU_REG_SIZE 0x400000
+
+struct IMX95DPUState {
+    SysBusDevice    parent_obj;
+    MemoryRegion    iomem;
+};
+
+/* Command-sequencer status, from the dpu95 blit register map. */
+#define DPU_CMDSEQ_STATUS           0x1019c
+#define DPU_CMDSEQ_STATUS_IDLE      0x40000000u
+#define DPU_CMDSEQ_STATUS_FIFOSPACE 0x0001ffffu
+
+static uint64_t imx95_dpu_read(void *opaque, hwaddr offset, unsigned size)
+{
+    if (offset == DPU_CMDSEQ_STATUS) {
+        /*
+         * Report the sequencer idle with a full FIFO. This is deliberately
+         * NOT the reset value (0x41000080: IDLE set but FIFOSPACE = 128, below
+         * the driver's threshold of 192), because the dpu95 probe polls
+         * FIFOSPACE before the enable handshake that would raise it on real
+         * silicon. Returning a constant full FIFO is sound only because the
+         * model is probe-time-only: no actual command sequence is ever
+         * submitted. If real DPU command submission is ever modelled,
+         * FIFOSPACE must track the command FIFO occupancy instead.
+         */
+        uint32_t status = DPU_CMDSEQ_STATUS_IDLE | DPU_CMDSEQ_STATUS_FIFOSPACE;
+        trace_imx95_dpu_cmdseq_status(status);
+        return status;
+    }
+    return 0;
+}
+
+static void imx95_dpu_write(void *opaque, hwaddr offset,
+                            uint64_t value, unsigned size)
+{
+}
+
+static const MemoryRegionOps imx95_dpu_ops = {
+    .read = imx95_dpu_read,
+    .write = imx95_dpu_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_dpu_init(Object *obj)
+{
+    IMX95DPUState *s = IMX95_DPU(obj);
+
+    memory_region_init_io(&s->iomem, obj, &imx95_dpu_ops, s,
+                          TYPE_IMX95_DPU, IMX95_DPU_REG_SIZE);
+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->iomem);
+}
+
+static void imx95_dpu_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    set_bit(DEVICE_CATEGORY_DISPLAY, dc->categories);
+    dc->desc = "NXP i.MX 95 DPU (status stub)";
+}
+
+static const TypeInfo imx95_dpu_info = {
+    .name           = TYPE_IMX95_DPU,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(IMX95DPUState),
+    .instance_init  = imx95_dpu_init,
+    .class_init     = imx95_dpu_class_init,
+};
+
+static void imx95_dpu_register_types(void)
+{
+    type_register_static(&imx95_dpu_info);
+}
+
+type_init(imx95_dpu_register_types)
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index d7d283a191b..9d629c8efef 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -180,3 +180,4 @@ 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'))
+system_ss.add(when: 'CONFIG_IMX95_DPU', if_true: files('imx95_dpu.c'))
diff --git a/hw/misc/trace-events b/hw/misc/trace-events
index 0555b15a1f3..e921b5f290a 100644
--- a/hw/misc/trace-events
+++ b/hw/misc/trace-events
@@ -466,3 +466,4 @@ 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"
+imx95_dpu_cmdseq_status(uint32_t value) "CMDSEQ_STATUS -> 0x%08x"
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 14/16] hw/arm: add i.MX 95 SoC container (fsl-imx95)
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (12 preceding siblings ...)
  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 ` 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
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, Peter Maydell, open list:ARM TCG CPUs

The i.MX 95 SoC container. Assembles the six Cortex-A55 application
cores with a GICv3, the Cortex-M33 that runs the System Manager and the
SM-managed Cortex-M7, the MU cross-connects that carry SCMI and rpmsg,
the bring-up peripherals, and the uSDHC used for SD boot; it lays out the
SoC memory map and wires PSCI for the A55 cluster. Blocks that are not on
the boot path are mapped as logging stubs so that enabling their
device-tree nodes degrades gracefully.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/arm/Kconfig             |   19 +
 hw/arm/fsl-imx95.c         | 1646 ++++++++++++++++++++++++++++++++++++
 hw/arm/meson.build         |    1 +
 include/hw/arm/fsl-imx95.h |  481 +++++++++++
 4 files changed, 2147 insertions(+)
 create mode 100644 hw/arm/fsl-imx95.c
 create mode 100644 include/hw/arm/fsl-imx95.h

diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 260d2f07516..7f1c0723ec0 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -745,3 +745,22 @@ config AXIADO_EVK
     default y
     depends on TCG && ARM
     select AXIADO_SOC
+config FSL_IMX95
+    bool
+    select ARM_GIC
+    select ARM_GICV3
+    select IMX_LPUART
+    select IMX_MU
+    select IMX95_ELE_SERVER
+    select IMX95_WDOG
+    select IMX95_XCACHE
+    select IMX95_PMIC
+    select IMX95_GPC
+    select IMX95_SRC
+    select IMX95_AONMIX
+    select IMX95_ANATOP
+    select IMX95_DPU
+    select IMX95_SYSCTR
+    select IMX_LPI2C
+    select SDHCI
+    select UNIMP
diff --git a/hw/arm/fsl-imx95.c b/hw/arm/fsl-imx95.c
new file mode 100644
index 00000000000..110e70bbce7
--- /dev/null
+++ b/hw/arm/fsl-imx95.c
@@ -0,0 +1,1646 @@
+/*
+ * NXP i.MX 95 SoC container
+ *
+ * Modeled on hw/arm/fsl-imx8mp.c by Bernhard Beschow
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Instantiates the 6x Cortex-A55 cluster, the GIC-600 (GICv3), DDR, the
+ * Cortex-M33 System Manager and Cortex-M7 real-time cores, and the bring-up
+ * peripherals on the Linux + SM critical path (LPUART, MU, System Counter,
+ * ANATOP/SRC/GPC, ELE, LPI2C + PMIC, uSDHC, ...). Peripherals that are not
+ * modelled are backed by create_unimplemented_device() so accesses log
+ * instead of faulting.
+ *
+ * Memory-map addresses and IRQ numbers below come from two sources:
+ *   - Linux DTS for direct-MMIO peripherals (LPUART, GIC, SM mailbox,
+ *     ELE mailbox, OCRAM, watchdogs):
+ *       Linux arch/arm64/boot/dts/freescale/imx95.dtsi
+ *   - NXP U-Boot's imx-regs.h for the SCMI-routed peripherals the SPL
+ *     pokes before SCMI is up (CCM, ANATOP, IOMUXC, SRC, TRDC, the
+ *     BLK_CTRL aggregates):
+ *       U-Boot arch/arm/include/asm/arch-imx9/imx-regs.h
+ *
+ * The SM firmware on the M33 is the SCMI provider at runtime; the
+ * SCMI-routed peripherals above are logging stubs that the SM programs.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/main-loop.h"
+#include "system/address-spaces.h"
+#include "system/cpus.h"
+#include "system/system.h"
+#include "hw/arm/bsa.h"
+#include "hw/arm/boot.h"
+#include "hw/arm/armv7m.h"
+#include "hw/arm/fsl-imx95.h"
+#include "hw/core/clock.h"
+#include "hw/core/qdev-clock.h"
+#include "hw/core/boards.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/qdev-properties-system.h"
+#include "hw/i2c/i2c.h"
+#include "hw/core/irq.h"
+#include "hw/intc/arm_gicv3.h"
+#include "hw/misc/unimp.h"
+#include "hw/sd/sdhci.h"
+#include "system/kvm.h"
+#include "system/reset.h"
+#include "target/arm/cpu.h"
+#include "target/arm/cpu-qom.h"
+#include "target/arm/kvm_arm.h"
+#include "qapi/error.h"
+#include "qobject/qlist.h"
+
+/*
+ * Single source of truth for the SoC memory map. Each entry maps a region
+ * ID (from enum FslImx95MemoryRegions) to its physical base address, size,
+ * and a debug name. Values are taken from the NXP BSP device tree at
+ * Linux arch/arm64/boot/dts/freescale/imx95.dtsi unless
+ * otherwise noted.
+ */
+static const struct {
+    hwaddr      addr;
+    size_t      size;
+    const char *name;
+} fsl_imx95_memmap[FSL_IMX95_NUM_REGIONS] = {
+    [FSL_IMX95_RAM]                  = {
+        FSL_IMX95_RAM_START, FSL_IMX95_RAM_SIZE_MAX, "ram"
+    },
+
+    /*
+     * GIC-600. dtsi: interrupt-controller@48000000. dist + redist are the
+     * real GICv3; the its window is a logging stub (no ITS is modelled).
+     */
+    [FSL_IMX95_GIC_DIST]             = { 0x48000000, 64 * KiB,   "gic_dist" },
+    [FSL_IMX95_GIC_REDIST]           = { 0x48060000, 768 * KiB,  "gic_redist" },
+    [FSL_IMX95_GIC_ITS]              = { 0x48040000, 128 * KiB,  "gic_its" },
+
+    /*
+     * On-chip RAM (OCRAM-A). The Linux DTS only declares a 96 KiB
+     * sram1 slice at 0x204C0000, but U-Boot SPL is linked for
+     * CONFIG_SPL_TEXT_BASE=0x20480000 with BSS+stack at 0x204D6000
+     * (see U-Boot configs/imx95_19x19_evk_defconfig).
+     * Cover the full SPL run + stack area as one writable region
+     * spanning 0x20480000-0x204D8000. Linux's sram1 slice sits
+     * inside this range and continues to work unchanged.
+     * 384 KiB covers the region SPL and Linux's sram1 slice use; the
+     * exact OCRAM-A size in the RM "Memory Map" chapter may differ but
+     * does not affect the modelled boot flow.
+     */
+    [FSL_IMX95_OCRAM]                = { 0x20480000, 384 * KiB,  "ocram" },
+
+    /*
+     * System counter. dtsi: timer@44290000 reg = <... 0x30000>.
+     * Logging stub; the ARM generic timer in QEMU's CPU
+     * model already provides time, and U-Boot SPL's timer_init()
+     * reads CNTFRQ_EL0 rather than this MMIO block. Promote to a
+     * real model if any guest write to this region surfaces.
+     */
+    [FSL_IMX95_SYSCNT]               = { 0x44290000, 192 * KiB,  "sysctr" },
+    [FSL_IMX95_BBNSM]                = { 0x44440000, 4 * KiB,    "bbnsm" },
+    [FSL_IMX95_XCACHE_PC]            = { 0x44400000, 0x800,      "xcache_pc" },
+    [FSL_IMX95_XCACHE_PS]            = { 0x44400800, 0x800,      "xcache_ps" },
+    [FSL_IMX95_BLK_CTRL_HSIOMIX]     = { 0x4c010000, 64 * KiB,   "hsiomix" },
+    [FSL_IMX95_GPC]                  = { 0x44470000, 64 * KiB,   "gpc" },
+
+    /*
+     * LPUART block. compatible: "fsl,imx95-lpuart". Each instance is
+     * a 4 KiB MMIO window. LPUART1/2 sit in the AON aips1 bus,
+     * LPUART3-6 in the Wakeup aips2 bus, LPUART7/8 in aips3.
+     * LPUART1 is the 19x19 EVK console (stdout-path = &lpuart1).
+     */
+    [FSL_IMX95_LPUART1]              = { 0x44380000, 4 * KiB,    "lpuart1" },
+    [FSL_IMX95_LPUART2]              = { 0x44390000, 4 * KiB,    "lpuart2" },
+    [FSL_IMX95_LPUART3]              = { 0x42570000, 4 * KiB,    "lpuart3" },
+    [FSL_IMX95_LPUART4]              = { 0x42580000, 4 * KiB,    "lpuart4" },
+    [FSL_IMX95_LPUART5]              = { 0x42590000, 4 * KiB,    "lpuart5" },
+    [FSL_IMX95_LPUART6]              = { 0x425a0000, 4 * KiB,    "lpuart6" },
+    [FSL_IMX95_LPUART7]              = { 0x42690000, 4 * KiB,    "lpuart7" },
+    [FSL_IMX95_LPUART8]              = { 0x426a0000, 4 * KiB,    "lpuart8" },
+
+    /*
+     * SCMI-routed peripherals. Bases from U-Boot imx-regs.h. Logging
+     * stubs only; SPL doesn't reach them through direct MMIO once the
+     * SM is up, but stubs catch any unexpected probing.
+     */
+    [FSL_IMX95_CCM]                  = { 0x44450000, 64 * KiB,   "ccm" },
+    [FSL_IMX95_ANATOP]               = { 0x44480000, 64 * KiB,   "anatop" },
+    [FSL_IMX95_IOMUXC]               = { 0x443c0000, 64 * KiB,   "iomuxc" },
+    [FSL_IMX95_SRC]                  = { 0x44460000, 64 * KiB,   "src" },
+    [FSL_IMX95_TRDC_AON]             = { 0x44270000, 64 * KiB,   "trdc_aon" },
+
+    /*
+     * BLK_CTRL aggregates per power domain. NETCMIX is Linux-visible
+     * (kernel touches it directly per imx95.dtsi:2874); the other three
+     * are SCMI-routed from U-Boot imx-regs.h.
+     */
+    [FSL_IMX95_BLK_CTRL_S_AONMIX]    = { 0x444f0000, 64 * KiB,   "s_aonmix" },
+    [FSL_IMX95_BLK_CTRL_NS_ANOMIX]   = { 0x44210000, 64 * KiB,   "ns_anomix" },
+    [FSL_IMX95_BLK_CTRL_WAKEUPMIX]   = { 0x42420000, 64 * KiB,   "wakeupmix" },
+    [FSL_IMX95_BLK_CTRL_NETCMIX]     = { 0x4c810000, 64 * KiB,   "netcmix" },
+
+    /*
+     * System Manager SCMI channel. dtsi: mu2 mailbox@445b0000 (4 KiB) with
+     * a 1 KiB sram0 child at 0x445b1000 holding the two SCMI A2P/P2A
+     * shared-memory buffers.
+     */
+    [FSL_IMX95_SM_MU]                = { 0x445b0000, 4 * KiB,    "sm_mu" },
+    [FSL_IMX95_SM_SHMEM]             = { 0x445b1000, 1 * KiB,    "sm_shmem" },
+
+    /* EdgeLock Secure Enclave mailboxes. dtsi: mailbox@47520000.. */
+    [FSL_IMX95_ELE_MU]               = { 0x47520000, 64 * KiB,   "elemu0" },
+    [FSL_IMX95_ELE_MU1]              = { 0x47530000, 64 * KiB,   "elemu1" },
+    /* Fuse Shadow Block (MIMX95_FSB.h FSB_BASE). */
+    [FSL_IMX95_FSB]                  = { 0x47510000, 64 * KiB,   "fsb" },
+    /* VFCCU / FCCU fault-control unit (AON_VFCCU). */
+    [FSL_IMX95_VFCCU]                = { 0x446b0000, 64 * KiB,   "vfccu" },
+    /* A1 MU SRAM page; SM keeps the A55 CPU wait-semaphore here (+0x3f8). */
+    [FSL_IMX95_CPU_SEMA]             = { 0x44231000, 4 * KiB,    "cpu-sema" },
+    /* CortexA TMPSNS (CORTEXA__TMPSNS_BASE). */
+    [FSL_IMX95_TMPSNS_CA]            = { 0x4a440000, 64 * KiB,   "tmpsns-ca" },
+    /* AON_VFCCU (FCCU_BASE), AON_ERMA, NOC_SRAMCTL - SM eMcem/fabric init. */
+    [FSL_IMX95_VFCCU_AON]            = { 0x44570000, 64 * KiB,   "vfccu-aon" },
+    [FSL_IMX95_ERMA]                 = { 0x44560000, 64 * KiB,   "erma" },
+    [FSL_IMX95_NOC_SRAMCTL]          = { 0x490a0000, 64 * KiB,   "sramctl" },
+
+    /*
+     * Other MU instances U-Boot proper probes from DT but we don't
+     * model yet (V2X, NETC, etc.). Logging stubs so mu_hal_init's
+     * register accesses don't external-abort.
+     */
+    [FSL_IMX95_MU_47320000] = { 0x47320000, 64 * KiB, "mu@47320000" },
+    [FSL_IMX95_MU_47350000] = { 0x47350000, 64 * KiB, "mu@47350000" },
+    [FSL_IMX95_MU_47540000] = { 0x47540000, 64 * KiB, "mu@47540000" },
+    [FSL_IMX95_MU_47550000] = { 0x47550000, 64 * KiB, "mu@47550000" },
+    [FSL_IMX95_MU_47560000] = { 0x47560000, 64 * KiB, "mu@47560000" },
+    [FSL_IMX95_MU_47570000] = { 0x47570000, 64 * KiB, "mu@47570000" },
+
+    /*
+     * Watchdogs. WDG3 is in the Linux DTS (the kernel sees it); WDG4
+     * and WDG5 are SPL-only - U-Boot's arch_cpu_init() disables all
+     * three. Bases from U-Boot arch/arm/include/asm/
+     * arch-imx9/imx-regs.h.
+     */
+    [FSL_IMX95_WDOG2]                = { 0x442e0000, 64 * KiB,   "wdog2" },
+    [FSL_IMX95_WDOG3]                = { 0x42490000, 64 * KiB,   "wdog3" },
+    [FSL_IMX95_WDOG4]                = { 0x424a0000, 64 * KiB,   "wdog4" },
+    [FSL_IMX95_WDOG5]                = { 0x424b0000, 64 * KiB,   "wdog5" },
+
+    /*
+     * GPIO bases from dtsi (gpio1@47400000) and U-Boot imx-regs.h
+     * (GPIO2-5 at 0x43810000, 0x43820000, 0x43840000, 0x43850000).
+     */
+    [FSL_IMX95_GPIO1]                = { 0x47400000, 64 * KiB,   "gpio1" },
+    [FSL_IMX95_GPIO2]                = { 0x43810000, 64 * KiB,   "gpio2" },
+    [FSL_IMX95_GPIO3]                = { 0x43820000, 64 * KiB,   "gpio3" },
+    [FSL_IMX95_GPIO4]                = { 0x43840000, 64 * KiB,   "gpio4" },
+    [FSL_IMX95_GPIO5]                = { 0x43850000, 64 * KiB,   "gpio5" },
+
+    /*
+     * ARM SMMU-v3 at 0x490d0000 (U-Boot imx-regs.h SMMU_BASE_ADDR).
+     * 128 KiB window covers CR0 / CR0_ACK / GBPA + the SMMU programming
+     * page. Stub returns 0 on read so SPL's disable_smmuv3() sees
+     * CR0.SMMUEN clear and early-exits.
+     */
+    [FSL_IMX95_SMMU]                 = { 0x490d0000, 128 * KiB,  "smmu" },
+
+    /*
+     * uSDHC controllers (compat "fsl,imx95-usdhc"). Bases + IRQs
+     * from imx95.dtsi:1303-1340. Logging stubs; a real fsl-esdhc
+     * model can be added once a backing-storage story exists (FIT
+     * or AHAB container on a disk image).
+     */
+    [FSL_IMX95_USDHC1]               = { 0x42850000, 64 * KiB,   "usdhc1" },
+    [FSL_IMX95_USDHC2]               = { 0x42860000, 64 * KiB,   "usdhc2" },
+    [FSL_IMX95_USDHC3]               = { 0x428b0000, 64 * KiB,   "usdhc3" },
+
+    /* LPI2C1..8 from imx95.dtsi - logging stubs. */
+    [FSL_IMX95_LPI2C1] = { 0x42530000, 64 * KiB, "lpi2c1" },
+    [FSL_IMX95_LPI2C2] = { 0x42540000, 64 * KiB, "lpi2c2" },
+    [FSL_IMX95_LPI2C3] = { 0x426b0000, 64 * KiB, "lpi2c3" },
+    [FSL_IMX95_LPI2C4] = { 0x426c0000, 64 * KiB, "lpi2c4" },
+    [FSL_IMX95_LPI2C5] = { 0x426d0000, 64 * KiB, "lpi2c5" },
+    [FSL_IMX95_LPI2C6] = { 0x426e0000, 64 * KiB, "lpi2c6" },
+    [FSL_IMX95_LPI2C7] = { 0x44340000, 64 * KiB, "lpi2c7" },
+    [FSL_IMX95_LPI2C8] = { 0x44350000, 64 * KiB, "lpi2c8" },
+
+    /* USB PHY + DWC3 controller (autoboot bootcmd touches these). */
+    [FSL_IMX95_USB_PHY]  = { 0x4c1f0000, 64 * KiB, "usb_phy" },
+    [FSL_IMX95_USB_DWC3] = { 0x4c200000, 64 * KiB, "usb_dwc3" },
+};
+
+/*
+ * For every peripheral region we don't yet model, install a stub that
+ * traces accesses but doesn't fault. This lets U-Boot / Linux probe
+ * registers safely while we iterate.
+ */
+/*
+ * NOP tr-write handler for the stub MUs. Its mere presence triggers
+ * the IMX_MU model's "set TSR.TEn after TR write" path - effectively
+ * making the stub mailboxes drop-and-ack on every send.
+ */
+static void fsl_imx95_stub_mu_tr_write(void *opaque, unsigned int idx,
+                                       uint32_t value)
+{
+}
+
+static bool fsl_imx95_install_unimplemented(FslImx95State *s, Error **errp)
+{
+    static const int unimplemented_regions[] = {
+        FSL_IMX95_CCM, FSL_IMX95_IOMUXC,
+        FSL_IMX95_TRDC_AON,
+        /* BLK_CTRL_S_AONMIX has a real model (M7 CPU-WAIT gate) below. */
+        FSL_IMX95_BLK_CTRL_NS_ANOMIX,
+        FSL_IMX95_BLK_CTRL_WAKEUPMIX, FSL_IMX95_BLK_CTRL_NETCMIX,
+        FSL_IMX95_WDOG4, FSL_IMX95_WDOG5,
+        FSL_IMX95_GPIO1, FSL_IMX95_GPIO2, FSL_IMX95_GPIO3,
+        FSL_IMX95_GPIO4, FSL_IMX95_GPIO5,
+        FSL_IMX95_SMMU,
+        FSL_IMX95_LPI2C1, FSL_IMX95_LPI2C2, FSL_IMX95_LPI2C3,
+        FSL_IMX95_LPI2C4, FSL_IMX95_LPI2C5, FSL_IMX95_LPI2C6,
+        /* LPI2C7 (0x44340000) is the SM's PMIC bus - real model below. */
+        FSL_IMX95_LPI2C8,
+        FSL_IMX95_USB_PHY, FSL_IMX95_USB_DWC3,
+        /*
+         * No GIC ITS is modelled (QEMU's GICv3 reports GICD_TYPER.LPIS=0,
+         * so Linux never probes the ITS node in the stock dtb); back its
+         * window with a logging stub rather than leaving it unmapped.
+         */
+        FSL_IMX95_GIC_ITS,
+    };
+
+    for (size_t i = 0; i < ARRAY_SIZE(unimplemented_regions); i++) {
+        int r = unimplemented_regions[i];
+        create_unimplemented_device(fsl_imx95_memmap[r].name,
+                                    fsl_imx95_memmap[r].addr,
+                                    fsl_imx95_memmap[r].size);
+    }
+
+    /*
+     * Bulk MU stubs for every mailbox the Linux imx95.dtsi declares
+     * that we haven't already wired with a real IMX_MU model. Linux's
+     * imx_mu driver probes each in turn (via fsl,imx95-mu /
+     * fsl,imx95-mu-v2x compatibles); without backing devices the
+     * probe takes a synchronous external abort reading PAR at offset
+     * 0x4. mu_hal_init() is safe against zero reads (rr_count=0
+     * skips the drain loop, SR bit 6 stays clear), so an
+     * unimplemented-device stub is enough to clear the abort.
+     *
+     * The addresses + names come from
+     * Linux arch/arm64/boot/dts/freescale/imx95.dtsi
+     * mailbox nodes. We exclude addresses already covered by real
+     * models (sm_mu, ele_mu/elemu0, ele_mu1/elemu1, and the six
+     * stub_mu[] entries at 0x47320000/0x47350000/0x47540000..0x47570000).
+     */
+    {
+        static const struct {
+            uint64_t addr;
+            const char *name;
+        } linux_mus[] = {
+            /* mu7 (0x42430000) is a real cross-connect to the M7, below. */
+            { 0x42730000, "mu8" },
+            { 0x44220000, "mu1" },
+            { 0x445d0000, "mu3" },
+            { 0x445f0000, "mu4" },
+            { 0x44630000, "mu6" },
+            { 0x47300000, "v2x_mu4" },
+            { 0x47330000, "v2x_mu7" },
+            { 0x47340000, "v2x_mu8" },
+            { 0x4ac60000, "cameramix_mu1" },
+            { 0x4ac70000, "cameramix_mu2" },
+            { 0x4ac80000, "cameramix_mu3" },
+            { 0x4ac90000, "cameramix_mu4" },
+            { 0x4aca0000, "cameramix_mu5" },
+            { 0x4acb0000, "cameramix_mu6" },
+            { 0x4acc0000, "cameramix_mu7" },
+            { 0x4acd0000, "cameramix_mu8" },
+            { 0x4ace0000, "cameramix_mu9" },
+        };
+        for (size_t i = 0; i < ARRAY_SIZE(linux_mus); i++) {
+            create_unimplemented_device(linux_mus[i].name,
+                                        linux_mus[i].addr, 64 * KiB);
+        }
+    }
+
+    /*
+     * Bulk stubs for the chip-wide register blocks the SM's config-op blob
+     * (DEV_SM_ExecOp) pokes during DEV_SM_Init: TRDC/RDC and the per-MIX
+     * block-control / clock-reset regions across the SoC. These are almost
+     * all blind writes (a few read-modify-writes whose read value is not
+     * acted on), so logging stubs that accept the accesses are enough to
+     * let DEV_SM_Init complete; none of these subsystems is modelled.
+     * create_unimplemented_device is a low-priority background, so the
+     * cpu-sema RAM at 0x44231000 still overrides inside 0x44230000.
+     * Addresses from the M33_ENUM enumeration of the config-op phase; the
+     * real SCMI MUs (0x445c0000 = sm_mu_b, 0x44620000 = m7_sm_mu_b) are
+     * intentionally absent - the SM's eager LMM_Init touches 0x44620000 as
+     * MU9/MU5_B, so the real MU + its SMT alias at 0x44621000 must own that
+     * window, not a dead logging stub.
+     */
+    {
+        static const uint64_t sm_cfgop_regions[] = {
+            0x42460000, 0x42470000, 0x42810000, 0x44230000, 0x44280000,
+            0x49000000, 0x49010000, 0x49020000, 0x49060000,
+            0x4ac40000, 0x4ac50000, 0x4aff0000, 0x4b040000, 0x4b050000,
+            0x4c040000, 0x4c050000, 0x4c440000, 0x4c450000, 0x4c840000,
+            0x4c850000, 0x4d810000, 0x4d840000, 0x4d850000,
+        };
+        for (size_t i = 0; i < ARRAY_SIZE(sm_cfgop_regions); i++) {
+            g_autofree char *name =
+                g_strdup_printf("sm-cfgop@0x%08" PRIx64, sm_cfgop_regions[i]);
+            create_unimplemented_device(name, sm_cfgop_regions[i], 64 * KiB);
+        }
+    }
+
+    /*
+     * Peripheral MMIO the Linux built-in drivers touch once the SM
+     * powers/clocks their domains. Once the SM answers SCMI they probe and
+     * would take a synchronous external abort on unmapped MMIO, killing
+     * PID 1. The list is the set the SM brings out of "deferred probe
+     * pending" (plus the Neutron NPU). Logging stubs let the probes proceed
+     * without faulting; these subsystems are not yet modelled for real. Low
+     * priority, so the modelled uSDHC / LPUART and the FSB RAM still override
+     * where they overlap.
+     */
+    {
+        static const struct {
+            uint64_t addr;
+            uint64_t size;
+        } linux_periph_regions[] = {
+            /*
+             * eDMA controllers: the driver maps the full DT reg window and
+             * touches the per-channel pages (0x40 channels x 0x8000 stride =
+             * 0x200000), so a 1 MiB stub faults on the high channels. Use the
+             * DT-declared sizes: edma5 = 0x210000, edma3 = 0x200000.
+             */
+            { 0x42000000, 0x210000 }, { 0x42210000, 0x210000 },
+            { 0x44000000, 0x200000 },
+            { 0x42430000, 64 * KiB }, /* cm7 mailbox */
+            { 0x42490000, 64 * KiB }, /* watchdog */
+            { 0x424e0000, 64 * KiB }, { 0x42510000, 64 * KiB }, /* pwm */
+            { 0x42530000, 64 * KiB }, { 0x42540000, 64 * KiB }, /* i2c */
+            { 0x42590000, 64 * KiB }, /* serial */
+            { 0x425e0000, 64 * KiB }, /* spi */
+            { 0x426b0000, 64 * KiB }, { 0x426c0000, 64 * KiB },
+            { 0x426d0000, 64 * KiB }, /* i2c */
+            { 0x42710000, 64 * KiB }, /* spi */
+            { 0x42850000, 64 * KiB }, { 0x42860000, 64 * KiB }, /* mmc */
+            { 0x43810000, 64 * KiB }, { 0x43820000, 64 * KiB },
+            { 0x43840000, 64 * KiB }, { 0x43850000, 64 * KiB }, /* gpio */
+            { 0x44350000, 64 * KiB }, /* i2c */
+            { 0x44530000, 64 * KiB }, /* adc */
+            { 0x4ac10000, 64 * KiB }, /* camera csr (clock provider) */
+            { 0x4ad00000, 64 * KiB }, /* display stream csr (clock provider) */
+            { 0x4ad10000, 64 * KiB }, /* display master csr (mmio-mux) */
+            { 0x4b010000, 64 * KiB }, /* syscon (dispmix csr) */
+            { 0x4b0b0000, 64 * KiB }, /* displaymix irqsteer */
+            { 0x4b0c0000, 64 * KiB }, /* lvds csr / ldb */
+            { 0x4b0d0000, 0x20000 },  /* pixel interleaver bridge */
+            /* 0x4b400000 display-controller (dpu) -> imx95.dpu status stub */
+            { 0x4c010000, 64 * KiB }, /* hsio blk-ctl (clock provider) */
+            { 0x4c100000, 64 * KiB }, /* usb3 dwc3 core */
+            { 0x4c1f0000, 64 * KiB }, /* usb3 phy */
+            { 0x4c200000, 0x100000 }, /* usb2 (chipidea) */
+            { 0x4c300000, 64 * KiB }, { 0x4c380000, 64 * KiB }, /* pcie */
+            { 0x4c410000, 64 * KiB }, /* syscon */
+            { 0x4c480000, 0x40000 },  /* vpu */
+            { 0x4c4c0000, 64 * KiB }, /* vpu-ctrl */
+            { 0x4c810000, 64 * KiB }, /* syscon */
+            { 0x4ca00000, 0x100000 }, /* netc pcie ecam 0 */
+            { 0x4cb00000, 0x100000 }, /* netc pcie ecam 1 */
+            { 0x4cde0000, 64 * KiB }, /* netc-blk-ctrl ierb */
+            { 0x4cdf0000, 64 * KiB }, /* netc-blk-ctrl prb */
+            { 0x4d900000, 0x100000 }, /* gpu */
+            { 0x4ab00000, 0x100000 }, /* neutron npu */
+            /*
+             * Audio (SAI/XCVR/MICFIL) and FlexCAN: not yet modelled. Stub
+             * them so that a board/use case enabling these nodes degrades
+             * gracefully (reads-as-0) instead of taking an unmapped-access
+             * fault on the first MMIO. The default NXP EVK boot does not
+             * probe them; FlexCAN modelling is on the roadmap.
+             */
+            { 0x42650000, 64 * KiB }, /* sai3 */
+            { 0x42660000, 64 * KiB }, /* sai4 */
+            { 0x42670000, 64 * KiB }, /* sai5 */
+            { 0x42680000, 64 * KiB }, /* xcvr (spdif) */
+            { 0x443b0000, 64 * KiB }, /* sai1 */
+            { 0x4c880000, 64 * KiB }, /* sai2 */
+            { 0x44520000, 64 * KiB }, /* micfil (pdm mic) */
+            { 0x425b0000, 64 * KiB }, /* flexcan2 */
+            { 0x42600000, 64 * KiB }, /* flexcan3 */
+            { 0x427c0000, 64 * KiB }, /* flexcan4 */
+            { 0x427d0000, 64 * KiB }, /* flexcan5 */
+            { 0x443a0000, 64 * KiB }, /* flexcan1 */
+            /*
+             * Display (MIPI DSI) and camera (MIPI CSI / ISI / ISP) interface
+             * controllers: not modelled. Stub them so enabling these DT nodes
+             * degrades gracefully (reads-as-0) instead of faulting on the
+             * first MMIO. The display-mix CSRs / LVDS-LDB CSR / DPU are stubbed
+             * elsewhere; real display scanout + camera capture are roadmap
+             * items (see the README).
+             */
+            { 0x4acf0000, 64 * KiB }, /* mipi dsi host + intf syscon */
+            { 0x4ad20000, 64 * KiB }, /* mipi tx phy csr */
+            { 0x4ad30000, 64 * KiB }, /* mipi csi0 */
+            { 0x4ad40000, 64 * KiB }, /* mipi csi1 */
+            { 0x4ad50000, 0x80000 },  /* isi (image sensing interface) */
+            { 0x4ae00000, 64 * KiB }, /* neo isp registers */
+            { 0x4afe0000, 64 * KiB }, /* neo isp stats */
+        };
+        for (size_t i = 0; i < ARRAY_SIZE(linux_periph_regions); i++) {
+            g_autofree char *name =
+                g_strdup_printf("linux-periph@0x%08" PRIx64,
+                                linux_periph_regions[i].addr);
+            create_unimplemented_device(name, linux_periph_regions[i].addr,
+                                        linux_periph_regions[i].size);
+        }
+    }
+
+    /*
+     * DPU (display-controller @0x4b400000). Not modelled, but unlike the bulk
+     * stubs above it cannot be a plain logging stub: Linux's dpu95 blit-engine
+     * probe busy-waits with no timeout on the command-sequencer status
+     * register, so a zero-returning stub hangs the probe kthread forever and
+     * wedges wait_for_device_probe() in kernel_init (userspace never starts).
+     * The imx95.dpu stub reports the sequencer idle with FIFO space so the
+     * polls pass and the probe completes; see hw/misc/imx95_dpu.c.
+     */
+    {
+        DeviceState *dpu = qdev_new("imx95.dpu");
+
+        if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(dpu), errp)) {
+            return false;
+        }
+        sysbus_mmio_map(SYS_BUS_DEVICE(dpu), 0, 0x4b400000);
+    }
+
+    return true;
+}
+
+/*
+ * Release the M33 only when SM firmware has actually been loaded into its
+ * ITCM. The SM image's vector table begins with the initial stack
+ * pointer, so a non-zero first ITCM word means firmware is present; a
+ * zeroed ITCM (no -device loader) leaves the M33 powered off, so a plain
+ * A55-only Linux boot does not run the M33 into a HardFault lockup.
+ *
+ * Run from a bottom half scheduled by the reset handler: by the time the
+ * BH executes, the initial system reset has already committed the loaded
+ * image into ITCM, side-stepping the reset-handler ordering problem (the
+ * generic -device loader commits its ROM blob during the reset, after any
+ * reset hook a device could register).
+ */
+/*
+ * Boot-ROM handover. On silicon the boot ROM loads each LM's
+ * image and records it in a handover table the SM reads at startup
+ * (DEV_SM_RomBootCpuGet via LMM_CpuInit) to learn which cores it owns and
+ * their boot vectors - which is what lets the SM boot, manage and
+ * fault-recover them. Our flow loads the M7 image directly (-device loader),
+ * bypassing the ROM, so we fabricate a minimal handover with a single M7
+ * EXEC entry. That makes LMM_CpuInit set s_bootFlags[M7], so the SM boots
+ * the M7 via LMM_CpuStart - releasing CPUWAIT through our AONMIX model and
+ * enabling the CM7_SYSRESETREQ fault IRQ. M7-only by design: the A55 boots
+ * outside the SM's LMM path, so leaving its handover absent doesn't perturb
+ * it (its s_bootFlags stays 0, exactly as before).
+ *
+ * The blob lives at 0x2003dc00 in the M33's DTCM, which is the SM's
+ * __StackTop - reserved DTCM above the stack the SM never writes, exactly
+ * where the ROM places it on hardware. Written here (M33 release time) so it
+ * is in place before the SM runs LMM_CpuInit, and only when M7 firmware was
+ * actually staged.
+ */
+#define M7_HANDOVER_DTCM_OFFSET (0x2003dc00ULL - FSL_IMX95_M33_DTCM_BASE)
+#define M7_HANDOVER_BARKER      0xc0ffee16u
+#define M7_HANDOVER_VER         0x0002u
+#define M7_HANDOVER_SIZE        0x0100u
+/*
+ * img->flags: bits[7:0] = cpu (M7 = CPU_IDX_M7P = 1), [15:8] = type (EXEC = 0).
+ */
+#define M7_HANDOVER_IMG_FLAGS   0x00000001u
+
+static void fsl_imx95_write_m7_handover(FslImx95State *s)
+{
+    uint8_t *dtcm = memory_region_get_ram_ptr(&s->m33_dtcm);
+    uint8_t *h = dtcm + M7_HANDOVER_DTCM_OFFSET;
+    const void *m7_itcm = memory_region_get_ram_ptr(&s->m7_itcm);
+
+    /* Only if M7 firmware was actually staged into its ITCM. */
+    if (ldl_le_p(m7_itcm) == 0) {
+        return;
+    }
+
+    memset(h, 0, M7_HANDOVER_SIZE);
+    stl_le_p(h + 0x00, M7_HANDOVER_BARKER);     /* barker */
+    stw_le_p(h + 0x04, M7_HANDOVER_VER);        /* ver   */
+    stw_le_p(h + 0x06, M7_HANDOVER_SIZE);       /* size  */
+    h[0x08] = 1;                                /* num images */
+    /* img[0] at 0x10: addr(u64), cpu(u16), resv(u16), flags(u32). */
+    stq_le_p(h + 0x10, FSL_IMX95_M7_ITCM_SYSVIEW);
+    stw_le_p(h + 0x18, 1);                      /* cpu = CPU_IDX_M7P */
+    stl_le_p(h + 0x1c, M7_HANDOVER_IMG_FLAGS);
+}
+
+static void fsl_imx95_m33_start_bh(void *opaque)
+{
+    FslImx95State *s = opaque;
+    const void *itcm = memory_region_get_ram_ptr(&s->m33_itcm);
+    uint32_t initial_sp = ldl_le_p(itcm);
+
+    if (initial_sp != 0 && s->m33.cpu) {
+        CPUState *cs = CPU(s->m33.cpu);
+        /* Place the M7 boot-ROM handover before the SM starts. */
+        fsl_imx95_write_m7_handover(s);
+        cs->halted = 0;
+        cpu_resume(cs);
+    }
+}
+
+/*
+ * Same shape as fsl_imx95_m33_start_bh, applied to the M7. Vector[0] in
+ * the M7's ITCM is the initial SP; firmware sets it to a non-zero value
+ * (typically the top of DTCM), so a non-zero first word means a
+ * "-device loader,...,cpu-num=7" actually populated the M7 image. With
+ * no firmware loaded the M7 stays halted and a plain A55+M33 boot is
+ * unaffected.
+ */
+static void fsl_imx95_m7_start_bh(void *opaque)
+{
+    FslImx95State *s = opaque;
+    const void *m7_itcm = memory_region_get_ram_ptr(&s->m7_itcm);
+    const void *m33_itcm = memory_region_get_ram_ptr(&s->m33_itcm);
+    uint32_t initial_sp = ldl_le_p(m7_itcm);
+
+    /*
+     * When the SM (M33 firmware) is loaded it owns the M7 lifecycle: it boots
+     * the M7 via DEV_SM_CpuStart (driven by our boot-ROM handover above),
+     * which our AONMIX M7_CFG.WAIT model turns into the release and which also
+     * enables the M7 fault IRQ. Force-starting the M7 here would race that
+     * managed release, so skip it when the SM is present. This force start is
+     * only for SM-less M7 boots, where nothing else releases
+     * the core.
+     */
+    if (ldl_le_p(m33_itcm) != 0) {
+        return;
+    }
+
+    if (initial_sp != 0 && s->m7.cpu) {
+        CPUState *cs = CPU(s->m7.cpu);
+        cs->halted = 0;
+        cpu_resume(cs);
+    }
+}
+
+/*
+ * System-reset handler: (re-)apply the M33 and M7 auto-start checks. This
+ * runs on the initial reset AND on every guest-triggered reboot, so the SM
+ * core is released again after a reset re-applies its start-powered-off
+ * state - without this a reboot would leave the M33 halted, no SCMI, and
+ * Linux would hang at arm-scmi probe. The releases are deferred to a BH so
+ * they run once the reset (including the -device loader repopulating the
+ * ITCMs and cpu_reset re-applying start-powered-off) has fully completed.
+ */
+static void fsl_imx95_reset(void *opaque)
+{
+    FslImx95State *s = opaque;
+
+    aio_bh_schedule_oneshot(qemu_get_aio_context(),
+                            fsl_imx95_m33_start_bh, s);
+    aio_bh_schedule_oneshot(qemu_get_aio_context(),
+                            fsl_imx95_m7_start_bh, s);
+}
+
+/*
+ * SRC-driven M7 release path. When the SM's
+ * DEV_SM_CpuStart(M7) writes SRC_GEN.SCR.BOOT_RESET_RELEASE_M7MIX
+ * (bit 12) from 0 to 1, hw/misc/imx95_src raises this irq. We defer
+ * the actual cpu_resume to a BH so it runs in the AIO context rather
+ * than inside the M33's TCG block that issued the SCR write. The
+ * underlying fsl_imx95_m7_start_bh's vector[0] guard keeps the release
+ * safe when no M7 firmware is loaded - in that case it's a no-op
+ * (preserves the Linux-only boot path's behaviour). Edge-triggered
+ * so a re-write of the bit (locked, won't change) doesn't repeatedly
+ * fire; the SRC model only raises on the 0 -> 1 transition.
+ */
+static void fsl_imx95_src_m7_release_handler(void *opaque, int n, int level)
+{
+    FslImx95State *s = opaque;
+
+    if (!level) {
+        return;
+    }
+    aio_bh_schedule_oneshot(qemu_get_aio_context(),
+                            fsl_imx95_m7_start_bh, s);
+}
+
+/*
+ * M7 run gate. The SM manages the M7 lifecycle via its CPUWAIT
+ * (AONMIX M7_CFG.WAIT): it releases the M7 on DEV_SM_CpuStart and holds it on
+ * DEV_SM_CpuStop. The fault-recovery path (reaction=lm_reset) does stop then
+ * start, cold-cycling the M7. imx95_aonmix drives the m7-run line with the
+ * released state; we halt the M7 when held and reset+resume it when released.
+ *
+ * Both run as work on the M7 vCPU so the state change is applied with the
+ * core stopped (the WAIT write happens in the M33's TCG block, a different
+ * CPU). On release we cpu_reset the M7 first: for ARMv7-M that reloads SP/PC
+ * from the reset vector in ITCM (which still holds the firmware image - we
+ * don't wipe it), so the M7 re-runs from the top, exactly as after the SM's
+ * real DEV_SM_CpuStart. cpu_reset re-applies start-powered-off (halted = 1),
+ * so we clear halted afterwards to release it.
+ */
+static void fsl_imx95_m7_power_off_work(CPUState *cs, run_on_cpu_data data)
+{
+    cs->halted = 1;
+}
+
+static void fsl_imx95_m7_power_on_work(CPUState *cs, run_on_cpu_data data)
+{
+    cpu_reset(cs);
+    cs->halted = 0;
+}
+
+static void fsl_imx95_m7_power_off_bh(void *opaque)
+{
+    FslImx95State *s = opaque;
+
+    if (s->m7.cpu) {
+        async_run_on_cpu(CPU(s->m7.cpu), fsl_imx95_m7_power_off_work,
+                         RUN_ON_CPU_NULL);
+    }
+}
+
+static void fsl_imx95_m7_power_on_bh(void *opaque)
+{
+    FslImx95State *s = opaque;
+
+    if (s->m7.cpu) {
+        async_run_on_cpu(CPU(s->m7.cpu), fsl_imx95_m7_power_on_work,
+                         RUN_ON_CPU_NULL);
+    }
+}
+
+/*
+ * The M7's NVIC SYSRESETREQ gpio-out (pulsed when firmware writes
+ * AIRCR.SYSRESETREQ) - forward it to the SM as CM7_SYSRESETREQ_IRQn on the
+ * M33's NVIC, the way the SRC routes it on silicon.
+ */
+static void fsl_imx95_m7_sysresetreq_handler(void *opaque, int n, int level)
+{
+    FslImx95State *s = opaque;
+
+    qemu_set_irq(qdev_get_gpio_in(DEVICE(&s->m33),
+                                  FSL_IMX95_M7_SYSRESETREQ_M33_IRQ), level);
+}
+
+static void fsl_imx95_m7_run_handler(void *opaque, int n, int level)
+{
+    FslImx95State *s = opaque;
+
+    /*
+     * Defer to a BH so the cross-CPU async work is queued from the AIO
+     * context rather than inside the M33's TCG block that wrote M7_CFG.
+     * level: 1 = released (start), 0 = held (stop).
+     */
+    aio_bh_schedule_oneshot(qemu_get_aio_context(),
+                            level ? fsl_imx95_m7_power_on_bh
+                                  : fsl_imx95_m7_power_off_bh, s);
+}
+
+static void fsl_imx95_realize(DeviceState *dev, Error **errp)
+{
+    MachineState *ms = MACHINE(qdev_get_machine());
+    FslImx95State *s = FSL_IMX95(dev);
+    DeviceState *gicdev = DEVICE(&s->gic);
+    const char *cpu_type = ms->cpu_type ?: ARM_CPU_TYPE_NAME("cortex-a55");
+    /*
+     * The 19x19 EVK has a fixed 6-core A55 cluster. The Cortex-M33 SM
+     * core and the Cortex-M7 real-time core are additional, always-present
+     * vCPUs not part of the A55 cluster (each has its own NVIC, not the
+     * GIC), so the A55 wiring below is sized by the fixed cluster count
+     * rather than ms->smp.cpus, which also counts the M33 and M7.
+     */
+    const unsigned n_a55 = FSL_IMX95_NUM_A55_CPUS;
+    int i;
+
+    if (ms->smp.cpus != n_a55 + 2) {
+        error_setg(errp,
+                   "%s: fixed topology is %u A55 + 1 M33 + 1 M7; "
+                   "run with -smp %u (the default) - %d requested",
+                   TYPE_FSL_IMX95, n_a55, n_a55 + 2, (int)ms->smp.cpus);
+        return;
+    }
+
+    /* Instantiate the A55 cluster. */
+    for (i = 0; i < n_a55; i++) {
+        g_autofree char *name = g_strdup_printf("cpu%d", i);
+        object_initialize_child(OBJECT(dev), name, &s->cpu[i], cpu_type);
+    }
+
+    for (i = 0; i < n_a55; i++) {
+        if (n_a55 > 1 &&
+            object_property_find(OBJECT(&s->cpu[i]), "reset-cbar")) {
+            object_property_set_int(OBJECT(&s->cpu[i]), "reset-cbar",
+                                    fsl_imx95_memmap[FSL_IMX95_GIC_DIST].addr,
+                                    &error_abort);
+        }
+
+        /*
+         * CNTFRQ for the ARM generic timer. The i.MX 95 system counter
+         * runs at 24 MHz (RM "System Counter" section).
+         */
+        object_property_set_int(OBJECT(&s->cpu[i]), "cntfrq", 24000000,
+                                &error_abort);
+
+        /*
+         * MPIDR per-CPU. The Linux DTS (imx95.dtsi cpu@0..cpu@500)
+         * puts each A55 in its own cluster: Aff1 = CPU index, Aff0 = 0.
+         * So cpu[i] gets mp-affinity = i << 8. PSCI CPU_ON from Linux
+         * uses these MPIDR values to identify the target CPU; without
+         * the override QEMU would default to Aff0=i which doesn't match
+         * the DT and PSCI CPU_ON fails for every secondary.
+         * Source: Linux arch/arm64/boot/dts/freescale/imx95.dtsi
+         */
+        object_property_set_uint(OBJECT(&s->cpu[i]), "mp-affinity",
+                                 (uint64_t)i << 8, &error_abort);
+
+        if (object_property_find(OBJECT(&s->cpu[i]), "has_el2")) {
+            object_property_set_bool(OBJECT(&s->cpu[i]), "has_el2",
+                                     !kvm_enabled(), &error_abort);
+        }
+        if (object_property_find(OBJECT(&s->cpu[i]), "has_el3")) {
+            object_property_set_bool(OBJECT(&s->cpu[i]), "has_el3",
+                                     !kvm_enabled(), &error_abort);
+        }
+
+        /*
+         * U-Boot proper runs at EL3 (no TF-A below it in our model)
+         * and issues OPTEE/PSCI SMCs assuming firmware will service
+         * them. Without psci-conduit, those SMCs trap to U-Boot's own
+         * EL3 sync vector and crash. Wire QEMU's built-in PSCI handler
+         * to the SMC conduit so PSCI calls return cleanly and the
+         * vendor-OS calls return UNDEFINED, which U-Boot handles as
+         * "no OP-TEE present" and moves on.
+         */
+        object_property_set_int(OBJECT(&s->cpu[i]), "psci-conduit",
+                                QEMU_PSCI_CONDUIT_SMC, &error_abort);
+
+        if (i) {
+            /* Secondary CPUs come up via PSCI / SRC. */
+            object_property_set_bool(OBJECT(&s->cpu[i]), "start-powered-off",
+                                     true, &error_abort);
+        }
+
+        if (!qdev_realize(DEVICE(&s->cpu[i]), NULL, errp)) {
+            return;
+        }
+    }
+
+    /* GIC-600 (GICv3) */
+    {
+        SysBusDevice *gicsbd = SYS_BUS_DEVICE(&s->gic);
+        QList *redist_region_count;
+
+        qdev_prop_set_uint32(gicdev, "num-cpu", n_a55);
+        qdev_prop_set_uint32(gicdev, "num-irq",
+                             FSL_IMX95_NUM_IRQS + GIC_INTERNAL);
+
+        redist_region_count = qlist_new();
+        qlist_append_int(redist_region_count, n_a55);
+        qdev_prop_set_array(gicdev, "redist-region-count", redist_region_count);
+
+        object_property_set_link(OBJECT(&s->gic), "sysmem",
+                                 OBJECT(get_system_memory()), &error_fatal);
+        if (!sysbus_realize(gicsbd, errp)) {
+            return;
+        }
+        sysbus_mmio_map(gicsbd, 0, fsl_imx95_memmap[FSL_IMX95_GIC_DIST].addr);
+        sysbus_mmio_map(gicsbd, 1, fsl_imx95_memmap[FSL_IMX95_GIC_REDIST].addr);
+
+        /* Wire the per-CPU timer PPIs and IRQ/FIQ lines. */
+        for (i = 0; i < n_a55; i++) {
+            DeviceState *cpudev = DEVICE(&s->cpu[i]);
+            int intidbase = FSL_IMX95_NUM_IRQS + i * GIC_INTERNAL;
+            qemu_irq irq;
+
+            static const int timer_irqs[] = {
+                [GTIMER_PHYS] = ARCH_TIMER_NS_EL1_IRQ,
+                [GTIMER_VIRT] = ARCH_TIMER_VIRT_IRQ,
+                [GTIMER_HYP]  = ARCH_TIMER_NS_EL2_IRQ,
+                [GTIMER_SEC]  = ARCH_TIMER_S_EL1_IRQ,
+            };
+            for (size_t j = 0; j < ARRAY_SIZE(timer_irqs); j++) {
+                irq = qdev_get_gpio_in(gicdev, intidbase + timer_irqs[j]);
+                qdev_connect_gpio_out(cpudev, j, irq);
+            }
+
+            sysbus_connect_irq(gicsbd, i,
+                qdev_get_gpio_in(cpudev, ARM_CPU_IRQ));
+            sysbus_connect_irq(gicsbd, i + n_a55,
+                qdev_get_gpio_in(cpudev, ARM_CPU_FIQ));
+            sysbus_connect_irq(gicsbd, i + 2 * n_a55,
+                qdev_get_gpio_in(cpudev, ARM_CPU_VIRQ));
+            sysbus_connect_irq(gicsbd, i + 3 * n_a55,
+                qdev_get_gpio_in(cpudev, ARM_CPU_VFIQ));
+        }
+    }
+
+    /* On-chip RAM. */
+    memory_region_init_ram(&s->ocram, NULL, "imx95-ocram",
+                           fsl_imx95_memmap[FSL_IMX95_OCRAM].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_OCRAM].addr,
+                                &s->ocram);
+
+    /* LPUARTs. LPUART1 is the 19x19 EVK console. */
+    {
+        static const struct {
+            int region;
+            int irq;
+        } lpuart_table[FSL_IMX95_NUM_LPUARTS] = {
+            { FSL_IMX95_LPUART1, FSL_IMX95_LPUART1_IRQ },
+            { FSL_IMX95_LPUART2, FSL_IMX95_LPUART2_IRQ },
+            { FSL_IMX95_LPUART3, FSL_IMX95_LPUART3_IRQ },
+            { FSL_IMX95_LPUART4, FSL_IMX95_LPUART4_IRQ },
+            { FSL_IMX95_LPUART5, FSL_IMX95_LPUART5_IRQ },
+            { FSL_IMX95_LPUART6, FSL_IMX95_LPUART6_IRQ },
+            { FSL_IMX95_LPUART7, FSL_IMX95_LPUART7_IRQ },
+            { FSL_IMX95_LPUART8, FSL_IMX95_LPUART8_IRQ },
+        };
+
+        for (i = 0; i < FSL_IMX95_NUM_LPUARTS; i++) {
+            SysBusDevice *sbd = SYS_BUS_DEVICE(&s->lpuart[i]);
+
+            qdev_prop_set_chr(DEVICE(&s->lpuart[i]), "chardev",
+                              serial_hd(i));
+            if (!sysbus_realize(sbd, errp)) {
+                return;
+            }
+            sysbus_mmio_map(sbd, 0,
+                            fsl_imx95_memmap[lpuart_table[i].region].addr);
+            sysbus_connect_irq(sbd, 0,
+                qdev_get_gpio_in(gicdev, lpuart_table[i].irq));
+        }
+    }
+
+    /*
+     * System Manager mailbox (MU2) shared memory.
+     *
+     * The Cortex-M33 SM firmware sits on the far side of MU2, talking
+     * SCMI back to whoever is running on the A55s (U-Boot SPL, U-Boot
+     * proper, Linux); it is the only SCMI provider on this machine.
+     *
+     * Convert SM_SHMEM (sram0) from an unimplemented logging stub to
+     * a real RAM region so the SMT shared-memory transport can be
+     * read and written by both sides.
+     */
+    memory_region_init_ram(&s->sm_shmem, OBJECT(dev), "imx95-sm-shmem",
+                           fsl_imx95_memmap[FSL_IMX95_SM_SHMEM].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_SM_SHMEM].addr,
+                                &s->sm_shmem);
+
+    /*
+     * System counter (see comment on s->sysctr in fsl-imx95.h): a real
+     * clockevent timer, since it is Linux's tick broadcast device for the
+     * local-timer-stop idle state. Not in the unimplemented-region list.
+     */
+    s->sysctr = qdev_new("imx95.sysctr");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->sysctr), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->sysctr), 0,
+                    fsl_imx95_memmap[FSL_IMX95_SYSCNT].addr);
+    sysbus_connect_irq(SYS_BUS_DEVICE(s->sysctr), 0,
+                       qdev_get_gpio_in(gicdev, FSL_IMX95_SYSCNT_IRQ));
+
+    /*
+     * BBNSM region as RAM (see comment on s->bbnsm in fsl-imx95.h). The
+     * M33 SM firmware reaches it via the m33_view alias of system memory.
+     */
+    memory_region_init_ram(&s->bbnsm, OBJECT(dev), "imx95-bbnsm",
+                           fsl_imx95_memmap[FSL_IMX95_BBNSM].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_BBNSM].addr,
+                                &s->bbnsm);
+
+    /* HSIO BLK_CTRL (HSIOMIX) as RAM (see comment on s->blk_ctrl_hsiomix). */
+    memory_region_init_ram(&s->blk_ctrl_hsiomix, OBJECT(dev),
+                           "imx95-blk-ctrl-hsiomix",
+                           fsl_imx95_memmap[FSL_IMX95_BLK_CTRL_HSIOMIX].size,
+                           &error_fatal);
+    memory_region_add_subregion(
+        get_system_memory(),
+        fsl_imx95_memmap[FSL_IMX95_BLK_CTRL_HSIOMIX].addr,
+        &s->blk_ctrl_hsiomix);
+
+    /*
+     * Fuse Shadow Block as RAM. On real silicon the ROM/ELE populates it
+     * from OTP before the SM runs; here the SM's DEV_SM_FuseInit just reads
+     * fuse words (FSB->FUSE[]), so a readable region is enough to get past
+     * the read. Fuse values default to zero; populate specific words later
+     * if a DEV_SM_Init consumer needs a non-zero value.
+     */
+    memory_region_init_ram(&s->fsb, OBJECT(dev), "imx95-fsb",
+                           fsl_imx95_memmap[FSL_IMX95_FSB].size, &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_FSB].addr, &s->fsb);
+
+    /*
+     * VFCCU (fault collection/control) as RAM. The SM's eMcem_Vfccu_InitCVfccu
+     * writes fault config/flags and only re-reads what it wrote (e.g. clears
+     * FHCFG0.FHIDEN and loops until it reads back 0), so a register-file is
+     * enough; no fault-injection or IRQ behaviour is modelled.
+     */
+    memory_region_init_ram(&s->vfccu, OBJECT(dev), "imx95-vfccu",
+                           fsl_imx95_memmap[FSL_IMX95_VFCCU].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_VFCCU].addr,
+                                &s->vfccu);
+
+    /*
+     * A55 CPU wait-semaphore SRAM. DEV_SM_CpuInit resets a Peterson-style
+     * semaphore (flag0/flag1/turn) the SM keeps at 0x442313f8 (in the A1 MU
+     * SRAM page); back it with RAM so the writes land.
+     */
+    memory_region_init_ram(&s->cpu_sema, OBJECT(dev), "imx95-cpu-sema",
+                           fsl_imx95_memmap[FSL_IMX95_CPU_SEMA].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_CPU_SEMA].addr,
+                                &s->cpu_sema);
+
+    /*
+     * CortexA TMPSNS as RAM. The SM's periodic sensor tick reads CTRL0
+     * (TMPSNS_GetFilterBusy) and the data/threshold registers; with the
+     * region zeroed the filter reads "idle" and the tick is a no-op. No
+     * real thermal model (canned/quiet); enough to keep the post-init
+     * sensor task from faulting on an unmapped read.
+     */
+    memory_region_init_ram(&s->tmpsns_ca, OBJECT(dev), "imx95-tmpsns-ca",
+                           fsl_imx95_memmap[FSL_IMX95_TMPSNS_CA].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_TMPSNS_CA].addr,
+                                &s->tmpsns_ca);
+
+    /*
+     * More register-file write targets the SM's eMcem/fabric init touches:
+     * AON_VFCCU (FCCU central, 0x44570000), AON_ERMA (error reporting,
+     * 0x44560000) and NOC_SRAMCTL (0x490a0000). The SM only writes config
+     * and reads back what it wrote, so plain RAM suffices; no fault or
+     * fabric behaviour is modelled.
+     */
+    memory_region_init_ram(&s->vfccu_aon, OBJECT(dev), "imx95-vfccu-aon",
+                           fsl_imx95_memmap[FSL_IMX95_VFCCU_AON].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_VFCCU_AON].addr,
+                                &s->vfccu_aon);
+    memory_region_init_ram(&s->erma, OBJECT(dev), "imx95-erma",
+                           fsl_imx95_memmap[FSL_IMX95_ERMA].size, &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_ERMA].addr,
+                                &s->erma);
+    memory_region_init_ram(&s->noc_sramctl, OBJECT(dev), "imx95-noc-sramctl",
+                           fsl_imx95_memmap[FSL_IMX95_NOC_SRAMCTL].size,
+                           &error_fatal);
+    memory_region_add_subregion(get_system_memory(),
+                                fsl_imx95_memmap[FSL_IMX95_NOC_SRAMCTL].addr,
+                                &s->noc_sramctl);
+
+    {
+        SysBusDevice *mu_sbd = SYS_BUS_DEVICE(&s->sm_mu);
+
+        if (!sysbus_realize(mu_sbd, errp)) {
+            return;
+        }
+        sysbus_mmio_map(mu_sbd, 0,
+                        fsl_imx95_memmap[FSL_IMX95_SM_MU].addr);
+        sysbus_connect_irq(mu_sbd, 0,
+            qdev_get_gpio_in(gicdev, FSL_IMX95_SM_MU_IRQ));
+    }
+
+    /*
+     * ELE mailbox 1 and its responder stub. The MU is a plain
+     * register file; the ele-server hangs off the MU's TR-write hook
+     * and answers ELE-protocol commands from SPL with stub responses.
+     * The responder only needs ele_get_info() to succeed for U-Boot
+     * SPL's imx9_probe_mu() to complete.
+     */
+    {
+        SysBusDevice *mu_sbd = SYS_BUS_DEVICE(&s->ele_mu1);
+
+        if (!sysbus_realize(mu_sbd, errp)) {
+            return;
+        }
+        sysbus_mmio_map(mu_sbd, 0,
+                        fsl_imx95_memmap[FSL_IMX95_ELE_MU1].addr);
+    }
+
+    /*
+     * Ordering is load-bearing here: the MU must be realised (so its
+     * MMIO + state are valid) BEFORE the responder is realised,
+     * because the responder's realize() calls imx_mu_set_tr_write_handler()
+     * on the MU. The dependency is via a runtime callback registration
+     * inside the responder's realize hook, not via a QOM property, so
+     * a grep for "ele_mu1" won't surface it. If you reorder these
+     * lines, the responder will register its handler on a still-zeroed
+     * MU state and SPL's first ELE TR write will silently no-op.
+     */
+    object_property_set_link(OBJECT(&s->ele_server), "mu",
+                             OBJECT(&s->ele_mu1), &error_abort);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->ele_server), errp)) {
+        return;
+    }
+
+    /*
+     * ELE MU 0 (elemu0, 0x47520000) + its responder. This is the EdgeLock
+     * channel the M33 SM uses during DEV_SM_Init (ELE_MuTx). Without a real
+     * MU here the SM's MU_SendMsg spins on TSR.TEn, and without a responder
+     * it then waits forever for an ELE reply. Same MU-then-responder
+     * ordering rule as ele_mu1 above.
+     */
+    {
+        SysBusDevice *mu_sbd = SYS_BUS_DEVICE(&s->ele_mu0);
+
+        if (!sysbus_realize(mu_sbd, errp)) {
+            return;
+        }
+        sysbus_mmio_map(mu_sbd, 0,
+                        fsl_imx95_memmap[FSL_IMX95_ELE_MU].addr);
+    }
+    object_property_set_link(OBJECT(&s->ele_server0), "mu",
+                             OBJECT(&s->ele_mu0), &error_abort);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->ele_server0), errp)) {
+        return;
+    }
+
+    /*
+     * Realise the 6 stub MUs. Wiring imx_mu_set_tr_write_handler() to
+     * a NOP makes IMX_MU re-assert TSR.TEn after every TR write, so
+     * U-Boot's TX-empty poll on these unmodeled mailboxes completes
+     * instead of looping. RR stays empty - no responder, no replies -
+     * which matches "the mailbox is here but nothing is listening on
+     * the other end."
+     */
+    {
+        static const int stub_mu_regions[] = {
+            FSL_IMX95_MU_47320000, FSL_IMX95_MU_47350000,
+            FSL_IMX95_MU_47540000, FSL_IMX95_MU_47550000,
+            FSL_IMX95_MU_47560000, FSL_IMX95_MU_47570000,
+        };
+        QEMU_BUILD_BUG_ON(ARRAY_SIZE(stub_mu_regions) !=
+                          ARRAY_SIZE(s->stub_mu));
+        for (i = 0; i < ARRAY_SIZE(s->stub_mu); i++) {
+            SysBusDevice *sbd = SYS_BUS_DEVICE(&s->stub_mu[i]);
+            if (!sysbus_realize(sbd, errp)) {
+                return;
+            }
+            sysbus_mmio_map(sbd, 0,
+                            fsl_imx95_memmap[stub_mu_regions[i]].addr);
+            imx_mu_set_tr_write_handler(&s->stub_mu[i],
+                                        fsl_imx95_stub_mu_tr_write, NULL);
+        }
+    }
+
+    /*
+     * MUI_A5 @0x44610000 - the M7-side MUA of the M7<->SM SCMI channel.
+     * Realised + mapped here so the window exists; its peer (MUB), the
+     * SMT shared-memory page, and both cores' IRQs are wired in one block
+     * after the M33 and M7 are realised (see the M7 cross-connect below).
+     */
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->m7_sm_mu), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->m7_sm_mu), 0, 0x44610000);
+
+    /*
+     * Wire the second ELE responder to the stub MU at 0x47550000.
+     * imx9_probe_mu() in U-Boot proper (SCMI variant, arch/arm/
+     * mach-imx/imx9/scmi/soc.c:1953) hard-codes "mailbox@47550000" as
+     * the ELE channel; SPL's matching #if XPL_BUILD branch uses
+     * "mailbox@47530000" (our ele_mu1). Mirror the responder here so
+     * U-Boot proper's ele_get_info() completes instead of timing out.
+     * The realize call below will overwrite the NOP tr-write handler
+     * the stub_mu loop above just installed.
+     *
+     * stub_mu[3] = FSL_IMX95_MU_47550000 (must match stub_mu_regions
+     * ordering above; the QEMU_BUILD_BUG_ON in the loop body anchors
+     * the count but not the order).
+     */
+    object_property_set_link(OBJECT(&s->ele_server2), "mu",
+                             OBJECT(&s->stub_mu[3]), &error_abort);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->ele_server2), errp)) {
+        return;
+    }
+
+    /*
+     * Watchdog 3 (the Wakeup-domain wdog the kernel sees). U-Boot SPL
+     * also disables it in arch_cpu_init() before the console comes up.
+     * Stub model is enough to satisfy disable_wdog().
+     */
+    s->wdog3 = qdev_new("imx95.wdog");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->wdog3), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->wdog3), 0,
+                    fsl_imx95_memmap[FSL_IMX95_WDOG3].addr);
+
+    /*
+     * WDOG2 is the M33 SM's own watchdog: its reset handler unlocks it
+     * (key 0xD928C520 -> CNT) and configures it before continuing. Same
+     * ULP WDOG IP as wdog3; reuse the model (no timer behaviour, so it
+     * never fires - the SM is free to configure and refresh it).
+     */
+    s->wdog2 = qdev_new("imx95.wdog");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->wdog2), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->wdog2), 0,
+                    fsl_imx95_memmap[FSL_IMX95_WDOG2].addr);
+
+    /*
+     * M33 XCACHE controllers. The SM enables + invalidates its caches at
+     * boot (CCR ENCACHE + INVWn + GO, then polls); the model self-clears
+     * the command bits so those polls converge.
+     */
+    s->xcache_pc = qdev_new("imx95.xcache");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->xcache_pc), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->xcache_pc), 0,
+                    fsl_imx95_memmap[FSL_IMX95_XCACHE_PC].addr);
+    s->xcache_ps = qdev_new("imx95.xcache");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->xcache_ps), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->xcache_ps), 0,
+                    fsl_imx95_memmap[FSL_IMX95_XCACHE_PS].addr);
+
+    /*
+     * LPI2C master the SM uses for the PMIC + IO-expander (SDK LPI2C1 at
+     * 0x44340000 = our FSL_IMX95_LPI2C7). Real model with the serial
+     * devices BRD_SM_SerialDevicesInit probes on its bus, so SM init
+     * completes: PCAL6408A IO-expander (0x20), PF09 main PMIC (0x08), and
+     * the PF5301/PF5302 (PF53) buck regulators (0x2a/0x29). The PCA2131 RTC
+     * (0x53) needs no model - its PCA2131_Init() is a no-op.
+     */
+    s->lpi2c_pmic = qdev_new("imx.lpi2c");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->lpi2c_pmic), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->lpi2c_pmic), 0,
+                    fsl_imx95_memmap[FSL_IMX95_LPI2C7].addr);
+    {
+        I2CBus *i2c = I2C_BUS(qdev_get_child_bus(s->lpi2c_pmic, "i2c"));
+        i2c_slave_create_simple(i2c, "pf09-pmic", 0x08);
+        i2c_slave_create_simple(i2c, "pcal6408a", 0x20);
+        i2c_slave_create_simple(i2c, "pf53-pmic", 0x2a);
+        i2c_slave_create_simple(i2c, "pf53-pmic", 0x29);
+    }
+
+    /*
+     * GPC: the SM polls per-domain CMC_MODE_STAT and uses the sleep/wake
+     * handshakes for CPU power-mode control. The model mirrors each status
+     * register to its paired control so the SM's "request mode, wait for
+     * mode" loops converge.
+     */
+    s->gpc = qdev_new("imx95.gpc");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->gpc), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->gpc), 0,
+                    fsl_imx95_memmap[FSL_IMX95_GPC].addr);
+
+    /*
+     * SRC: the SM powers mix domains down/up via SLICE_SW_CTRL.PDN_SOFT and
+     * polls each slice's FUNC_STAT until the transition completes. The model
+     * derives FUNC_STAT from PDN_SOFT (instantaneous), so DEV_SM_Init's
+     * power-down/up poll loops converge instead of spinning forever.
+     */
+    s->src = qdev_new("imx95.src");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->src), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->src), 0,
+                    fsl_imx95_memmap[FSL_IMX95_SRC].addr);
+    /*
+     * Wire SRC_GEN.SCR.BOOT_RESET_RELEASE_M7MIX rising edge to the M7
+     * release path. This is the silicon-faithful M7 lifecycle handle:
+     * the SM's CpuStart(M7_cpu_id) writes the bit, our SRC model raises
+     * the named gpio-out, the handler schedules fsl_imx95_m7_start_bh.
+     * Complements the existing reset-time release (which serves the
+     * M7-only test boots that have no SM running); fsl_imx95_m7_start_bh
+     * is idempotent so both paths firing for the same M7 boot is safe.
+     */
+    qdev_connect_gpio_out_named(s->src, "m7mix-release", 0,
+        qemu_allocate_irq(fsl_imx95_src_m7_release_handler, s, 0));
+
+    /*
+     * BLK_CTRL_S_AONMIX: real model for the M7 CPU-WAIT gate. Its M7_CFG.WAIT
+     * bit reads as held at reset, so the SM's CPU_RunModeGet sees the M7 in
+     * HOLD and runs the full DEV_SM_CpuStart (releasing CPUWAIT and enabling
+     * the M7 fault IRQ). The m7-run gpio carries the released state (1 = run,
+     * 0 = held); we wire it to the M7 run handler so the SM's stop/start of
+     * the M7 LM (incl. fault-recovery lm_reset) actually cycles the core.
+     */
+    s->aonmix = qdev_new("imx95.aonmix");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->aonmix), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->aonmix), 0,
+                    fsl_imx95_memmap[FSL_IMX95_BLK_CTRL_S_AONMIX].addr);
+    qdev_connect_gpio_out_named(s->aonmix, "m7-run", 0,
+        qemu_allocate_irq(fsl_imx95_m7_run_handler, s, 0));
+
+    /*
+     * ANATOP/PLL: the SM's DVFS path powers a PLL up and polls
+     * PLL_STATUS.LOCK, then enables each DFS and polls DFS_STATUS.DFS_OK
+     * (an unbounded wait). The model makes LOCK mirror CTRL.POWERUP and
+     * DFS_OK mirror each DFS's enable, so the A55 perf-level set completes
+     * instead of timing out (-9) or hanging.
+     */
+    s->anatop = qdev_new("imx95.anatop");
+    if (!sysbus_realize_and_unref(SYS_BUS_DEVICE(s->anatop), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(s->anatop), 0,
+                    fsl_imx95_memmap[FSL_IMX95_ANATOP].addr);
+
+    /*
+     * uSDHC1/2/3. Reuses QEMU's TYPE_IMX_USDHC (an SDHCI subclass with
+     * the i.MX MMIO quirks). With no backing -drive the card line
+     * stays unpopulated and SPL fails its MMC1 probe cleanly.
+     * Attach storage with e.g.:
+     *   -drive if=none,format=raw,file=sd.img,id=mmc0
+     *   -device sd-card,drive=mmc0
+     *
+     * Realization order is intentional: we want usdhc1's sd-bus to be
+     * the first one a bare `-device sd-card,drive=...` (no explicit
+     * bus=) picks up, since SPL is hard-wired to boot from MMC1.
+     * QEMU's anonymous-bus matching prefers the most-recently-added
+     * bus of the type, so we realize the array in reverse so that
+     * usdhc1 ends up at the top of the stack.
+     */
+    {
+        static const struct {
+            int region;
+            int irq;
+        } usdhc_table[FSL_IMX95_NUM_USDHCS] = {
+            { FSL_IMX95_USDHC1, FSL_IMX95_USDHC1_IRQ },
+            { FSL_IMX95_USDHC2, FSL_IMX95_USDHC2_IRQ },
+            { FSL_IMX95_USDHC3, FSL_IMX95_USDHC3_IRQ },
+        };
+
+        for (i = FSL_IMX95_NUM_USDHCS - 1; i >= 0; i--) {
+            SysBusDevice *sbd = SYS_BUS_DEVICE(&s->usdhc[i]);
+
+            /*
+             * The i.MX 95 uSDHC has no software SD-clock-enable bit (the card
+             * clock is auto-gated) and its SDMA ignores the SDHCI buffer
+             * boundary. Enable the matching quirks on these instances only, so
+             * other machines using TYPE_IMX_USDHC are unaffected.
+             */
+            s->usdhc[i].quirks |= SDHCI_QUIRK_SDCLK_AUTO_GATE |
+                                  SDHCI_QUIRK_NO_SDMA_BOUNDARY;
+            if (!sysbus_realize(sbd, errp)) {
+                return;
+            }
+            sysbus_mmio_map(sbd, 0,
+                            fsl_imx95_memmap[usdhc_table[i].region].addr);
+            sysbus_connect_irq(sbd, 0,
+                qdev_get_gpio_in(gicdev, usdhc_table[i].irq));
+        }
+    }
+
+    /*
+     * Cortex-M33 System Manager core. Build the M33's 32-bit
+     * address space: its private ITCM/DTCM RAM layered over a
+     * low-priority alias of the A55 system memory (so the M33 can also
+     * reach already-modelled peripherals and DRAM). The SM firmware is
+     * loaded into ITCM separately via -device loader.
+     *
+     * The M33 boots first on real silicon; here the A55 cluster is the
+     * primary boot path and the M33 is an additional core that runs only
+     * when SM firmware is loaded. With no firmware, ITCM reads back zero,
+     * so the M33 takes an early fault and locks up harmlessly without
+     * disturbing the A55s.
+     */
+    memory_region_init(&s->m33_view, OBJECT(s), "imx95-m33-view",
+                       4 * GiB);
+    /* Low-priority window onto the A55 system memory (peripherals, DRAM). */
+    memory_region_init_alias(&s->m33_sysmem_alias, OBJECT(s),
+                             "imx95-m33-sysmem", get_system_memory(),
+                             0, 4 * GiB);
+    memory_region_add_subregion_overlap(&s->m33_view, 0,
+                                        &s->m33_sysmem_alias, -1);
+    /* Private TCM, layered on top. */
+    memory_region_init_ram(&s->m33_itcm, OBJECT(s), "imx95-m33-itcm",
+                           FSL_IMX95_M33_TCM_SIZE, &error_fatal);
+    memory_region_add_subregion(&s->m33_view, FSL_IMX95_M33_ITCM_BASE,
+                                &s->m33_itcm);
+    memory_region_init_ram(&s->m33_dtcm, OBJECT(s), "imx95-m33-dtcm",
+                           FSL_IMX95_M33_TCM_SIZE, &error_fatal);
+    memory_region_add_subregion(&s->m33_view, FSL_IMX95_M33_DTCM_BASE,
+                                &s->m33_dtcm);
+
+    s->m33_cpuclk = clock_new(OBJECT(s), "m33-cpuclk");
+    clock_set_hz(s->m33_cpuclk, FSL_IMX95_M33_CLK_HZ);
+
+    qdev_prop_set_string(DEVICE(&s->m33), "cpu-type",
+                         ARM_CPU_TYPE_NAME("cortex-m33"));
+    qdev_prop_set_uint32(DEVICE(&s->m33), "num-irq", FSL_IMX95_M33_NUM_IRQ);
+    qdev_prop_set_uint32(DEVICE(&s->m33), "init-svtor", FSL_IMX95_M33_SVTOR);
+    /*
+     * Hold the M33 in reset until we know SM firmware was loaded into its
+     * ITCM (released by fsl_imx95_reset). This keeps a plain A55 Linux
+     * boot - which loads no SM image - from running the M33 on a zeroed
+     * ITCM straight into a HardFault lockup.
+     */
+    qdev_prop_set_bit(DEVICE(&s->m33), "start-powered-off", true);
+    qdev_connect_clock_in(DEVICE(&s->m33), "cpuclk", s->m33_cpuclk);
+    object_property_set_link(OBJECT(&s->m33), "memory",
+                             OBJECT(&s->m33_view), &error_abort);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->m33), errp)) {
+        return;
+    }
+
+    /*
+     * MU2 SCMI: instantiate the M33-side (MUB) endpoint of MU2 and peer-link
+     * it to the A55-side MU, so the real SM firmware on the M33 services the
+     * A55's SCMI. Linux's SCMI doorbell on MUA (0x445b0000) latches the
+     * matching GIP on MUB and raises the M33's MU2_B IRQ; the SM's response
+     * doorbell on MUB raises the A55's MU IRQ in turn. The MUB and its sram0
+     * alias sit 0x10000 above their MUA counterparts (A2_MUB = A2_MUA +
+     * 0x10000), and the SM's mb_mu computes its SMT buffer at MUB+0x1000 -
+     * the same backing RAM Linux uses at MUA+0x1000, so both sides exchange
+     * messages through one buffer. Done after the M33 is realized so its NVIC
+     * GPIO inputs exist. (A plain A55 boot with no SM firmware loaded leaves
+     * the M33 halted, so nothing answers SCMI - the SM image is required.)
+     */
+    {
+        IMXMUState *mub = &s->sm_mu_b;
+        hwaddr mub_base = fsl_imx95_memmap[FSL_IMX95_SM_MU].addr + 0x10000;
+        hwaddr shmem_b = fsl_imx95_memmap[FSL_IMX95_SM_SHMEM].addr + 0x10000;
+
+        object_initialize_child(OBJECT(s), "sm_mu_b", mub, TYPE_IMX_MU);
+        if (!sysbus_realize(SYS_BUS_DEVICE(mub), errp)) {
+            return;
+        }
+        sysbus_mmio_map(SYS_BUS_DEVICE(mub), 0, mub_base);
+        sysbus_connect_irq(SYS_BUS_DEVICE(mub), 0,
+            qdev_get_gpio_in(DEVICE(&s->m33), FSL_IMX95_SM_MU_B_M33_IRQ));
+        object_property_set_link(OBJECT(&s->sm_mu), "peer", OBJECT(mub),
+                                 &error_abort);
+        object_property_set_link(OBJECT(mub), "peer", OBJECT(&s->sm_mu),
+                                 &error_abort);
+
+        memory_region_init_alias(&s->sm_shmem_b, OBJECT(s), "imx95-sm-shmem-b",
+                                 &s->sm_shmem, 0,
+                                 fsl_imx95_memmap[FSL_IMX95_SM_SHMEM].size);
+        memory_region_add_subregion(get_system_memory(), shmem_b,
+                                    &s->sm_shmem_b);
+    }
+
+    /*
+     * Cortex-M7 real-time core. Mirror of the M33 setup above with
+     * M7-specific addresses: ITCM at M7-view 0x00000000 (the M7 boots
+     * here, so its reset VTOR is 0 - the ARMv7-M default), DTCM at
+     * M7-view 0x20000000. The same TCM RAM is also aliased into the
+     * system view at 0x203c0000 / 0x20400000 per the upstream Linux
+     * imx_rproc_att_imx95_m7 table, so an A55-side loader (Linux
+     * remoteproc, U-Boot) can populate the M7's TCM from there. The
+     * standalone -device loader,...,cpu-num=7 path writes through the
+     * M7's own address space and does not need those aliases, but they
+     * match real silicon and don't get in the way.
+     */
+    memory_region_init(&s->m7_view, OBJECT(s), "imx95-m7-view",
+                       4ULL * GiB);
+    memory_region_init_alias(&s->m7_sysmem_alias, OBJECT(s),
+                             "imx95-m7-sysmem", get_system_memory(),
+                             0, 4ULL * GiB);
+    memory_region_add_subregion_overlap(&s->m7_view, 0,
+                                        &s->m7_sysmem_alias, -1);
+
+    memory_region_init_ram(&s->m7_itcm, OBJECT(s), "imx95-m7-itcm",
+                           FSL_IMX95_M7_TCM_SIZE, &error_fatal);
+    memory_region_add_subregion(&s->m7_view, FSL_IMX95_M7_ITCM_M7VIEW,
+                                &s->m7_itcm);
+    memory_region_init_ram(&s->m7_dtcm, OBJECT(s), "imx95-m7-dtcm",
+                           FSL_IMX95_M7_TCM_SIZE, &error_fatal);
+    memory_region_add_subregion(&s->m7_view, FSL_IMX95_M7_DTCM_M7VIEW,
+                                &s->m7_dtcm);
+
+    /* System-view aliases (A55 sees the same TCM RAM at these addrs). */
+    memory_region_init_alias(&s->m7_itcm_sysalias, OBJECT(s),
+                             "imx95-m7-itcm-sysalias", &s->m7_itcm, 0,
+                             FSL_IMX95_M7_TCM_SIZE);
+    memory_region_add_subregion(get_system_memory(),
+                                FSL_IMX95_M7_ITCM_SYSVIEW,
+                                &s->m7_itcm_sysalias);
+    memory_region_init_alias(&s->m7_dtcm_sysalias, OBJECT(s),
+                             "imx95-m7-dtcm-sysalias", &s->m7_dtcm, 0,
+                             FSL_IMX95_M7_TCM_SIZE);
+    memory_region_add_subregion(get_system_memory(),
+                                FSL_IMX95_M7_DTCM_SYSVIEW,
+                                &s->m7_dtcm_sysalias);
+
+    s->m7_cpuclk = clock_new(OBJECT(s), "m7-cpuclk");
+    clock_set_hz(s->m7_cpuclk, FSL_IMX95_M7_CLK_HZ);
+
+    qdev_prop_set_string(DEVICE(&s->m7), "cpu-type",
+                         ARM_CPU_TYPE_NAME("cortex-m7"));
+    qdev_prop_set_uint32(DEVICE(&s->m7), "num-irq", FSL_IMX95_M7_NUM_IRQ);
+    qdev_prop_set_uint32(DEVICE(&s->m7), "init-svtor", 0);
+    qdev_prop_set_bit(DEVICE(&s->m7), "start-powered-off", true);
+    /*
+     * The i.MX 95 RT core (Cortex-M7, ARMv7-M / PMSAv7) SDK firmware programs
+     * an MPU region whose base is not aligned to its region size (e.g. DRBAR
+     * 0x4c800000 with a 512MB size, intended as 0x40000000); align down to
+     * match the silicon instead of dropping the region as UNPREDICTABLE. The
+     * M33 is a v8-M core (PMSAv8) and needs no such quirk.
+     */
+    qdev_prop_set_bit(DEVICE(&s->m7), "pmsav7-rbar-align-down", true);
+    qdev_connect_clock_in(DEVICE(&s->m7), "cpuclk", s->m7_cpuclk);
+    object_property_set_link(OBJECT(&s->m7), "memory",
+                             OBJECT(&s->m7_view), &error_abort);
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->m7), errp)) {
+        return;
+    }
+
+    /*
+     * Route the M7's NVIC SYSRESETREQ to the SM. On silicon the
+     * SRC turns an M7 SYSRESETREQ into CM7_SYSRESETREQ_IRQn on the M33; the SM
+     * takes the fault and, for the M7 LM (reaction=lm_reset), cold-resets it
+     * via the SRC M7 mix-slice - which our m7mix-power path then turns into a
+     * real halt+reset+resume of the M7. Without this wire, the NVIC's default
+     * unconnected behaviour would reset the whole machine. Done after both
+     * cores are realised so the M33 NVIC input exists.
+     */
+    qdev_connect_gpio_out_named(DEVICE(&s->m7), "SYSRESETREQ", 0,
+        qemu_allocate_irq(fsl_imx95_m7_sysresetreq_handler, s, 0));
+
+    /*
+     * M7<->SM SCMI channel cross-connect (MUI_A5), the mirror of the MU2
+     * block above. m7_sm_mu (MUA @0x44610000) is already mapped; here we
+     * route its IRQ to the M7's NVIC, instantiate the SM-side MUB
+     * (@0x44620000, = MUA + 0x10000) with its IRQ on the M33's NVIC, peer
+     * the two, and back the SMT shared-memory page. The M7 rings its
+     * doorbell on MUA -> GIP latches on MUB -> M33 MU5_B IRQ -> the SM's
+     * MB_MU_Handler services MU9 and replies on MUB -> GIP on MUA -> M7
+     * MU5_A IRQ. The SM already brought MU9 up in LMM_Init and is waiting.
+     * Done after both cores are realised so their NVIC inputs exist.
+     */
+    {
+        IMXMUState *mub = &s->m7_sm_mu_b;
+        hwaddr mua_base = 0x44610000;
+        hwaddr mub_base = mua_base + 0x10000;     /* 0x44620000 */
+        hwaddr shmem    = mua_base + 0x1000;      /* 0x44611000 */
+        hwaddr shmem_b  = mub_base + 0x1000;      /* 0x44621000 */
+
+        sysbus_connect_irq(SYS_BUS_DEVICE(&s->m7_sm_mu), 0,
+            qdev_get_gpio_in(DEVICE(&s->m7), FSL_IMX95_M7_SM_MU_M7_IRQ));
+
+        object_initialize_child(OBJECT(s), "m7_sm_mu_b", mub, TYPE_IMX_MU);
+        if (!sysbus_realize(SYS_BUS_DEVICE(mub), errp)) {
+            return;
+        }
+        sysbus_mmio_map(SYS_BUS_DEVICE(mub), 0, mub_base);
+        sysbus_connect_irq(SYS_BUS_DEVICE(mub), 0,
+            qdev_get_gpio_in(DEVICE(&s->m33), FSL_IMX95_M7_SM_MU_B_M33_IRQ));
+        object_property_set_link(OBJECT(&s->m7_sm_mu), "peer", OBJECT(mub),
+                                 &error_abort);
+        object_property_set_link(OBJECT(mub), "peer", OBJECT(&s->m7_sm_mu),
+                                 &error_abort);
+
+        /*
+         * 1 KiB SMT page at MUA+0x1000, aliased to MUB+0x1000 so both sides
+         * share one buffer. The SM's three MU9 SCMI channels live at
+         * +0x000/+0x080/+0x100 within it (SM_MB_MU_BUF_SIZE = 128), well
+         * inside the page.
+         */
+        memory_region_init_ram(&s->m7_shmem, OBJECT(s), "imx95-m7-shmem",
+                               1 * KiB, &error_fatal);
+        memory_region_add_subregion(get_system_memory(), shmem, &s->m7_shmem);
+        memory_region_init_alias(&s->m7_shmem_b, OBJECT(s), "imx95-m7-shmem-b",
+                                 &s->m7_shmem, 0, 1 * KiB);
+        memory_region_add_subregion(get_system_memory(), shmem_b,
+                                    &s->m7_shmem_b);
+    }
+
+    /*
+     * MU7: the A55<->M7 rpmsg notification mailbox. Linux's cm7 remoteproc
+     * kicks the M7 by writing the virtqueue id into mu7's TR register
+     * (mbox "tx" = <&mu7 0 1>); the M7's rpmsg_lite ISR reads it from RR on
+     * its side and vice-versa. Unlike the SCMI MUs (doorbell + shared SMT
+     * page) this channel carries data in TR/RR, which imx_mu forwards
+     * across the peer link. No tr-write handler here - the peer link is the
+     * consumer. Both endpoints are real IMX_MU: mu7_a (MUA @0x42430000,
+     * Linux side, IRQ -> GIC SPI 234) and mu7_b (MUB @0x42440000, M7 side,
+     * IRQ -> M7 NVIC 207). The vrings/buffers sit in the 0x88000000
+     * carveout, ordinary DRAM both cores already see. Done after the M7 is
+     * realised so its NVIC input exists.
+     */
+    {
+        SysBusDevice *mua = SYS_BUS_DEVICE(&s->mu7_a);
+        SysBusDevice *mub = SYS_BUS_DEVICE(&s->mu7_b);
+
+        if (!sysbus_realize(mua, errp)) {
+            return;
+        }
+        sysbus_mmio_map(mua, 0, 0x42430000);
+        sysbus_connect_irq(mua, 0,
+            qdev_get_gpio_in(gicdev, FSL_IMX95_MU7_A_IRQ));
+
+        if (!sysbus_realize(mub, errp)) {
+            return;
+        }
+        sysbus_mmio_map(mub, 0, 0x42440000);
+        sysbus_connect_irq(mub, 0,
+            qdev_get_gpio_in(DEVICE(&s->m7), FSL_IMX95_MU7_B_M7_IRQ));
+
+        object_property_set_link(OBJECT(&s->mu7_a), "peer",
+                                 OBJECT(&s->mu7_b), &error_abort);
+        object_property_set_link(OBJECT(&s->mu7_b), "peer",
+                                 OBJECT(&s->mu7_a), &error_abort);
+    }
+
+    /*
+     * Register the M33 and M7 auto-start checks as a reset handler so they
+     * re-run on every system reset (initial boot and guest reboot alike),
+     * not just once at startup. The releases themselves are
+     * deferred to a BH (see fsl_imx95_reset), so they run after the generic
+     * -device loader has repopulated the ITCMs.
+     */
+    qemu_register_reset_nosnapshotload(fsl_imx95_reset, s);
+
+    /* All peripherals not yet modeled get logging stubs. */
+    if (!fsl_imx95_install_unimplemented(s, errp)) {
+        return;
+    }
+}
+
+static void fsl_imx95_init(Object *obj)
+{
+    FslImx95State *s = FSL_IMX95(obj);
+    int i;
+
+    object_initialize_child(obj, "gic", &s->gic, TYPE_ARM_GICV3);
+
+    object_initialize_child(obj, "m33", &s->m33, TYPE_ARMV7M);
+    object_initialize_child(obj, "m7", &s->m7, TYPE_ARMV7M);
+
+    for (i = 0; i < FSL_IMX95_NUM_LPUARTS; i++) {
+        g_autofree char *name = g_strdup_printf("lpuart%d", i + 1);
+        object_initialize_child(obj, name, &s->lpuart[i], TYPE_IMX_LPUART);
+    }
+
+    object_initialize_child(obj, "sm_mu", &s->sm_mu, TYPE_IMX_MU);
+    object_initialize_child(obj, "ele_mu0", &s->ele_mu0, TYPE_IMX_MU);
+    object_initialize_child(obj, "ele_mu1", &s->ele_mu1, TYPE_IMX_MU);
+    for (i = 0; i < ARRAY_SIZE(s->stub_mu); i++) {
+        g_autofree char *name = g_strdup_printf("stub_mu%d", i);
+        object_initialize_child(obj, name, &s->stub_mu[i], TYPE_IMX_MU);
+    }
+    object_initialize_child(obj, "m7_sm_mu", &s->m7_sm_mu, TYPE_IMX_MU);
+    object_initialize_child(obj, "mu7_a", &s->mu7_a, TYPE_IMX_MU);
+    object_initialize_child(obj, "mu7_b", &s->mu7_b, TYPE_IMX_MU);
+    object_initialize_child(obj, "ele-server", &s->ele_server,
+                            TYPE_IMX95_ELE_SERVER);
+    object_initialize_child(obj, "ele-server0", &s->ele_server0,
+                            TYPE_IMX95_ELE_SERVER);
+    object_initialize_child(obj, "ele-server2", &s->ele_server2,
+                            TYPE_IMX95_ELE_SERVER);
+
+    for (i = 0; i < FSL_IMX95_NUM_USDHCS; i++) {
+        g_autofree char *name = g_strdup_printf("usdhc%d", i + 1);
+        object_initialize_child(obj, name, &s->usdhc[i], TYPE_IMX_USDHC);
+    }
+}
+
+static void fsl_imx95_class_init(ObjectClass *oc, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(oc);
+
+    dc->realize = fsl_imx95_realize;
+    /* This is an SoC, not user-creatable. */
+    dc->user_creatable = false;
+}
+
+static const TypeInfo fsl_imx95_types[] = {
+    {
+        .name           = TYPE_FSL_IMX95,
+        .parent         = TYPE_SYS_BUS_DEVICE,
+        .instance_size  = sizeof(FslImx95State),
+        .instance_init  = fsl_imx95_init,
+        .class_init     = fsl_imx95_class_init,
+    },
+};
+
+DEFINE_TYPES(fsl_imx95_types)
diff --git a/hw/arm/meson.build b/hw/arm/meson.build
index 8ee5307a919..96fef4c5b5f 100644
--- a/hw/arm/meson.build
+++ b/hw/arm/meson.build
@@ -117,5 +117,6 @@ arm_common_ss.add(when: ['CONFIG_AXIADO_EVK', 'TARGET_AARCH64'], if_true: files(
   'ax3000-evk.c'))
 
 arm_common_ss.add(files('boot.c'))
+arm_common_ss.add(when: 'CONFIG_FSL_IMX95', if_true: files('fsl-imx95.c'))
 
 hw_common_arch += {'arm': arm_common_ss}
diff --git a/include/hw/arm/fsl-imx95.h b/include/hw/arm/fsl-imx95.h
new file mode 100644
index 00000000000..473b1bdf483
--- /dev/null
+++ b/include/hw/arm/fsl-imx95.h
@@ -0,0 +1,481 @@
+/*
+ * NXP i.MX 95 SoC definitions
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * Modeled on hw/arm/fsl-imx8mp.h
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Address and IRQ ground truth comes from two sources:
+ *   - Linux DTS for everything Linux probes directly:
+ *     arch/arm64/boot/dts/freescale/imx95.dtsi
+ *   - NXP U-Boot's RM-derived header for the SCMI-routed peripherals
+ *     U-Boot SPL pokes before SCMI is up:
+ *     arch/arm/include/asm/arch-imx9/imx-regs.h
+ * The latter set (CCM, ANATOP, IOMUXC, SRC, TRDC, the BLK_CTRL
+ * aggregates) is currently logging-stub only - they are not modelled
+ * because the NXP imx95-evk SPL routes all clock/pinmux/power-domain
+ * access through SCMI to the M33 SM, which is the only SCMI provider.
+ */
+
+#ifndef FSL_IMX95_H
+#define FSL_IMX95_H
+
+#include "target/arm/cpu.h"
+#include "hw/arm/armv7m.h"
+#include "hw/char/imx_lpuart.h"
+#include "hw/core/clock.h"
+#include "hw/intc/arm_gicv3_common.h"
+#include "hw/misc/imx95_ele_server.h"
+#include "hw/misc/imx_mu.h"
+#include "hw/sd/sdhci.h"
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+#include "qemu/notify.h"
+#include "qemu/units.h"
+
+#define TYPE_FSL_IMX95 "fsl-imx95"
+OBJECT_DECLARE_SIMPLE_TYPE(FslImx95State, FSL_IMX95)
+
+/*
+ * Main DDR window. i.MX 95 maps DRAM starting at 0x8000_0000 on both
+ * the 19x19 (LPDDR5) and 15x15 (LPDDR4X) EVK variants.
+ */
+#define FSL_IMX95_RAM_START         0x80000000ULL
+#define FSL_IMX95_RAM_SIZE_MAX      (16ULL * GiB)
+/* Enough to hold the board's DTB (+128 MiB) and initrd (+256 MiB) presets. */
+#define FSL_IMX95_RAM_SIZE_MIN      (512ULL * MiB)
+
+/*
+ * Cortex-M33 System Manager (SM) core. The SM firmware is linked to run
+ * from the M33's tightly-coupled memories: code in ITCM, data/bss/stack
+ * in DTCM. Sizes are rounded up to 256 KiB (the real TCM banks are
+ * smaller; the SM image fits well within this). The reset vector table
+ * sits at the ITCM base, so the M33's reset VTOR (init-svtor) points
+ * there. The M33's 32-bit view is otherwise a superset of the A55 view
+ * (it also reaches CCM/ANATOP/SRC/etc. that the A55 cannot) - we give it
+ * the A55 system memory plus its private TCM; unmodelled SM-only
+ * peripherals are backed by logging stubs.
+ */
+#define FSL_IMX95_M33_ITCM_BASE     0x1ffc0000ULL
+#define FSL_IMX95_M33_DTCM_BASE     0x20000000ULL
+#define FSL_IMX95_M33_TCM_SIZE      (256 * KiB)
+#define FSL_IMX95_M33_SVTOR         0x1ffc0000U
+#define FSL_IMX95_M33_NUM_IRQ       256
+#define FSL_IMX95_M33_CLK_HZ        333333333U  /* SM runs the M33 ~333 MHz */
+
+/*
+ * Cortex-M7 real-time core. Memory map matches the upstream Linux
+ * imx_rproc driver's imx_rproc_att_imx95_m7 table: the M7 sees ITCM at
+ * its 0x00000000 (boot reset vector) and DTCM at 0x20000000; the A55
+ * (and any other system-view observer) sees the same TCM RAM at
+ * 0x203c0000 and 0x20400000 respectively, so a system-side firmware
+ * loader (Linux remoteproc, U-Boot, ...) can populate the M7's TCM from
+ * the A55 side. The M7's reset VTOR is 0 - the ARMv7-M default - because
+ * ITCM is at M7-view 0 in this map.
+ */
+#define FSL_IMX95_M7_ITCM_M7VIEW    0x00000000ULL  /* M7's view of ITCM */
+#define FSL_IMX95_M7_DTCM_M7VIEW    0x20000000ULL  /* M7's view of DTCM */
+#define FSL_IMX95_M7_ITCM_SYSVIEW   0x203c0000ULL  /* A55's view of M7 ITCM */
+#define FSL_IMX95_M7_DTCM_SYSVIEW   0x20400000ULL  /* A55's view of M7 DTCM */
+#define FSL_IMX95_M7_TCM_SIZE       (256 * KiB)
+#define FSL_IMX95_M7_NUM_IRQ        256
+#define FSL_IMX95_M7_CLK_HZ         800000000U  /* M7 runs ~800 MHz */
+
+/*
+ * i.MX 95 application processor complex:
+ *   - 6x Cortex-A55 (the main APUs)
+ *   - 1x Cortex-M33 (System Manager, runs real NXP SM firmware)
+ *   - 1x Cortex-M7  (real-time domain, runs an RT/MCUXpresso workload)
+ * All three are instantiated; the M33 and M7 are heterogeneous CPU
+ * contexts in the same QEMU instance, each with its own private TCMs
+ * and ARMv7-M NVIC.
+ */
+enum FslImx95Configuration {
+    FSL_IMX95_NUM_A55_CPUS  = 6,
+    FSL_IMX95_NUM_LPUARTS   = 8,    /* LPUART1..LPUART8 */
+    FSL_IMX95_NUM_USDHCS    = 3,    /* uSDHC1..uSDHC3 */
+    FSL_IMX95_NUM_IRQS      = 320,  /* GIC SPI budget (max is 1020) */
+};
+
+struct FslImx95State {
+    SysBusDevice            parent_obj;
+
+    ARMCPU                  cpu[FSL_IMX95_NUM_A55_CPUS];
+
+    /*
+     * Cortex-M33 System Manager core. Wires the CPU + its private
+     * TCM so the real NXP SM firmware (m33_image.elf) can be loaded and
+     * started; the SM firmware it runs is this machine's only SCMI provider.
+     * m33_view is the M33's 32-bit address space: ITCM + DTCM RAM layered
+     * over a low-priority alias of the A55 system memory.
+     *
+     * The M33 starts powered-off and is released (on every system reset, by
+     * fsl_imx95_reset) only if SM firmware was actually loaded into its ITCM;
+     * without firmware it stays halted, so a plain A55 Linux boot is
+     * unaffected. The same check releases the M7 by its own vector[0] test.
+     */
+    ARMv7MState             m33;
+    MemoryRegion            m33_view;
+    MemoryRegion            m33_sysmem_alias;
+    MemoryRegion            m33_itcm;
+    MemoryRegion            m33_dtcm;
+    Clock                  *m33_cpuclk;
+
+    /*
+     * Cortex-M7 real-time core. Same structural pattern as the
+     * M33: an ARMv7-M instance backed by its own ITCM + DTCM RAM regions
+     * layered over a low-priority alias of the A55 system memory, in an
+     * m7-private 32-bit view. The two *_sysalias regions expose the same
+     * TCM RAM at the system-view addresses (0x203c0000 / 0x20400000) so
+     * an A55-side loader can deposit M7 firmware into TCM from there;
+     * the standalone "-device loader,file=cm7_image.elf,cpu-num=7" path
+     * writes through the M7's own address space and does not need them.
+     * The M7 starts powered-off and is released (on system reset, by
+     * fsl_imx95_reset) alongside the M33, gated on a non-zero vector[0] in
+     * its ITCM (i.e. firmware was actually loaded).
+     */
+    ARMv7MState             m7;
+    MemoryRegion            m7_view;
+    MemoryRegion            m7_sysmem_alias;
+    MemoryRegion            m7_itcm;
+    MemoryRegion            m7_dtcm;
+    MemoryRegion            m7_itcm_sysalias;
+    MemoryRegion            m7_dtcm_sysalias;
+    Clock                  *m7_cpuclk;
+
+    GICv3State              gic;
+    MemoryRegion            ocram;
+    MemoryRegion            sm_shmem;
+    /* Alias of sm_shmem into the MU2 MUB window. */
+    MemoryRegion            sm_shmem_b;
+    /* Fuse Shadow Block (FSB) backing RAM - SM reads fuse words at boot. */
+    MemoryRegion            fsb;
+    /* VFCCU backing RAM - SM eMcem init writes fault config/flags. */
+    MemoryRegion            vfccu;
+    /* A55 CPU wait-semaphore SRAM (A1 MU SRAM page). */
+    MemoryRegion            cpu_sema;
+    /* CortexA TMPSNS backing RAM (SM sensor tick; CTRL0=0 -> filter idle). */
+    MemoryRegion            tmpsns_ca;
+    /* More eMcem/fabric init targets backed by RAM (write-acceptors). */
+    MemoryRegion            vfccu_aon;
+    MemoryRegion            erma;
+    MemoryRegion            noc_sramctl;
+    /*
+     * System counter (timer@44290000): a real clockevent model with a live
+     * counter + compare-match IRQ (hw/timer/imx95_sysctr.c). It is Linux's
+     * tick BROADCAST device - the cpu-pd-wait idle state has local-timer-stop,
+     * so an idling core shuts down its per-CPU arch timer and depends on this
+     * counter's compare interrupt to wake. (The earlier RAM stub gave write-
+     * then-read-back but no live counter / no IRQ, so idle cores never woke
+     * and the boot needed cpuidle.off=1.)
+     */
+    DeviceState            *sysctr;
+    /*
+     * BBNSM (Battery-Backed Non-Secure Module: RTC, tamper, 8 general-
+     * purpose registers) backed by RAM so writes stick. The NXP SM
+     * firmware touches it during early init (brd_sm reset-record GPRs via
+     * BBNSM_GprSetValue, and RTC setup that polls CTRL.RTC_EN back). The
+     * driver only does write-then-read-back on these registers and never
+     * reads VID/FEATURES, so a plain RAM region is sufficient. The A55
+     * Linux side does not currently touch it.
+     */
+    MemoryRegion            bbnsm;
+    /*
+     * HSIO BLK_CTRL (HSIOMIX) backed by RAM. The SM's SystemInit does a
+     * read-modify-write of the LFAST IO register at 0x4c0100c0; RAM gives
+     * the read-back the write expects.
+     */
+    MemoryRegion            blk_ctrl_hsiomix;
+    IMXLPUARTState          lpuart[FSL_IMX95_NUM_LPUARTS];
+    IMXMUState              sm_mu;
+    /* ELE MU the M33 SM uses for its EdgeLock channel (elemu0, 0x47520000). */
+    IMXMUState              ele_mu0;
+    IMXMUState              ele_mu1;
+    /*
+     * M33-side (MUB) endpoint of MU2, peer-linked to sm_mu (the A55-side
+     * MUA @0x445b0000), mapped at 0x445c0000 with its IRQ routed to the M33
+     * NVIC, so the real SM firmware services the A55's SCMI doorbells.
+     */
+    IMXMUState              sm_mu_b;
+    /*
+     * Stub MUs for the V2X / NETC / spare instances the DT references
+     * but nothing in the model talks to. Wired with a NOP tr-write
+     * handler so writes drop and TSR.TEn re-asserts, letting U-Boot
+     * iterate them cleanly without hanging on TX-empty polls.
+     */
+    IMXMUState              stub_mu[6];
+    /*
+     * M7<->SM SCMI-over-SMT channel (MUI_A5), the mirror of the A55<->SM
+     * MU2 cross-connect above for the M7's SCMI agent. m7_sm_mu is the
+     * M7-side MUA @0x44610000; m7_sm_mu_b is the SM-side MUB @0x44620000,
+     * peer-linked to it with its IRQ routed to the M33 NVIC. m7_shmem is
+     * the 1 KiB SMT shared-memory page at MUA+0x1000 (0x44611000), with
+     * m7_shmem_b aliasing the same RAM at MUB+0x1000 (0x44621000) so both
+     * sides exchange messages through one buffer. The SM brings this MU up
+     * eagerly in LMM_Init and waits interrupt-driven for the M7's doorbell.
+     */
+    IMXMUState              m7_sm_mu;
+    IMXMUState              m7_sm_mu_b;
+    MemoryRegion            m7_shmem;
+    MemoryRegion            m7_shmem_b;
+    /*
+     * MU7: the A55<->M7 rpmsg notification mailbox (Linux cm7 remoteproc
+     * kicks through it). mu7_a is the Linux-side MUA @0x42430000 (IRQ ->
+     * GIC SPI 234); mu7_b is the M7-side MUB @0x42440000 (IRQ -> M7 NVIC
+     * 207). Peer-linked so a TR write on one lands in the other's RR and
+     * raises its RX interrupt; the vrings/buffers live in the 0x88000000
+     * carveout (ordinary DRAM, seen by both cores).
+     */
+    IMXMUState              mu7_a;
+    IMXMUState              mu7_b;
+    IMX95ELEServerState     ele_server;
+    /* ELE responder for the SM's elemu0 channel (0x47520000). */
+    IMX95ELEServerState     ele_server0;
+    /*
+     * Second ELE server attached to the stub MU at 0x47550000. The
+     * imx9_probe_mu() SCMI variant in U-Boot proper hardcodes
+     * "mailbox@47550000" as the ELE channel (the SPL variant uses
+     * "mailbox@47530000", which is ele_mu1). Without an ELE responder
+     * at 0x47550000, ele_get_info() times out (-110), board_init_f's
+     * initcall fails, and the boot hangs before serial_init prints.
+     * Mirror the responder so both SPL and U-Boot proper reach it.
+     */
+    IMX95ELEServerState     ele_server2;
+    SDHCIState              usdhc[FSL_IMX95_NUM_USDHCS];
+    DeviceState            *wdog2;
+    DeviceState            *wdog3;
+    /* M33 XCACHE controllers (PC @0x44400000, PS @0x44400800). */
+    DeviceState            *xcache_pc;
+    DeviceState            *xcache_ps;
+    /*
+     * LPI2C the SM uses for the PMIC / IO-expander (SDK "LPI2C1" at
+     * 0x44340000, our memmap FSL_IMX95_LPI2C7). Real master model with a
+     * PF09 PMIC + PCAL6408A on its bus; the other LPI2C instances stay
+     * logging stubs.
+     */
+    DeviceState            *lpi2c_pmic;
+    /* GPC (General Power Controller) - SM power-domain / CPU-mode control. */
+    DeviceState            *gpc;
+    /* SRC (System Reset Controller) - SM mix-slice power-down/up control. */
+    DeviceState            *src;
+    /* BLK_CTRL_S_AONMIX - M7 CPU-WAIT gate (SM-managed M7 lifecycle). */
+    DeviceState            *aonmix;
+    /* ANATOP/PLL - SM DVFS PLL lock + DFS status (A55 perf level). */
+    DeviceState            *anatop;
+
+};
+
+/*
+ * Memory map region identifiers. The actual addresses live in the memmap
+ * table in fsl-imx95.c.
+ */
+enum FslImx95MemoryRegions {
+    FSL_IMX95_RAM,
+
+    /* GIC-600 (GICv3-compatible) */
+    FSL_IMX95_GIC_DIST,
+    FSL_IMX95_GIC_REDIST,
+    FSL_IMX95_GIC_ITS,
+
+    /* On-chip SRAM "sram1" */
+    FSL_IMX95_OCRAM,
+
+    /* System counter (drives the ARM generic timer). */
+    FSL_IMX95_SYSCNT,
+
+    /* BBNSM (RTC / tamper / GPRs) - touched by the M33 SM firmware. */
+    FSL_IMX95_BBNSM,
+
+    /* M33 XCACHE controllers (enabled/invalidated by the SM at boot). */
+    FSL_IMX95_XCACHE_PC,
+    FSL_IMX95_XCACHE_PS,
+
+    /* HSIO BLK_CTRL (HSIOMIX) - SM SystemInit RMWs the LFAST IO reg. */
+    FSL_IMX95_BLK_CTRL_HSIOMIX,
+
+    /* GPC (per-domain CPU_CTRL + GLOBAL) - SM power/CPU-mode control. */
+    FSL_IMX95_GPC,
+
+    /* LPUART block (8 instances; all modelled — TYPE_IMX_LPUART) */
+    FSL_IMX95_LPUART1,
+    FSL_IMX95_LPUART2,
+    FSL_IMX95_LPUART3,
+    FSL_IMX95_LPUART4,
+    FSL_IMX95_LPUART5,
+    FSL_IMX95_LPUART6,
+    FSL_IMX95_LPUART7,
+    FSL_IMX95_LPUART8,
+
+    /*
+     * Clock / reset / pinmux infrastructure. SCMI-routed when M33 SM
+     * is up; SPL touches them only before SCMI, kernel never. Logging
+     * stubs.
+     */
+    FSL_IMX95_CCM,
+    FSL_IMX95_ANATOP,
+    FSL_IMX95_IOMUXC,
+    FSL_IMX95_SRC,
+    FSL_IMX95_TRDC_AON,
+
+    /* BLK_CTRL aggregates per power domain. Logging stubs. */
+    FSL_IMX95_BLK_CTRL_S_AONMIX,
+    FSL_IMX95_BLK_CTRL_NS_ANOMIX,
+    FSL_IMX95_BLK_CTRL_WAKEUPMIX,
+    FSL_IMX95_BLK_CTRL_NETCMIX,
+
+    /* System Manager SCMI: mu2 mailbox + sram0 shared-memory buffer */
+    FSL_IMX95_SM_MU,
+    FSL_IMX95_SM_SHMEM,
+
+    /* EdgeLock Secure Enclave mailboxes (elemu0/elemu1 needed for SPL) */
+    FSL_IMX95_ELE_MU,
+    FSL_IMX95_ELE_MU1,
+
+    /* Fuse Shadow Block - read by the SM's DEV_SM_FuseInit at boot. */
+    FSL_IMX95_FSB,
+
+    /* VFCCU (Fault Collection & Control Unit) - SM eMcem init touches it. */
+    FSL_IMX95_VFCCU,
+    /* A1 MU SRAM page holding the A55 CPU wait-semaphore (DEV_SM_CpuInit). */
+    FSL_IMX95_CPU_SEMA,
+    /* CortexA TMPSNS - SM sensor tick reads it (post-init periodic task). */
+    FSL_IMX95_TMPSNS_CA,
+    /* More SM eMcem/fabric init targets: AON_VFCCU, AON_ERMA, NOC_SRAMCTL. */
+    FSL_IMX95_VFCCU_AON,
+    FSL_IMX95_ERMA,
+    FSL_IMX95_NOC_SRAMCTL,
+
+    /*
+     * Other MU instances U-Boot proper probes from DT (mailbox@*).
+     * Not yet modelled; logging stubs are enough for mu_hal_init to
+     * complete cleanly (it reads PAR, writes RCR/TCR=0, reads SR — all
+     * zeros are accepted and the drain loop is skipped).
+     */
+    FSL_IMX95_MU_47320000,
+    FSL_IMX95_MU_47350000,
+    FSL_IMX95_MU_47540000,
+    FSL_IMX95_MU_47550000,
+    FSL_IMX95_MU_47560000,
+    FSL_IMX95_MU_47570000,
+
+    /*
+     * Watchdogs. SPL disables WDG3/4/5 in arch_cpu_init(). WDOG2 is the
+     * M33 SM's own watchdog (unlocked + configured in its reset handler).
+     */
+    FSL_IMX95_WDOG2,
+    FSL_IMX95_WDOG3,
+    FSL_IMX95_WDOG4,
+    FSL_IMX95_WDOG5,
+
+    /* GPIO controllers (1..5). SPL gpio_reset()s 2..5; #1 added for parity. */
+    FSL_IMX95_GPIO1,
+    FSL_IMX95_GPIO2,
+    FSL_IMX95_GPIO3,
+    FSL_IMX95_GPIO4,
+    FSL_IMX95_GPIO5,
+
+    /* ARM SMMU-v3 - SPL's disable_smmuv3() reads CR0; stub returns 0. */
+    FSL_IMX95_SMMU,
+
+    /*
+     * uSDHC (SD / eMMC) controllers. SPL probes them to find a boot
+     * device; stubbed so we can observe access patterns before
+     * promoting to a real fsl-esdhc model.
+     */
+    FSL_IMX95_USDHC1,
+    FSL_IMX95_USDHC2,
+    FSL_IMX95_USDHC3,
+
+    /*
+     * LPI2C controllers. U-Boot proper probes them during the post-
+     * banner dram_init / board_init phase (PMIC over I2C2 is the
+     * critical one - PCA9450 on the EVK). Logging stubs are enough
+     * to let the I2C transfer time out gracefully and DRAM init
+     * fall back to defaults.
+     */
+    FSL_IMX95_LPI2C1,
+    FSL_IMX95_LPI2C2,
+    FSL_IMX95_LPI2C3,
+    FSL_IMX95_LPI2C4,
+    FSL_IMX95_LPI2C5,
+    FSL_IMX95_LPI2C6,
+    FSL_IMX95_LPI2C7,
+    FSL_IMX95_LPI2C8,
+
+    /*
+     * USB PHY + DWC3 controller. U-Boot autoboot's bootcmd touches
+     * these via dwc3_imx8mp_glue_configure (reads at 0x4c1f0000).
+     * Logging stubs prevent the data abort that otherwise resets
+     * the board mid-autoboot.
+     */
+    FSL_IMX95_USB_PHY,
+    FSL_IMX95_USB_DWC3,
+
+    FSL_IMX95_NUM_REGIONS,
+};
+
+/*
+ * IRQ assignments. All values are GIC SPI numbers extracted from
+ * Linux arch/arm64/boot/dts/freescale/imx95.dtsi.
+ */
+enum FslImx95Irqs {
+    FSL_IMX95_LPUART1_IRQ   = 19,
+    FSL_IMX95_LPUART2_IRQ   = 20,
+    FSL_IMX95_LPUART3_IRQ   = 64,
+    FSL_IMX95_LPUART4_IRQ   = 65,
+    FSL_IMX95_LPUART5_IRQ   = 66,
+    FSL_IMX95_LPUART6_IRQ   = 67,
+    FSL_IMX95_LPUART7_IRQ   = 68,
+    FSL_IMX95_LPUART8_IRQ   = 69,
+    FSL_IMX95_WDOG3_IRQ     = 77,
+    /* uSDHC1/2/3 SPIs (dtsi: usdhc1=86, usdhc2=87, usdhc3=191). */
+    FSL_IMX95_USDHC1_IRQ    = 86,
+    FSL_IMX95_USDHC2_IRQ    = 87,
+    FSL_IMX95_USDHC3_IRQ    = 191,
+    /* system counter compare-match (dtsi: timer@44290000). */
+    FSL_IMX95_SYSCNT_IRQ    = 72,
+    /* mu2 is the SCMI mailbox to the M33 SM (dtsi:1655). */
+    FSL_IMX95_SM_MU_IRQ     = 226,
+};
+
+/*
+ * M33 NVIC external interrupt number for the MU2 B-side (MU2_B_IRQn in the
+ * SM's MIMX95 device header) - the IRQ the SM enables to receive A55 SCMI
+ * doorbells on its side of MU2.
+ */
+#define FSL_IMX95_SM_MU_B_M33_IRQ   227
+
+/*
+ * MUI_A5 (the M7<->SM SCMI channel) interrupt numbers, from the SM's
+ * MIMX95 device header (MIMX95_COMMON.h IRQn enum):
+ *   - MU5_A_IRQn = 205: the M7-side IRQ, raised on the M7's NVIC when the
+ *     SM rings its doorbell back (m7_sm_mu @0x44610000 -> M7).
+ *   - MU5_B_IRQn = 232: the SM-side IRQ, raised on the M33's NVIC when the
+ *     M7 rings its doorbell (m7_sm_mu_b @0x44620000 -> M33). The SM enables
+ *     this in MB_MU_Init for MU9 and dispatches SCMI from its handler.
+ */
+#define FSL_IMX95_M7_SM_MU_M7_IRQ   205
+#define FSL_IMX95_M7_SM_MU_B_M33_IRQ 232
+
+/*
+ * MU7 (A55<->M7 rpmsg) interrupts. MUA is the Linux side: GIC SPI 234
+ * (= MU7_A_IRQn, matching the dtsi mu7 node). MUB is the M7 side: M7 NVIC
+ * 207 (= MU7_B_IRQn in MIMX95_COMMON.h).
+ */
+#define FSL_IMX95_MU7_A_IRQ         234
+#define FSL_IMX95_MU7_B_M7_IRQ      207
+
+/*
+ * M33 NVIC inputs for the Cortex-M7 fault/reset sources the SRC routes to
+ * the System Manager (MIMX95_COMMON.h IRQn enum). When the M7 asserts
+ * SYSRESETREQ (or locks up) the SM takes the fault and, for an LM with
+ * reaction=lm_reset (the M7 LM on mx95evk), cold-resets the M7 LM via the
+ * SRC M7 mix-slice. We wire the M7's NVIC SYSRESETREQ gpio-out to
+ * CM7_SYSRESETREQ_IRQn so the real SM firmware drives the recovery.
+ */
+#define FSL_IMX95_M7_SYSRESETREQ_M33_IRQ 167
+#define FSL_IMX95_M7_LOCKUP_M33_IRQ      168
+
+#endif /* FSL_IMX95_H */
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 15/16] hw/arm: add i.MX 95 19x19 EVK board
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (13 preceding siblings ...)
  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 ` Kyle Fox
  2026-08-20  2:48 ` [PATCH 16/16] docs, MAINTAINERS, tests/functional: add i.MX 95 EVK Kyle Fox
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel; +Cc: Kyle Fox, Paolo Bonzini, Peter Maydell, open list:ARM TCG CPUs

The i.MX 95 19x19 EVK board (machine type imx95-19x19-evk).
Instantiates the SoC, sets up the LPDDR5 DRAM, and provides the direct
-kernel/-dtb/-initrd Linux boot path alongside the System Manager
firmware loaded on the M33. This is the user-facing machine type.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 hw/arm/Kconfig     |   7 +++
 hw/arm/imx95-evk.c | 136 +++++++++++++++++++++++++++++++++++++++++++++
 hw/arm/meson.build |   1 +
 3 files changed, 144 insertions(+)
 create mode 100644 hw/arm/imx95-evk.c

diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 7f1c0723ec0..d93808a48ee 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -764,3 +764,10 @@ config FSL_IMX95
     select IMX_LPI2C
     select SDHCI
     select UNIMP
+
+config FSL_IMX95_EVK
+    bool
+    default y
+    depends on AARCH64
+    depends on TCG
+    select FSL_IMX95
diff --git a/hw/arm/imx95-evk.c b/hw/arm/imx95-evk.c
new file mode 100644
index 00000000000..470ca112376
--- /dev/null
+++ b/hw/arm/imx95-evk.c
@@ -0,0 +1,136 @@
+/*
+ * NXP i.MX 95 19x19 Evaluation Kit (LPDDR5) - QEMU machine
+ *
+ * Modeled on hw/arm/imx8mp-evk.c by Bernhard Beschow
+ *
+ * Copyright (c) 2026, Kyle Fox
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Instantiates the SoC, attaches DDR, and hands control to
+ * arm_load_kernel() so -kernel works.
+ */
+
+#include "qemu/osdep.h"
+#include "system/address-spaces.h"
+#include "hw/arm/boot.h"
+#include "hw/arm/fsl-imx95.h"
+#include "hw/arm/machines-qom.h"
+#include "hw/core/boards.h"
+#include "hw/core/qdev-properties.h"
+#include "system/kvm.h"
+#include "system/qtest.h"
+#include "qemu/error-report.h"
+#include "qapi/error.h"
+
+static void imx95_evk_init(MachineState *machine)
+{
+    static struct arm_boot_info boot_info;
+    FslImx95State *s;
+
+    if (kvm_enabled()) {
+        error_report("The imx95-19x19-evk machine requires TCG: it emulates "
+                     "Cortex-M33 and Cortex-M7 cores that KVM cannot run");
+        exit(1);
+    }
+
+    if (machine->ram_size > FSL_IMX95_RAM_SIZE_MAX) {
+        error_report("RAM size " RAM_ADDR_FMT
+                     " above max supported (0x%" PRIx64 ")",
+                     machine->ram_size, (uint64_t)FSL_IMX95_RAM_SIZE_MAX);
+        exit(1);
+    }
+
+    /*
+     * The board presets the DTB at RAM_START + 128 MiB and the initrd at
+     * RAM_START + 256 MiB (see below), so require enough RAM to hold them.
+     */
+    if (machine->ram_size < FSL_IMX95_RAM_SIZE_MIN) {
+        error_report("RAM size " RAM_ADDR_FMT
+                     " below min supported (0x%" PRIx64 ")",
+                     machine->ram_size, (uint64_t)FSL_IMX95_RAM_SIZE_MIN);
+        exit(1);
+    }
+
+    boot_info = (struct arm_boot_info) {
+        .loader_start = FSL_IMX95_RAM_START,
+        .board_id     = -1,
+        .ram_size     = machine->ram_size,
+        .psci_conduit = QEMU_PSCI_CONDUIT_SMC,
+        /*
+         * The default arm_load_kernel() heuristic lands the initrd (and the
+         * DTB right after it) at loader_start + 128 MiB == 0x88000000. That
+         * is exactly where the NXP BSP device tree places the Cortex-M7
+         * remoteproc carveout (vdev vrings + rsc-table @0x88220000). The
+         * collision makes Linux reserve those pages for the initrd/FDT, so
+         * the later reserved-memory no-map pass fails (-EBUSY) and imx-rproc
+         * cannot ioremap the rsc-table (probe fails with -ENOMEM). Force the
+         * initrd/DTB up to 0x90000000, in the free gap above the M7 carveout
+         * and below the GPU/VPU carveouts at 0xa0000000.
+         */
+        .initrd_start = FSL_IMX95_RAM_START + 256 * MiB,
+    };
+
+    s = FSL_IMX95(object_new(TYPE_FSL_IMX95));
+    object_property_add_child(OBJECT(machine), "soc", OBJECT(s));
+    sysbus_realize_and_unref(SYS_BUS_DEVICE(s), &error_fatal);
+
+    memory_region_add_subregion(get_system_memory(), FSL_IMX95_RAM_START,
+                                machine->ram);
+
+    if (!qtest_enabled()) {
+        arm_load_kernel(&s->cpu[0], machine, &boot_info);
+
+        /*
+         * arm_load_kernel() registers its boot reset hook on every CPU in
+         * the system and treats all non-boot cores as A-profile PSCI
+         * secondaries. That is correct for the five A55 secondaries, but
+         * the Cortex-M33 SM core and the Cortex-M7 RT core are not part of
+         * the A55 boot flow - each boots from its own ITCM vector table.
+         * Detach them from the A-profile boot machinery so their reset just
+         * runs the normal M-profile vector-table reset. Whether each then
+         * actually runs is decided by the SoC's M33/M7 reset hooks (only if
+         * firmware was loaded into the respective ITCM).
+         */
+        if (s->m33.cpu) {
+            s->m33.cpu->env.boot_info = NULL;
+        }
+        if (s->m7.cpu) {
+            s->m7.cpu->env.boot_info = NULL;
+        }
+    }
+}
+
+static const char *imx95_evk_get_default_cpu_type(const MachineState *ms)
+{
+    return ARM_CPU_TYPE_NAME("cortex-a55");
+}
+
+static const char * const imx95_evk_valid_cpu_types[] = {
+    ARM_CPU_TYPE_NAME("cortex-a55"),
+    NULL
+};
+
+static void imx95_19x19_evk_machine_init(MachineClass *mc)
+{
+    mc->desc                  = "NXP i.MX 95 19x19 EVK (LPDDR5)";
+    mc->init                  = imx95_evk_init;
+    /*
+     * Total vCPUs = 6 A55 + 1 Cortex-M33 System Manager core + 1 Cortex-M7
+     * real-time core that the SoC always instantiates. TCG sizes its
+     * per-CPU context table from the resolved smp.max_cpus, which defaults
+     * to smp.cpus when -smp is not given - so both the default and the
+     * max must include the M33 and the M7, otherwise the 8th CPU's
+     * tcg_register_thread() asserts. The A55 cluster size is fixed in
+     * the SoC regardless of -smp; this count is really "A55 cluster +
+     * SM core + RT core".
+     */
+    mc->default_cpus          = FSL_IMX95_NUM_A55_CPUS + 2;
+    mc->max_cpus              = FSL_IMX95_NUM_A55_CPUS + 2;
+    mc->default_ram_id        = "imx95-19x19-evk.ram";
+    mc->default_ram_size      = 8 * GiB;   /* 19x19 EVK has 8 GiB LPDDR5 */
+    mc->get_default_cpu_type  = imx95_evk_get_default_cpu_type;
+    mc->valid_cpu_types       = imx95_evk_valid_cpu_types;
+}
+
+DEFINE_MACHINE_AARCH64("imx95-19x19-evk", imx95_19x19_evk_machine_init)
diff --git a/hw/arm/meson.build b/hw/arm/meson.build
index 96fef4c5b5f..cdb61fa1612 100644
--- a/hw/arm/meson.build
+++ b/hw/arm/meson.build
@@ -118,5 +118,6 @@ arm_common_ss.add(when: ['CONFIG_AXIADO_EVK', 'TARGET_AARCH64'], if_true: files(
 
 arm_common_ss.add(files('boot.c'))
 arm_common_ss.add(when: 'CONFIG_FSL_IMX95', if_true: files('fsl-imx95.c'))
+arm_common_ss.add(when: 'CONFIG_FSL_IMX95_EVK', if_true: files('imx95-evk.c'))
 
 hw_common_arch += {'arm': arm_common_ss}
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH 16/16] docs, MAINTAINERS, tests/functional: add i.MX 95 EVK
  2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
                   ` (14 preceding siblings ...)
  2026-08-20  2:48 ` [PATCH 15/16] hw/arm: add i.MX 95 19x19 EVK board Kyle Fox
@ 2026-08-20  2:48 ` Kyle Fox
  15 siblings, 0 replies; 17+ messages in thread
From: Kyle Fox @ 2026-08-20  2:48 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kyle Fox, Pierrick Bouvier, Peter Maydell,
	open list:MCIMX95-19X19-EVK...

Add the machine documentation (docs/system/arm/imx95-evk.rst, linked
into the target-arm manual), the MAINTAINERS entry, and an
environment-gated functional test that boots Linux to userspace on the
real System Manager firmware.

Signed-off-by: Kyle Fox <kylefoxaustin.github@gmail.com>
---
 MAINTAINERS                                |  18 ++
 docs/system/arm/imx95-evk.rst              | 242 +++++++++++++++++++++
 docs/system/target-arm.rst                 |   1 +
 tests/functional/aarch64/meson.build       |   2 +
 tests/functional/aarch64/test_imx95_evk.py | 157 +++++++++++++
 5 files changed, 420 insertions(+)
 create mode 100644 docs/system/arm/imx95-evk.rst
 create mode 100644 tests/functional/aarch64/test_imx95_evk.py

diff --git a/MAINTAINERS b/MAINTAINERS
index b51f5c3e602..f66082eadc4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -956,6 +956,24 @@ F: docs/system/arm/imx8m.rst
 F: tests/functional/aarch64/test_imx8mp_evk.py
 F: tests/qtest/rs5c372-test.c
 
+MCIMX95-19X19-EVK / i.MX95
+M: Kyle Fox <kylefoxaustin.github@gmail.com>
+L: qemu-arm@nongnu.org
+S: Maintained
+F: hw/arm/imx95-evk.c
+F: hw/arm/fsl-imx95.c
+F: hw/misc/imx95_*.c
+F: hw/timer/imx95_sysctr.c
+F: hw/char/imx_lpuart.c
+F: hw/i2c/imx_lpi2c.c
+F: hw/misc/imx_mu.c
+F: include/hw/arm/fsl-imx95.h
+F: include/hw/char/imx_lpuart.h
+F: include/hw/misc/imx_mu.h
+F: include/hw/misc/imx95_*.h
+F: docs/system/arm/imx95-evk.rst
+F: tests/functional/aarch64/test_imx95_evk.py
+
 MPS2 / MPS3
 M: Peter Maydell <peter.maydell@linaro.org>
 L: qemu-arm@nongnu.org
diff --git a/docs/system/arm/imx95-evk.rst b/docs/system/arm/imx95-evk.rst
new file mode 100644
index 00000000000..d67cd2de16f
--- /dev/null
+++ b/docs/system/arm/imx95-evk.rst
@@ -0,0 +1,242 @@
+NXP i.MX 95 19x19 Evaluation Kit (``imx95-19x19-evk``)
+======================================================
+
+The ``imx95-19x19-evk`` machine models the NXP i.MX 95 19x19 LPDDR5
+Evaluation Kit. The i.MX 95 is a heterogeneous SoC; the machine
+correspondingly emulates a fixed topology of **6 Cortex-A55 application
+cores, 1 Cortex-M33** that runs the NXP **System Manager** (SM)
+firmware, **and 1 Cortex-M7** real-time core that the System Manager
+boots, manages and fault-recovers (see ``Cortex-M7 real-time core``
+below).
+
+A defining property of this SoC is that the System Manager firmware is
+the only SCMI provider: Linux's clock, perf-domain, power, sensor, and
+reset operations are all served by the SM running on the M33 over a
+shared mailbox (MU2), not by software inside QEMU. Booting Linux to
+userspace on this machine therefore requires the SM firmware as an
+input — see ``Required artifacts`` below.
+
+Supported devices
+-----------------
+
+The ``imx95-19x19-evk`` machine implements the following devices:
+
+ * 6 Cortex-A55 application cores
+ * 1 Cortex-M33 (System Manager core; runs NXP SM firmware)
+ * 1 Cortex-M7 (real-time core; SM-booted and SM-managed)
+ * Generic Interrupt Controller (GICv3, GIC-600 layout)
+ * LPUART serial controllers (LPUART1 = Linux console; LPUART2 = SM
+   debug monitor; LPUART3 = M7 console)
+ * NXP Messaging Unit v2 (MU2) with the dual-aperture A55-MUA <->
+   M33-MUB cross-connect that carries SCMI; plus MUI_A5 (M7 <-> SM
+   SCMI) and MU7 (A55 <-> M7 rpmsg) cross-connects
+ * BLK_CTRL_S_AONMIX M7 CPU-WAIT gate (the SM's M7 hold/run control)
+ * System Counter (24 MHz live up-counter with compare-match IRQ; the
+   Linux broadcast clockevent on this machine)
+ * uSDHC storage controller (SDMA, used by U-Boot SD boot)
+ * LPI2C master (polled; used by the SM for PMIC / IO
+   expander bring-up)
+ * Functional models for the SM bring-up path: ANATOP (PLL lock and
+   DFS), SRC (system reset controller), GPC (general power
+   controller), ELE responder, FSB (fuse shadow block), eMcem, PF53
+   and PCAL6408A on LPI2C
+ * Logging stubs for blocks that are not on the boot critical path or
+   that have no behaviour to emulate (NETC, USB host, PCIe link
+   training, GPU/VPU/NPU MMIO, DPU command sequencer, watchdog, etc.)
+
+The Mali-G310 (GPU), Amphion (VPU), Neutron (NPU), and DPU (display
+controller) are intentionally probe-time stubs only; no rendering,
+codec, inference, or scanout occurs. See ``Caveats`` below.
+
+Boot options
+------------
+
+The ``imx95-19x19-evk`` machine can start a Linux kernel directly
+using ``-kernel``, but **only together with the System Manager firmware
+loaded onto the Cortex-M33** via ``-device loader,...,cpu-num=6``.
+Booting Linux without the SM firmware leaves the M33 halted, nothing
+answers SCMI, and Linux will hang at ``arm-scmi`` probe.
+
+Required artifacts
+''''''''''''''''''
+
+Four artifacts are needed; all are built from NXP source trees:
+
+* **System Manager firmware** (``m33_image.elf``). Built from the NXP
+  ``imx-sm`` source tree, with the ``mx95evk`` board configuration:
+
+  .. code-block:: bash
+
+     git clone https://github.com/nxp-imx/imx-sm.git
+     cd imx-sm
+     make config=mx95evk
+     # Result: build/mx95evk/m33_image.elf
+
+* **aarch64 Linux kernel** (``Image``). Either the NXP BSP kernel
+  (e.g. ``linux-imx`` 6.12.49) built with the NXP defconfig, or a
+  mainline kernel built with the standard arm64 defconfig — both have
+  been tested.
+
+* **Device tree blob** (``imx95-19x19-evk.dtb``). Produced by the same
+  kernel build under
+  ``arch/arm64/boot/dts/freescale/imx95-19x19-evk.dtb``.
+
+* **Initramfs** (``*.cpio.gz``) containing an ``/init``. Any aarch64
+  initramfs will do; a small static BusyBox initramfs (for example one
+  built from a prebuilt static-aarch64 BusyBox, no cross compiler
+  required) whose ``/init`` prints a recognisable marker is enough.
+
+Direct Linux Kernel Boot
+''''''''''''''''''''''''
+
+Once the four artifacts are built, boot Linux to userspace as
+follows:
+
+.. code-block:: bash
+
+  $ qemu-system-aarch64 -M imx95-19x19-evk -m 2G -display none \
+      -kernel Image \
+      -dtb imx95-19x19-evk.dtb \
+      -initrd initramfs.cpio.gz \
+      -device loader,file=m33_image.elf,cpu-num=6 \
+      -append "earlycon=lpuart32,mmio32,0x44380010 \
+               console=ttyLP0,115200 cpuidle.off=1 rdinit=/init" \
+      -serial mon:stdio -serial null
+
+On a successful boot, Linux negotiates SCMI with the real SM
+(``SCMI Protocol v2.1 'NXP:IMX' Firmware version 0x333``), brings up
+all 6 A55 cores via PSCI, mounts the initramfs, and starts ``/init``.
+
+LPUART1 (``ttyLP0``) is Linux's console; it is wired to the first
+``-serial`` backend above. LPUART2 carries the System Manager's own
+debug monitor; it is wired to the second ``-serial`` backend
+(``null`` above), and can be redirected to a separate file or pty if
+desired.
+
+System Manager standalone
+'''''''''''''''''''''''''
+
+To exercise the SM in isolation (no Linux), boot it with LPUART2 as
+the only console and stop at the SM debug monitor prompt:
+
+.. code-block:: bash
+
+  $ qemu-system-aarch64 -M imx95-19x19-evk -m 2G -display none \
+      -device loader,file=m33_image.elf,cpu-num=6 \
+      -serial null -serial mon:stdio
+
+Cortex-M7 real-time core
+''''''''''''''''''''''''
+
+The i.MX 95 System Manager owns the Cortex-M7's whole lifecycle. Unlike a
+generic remoteproc model where Linux starts the M-core, on this SoC the SM
+boots the M7 **before** the A-cluster and the AP logical machine does not own
+the M7; Linux's ``imx_rproc`` driver therefore only *attaches* to the
+already-running M7 (kernel log: ``imx-rproc imx95-cm7: lmm(1) not under Linux
+Control``).
+
+Load M7 firmware onto the M7 (CPU index 7) with a second loader device:
+
+.. code-block:: bash
+
+  $ qemu-system-aarch64 -M imx95-19x19-evk -m 2G -display none \
+      -device loader,file=m33_image.elf,cpu-num=6 \
+      -device loader,file=cm7_firmware.elf,cpu-num=7 \
+      ... (kernel/dtb/initrd as above for a full Linux + M7 boot)
+
+When the SM firmware is present it boots the M7 itself: the machine fabricates
+the boot-ROM image handover the SM reads at startup to learn it owns the M7
+(real hardware gets this from the boot ROM; the direct ``-device loader`` path
+bypasses it), so the SM runs its full ``DEV_SM_CpuStart`` for the M7. With no SM
+loaded, the M7 is released at reset if its reset vector was
+populated. The M7's console is LPUART3.
+
+Two SM-managed behaviours are modelled:
+
+* **rpmsg.** Once Linux has attached, the stock NXP ``imx_rpmsg_pingpong``
+  kernel module exchanges 100 messages with an M7 rpmsg-lite client over the
+  MU7 cross-connect (doorbell kicks) and shared vrings (payload) in the M7 DRAM
+  carveout, printing ``goodbye!`` after the final round-trip. The
+  ``Cortex-M7 rpmsg ping/pong`` functional test below exercises this end to
+  end.
+
+* **Fault recovery.** The M7 logical machine is configured ``reaction=lm_reset``,
+  so an M7 fault triggers an SM-driven cold reset of the M7. When the M7 asserts
+  ``SYSRESETREQ`` it is routed to the SM as ``CM7_SYSRESETREQ_IRQn``; the SM runs
+  ``LMM_SystemLmReset`` (stop then start the M7), printing ``Reset LM 1`` on its
+  debug monitor. The ``Cortex-M7 fault recovery`` functional test below
+  exercises this end to end.
+
+Caveats
+-------
+
+* **Deep cpuidle requires** ``cpuidle.off=1`` **on the kernel command
+  line.** Linux's ``cpu-pd-wait`` idle state quiesces the per-CPU GIC
+  CPU interface before WFI; QEMU's GICv3 model has no WakeRequest
+  path, so a halted CPU is not woken by a pending interrupt while its
+  interface is disabled. The kernel-command-line workaround is the
+  recommendation; the deeper fix needs upstream QEMU work in
+  ``hw/intc/arm_gicv3_cpuif.c``.
+
+* **No hardware-accelerated rendering, codec, or inference.** Mali-G310,
+  Amphion VPU, and Neutron NPU are logging stubs. The Mali kernel
+  driver in particular fails probe with ``-22`` at the product-ID
+  check (``Unknown GPU Product ID 0``), so no Mali Vulkan ICD is
+  installed and ``vulkaninfo`` reports
+  ``ERROR_INCOMPATIBLE_DRIVER``. The DPU stub does register
+  ``/dev/dri/card0``, but it has no connectors and no scanout, so
+  ``kmscube`` fails with ``no connected connector``.
+
+* **No networking interfaces.** The NETC blocks are stubs; only the
+  loopback interface ``lo`` appears in the guest.
+
+* **No Linux-visible block storage.** Linux's
+  ``sdhci-esdhc-imx`` driver defers probe on an unmet dependency, so
+  ``/dev/mmcblk*`` is empty even with an SD-card image attached via
+  ``-drive ...,if=sd``. (The uSDHC model itself works — U-Boot SPL
+  boots from SD on this machine — but the Linux MMC stack does not
+  complete probe.)
+
+* **icount is not the default boot mode.** ``-icount shift=auto``
+  delays interrupt/timer delivery to idle CPUs on this heterogeneous
+  A55+M33 machine, where both sides can WFI between SCMI/MU
+  round-trips, inflating the boot ~9x. Enable icount only when
+  debugging a race; do not use it for normal boots.
+
+Functional test
+---------------
+
+``tests/functional/aarch64/test_imx95_evk.py`` has three env-gated cases; each
+takes its artifacts from environment variables and skips cleanly (naming the
+unset variables) if any are absent, since the NXP-source-built artifacts are
+not redistributable:
+
+* **Linux to userspace on the real SM** — needs ``QEMU_TEST_IMX95_KERNEL``,
+  ``QEMU_TEST_IMX95_DTB``, ``QEMU_TEST_IMX95_INITRD`` and
+  ``QEMU_TEST_IMX95_SM_FW``. Boots Linux and checks for the SCMI handshake
+  followed by an init-printed userspace banner (~14 s on a development host).
+
+* **Cortex-M7 rpmsg ping/pong** — needs ``QEMU_TEST_IMX95_KERNEL``,
+  ``QEMU_TEST_IMX95_DTB``, ``QEMU_TEST_IMX95_SM_FW``,
+  ``QEMU_TEST_IMX95_M7_RPMSG_FW`` (the M7 rpmsg-lite pingpong firmware) and
+  ``QEMU_TEST_IMX95_RPMSG_INITRD`` (an initramfs bundling the matching
+  ``imx_rpmsg_pingpong.ko`` and an ``/init`` that loads it). Boots the full
+  A55 + SM + M7 stack and checks for the SCMI handshake followed by the
+  module's ``goodbye!`` line — printed only after the 100-message exchange
+  with the M7 over MU7 completes. The M7 firmware is NXP's
+  ``rpmsg_lite_pingpong_rtos_linux_remote`` multicore example; the NXP BSP
+  ships it prebuilt in the target rootfs at
+  ``/usr/lib/firmware/imx95-19x19-evk_m7_TCM_rpmsg_lite_pingpong_rtos_linux_remote.{elf,bin}``,
+  or it can be built from the MCUXpresso SDK multicore examples.
+
+* **Cortex-M7 fault recovery** — needs ``QEMU_TEST_IMX95_SM_FW`` and
+  ``QEMU_TEST_IMX95_M7_FW`` (an M7 fixture that asserts ``SYSRESETREQ`` on its
+  first boot). Boots
+  the SM + M7 alone and checks the SM debug monitor for ``Reset LM 1`` — the
+  SM taking the M7 fault and cold-resetting the M7 logical machine.
+
+.. code-block:: bash
+
+  $ QEMU_TEST_IMX95_SM_FW=/path/to/m33_image.elf \
+    QEMU_TEST_IMX95_M7_FW=/path/to/cm7_fault.elf \
+    meson test -C build --suite thorough func-aarch64-imx95_evk
diff --git a/docs/system/target-arm.rst b/docs/system/target-arm.rst
index e34492402f0..514d89e2d59 100644
--- a/docs/system/target-arm.rst
+++ b/docs/system/target-arm.rst
@@ -95,6 +95,7 @@ Board-specific documentation
    arm/mcimx6ul-evk
    arm/mcimx7d-sabre
    arm/imx8m
+   arm/imx95-evk
    arm/orangepi
    arm/raspi
    arm/collie
diff --git a/tests/functional/aarch64/meson.build b/tests/functional/aarch64/meson.build
index e81afd6c39f..972e896c149 100644
--- a/tests/functional/aarch64/meson.build
+++ b/tests/functional/aarch64/meson.build
@@ -7,6 +7,7 @@ test_aarch64_timeouts = {
   'device_passthrough' : 720,
   'imx8mm_evk' : 240,
   'imx8mp_evk' : 240,
+  'imx95_evk' : 240,
   'raspi4' : 480,
   'reverse_debug' : 180,
   'rme_virt' : 1200,
@@ -33,6 +34,7 @@ tests_aarch64_system_thorough = [
   'hotplug_pci',
   'imx8mm_evk',
   'imx8mp_evk',
+  'imx95_evk',
   'kvm',
   'multiprocess',
   'raspi3',
diff --git a/tests/functional/aarch64/test_imx95_evk.py b/tests/functional/aarch64/test_imx95_evk.py
new file mode 100644
index 00000000000..69ac27f4927
--- /dev/null
+++ b/tests/functional/aarch64/test_imx95_evk.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+#
+# Functional tests for imx95-19x19-evk: boot Linux on the 6 Cortex-A55s
+# with the real NXP System Manager firmware on the Cortex-M33, and exercise
+# the System-Manager-orchestrated Cortex-M7 (boot + fault recovery).
+#
+# The boot artifacts (aarch64 Linux Image, imx95-19x19-evk.dtb, an initramfs
+# cpio.gz, the NXP System Manager firmware m33_image.elf, and the M7 fault
+# fixture cm7_fault.elf) are built from NXP source trees / the in-tree M7
+# firmware and are not redistributable as QEMU test assets, so the tests take
+# them from environment variables and skip cleanly if any are absent. See
+# docs/system/arm/imx95-evk.rst for the build recipes.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+
+from qemu_test import LinuxKernelTest
+
+
+class Imx95EvkMachine(LinuxKernelTest):
+    """
+    Boot stock NXP BSP Linux on imx95-19x19-evk together with the real NXP
+    System Manager (SM) firmware on the emulated Cortex-M33, and exercise the
+    SM-orchestrated Cortex-M7. The SM is the only SCMI provider on this machine
+    (there is no built-in software SCMI server), so a successful boot validates
+    the M33 <-> A55 mailbox cross-connect as well as the Linux boot path; the
+    M7 test validates the SM's ownership of the M7's lifecycle.
+    """
+
+    def _require(self, *envvars):
+        """Return a dict of resolved artifact paths, or skip if any is unset."""
+        artifacts = {}
+        missing = []
+        for envvar in envvars:
+            path = os.environ.get(envvar)
+            if path and os.path.exists(path):
+                artifacts[envvar] = path
+            else:
+                missing.append(envvar)
+        if missing:
+            self.skipTest(
+                'requires NXP-source-built boot artifacts (see '
+                'docs/system/arm/imx95-evk.rst for build recipes); set '
+                + ', '.join(missing))
+        return artifacts
+
+    def test_aarch64_imx95_evk_real_sm(self):
+        """Linux boots to userspace on 6 A55s, SCMI served by the real SM."""
+        art = self._require('QEMU_TEST_IMX95_KERNEL', 'QEMU_TEST_IMX95_DTB',
+                            'QEMU_TEST_IMX95_INITRD', 'QEMU_TEST_IMX95_SM_FW')
+        self.require_accelerator("tcg")
+        self.set_machine('imx95-19x19-evk')
+        # LPUART1 (ttyLP0, index 0) is the Linux console; LPUART2 carries
+        # the SM debug monitor and is left null here.
+        self.vm.set_console(console_index=0)
+        # The machine has a fixed heterogeneous topology of 6 Cortex-A55 +
+        # 1 Cortex-M33 (SM) + 1 Cortex-M7 (RT), so do not pass -smp; the
+        # machine's default of 8 is the only accepted value.
+        self.vm.add_args(
+            '-m', '2G',
+            '-kernel', art['QEMU_TEST_IMX95_KERNEL'],
+            '-dtb',    art['QEMU_TEST_IMX95_DTB'],
+            '-initrd', art['QEMU_TEST_IMX95_INITRD'],
+            '-device',
+            'loader,file=%s,cpu-num=6' % art['QEMU_TEST_IMX95_SM_FW'],
+            '-append',
+            'earlycon=lpuart32,mmio32,0x44380010 '
+            'console=ttyLP0,115200 cpuidle.off=1 rdinit=/init',
+        )
+        self.vm.launch()
+        # Load-bearing signal #1: SCMI handshake against the real SM.
+        # Confirms the dual-aperture MU2 cross-connect (A55 MUA <-> M33 MUB)
+        # is working end-to-end and the SM answered protocol negotiation.
+        self.wait_for_console_pattern(
+            "SCMI Protocol v2.1 'NXP:IMX' Firmware version 0x333")
+        # Load-bearing signal #2: PID 1 ran. The string is printed by the
+        # /init in the supplied initramfs (QEMU_TEST_IMX95_INITRD).
+        self.wait_for_console_pattern('=== imx95 busybox userspace ===')
+
+    def test_aarch64_imx95_evk_m7_rpmsg_pingpong(self):
+        """Linux + the SM-booted M7 run NXP's rpmsg_lite ping/pong over MU7.
+
+        The full A55 + M33(SM) + M7 stack: the SM boots the M7 running NXP's
+        rpmsg_lite pingpong firmware, Linux's imx_rproc attaches to the
+        SM-managed core, and the stock imx_rpmsg_pingpong module exchanges
+        100 messages with the M7 - kicks over the modelled MU7 cross-connect,
+        payload in the shared vrings. The module prints "goodbye!" after the
+        final round-trip. The initramfs must bundle the matching
+        imx_rpmsg_pingpong.ko and an /init that loads it (see
+        docs/system/arm/imx95-evk.rst).
+        """
+        art = self._require('QEMU_TEST_IMX95_KERNEL', 'QEMU_TEST_IMX95_DTB',
+                            'QEMU_TEST_IMX95_RPMSG_INITRD',
+                            'QEMU_TEST_IMX95_SM_FW',
+                            'QEMU_TEST_IMX95_M7_RPMSG_FW')
+        self.require_accelerator("tcg")
+        self.set_machine('imx95-19x19-evk')
+        self.vm.set_console(console_index=0)
+        self.vm.add_args(
+            '-m', '2G',
+            '-kernel', art['QEMU_TEST_IMX95_KERNEL'],
+            '-dtb',    art['QEMU_TEST_IMX95_DTB'],
+            '-initrd', art['QEMU_TEST_IMX95_RPMSG_INITRD'],
+            '-device',
+            'loader,file=%s,cpu-num=6' % art['QEMU_TEST_IMX95_SM_FW'],
+            # The rpmsg pingpong firmware is a raw M7 TCM image; load it at the
+            # M7 TCM alias the remoteproc carveout points at (0x203c0000),
+            # rather than as an ELF with cpu-num.
+            '-device', 'loader,file=%s,addr=0x203c0000,force-raw=on'
+                       % art['QEMU_TEST_IMX95_M7_RPMSG_FW'],
+            '-append',
+            'earlycon=lpuart32,mmio32,0x44380010 '
+            'console=ttyLP0,115200 cpuidle.off=1 rdinit=/init',
+        )
+        self.vm.launch()
+        # SCMI handshake against the real SM (as in the boot test) - the M7
+        # cannot come up without the SM, which is the only SCMI provider.
+        self.wait_for_console_pattern(
+            "SCMI Protocol v2.1 'NXP:IMX' Firmware version 0x333")
+        # The 100-message ping/pong completed: imx_rpmsg_pingpong prints
+        # "goodbye!" after the final round-trip with the M7 rpmsg-lite client.
+        self.wait_for_console_pattern('goodbye!')
+
+    def test_aarch64_imx95_evk_m7_fault_recovery(self):
+        """The real SM boots the M7, takes its fault, and cold-resets it.
+
+        No A55/Linux: the SM (M33) and the M7 fault fixture run alone. The
+        fixture asserts SYSRESETREQ on its first boot; the SM routes that to
+        CM7_SYSRESETREQ_IRQn, runs lm_reset on the M7 logical machine, and
+        prints the recovery on its debug monitor (LPUART2). Observing that
+        line confirms the whole SM-managed M7 lifecycle - the SM had to learn
+        it owns the M7 (from the machine's fabricated boot-ROM handover),
+        enable the fault IRQ, and drive the CpuStop/CpuStart cold cycle.
+        """
+        art = self._require('QEMU_TEST_IMX95_SM_FW', 'QEMU_TEST_IMX95_M7_FW')
+        self.require_accelerator("tcg")
+        self.set_machine('imx95-19x19-evk')
+        # Console on LPUART2 (index 1) = the SM debug monitor; LPUART1 (the
+        # A55 console) is left null since no Linux runs here.
+        self.vm.set_console(console_index=1)
+        self.vm.add_args(
+            '-m', '2G',
+            '-device',
+            'loader,file=%s,cpu-num=6' % art['QEMU_TEST_IMX95_SM_FW'],
+            '-device',
+            'loader,file=%s,cpu-num=7' % art['QEMU_TEST_IMX95_M7_FW'],
+        )
+        self.vm.launch()
+        # The SM comes up...
+        self.wait_for_console_pattern('SM Debug Monitor')
+        # ...then the M7 faults and the SM cold-resets the M7 logical machine.
+        self.wait_for_console_pattern('Reset LM 1')
+
+
+if __name__ == '__main__':
+    LinuxKernelTest.main()
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 17+ messages in thread

end of thread, other threads:[~2026-08-20  2:52 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-20  2:48 [PATCH 00/16] hw/arm: add the NXP i.MX 95 EVK machine Kyle Fox
2026-08-20  2:48 ` [PATCH 01/16] hw/sd/sdhci: add i.MX uSDHC SDCLK_AUTO_GATE and NO_SDMA_BOUNDARY quirks Kyle Fox
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 ` [PATCH 09/16] hw/timer: add i.MX 95 system counter Kyle Fox
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

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.