From Silicon to Boot Prompt: Building a Custom Yocto BSP for the Zybo Z7-10
Most Zynq-7000 tutorials stop at PetaLinux. Xilinx’s own tooling wraps Yocto in a friendly CLI, generates your BSP for you, and mostly just works until you need something PetaLinux doesn’t give you: a layer you fully own, pinned to upstream meta-xilinx/poky revisions of your choosing, structured so a CI pipeline or another engineer can reproduce it without a PetaLinux license or installer.
This post walks through building embedded Linux for the Digilent Zybo Z7-10 (Zynq-7000, XC7Z010) the long way: a Vivado hardware design, a Vitis-generated first-stage bootloader, and a hand-built Yocto layer structured deliberately to mirror PetaLinux’s project-spec layout, so the hardware and software concerns stay cleanly separated.
The pipeline, end to end
Vivado (block design) → .xsa + .bit │ ▼Vitis (platform + FSBL) → zynq_fsbl.elf │ ▼Yocto (meta-xilinx BSP) → BOOT.bin, u-boot, zImage, rootfs
Each stage hands the next a small number of artifacts. Understanding exactly what those artifacts are and where they plug into Yocto’s recipe graph is what makes the whole thing reproducible.
Stage 1: Vivado — the hardware design
The starting point is a Vivado block design (design_1) built around the Zynq-7000 Processing System IP. For a board bring-up like this, the design is intentionally minimal:
- ZYNQ7 Processing System IP, configured via the board preset for the Zybo Z7-10 (DDR3 controller, UART, SD/MMC, USB, GPIO, and clocking pulled straight from Digilent’s board files).
- Any project-specific programmable-logic IP sits alongside the PS — but even with zero custom PL, the PS still needs to be wrapped and built, because Zynq won’t boot without a bitstream configuring its PL-side clocking and AXI interconnects.
- Run Block Automation and Run Connection Automation in the IP integrator to wire up the PS’s fixed I/O and DDR interfaces automatically.
- Generate the top-level wrapper (
design_1_wrapper.v) and run synthesis + implementation to producedesign_1_wrapper.bit.

Once the bitstream is in hand, the export that actually matters for everything downstream is:
File → Export → Export Hardware, with include bitstream checked, producing design_1_wrapper.xsa.
The XSA (Xilinx Support Archive) is the single source of truth for the rest of the pipeline, it’s a zip containing the bitstream, the processor configuration, address map, and (critically) enough metadata for downstream tools to regenerate a device tree. Every device tree node describing PS peripherals, DDR base/size, and clock inputs downstream is derived from this file, not hand-written.
Stage 2: Vitis — first-stage bootloader
The XSA feeds directly into Vitis to generate the FSBL (First Stage Boot Loader), the tiny bare-metal program the Zynq’s on-chip boot ROM runs first. It configures the PS (DDR, clocks, MIO), loads the bitstream to program the PL, and hands off to U-Boot.
The flow:
- Create Platform Project from
design_1_wrapper.xsa. - Under that platform, Create Application Project using the built-in Zynq FSBL template.
- Build the output is
zynq_fsbl.elf.

