Real-board ZCU102 DTBs describe the UART, SDHCI and GQSPI nodes
in terms of ZynqMP PM firmware providers for clocks, resets, power
domains and pin control. QEMU does not model those runtime firmware
providers, so using such a DTB with -kernel currently requires users
to edit the DTB before Linux can probe the boot-critical devices.

Add a board DTB fix-up for direct kernel boot that removes those
provider-only properties from the boot-critical nodes and rewrites
their clocks to fixed reference clocks already present in the DTB.
This keeps the user command line using the Buildroot-generated DTB
while limiting the compatibility adjustment to devices modeled by
QEMU.

Document the direct boot flow, the SD and QSPI image layout details,
and the remaining firmware and real-board peripheral limitations.

Signed-off-by: Bin Meng <[email protected]>

---

Changes in v2:
- Restrict DTB fixups only when direct Linux kernel boot

 docs/system/arm/xlnx-zcu102.rst | 139 +++++++++++++++++++++++++++++++-
 hw/arm/xlnx-zcu102.c            | 112 +++++++++++++++++++++++++
 2 files changed, 250 insertions(+), 1 deletion(-)

diff --git a/docs/system/arm/xlnx-zcu102.rst b/docs/system/arm/xlnx-zcu102.rst
index 534cd1dc88..41ddd15592 100644
--- a/docs/system/arm/xlnx-zcu102.rst
+++ b/docs/system/arm/xlnx-zcu102.rst
@@ -4,8 +4,30 @@ Xilinx ZynqMP ZCU102 (``xlnx-zcu102``)
 The ``xlnx-zcu102`` board models the Xilinx ZynqMP ZCU102 board.
 This board has 4 Cortex-A53 CPUs and 2 Cortex-R5F CPUs.
 
+Supported devices
+-----------------
+
+The machine is based on QEMU's Xilinx ZynqMP SoC model and includes
+the following Processing System devices:
+
+ * Cortex-A53 APU and Cortex-R5F RPU CPU cores
+ * Generic Interrupt Controller
+ * DDR memory
+ * On-chip memory
+ * Cadence UARTs
+ * Cadence GEM Ethernet controllers
+ * SDHCI controllers
+ * SPI controllers
+ * GQSPI controller
+ * RTC
+ * CAN controllers
+ * USB controllers
+ * SATA controller
+ * DMA controllers
+ * BBRAM and eFUSE devices
+
 Machine-specific options
-""""""""""""""""""""""""
+------------------------
 
 The following machine-specific options are supported:
 
@@ -17,3 +39,118 @@ virtualization
   Set ``on``/``off`` to enable/disable emulating a guest CPU which implements 
the
   Arm Virtualization Extensions. The default is ``off``.
 
