How-To: Build a Custom Yocto Image for PYNQ (v3.1.2) on a Custom Board

Hello everyone,

A Quick Note: Why Yocto and not PetaLinux?

You might be thinking, “Isn’t PetaLinux the standard for this?” Well, it seems the winds are changing. PetaLinux itself no longer seems to prioritize PYNQ, and more importantly, AMD/Xilinx is officially deprecating PetaLinux’s core BSP-generation feature starting in 2025.1, pushing users directly towards Yocto.

You can read the official note here: Yocto Project Machine Configuration Support

So, figuring out a “pure” Yocto build for PYNQ isn’t just a fun academic exercise—it’s quickly becoming the future-proof path. This guide is our attempt to document that path.


After a significant amount of debugging, we successfully got PYNQ v3.1.2 running on our custom ZynqMP-based board using a custom Yocto layer. The documentation for this specific combination is sparse, so I wanted to share our solution.

I cannot share the complete source code (because it’s too messy, split in several dynamic layers, probably too dirty to be shared…), but I can provide a step-by-step guide on the layer structure and the two critical fixes that were required to make it work.


1. The Custom PYNQ Layer Structure

We created a dedicated layer (e.g., meta-custom-pynq) to hold all our PYNQ-related recipes. Here is the file structure and the purpose of each part:

meta-custom-pynq/
├── classes
│   └── xilinx-pynq.bbclass         # A helper class to define common PYNQ dependencies (cma, pynqutils, etc.)
├── conf
│   └── layer.conf                  # Standard layer configuration file
├── recipes-bsp
│   └── device-tree
│       ├── device-tree.bbappend    # CRITICAL FIX #1: Injects board ID into device tree
│       └── files/                  # Standard PYNQ device tree overlays (UIO, ZOCL)
│           ├── pynq_uio_*.dtsi
│           └── pynq_zocl_*.dtsi
├── recipes-images
│   ├── images
│   │   └── custom-pynq-image.bb   # Our main image recipe
│   └── packagegroups
│       └── packagegroup-custom-pynq.bb # Pulls all PYNQ packages together
├── recipes-python
│   ├── python3-pynq                # PYNQ v3.1.2 recipe
│   ├── python3-pynqmetadata        # PYNQ dependency
│   └── python3-pynqutils           # PYNQ dependency
├── recipes-support
│   ├── libcma                      # PYNQ dependency (Contiguous Memory Allocator)
│   ├── pynq-runtime-config         # Custom recipe for board setup (hostname, etc.)
│   │   ├── files/
│   │   │   ├── boardname.sh        # Script for /etc/profile.d
│   │   │   └── pynq_hostname.sh    # Script to set hostname
│   │   └── pynq-runtime-config_1.0.bb
│   └── xrt-environment             # Helper recipe to install test notebooks/kernels
│       └── python3-ipykernel_%.bbappend # (Optional) Tweak for Jupyter kernels
└── recipes-xrt
    └── xrt
        ├── files
        │   └── 0001-Force-python-bindings-build-for-embedded.patch # Patch for XRT
        └── xrt_%.bbappend          # CRITICAL FIX #2: Builds and installs pyxrt.so


2. The Two Critical Fixes

Out of the box, PYNQ failed with two major issues. Here is how we fixed them.

Fix #1: The ON_TARGET = False Problem (Device Tree)

PYNQ failed to recognize our board, which we traced back to os.path.isfile('/proc/device-tree/chosen/pynq_board') returning False.

The Solution: We must inject this property into the device tree.

In recipes-bsp/device-tree/device-tree.bbappend, we added a task to run before the device tree is compiled. This task appends the required chosen node to the system-top.dts file.

device-tree.bbappend:

# Include PYNQ's dtsi files (UIO, ZOCL)
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
SRC_URI += " \
    file://pynq_uio_zynqmp.dtsi \
    file://pynq_zocl_intc_zynqmp.dtsi \
"

# THIS IS THE FIX:
# Append to the do_configure task to modify the dts before compilation
do_configure:append() {
    # Check if MACHINE is set, then echo the property into the dts
    if [ -n "${MACHINE}" ]; then
        echo "/ { chosen { pynq_board = \"${MACHINE}\"; }; };" >> ${B}/device-tree/system-top.dts
    else
        bbwarn "[PYNQ] MACHINE variable is empty, cannot set pynq_board property."
    fi
}

