Migrating From PetaLinux to Yocto EDF

A single-command driven, scripted Yocto flow

June 3, 202612 minutes

Migrating From PetaLinux to Yocto EDF

For the last couple of weeks I’ve been working on the PetaLinux-to-Yocto migration for one of Opsero’s reference designs. Since AMD announced the retirement of PetaLinux, I haven’t been looking forward to this task, and I’ve been putting it off for months. Luckily we now have these amazing AI tools to help us. With Claude Code, it turned out not too difficult to accomplish, at least if you already have a working PetaLinux project to start from.

In this post I’ll write up what I learned about the Yocto EDF flow and how I have added a Yocto flow build path to one of our ref design Git repos. The ref design is for our FPGA Drive FMC Gen4 product and it previously only had a PetaLinux build path - now it has both.

For those of you who already have access to Claude Code or another agentic AI tool, I suggest you use it on your own PetaLinux to Yocto migration, because it will pick up your current workflow and build something that is compatible with that. For those that don’t use agentic AI tools, I suggest you use the flow that I describe here, built into the Makefiles and scripts in this Opsero reference design.

This is a guide to that flow: what it is, how to use it, and how it works under the hood. It is written for developers who know the PetaLinux flow and are moving to AMD’s Embedded Development Framework (EDF), the Yocto-based successor to PetaLinux.

The whole flow is driven by a single command:

cd Yocto
make yocto TARGET=zcu104

That one command takes a Vivado design and produces a complete, flashable Linux image - BOOT.BIN, kernel, device tree, root filesystem, and an SD-card .wic in Yocto/zcu104/images/linux/.


Requirements

  • A Linux PC Either a virtual or physical machine. I’m using Ubuntu 24.04.2 LTS.

  • AMD Vivado + Vitis 2025.2. Vivado builds the hardware (XSA). Vitis provides xsct/sdtgen, which the flow uses to turn the XSA into a System Device Tree. Source both before building:

    source <xilinx>/2025.2/Vivado/settings64.sh
    source <xilinx>/2025.2/Vitis/settings64.sh
  • Google’s repo tool on your PATH (sudo apt-get install repo). The EDF manifest is fetched with repo, exactly as the PetaLinux manifest is.

  • (Optional) bmap-tools for faster writing of .wic files to SD cards (sudo apt-get install bmap-tools).

  • (Optional) an sstate-cache mirror for fast rebuilds - see Offline / faster builds.


Quick start

  1. Clone:

    git clone https://github.com/fpgadeveloper/fpga-drive-aximm-pcie.git
    cd fpga-drive-aximm-pcie/Yocto
  2. List the supported targets:

    make help
  3. Build one (default JOBS=8):

    make yocto TARGET=zcu104 JOBS=16
  4. Find your flashable image:

    ls zcu104/images/linux/

Other top-level commands:

CommandWhat it does
make yocto TARGET=<board>Build one target (this is the default goal, so make TARGET=<board> works too)
make allBuild every Yocto-enabled target in sequence
make status TARGET=<board>Report whether a target is built
make status_allReport build status of all targets
make clean TARGET=<board>Delete a target’s workspace
make clean_allDelete all target workspaces
make helpPrint usage and the list of valid targets

Folder structure

Only four things in Yocto/ are version-controlled - the Makefile, the scripts, the per-board BSPs, and this/the README. Everything else is generated into a per-target workspace on first build and is git-ignored.

Version-controlled (the flow itself)

Yocto/
├── Makefile                     # the single entry point; orchestrates everything
├── README.md                    # user-facing build instructions
├── scripts/
│   ├── init-workspace.sh        # fetch the EDF manifest  (= petalinux-create)
│   ├── configure-build.sh       # XSA -> SDT -> MACHINE      (= import XSA / petalinux-config)
│   ├── build-image.sh           # run bitbake              (= petalinux-build)
│   └── package-output.sh        # gather artifacts         (= petalinux-package)
└── bsp/
    └── <board>/                 # per-board customizations (= project-spec/meta-user)
        ├── conf/local.conf.append
        └── meta-user/
            ├── conf/{layer.conf, petalinuxbsp.conf}
            ├── recipes-bsp/device-tree/
            │   ├── device-tree.bbappend
            │   └── files/system-user.dtsi
            ├── recipes-core/images/edf-linux-disk-image.bbappend
            └── recipes-kernel/linux/
                ├── linux-xlnx_%.bbappend
                └── linux-xlnx/bsp.cfg