+Boot options
+------------
+
+The ``xlnx-zcu102`` machine can start a Linux kernel directly with
+``-kernel``. It does not provide a ZynqMP BootROM flow for loading a
+Xilinx boot image from SD or QSPI flash, so images such as ``boot.bin``
+or ``qspi.bin`` cannot be passed alone and expected to boot like they
+would on real hardware.
+
+QEMU does not generate a built-in device tree for this machine. Pass a DTB
+with ``-dtb``. For direct Linux boot with a real-board ZCU102 DTB, QEMU
+applies compatibility adjustments for the boot-critical UART, SDHCI and
+GQSPI nodes so that Linux can use the devices modeled by QEMU without
+requiring user-side DTB edits.
+
+This is not the same as emulating the complete ZynqMP firmware stack. The
+DTB can still contain non-boot devices that depend on board peripherals,
+external clock chips, power domains, pin control, or firmware services
+that QEMU does not model.
+
+Direct Linux boot with Buildroot
+--------------------------------
+
+Buildroot has a ZCU102 defconfig. Buildroot 2026.05 release is tested at the
+time of writing. From the Buildroot source tree:
+
+.. code-block:: bash
+
+  $ make zynqmp_zcu102_defconfig
+  $ make
+
+The generated files are in ``output/images/``. The examples below use:
+
+ * ``Image``
+ * ``zynqmp-zcu102-rev1.0.dtb``
+ * ``sdcard.img``
+ * ``qspi.bin`` if QSPI flash probing is desired
+
+QEMU's SD card model requires a power-of-two image size. Work on a copy
+of the Buildroot SD image and resize the copy, not the original output:
+
+.. code-block:: bash
+
+  $ cp output/images/sdcard.img sdcard-qemu.img
+  $ qemu-img resize -f raw sdcard-qemu.img 128M
+
+The real-board DTB enables the SDHCI controller at ``mmc@ff170000``.
+In QEMU this is SD index 1, so attach the SD card with
+``if=sd,index=1``.
+
+Boot Linux with the Buildroot kernel and the native Buildroot DTB:
+
+.. code-block:: bash
+
+  $ qemu-system-aarch64 -M xlnx-zcu102 -m 2G -nographic \
+      -kernel output/images/Image \
+      -dtb output/images/zynqmp-zcu102-rev1.0.dtb \
+      -append "earlycon=cdns,mmio,0xff000000,115200n8 \
+               console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait" \
+      -drive file=sdcard-qemu.img,if=sd,index=1,format=raw
+
+This should get the kernel booting all the way to the Buildroot login
+prompt on ``ttyPS0``.
+
+QSPI flash
+----------
+
+The board creates two PS SPI NOR flashes first and the ZynqMP GQSPI
+flashes after them. Therefore the first GQSPI flash is
+``if=mtd,index=2``. The remaining GQSPI flash backends use indices 3, 4
+and 5.
+
+The Buildroot ``qspi.bin`` image is smaller than the 64 MiB
+``n25q512a11`` flash model. To attach it as the first GQSPI flash,
+place it in a 64 MiB raw backing file:
+
+.. code-block:: bash
+
+  $ qemu-img create -f raw qspi-cs0.img 64M
+  $ dd if=output/images/qspi.bin of=qspi-cs0.img bs=1M conv=notrunc
+
+Add the following drive to the direct Linux boot command above:
+
+.. code-block:: bash
+
+      -drive file=qspi-cs0.img,if=mtd,index=2,format=raw
+
+With this drive attached, Linux should probe the ZynqMP GQSPI
+controller and register the SPI NOR partitions from the DTB, for
+example ``qspi-fsbl-uboot``, ``qspi-linux``, ``qspi-device-tree`` and
+``qspi-rootfs``.
+
+The default Buildroot ``qspi.bin`` is a boot image, not a root
+filesystem image. The default DTB's ``qspi-rootfs`` partition is also
+too small for Buildroot's ``rootfs.ext2``. Booting with the root
+filesystem in QSPI flash therefore requires a custom image layout and
+matching DTB changes.
+
+Known limitations
+-----------------
+
+The machine does not emulate the complete real-board firmware stack.
+In particular:
+
+ * ZynqMP BootROM boot-mode selection is not modeled.
+ * Passing the Buildroot ``boot.bin`` with ``-bios`` does not boot Linux.
+ * Attaching ``qspi.bin`` as an MTD drive without ``-kernel`` does not
+   boot from QSPI flash.
+ * ZynqMP PM firmware services are not modeled. For direct Linux boot,
+   QEMU adjusts the supplied DTB only for the boot-critical UART, SDHCI
+   and GQSPI nodes.
+ * Other real-board DTB nodes can still defer or fail probing when they
+   depend on devices that are not present in the QEMU model, such as
+   external I2C clock generators, board sensors, some GPIO consumers,
+   PHYs, or non-boot DMA/display paths.
diff --git a/hw/arm/xlnx-zcu102.c b/hw/arm/xlnx-zcu102.c
index bb59621854..953281dca2 100644
--- a/hw/arm/xlnx-zcu102.c
+++ b/hw/arm/xlnx-zcu102.c
@@ -25,6 +25,7 @@
 #include "system/device_tree.h"
 #include "qom/object.h"
 #include "net/can_emu.h"