Fix #2: The NameError: name 'pyxrt' is not defined Problem (XRT)

Even with the correct device tree, PYNQ failed because it still tries to import pyxrt. The modern XRT recipe (e.g., 2024.1) does not build the pyxrt.so Python bindings for embedded targets by default.

The Solution: We must force XRT to build the Python bindings and install them correctly.

recipes-xrt/xrt/xrt_%.bbappend: This file is complex, but it does three things:

  1. Apply a Patch: We use SRC_URI += "file://0001-Force-python-bindings-build-for-embedded.patch" to patch XRT’s CMakeLists.txt. The patch forces the python subdirectory to be included in the embedded build.
    diff --git a/CMake/embedded_system.cmake b/CMake/embedded_system.cmake
    index 4b298fced6..cdee7e7a77 100644
    — a/CMake/embedded_system.cmake
    +++ b/CMake/embedded_system.cmake
    @@ -134,3 +134,7 @@ if (DEFINED ENV{DKMS_FLOW})
    set (XRT_DKMS_DRIVER_SRC_BASE_DIR “${CMAKE_CURRENT_SOURCE_DIR}/runtime_src/core”)
    include (CMake/dkms-edge.cmake)
    endif()
    +
    +# — PYNQv3.1 - patches to force cmake to build pyxrt
    +set (XRT_INSTALL_PYTHON_DIR “${XRT_INSTALL_DIR}/python”)
    +add_subdirectory(python)

  2. Force Build Flags: We add EXTRA_OECMAKE flags to ensure CMake enables the Python bindings.

  3. Package pyxrt.so: This is the most important part. The pyxrt.so file is built but not packaged. We use a do_install:append to:

    • Find the compiled Python binding (e.g., pyxrt.cpython-312-aarch64-linux-gnu.so).

    • Rename it to pyxrt.so.

    • Install it to the correct path: /usr/lib/pythonX.Y/pyxrt.so. Note: It must go here, not in site-packages.

    • We create a new package, python3-xrt, to contain this single file and add it to our image’s dependencies.


3. PYNQ Runtime & Board Configuration

To finish the setup, we created a small helper recipe pynq-runtime-config_1.0.bb. This recipe handles board-specific configuration:

  • Creates /etc/xocl.txt: Writes the ${MACHINE} name into /etc/xocl.txt.

  • Sets Hostname: Installs the pynq_hostname.sh script and a simple systemd service (pynq-set-hostname.service) that runs once on first boot to set the hostname to pynq-${MACHINE}.

  • Sets Profile: Installs boardname.sh into /etc/profile.d/ to provide a nice shell prompt.


4. Bonus: Interactive Plotting in Jupyter

As a final touch, to get interactive “Matlab-style” plotting inside Jupyter notebooks, we simply added the python3-ipympl package to our main image recipe (custom-pynq-image.bb). This enables the Matplotlib “widget” backend.

I hope this guide helps others who are trying to get PYNQ working in a custom Yocto environment.

P.S. (A Note on Open Sourcing)

I see some of you might be wondering, “This is great, but… where’s the GitHub repo?” :grinning_face_with_smiling_eyes:

If this guide generates a lot of interest, I might be convinced to clean up this layer and post it publicly.

One tiny problem: I simply don’t have the time or the deep Yocto expertise to keep it alive and updated with every new release.

So, if I do post it, consider it a “use-at-your-own-risk,” “no-warranty-expressed-or-implied” kind of starting point!

Let me know if it’s something you’d still find useful. Cheers!

Good luck!

3 Likes

OMG…this workflow is so crazy

Haha, you are 100% right, it is completely crazy! :grinning_face_with_smiling_eyes:

That’s exactly why I shared the steps. This was just the only path we found that finally worked after days of debugging, and I’m positive there has to be a simpler, more “sane” way.

If you (or anyone else!) see a better approach or have ideas on how to simplify this workflow, I am all ears and completely open to it. The whole point of posting this was to hopefully find a cleaner solution for the community!

What do you have in mind?

Wow, this flow is truly amazing. I’ve been trying for weeks to get PYNQ running on the KV260 via Yocto, but without any stable results.
It would be fantastic to have access to the GitHub of this layer (even if it’s “rough” or “incomplete”), so we could study it, adapt it, improve it, and maybe even contribute back.

Thank you very much for sharing this guideline. I’ll give it a try.