On Sat, 2026-06-27 at 23:42 +0800, Bin Meng wrote:
> 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]>
>
> ---
>
> docs/system/arm/xlnx-zcu102.rst | 139
> +++++++++++++++++++++++++++++++-
> hw/arm/xlnx-zcu102.c | 109 +++++++++++++++++++++++++
> 2 files changed, 247 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 4e48970274..839e4e90d5 100644
> --- a/hw/arm/xlnx-zcu102.c
> +++ b/hw/arm/xlnx-zcu102.c
> @@ -26,6 +26,7 @@
> #include "system/device_tree.h"
> #include "qom/object.h"
> #include "net/can_emu.h"
> +#include <libfdt.h>
>
> struct XlnxZCU102 {
> MachineState parent_obj;
> @@ -43,6 +44,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);
I don't think this is something QEMU should be doing. Modifying the DTB
on the fly just seems very brittle and prone to breakage and confusion.
Alistair
> +}
>
> static bool zcu102_get_secure(Object *obj, Error **errp)
> {
> @@ -98,6 +205,8 @@ static void zcu102_modify_dtb(const struct
> arm_boot_info *binfo, void *fdt)
> }
> g_strfreev(node_path);
> }
> +
> + zcu102_fdt_fixup_qemu_direct_boot_nodes(fdt);
> }
>
> static void bbram_attach_drive(XlnxBBRam *dev)