Generated per target (gitignored)

The first make yocto TARGET=zcu104 creates Yocto/zcu104/:

Yocto/zcu104/
├── .repo/                # Google repo metadata
├── sources/             # all Yocto layers (poky, meta-xilinx, meta-amd-edf, …)
├── edf-init-build-env   # the EDF env setup script (PetaLinux's setupsdk equivalent)
├── hw/                  # the Vivado XSA, copied in
├── sdt/                 # the System Device Tree generated from the XSA
├── build/               # bitbake build dir (conf/, tmp/, sstate-cache/, downloads/)
├── images/linux/        # ← gathered output products you flash
└── configdone.txt       # marker: this workspace has been configured

PetaLinux note: a Yocto/<target>/ workspace is the rough equivalent of a PetaLinux project directory. sources/ is the layer collection, build/conf/ is where local.conf/bblayers.conf live, and images/linux/ mirrors the PetaLinux images/linux/ you already know.


The Makefile

The Yocto/Makefile is like the orchestrator for the Yocto build flow. When you run make yocto it goes down a chain of dependencies where the bottom of the chain is the Vivado design (XSA) and the top of the chain is the boot collateral that you would flash to an SD card:

make yocto TARGET=zcu104
        │
        ▼  (per-target lock prevents concurrent builds of the same target)
   package  ──────────────► images/linux/{BOOT.BIN, Image|uImage, boot.scr, …}
        │                       run by: package-output.sh
        ▼
    build   ──────────────► bitbake edf-linux-disk-image
        │                       run by: build-image.sh
        ▼
 configdone.txt ──────────► build/conf (MACHINE = fpgadrv-zcu104)
        │                       run by: configure-build.sh
        ├─ requires hw/fpgadrv_wrapper.xsa  ◄── copied from Vivado/<target>/
        │                                        (Vivado builds it if missing)
        └─ requires edf-init-build-env      ◄── run by: init-workspace.sh

Key Makefile variables:

  • TARGET - required; the design to build (e.g. zcu104, vck190_fmcp1).
  • JOBS - bitbake/parallel-make threads (default 8).
  • TARGET_BOARD - the first underscore-delimited word of TARGET; selects the BSP directory. So zcu106_hpc0 and zcu106_hpc1 both use bsp/zcu106.
  • The MACHINE is the generated fpgadrv-<TARGET> (see configure-build.sh).

The Makefile also manages a “lock file” system which ensures that you can’t launch a build for the same target from different terminals. This is mainly for my own use because part of my workflow is to run a make all from different terminals to do multiple builds in parallel.


The scripts

All four are in Yocto/scripts/ and are called by the Makefile in order. You normally never run them by hand, but here’s what they do if you want to understand the flow.

1. init-workspace.sh - fetch the manifest

In PetaLinux we would do petalinux-create -t project (sets up the project and pulls in the layers). In Yocto flow, we use Google’s repo tool.

The script runs Google’s repo against AMD’s manifest repo:

repo init -u https://github.com/Xilinx/yocto-manifests.git \
          -b rel-v2025.2 -m default-edf.xml
repo sync

into Yocto/<target>/. This populates .repo/, sources/ (poky + meta-xilinx

  • meta-amd-edf + dependencies), and the EDF setup script edf-init-build-env.

2. configure-build.sh - XSA to System Device Tree to MACHINE

In a Yocto build you need this thing called a MACHINE which is basically a hardware configuration for the target we are building Linux for. In PetaLinux we would use petalinux-config --get-hw-description <.XSA file> to autogenerate this hardware configuration for us. In the Yocto flow, we use a series of tools to achieve the same thing, and that’s what the configure-build.sh script is for.