That single ELF, alongside the .bit and .xsa, is everything Yocto needs from the hardware side. No Vivado or Vitis installation is required on the Yocto build host this is deliberate, since the build machine here has neither tool installed and never needs to regenerate hardware artifacts on its own.
Stage 3: Yocto — the BSP layer
This is where PetaLinux normally takes over and hides the machinery. Building it by hand means recreating, explicitly, what PetaLinux does implicitly: turning the XSA’s exported device-tree sources into a real meta-* BSP layer against upstream meta-xilinx.
Layer stack
poky(core,scarthgap/5.0 LTS)meta-arm/meta-arm-toolchainmeta-xilinx-core+meta-xilinx-standalone— the modern SDT-based embedded-software flow, not the deprecatedxsct/meta-xilinx-toolspathmeta-openembedded/meta-oe- a custom BSP layer,
meta-user
All of poky, meta-openembedded, meta-arm, and meta-xilinx are cloned and pinned to exact commits, rather than tracking branch tips, this is what makes a from-scratch Yocto BSP reproducible in a way a moving PetaLinux install isn’t. This is available here github.com/oluseyivictor/zybo-z7-yocto
Project layout
Rather than dropping hardware artifacts and layer files wherever felt convenient, the project mirrors PetaLinux’s own project-spec convention — hardware and layer concerns get separate directories, and the hardware directory is never hand-edited:
project-spec/ hw-description/ # everything hardware-generated, untouched design_1_wrapper.bit design_1_wrapper.xsa zynq_fsbl.elf system-top.dts pcw.dtsi pl.dtsi zynq-7000.dtsi meta-user/ recipes-bsp/device-tree/ device-tree.bbappend # FILESEXTRAPATHS pulls from both dirs files/ system-user.dtsi # board fixes, hand-maintained pl-custom.dtsi # dormant scaffold for future PL IP configs/ board.conf # MACHINE, BITSTREAM_PATH, FSBL_FILE, ...
The key design decision is in device-tree.bbappend: it never modifies system-top.dts directly. Instead it appends system-user.dtsi onto the DTG-generated tree at do_configure time via EXTRA_DT_INCLUDE_FILES. That means the entire contents of hw-description/ can be overwritten wholesale by a fresh Vivado/Vitis export — a new PS configuration, added PL IP, a bumped board revision — without losing a single board-specific fix. All hand-maintained logic lives in meta-user; everything in hw-description is treated as disposable, regenerable output.
board.conf is the other piece that keeps build/ itself disposable: it’s the one file local.conf pulls in via include, setting MACHINE, LICENSE_FLAGS_ACCEPTED, XILINX_WITH_ESW, and pointers (BITSTREAM_PATH, FSBL_FILE, CONFIG_DTFILE) into hw-description/. Delete build/ entirely, re-run oe-init-build-env, re-point bblayers.conf at the four upstream sources plus meta-user, and the whole image reproduces.
What Yocto actually builds
With XILINX_WITH_ESW = "1" set, meta-xilinx-core‘s device-tree recipe becomes the preferred provider for virtual/dtb, compiling system-top.dts (plus the system-user.dtsi fixes) into system-top.dtb. u-boot-xlnx picks that DTB up automatically as its EXT_DTB. The FSBL and bitstream from hw-description/ get packaged by meta-xilinx‘s bitstream and fsbl recipes into the final BOOT.bin, alongside U-Boot.
A single bitbake core-image-minimal then produces the full artifact set for the SD card:
BOOT.bin— boot-ROM-loadable image containing FSBL, bitstream, and U-Bootu-boot.elf/u-boot.dtbzImageandsystem.dtbfor the kernel- a root filesystem image


Why go this route instead of PetaLinux
PetaLinux is the path of least resistance for a single developer bringing up one board once. This approach earns its extra setup cost when:
- You need the BSP pinned to exact, auditable upstream commits rather than whatever a PetaLinux release snapshot bundled.
- CI needs to rebuild the image without a PetaLinux license or a multi-GB installer.
- The hardware and software teams genuinely need to iterate independently, a new XSA export should never require touching Yocto recipes, and it doesn’t here.
- You want the resulting layer to be a normal, portable
meta-*layer that composes with any other Yocto-based project instead of a PetaLinux-specific artifact.
The tradeoff is real: PetaLinux hides device-tree generation, FSBL packaging, and BSP wiring behind a CLI. Rebuilding that by hand means understanding and owning every one of those steps yourself. For a one-off prototype, that’s overhead you don’t need. For a BSP that has to outlive the person who built it, it’s the difference between a reproducible layer and a black box.
Board: Digilent Zybo Z7-10 (Zynq-7000 XC7Z010) · Yocto: scarthgap (5.0 LTS) · Layers: poky, meta-arm, meta-xilinx-core/meta-xilinx-standalone, meta-openembedded