A single-command driven, scripted Yocto flow
June 3, 202612 minutes

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=zcu104That 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/.
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.shGoogle’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.
Clone:
git clone https://github.com/fpgadeveloper/fpga-drive-aximm-pcie.git
cd fpga-drive-aximm-pcie/YoctoList the supported targets:
make helpBuild one (default JOBS=8):
make yocto TARGET=zcu104 JOBS=16Find your flashable image:
ls zcu104/images/linux/Other top-level commands:
| Command | What it does |
|---|---|
make yocto TARGET=<board> | Build one target (this is the default goal, so make TARGET=<board> works too) |
make all | Build every Yocto-enabled target in sequence |
make status TARGET=<board> | Report whether a target is built |
make status_all | Report build status of all targets |
make clean TARGET=<board> | Delete a target’s workspace |
make clean_all | Delete all target workspaces |
make help | Print usage and the list of valid targets |
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.
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.cfgThe 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 configuredPetaLinux note: a
Yocto/<target>/workspace is the rough equivalent of a PetaLinux project directory.sources/is the layer collection,build/conf/is wherelocal.conf/bblayers.conflive, andimages/linux/mirrors the PetaLinuximages/linux/you already know.
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.shKey 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.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.
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.
init-workspace.sh - fetch the manifestIn 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 syncinto Yocto/<target>/. This populates .repo/, sources/ (poky + meta-xilinx
edf-init-build-env.configure-build.sh - XSA to System Device Tree to MACHINEIn 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:
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.meta-user layer to build/conf/bblayers.conf.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).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.
build-image.sh - run bitbakeIn 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-imageedf-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.
package-output.sh - gather the artifactsIn 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 name | What it is |
|---|---|
BOOT.BIN | Boot image (FSBL/PLM + bitstream/PDI + U-Boot) |
Image or uImage | Kernel - Image on ZynqMP/Versal, uImage on Zynq-7000 |
boot.scr | U-Boot boot script |
system.dtb | Linux device tree blob |
u-boot.elf | U-Boot (for debug/JTAG) |
rootfs.wic.xz | Compressed SD-card image (flash this) |
rootfs.tar.gz | Root filesystem tarball |
rootfs.wic.bmap | Block 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.
config/data.jsonOpsero’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.
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.
| File | Purpose | PetaLinux analogue |
|---|---|---|
conf/local.conf.append | Bootargs (APPEND), hostname, image-size tweaks - appended verbatim to local.conf | system-level petalinux-config settings |
meta-user/conf/layer.conf | Standard meta-user layer definition (priority 7) | project-spec/meta-user/conf/layer.conf |
recipes-bsp/device-tree/device-tree.bbappend | Injects system-user.dtsi via EXTRA_DT_INCLUDE_FILES, guarded to the Linux domain only | device-tree.bbappend |
recipes-bsp/device-tree/files/system-user.dtsi | SoC-side board fixups (same filename, same idea) | system-user.dtsi |
recipes-core/images/edf-linux-disk-image.bbappend | Extra rootfs packages via IMAGE_INSTALL:append | rootfs config / petalinux-config -c rootfs |
recipes-kernel/linux/linux-xlnx_%.bbappend + linux-xlnx/bsp.cfg | Kernel 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.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.
Putting the scripts and the workspace together, here is the full sequence the single command runs the first time you build a target:
init-workspace.sh -> repo init + repo sync -> Yocto/<t>/{.repo, sources, edf-init-build-env}.Vivado/<t>/fpgadrv_wrapper.xsa -> Yocto/<t>/hw/
(Make builds the Vivado project first if the XSA is missing).configure-build.sh -> sdtgen -> Yocto/<t>/sdt/; gen-machineconf parse-sdt
-> build/conf/machine/fpgadrv-<t>.conf; apply BSP overrides -> write configdone.txt.build-image.sh -> bitbake edf-linux-disk-image -> build/tmp/deploy/images/.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.
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/sstateNote 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.
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=fsyncor, if you have bmap-tool installed, this is much faster:
bmaptool copy zcu104/images/linux/rootfs.wic.xz /dev/sdXAnother 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.
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.