It sources edf-init-build-env (creating build/ with a default config), then:

  1. Generates a System Device Tree from the XSA with xsct/sdtgen into Yocto/<target>/sdt/. The SDT includes pl.dtsi - your PL hardware (the IP in your block design), extracted straight from the design.
  2. Adds the board’s meta-user layer to build/conf/bblayers.conf.
  3. Runs gen-machineconf parse-sdt, which turns the SDT into a custom machine config build/conf/machine/fpgadrv-<target>.conf and the lopper-generated per-domain device trees (e.g. cortexa53-linux.dts on ZynqMP, cortexa72-linux.dts on Versal).
  4. Appends to local.conf: MACHINE = "fpgadrv-<target>", a few EDF sanity-check toggles, the verbatim contents of the board’s bsp/<board>/conf/local.conf.append, and - if configured - the offline sstate mirror.

It might be useful for you to know that instead of using the XSA to generate a MACHINE, you could use one of the “AMD-verified MACHINEs”. These are useful if you are building Linux for one of AMD’s FPGA/SoC development boards, and you don’t have any custom PL hardware in your design. In our case, we always have something going on in the PL, so we need to generate our own MACHINE. Also, we have ref designs for boards from other vendors (eg. UltraZed-EV Carrier) which don’t have AMD-verified MACHINEs.

3. build-image.sh - run bitbake

In the PetaLinux flow we would use the petalinux-build command. In Yocto flow, we call bitbake directly.

The script sources the EDF environment, sets BB_NUMBER_THREADS/PARALLEL_MAKE from JOBS, and runs:

bitbake edf-linux-disk-image

edf-linux-disk-image is the EDF image recipe - the analogue of petalinux-image-minimal. It produces the kernel, device tree, root filesystem, U-Boot, the BOOT.BIN, and a partitioned .wic SD-card image.

4. package-output.sh - gather the artifacts

In the PetaLinux flow it was the petalinux-package command. That would place the boot collateral in the images/linux/ directory. For the Yocto flow I designed this script to do the same.

bitbake scatters its outputs under build/tmp/deploy/images/<machine>/ with long, versioned, symlinked filenames. This script finds the right deploy directory (on Versal it skips the extra MicroBlaze PLM/PSM and RPU multiconfig sub-dirs) and copies everything into Yocto/<target>/images/linux/ always using these filenames:

Canonical nameWhat it is
BOOT.BINBoot image (FSBL/PLM + bitstream/PDI + U-Boot)
Image or uImageKernel - Image on ZynqMP/Versal, uImage on Zynq-7000
boot.scrU-Boot boot script
system.dtbLinux device tree blob
u-boot.elfU-Boot (for debug/JTAG)
rootfs.wic.xzCompressed SD-card image (flash this)
rootfs.tar.gzRoot filesystem tarball
rootfs.wic.bmapBlock map for fast bmaptool flashing

That directory contains all the boot collateral and what you would want to use to prepare an SD card for booting.


Centralized config: config/data.json

Opsero’s ref design repositories often have multiple targets, each with their particularities. They also have multiple Makefiles and documentation that lists the targets and their configurations. In order to centralize this information and simplify the process of adding new targets, I designed them with a config folder which contains a JSON file and a Python script. The config/data.json contains all the target designs and their parameters, while the config/update.py script propagates that information to the Makefiles, Tcl scripts and documentation that need it.

You can check this out if you think it’s interesting, but the main reason you might want to know about it is if you want to add a new target design to the repo or you’re making modifications to a target design and you want to ensure that you’re doing it the “right” way. Of course you can just manually edit all the Makefiles and scripts, but knowing about the config directory might save you some headaches.


The BSP

Yocto/bsp/<board>/ is where each board’s customizations live, and the structure maps onto PetaLinux’s project-spec/meta-user almost one-to-one. configure-build.sh adds this directory to bblayers.conf, so its recipes and bbappends are applied on top of the generated machine.

FilePurposePetaLinux analogue
conf/local.conf.appendBootargs (APPEND), hostname, image-size tweaks - appended verbatim to local.confsystem-level petalinux-config settings
meta-user/conf/layer.confStandard meta-user layer definition (priority 7)project-spec/meta-user/conf/layer.conf
recipes-bsp/device-tree/device-tree.bbappendInjects system-user.dtsi via EXTRA_DT_INCLUDE_FILES, guarded to the Linux domain onlydevice-tree.bbappend
recipes-bsp/device-tree/files/system-user.dtsiSoC-side board fixups (same filename, same idea)system-user.dtsi
recipes-core/images/edf-linux-disk-image.bbappendExtra rootfs packages via IMAGE_INSTALL:appendrootfs config / petalinux-config -c rootfs
recipes-kernel/linux/linux-xlnx_%.bbappend + linux-xlnx/bsp.cfgKernel config fragment (PCIe + NVMe, etc.)kernel .cfg fragment