+#include <libfdt.h>
 
 struct XlnxZCU102 {
     MachineState parent_obj;
@@ -42,6 +43,112 @@ struct XlnxZCU102 {
 #define TYPE_ZCU102_MACHINE   MACHINE_TYPE_NAME("xlnx-zcu102")
 OBJECT_DECLARE_SIMPLE_TYPE(XlnxZCU102, ZCU102_MACHINE)
 
+static bool zcu102_fdt_get_phandle(void *fdt, const char *node_path,
+                                   uint32_t *phandle)
+{
+    int offset;
+
+    offset = fdt_path_offset(fdt, node_path);
+    if (offset < 0) {
+        return false;
+    }
+
+    *phandle = fdt_get_phandle(fdt, offset);
+    return *phandle != 0;
+}
+
+static void zcu102_fdt_nop_prop(void *fdt, const char *node_path,
+                                const char *prop)
+{
+    int ret;
+    int offset;
+
+    offset = fdt_path_offset(fdt, node_path);
+    if (offset < 0) {
+        return;
+    }
+
+    ret = fdt_nop_property(fdt, offset, prop);
+    if (ret < 0 && ret != -FDT_ERR_NOTFOUND) {
+        error_report("%s: Couldn't nop %s/%s: %s", __func__, node_path,
+                     prop, fdt_strerror(ret));
+        exit(1);
+    }
+}
+
+static void zcu102_fdt_fixup_clocks(void *fdt, const char *node_path,
+                                    uint32_t phandle)
+{
+    if (fdt_path_offset(fdt, node_path) < 0) {
+        return;
+    }
+
+    qemu_fdt_setprop_cells(fdt, node_path, "clocks", phandle, phandle);
+}
+
+static void zcu102_fdt_fixup_qemu_direct_boot_nodes(void *fdt)
+{
+    uint32_t pss_ref_clk;
+    int i, j;
+
+    static const char * const provider_props[] = {
+        "power-domains",
+        "resets",
+        "assigned-clocks",
+        "assigned-clock-rates",
+        "assigned-clock-parents",
+        "pinctrl-names",
+        "pinctrl-0",
+    };
+    static const char * const direct_boot_nodes[] = {
+        "/axi/serial@ff000000",
+        "/axi/serial@ff010000",
+        "/axi/mmc@ff170000",
+        "/axi/spi@ff0f0000",
+    };
+
+    /*
+     * The Linux ZCU102 DTB inherits these boot-critical nodes from
+     * arch/arm64/boot/dts/xilinx/zynqmp.dtsi:
+     *
+     *   uart0: serial@ff000000
+     *   uart1: serial@ff010000
+     *   sdhci1: mmc@ff170000
+     *   qspi: spi@ff0f0000
+     *
+     * zynqmp.dtsi, zynqmp-clk-ccf.dtsi and zynqmp-zcu102-revA.dts
+     * wire them to PM firmware-backed clock, reset and pinctrl providers.
+     * Linux reaches that firmware through zynqmp_pm_invoke_fn() in
+     * drivers/firmware/xilinx/zynqmp-core.c; examples include
+     * zynqmp_pm_query_data() for drivers/clk/zynqmp/clkc.c,
+     * zynqmp_pm_reset_assert() for drivers/reset/reset-zynqmp.c, and
+     * zynqmp_pm_pinctrl_request() for drivers/pinctrl/pinctrl-zynqmp.c.
+     *
+     * Direct kernel boot has no PM firmware stage for those calls. Bypass the
+     * firmware-backed providers for just these nodes and describe their clocks
+     * as fixed inputs, avoiding a QEMU model of the runtime PM firmware.
+     *
+     * The Linux ZynqMP clock description defines pss-ref-clk as a 33.333333 
MHz
+     * fixed clock with #clock-cells = <0>. Use it as a conservative 
direct-boot
+     * fallback for both clock inputs of these devices. That satisfies the
+     * existing two clock-names entries while keeping the DTB independent from
+     * the firmware-backed ZynqMP clock controller.
+     */
+    if (!zcu102_fdt_get_phandle(fdt, "/pss-ref-clk", &pss_ref_clk)) {
+        return;
+    }
+
+    for (i = 0; i < ARRAY_SIZE(direct_boot_nodes); i++) {
+        for (j = 0; j < ARRAY_SIZE(provider_props); j++) {
+            zcu102_fdt_nop_prop(fdt, direct_boot_nodes[i], provider_props[j]);
+        }
+    }
+
+    zcu102_fdt_fixup_clocks(fdt, "/axi/serial@ff000000", pss_ref_clk);
+    zcu102_fdt_fixup_clocks(fdt, "/axi/serial@ff010000", pss_ref_clk);
+    zcu102_fdt_fixup_clocks(fdt, "/axi/mmc@ff170000", pss_ref_clk);
+    zcu102_fdt_fixup_clocks(fdt, "/axi/spi@ff0f0000", pss_ref_clk);
+}
 
 static bool zcu102_get_secure(Object *obj, Error **errp)
 {
@@ -97,6 +204,11 @@ static void zcu102_modify_dtb(const struct arm_boot_info 
*binfo, void *fdt)
         }
         g_strfreev(node_path);
     }
+
+    /* Run the DTB callback only for direct Linux kernel booting */
+    if (binfo->is_linux) {
+        zcu102_fdt_fixup_qemu_direct_boot_nodes(fdt);
+    }
 }
 
 static void bbram_attach_drive(XlnxBBRam *dev)
---
base-commit: 9ded45a67ff5daa7f30264bc8a9868b420da2a54
branch: zcu102

-- 
2.53.0


Reply via email to