A couple of details worth knowing when you port a board:

  • system-user.dtsi is Linux-domain only. It is #included onto the lopper-generated Linux device tree, guarded so it is not applied to the FSBL/PMU/PLM domain device trees. Those domains don’t define the SoC peripheral labels the overrides reference, so including it there would make dtc fail with “Label or path … not found”. The bbappend keys on linux being in CONFIG_DTFILE to apply only where it’s safe.
  • One bsp can serve several targets on the same chip. bsp/vck190 serves vck190_fmcp1 and vck190_fmcp2; bsp/zcu106 serves zcu106_hpc0/_hpc1; bsp/pz serves pz_7015/pz_7030. Each target still gets its own MACHINE/SDT/device tree from its own XSA - only the SoC-side fixups are shared.

In practice, the only file you hand-write per board is system-user.dtsi (plus maybe a kernel bsp.cfg line). Everything else is generated from the hardware.


How a build flows

Putting the scripts and the workspace together, here is the full sequence the single command runs the first time you build a target:

  1. init-workspace.sh -> repo init + repo sync -> Yocto/<t>/{.repo, sources, edf-init-build-env}.
  2. XSA handoff -> copy Vivado/<t>/fpgadrv_wrapper.xsa -> Yocto/<t>/hw/ (Make builds the Vivado project first if the XSA is missing).
  3. configure-build.sh -> sdtgen -> Yocto/<t>/sdt/; gen-machineconf parse-sdt -> build/conf/machine/fpgadrv-<t>.conf; apply BSP overrides -> write configdone.txt.
  4. build-image.sh -> bitbake edf-linux-disk-image -> build/tmp/deploy/images/.
  5. package-output.sh -> gather -> Yocto/<t>/images/linux/.

Re-running make yocto after a change skips whatever is already done: the workspace and config are kept, and bitbake rebuilds only what your change touched. Edit a bsp/<board>/ file and re-run, and only the affected recipes (e.g. the device tree, the kernel, the image) rebuild.


Offline / faster builds

The first build of a target downloads (around) 5 GB of git history and the build can take hours. To speed it up, put the absolute path of an extracted AMD sstate-cache mirror in Yocto/offline.txt:

/path/to/sstate

Note that the Yocto/offline.txt file is not a version controlled file - you need to create it.

The configure-build.sh script auto-detects which architecture subdirectories exist under that path and wires one SSTATE_MIRRORS entry per architecture, plus a download mirror if a downloads/ subdirectory is present. Rebuilds then drop to roughly half an hour in my experience, but this is probably highly dependent on the machine you are using. For reference, I’m using a Lenovo ThinkStation P910.

You need both the aarch64 and microblaze sstate subdirs, because the generated MACHINE builds the PMU/PLM firmware as a MicroBlaze multiconfig.


Flashing

The flashable image is Yocto/<target>/images/linux/rootfs.wic.xz:

xzcat zcu104/images/linux/rootfs.wic.xz | sudo dd of=/dev/sdX bs=4M conv=fsync

or, if you have bmap-tool installed, this is much faster:

bmaptool copy zcu104/images/linux/rootfs.wic.xz /dev/sdX
WARNING: Make sure you pick the right device name for your SD card because if you mistakely choose a hard drive or something important, it will get wiped.

Another peculiarity: For ZynqMP designs, the EDF .wic leaves the FAT esp partition empty, and the ZynqMP BootROM reads BOOT.BIN from the first FAT partition (ie. esp). So this means that after flashing the .wic to the SD card, you still need to copy the BOOT.BIN onto esp by hand. For the Versal designs, you don’t have to do this manual copy because the BOOT.BIN (and boot.scr) will already be found in the .wic.

The Yocto/README.md file has the steps in more detail.


Where to get this

Everything is in the repo on Github: github.com/fpgadeveloper/fpga-drive-aximm-pcie.

I hope you find this work useful for migrating your own projects.