Tutorial · start here

Board bring-up

Take a bare KrIO and a K26 from power-on to running your own logic, covering boot images, console, network, a Linux image built with PetaLinux or Yocto, and the rules this board imposes on anything you build for it.

Updated 2026-08-28

This is the path from an unpopulated carrier to a board that boots Linux, joins the network, and runs programmable logic you built yourself. Work through it in order, because each step assumes the previous one.

Tool versions are deliberately left open. Anything from Vivado/Vitis 2022.1 onward works. The commands below use $XILINX for whichever you have installed, and the two places where a version genuinely matters are noted explicitly.

export XILINX=$HOME/Xilinx/2025.2          # whichever release you installed
export PATH=$XILINX/Vitis/bin:$XILINX/Vivado/bin:$PATH

What you need

CarrierKrIO
ModuleKria K26 SOM (xck26-sfvc784-2LV-c) in J1
Power12 V into the 6-pin PCIe connector
ConsoleUSB serial adapter on the debug header, 115200 8N1
OptionalDigilent JTAG adapter for the first flash
HostLinux with Vivado/Vitis, and a wired network the board can reach

1. Power and console

Bring power up and watch the console before anything else. The board tells you how far it got.

ls /dev/serial/by-id/                  # find the adapter; the number moves, the id does not
picocom -b 115200 /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0

Use the by-id path. A JTAG adapter on the same host will take ttyUSB0 and shift the console to ttyUSB1 without warning.

Power-on line noise can abort U-Boot’s autoboot and leave a half-typed command in its buffer. Before typing anything, send a lone " and a carriage return to close any unterminated quote, then Ctrl-C. Type slowly, because fast writes drop characters on this adapter.

Rule Never reboot. Warm reset does not work on this board: the boot-time QSPI drivers leave the flash in a non-power-on state and the BootROM hangs silently before any output. Cut power instead. It is 100% reproducible and cold boot is 100% reliable. See the errata.

2. What is in a boot image

BOOT.BIN is a container. Built with bootgen from a .bif, it holds, in order:

ComponentJob
FSBLconfigures the PS from psu_init (clocks, DDR, I/O, AXI ports)
PMU firmwarepower management
BL31ARM trusted firmware
U-Bootloads the kernel
(optionally) a bitstreamPL configured at boot rather than from Linux

Omit the bitstream. A boot image without one is about 1.7 MB instead of 9.5 MB, which matters for flashing (below), and loading the PL from Linux is more flexible anyway.

the_ROM_image:
{
    [bootloader, destination_cpu=a53-0] fsbl.elf
    [pmufw_image] pmufw.elf
    [destination_cpu=a53-0, exception_level=el-3, trustzone] bl31.elf
    [destination_cpu=a53-0, exception_level=el-2] u-boot.elf
}
bootgen -image boot.bif -arch zynqmp -o BOOT.BIN -w

Do not pass --fpga <file> for a file that does not exist. bootgen drops the FSBL silently and produces an image that boots to nothing at all.

3. Getting the boot image onto QSPI

Two routes. Use JTAG the first time, then the board can flash itself.

JTAG, from Vivado’s hardware manager. This works reliably only for images under about 2 MB, and larger ones hang partway, which is why the bitstream stays out of BOOT.BIN.

open_hw_manager
connect_hw_server -url localhost:3121
open_hw_target
create_hw_cfgmem -hw_device [lindex [get_hw_devices] 0] \
    [lindex [get_cfgmem_parts {mt25qu512-qspi-x4-single}] 0]
program_hw_cfgmem -hw_cfgmem [current_hw_cfgmem]

Self-flash from U-Boot is faster, and the only option for large images. Serve the file over TFTP from the host, then write it:

ZynqMP> setenv ipaddr 192.168.0.60; setenv serverip <your host's address>
ZynqMP> tftpboot 0x10000000 BOOT.BIN
ZynqMP> sf probe 0 0 0
ZynqMP> sf update 0x10000000 0 $filesize

Rule Never run saveenv. U-Boot’s environment offset on this board falls inside the region BOOT.BIN occupies, so saving the environment corrupts the boot image.

Rule Never cut power during a flash write.

4. Building a Linux image

There are two ways to produce the kernel, device tree and root filesystem. They end in the same place, an Image, a .dtb and a rootfs the board boots, and every section after this one is identical whichever you pick.

PetaLinux is the vendor flow. It wraps Yocto, hides most of it, and is the shortest route to a booting board. Take it if you want the board up today, or if you already have a PetaLinux project for another Xilinx design.

Yocto is the same machinery without the wrapper: upstream layers, a machine you define, recipes you can read. Take it if you are going to maintain this for years, need to pin exactly what goes into the image, or want a build a colleague can reproduce from a git hash.

Which path do you want to follow?

The rest of this page follows your choice. You can switch at any time.

Both build paths are shown below, each under its own label.

PetaLinux path

Put the kernel, device tree and root filesystem on eMMC, with a FAT boot partition holding Image, system.dtb and boot.scr, and an ext4 root.

Compile boot.cmd into the script U-Boot reads:

env set -f ethaddr 00:0a:35:24:07:fd
env set -f eth1addr 00:0a:35:24:07:fe
load mmc 0:1 0x18000000 Image
load mmc 0:1 0x14000000 system.dtb
setenv bootargs console=ttyPS1,115200 earlycon clk_ignore_unused cma=900M \
    root=/dev/mmcblk0p2 rw rootwait
booti 0x18000000 - 0x14000000
mkimage -c none -A arm64 -T script -d boot.cmd boot.scr

env set -f is not optional. U-Boot pre-populates a random, write-protected ethaddr; a plain setenv fails silently and the device-tree fixup then stamps that random MAC into the kernel. Your board gets a new address on every boot and DHCP reservations never stick.

Yocto path

Budget about 100 GB of disk and several hours for the first build. Put the tree on a volume with room, not a home partition that is nearly full.

Run the build in a container

On Ubuntu 24.04 BitBake refuses to start:

ERROR: User namespaces are not usable by BitBake, possibly due to AppArmor.

kernel.apparmor_restrict_unprivileged_userns=1 lets an unprivileged process create a user namespace but denies the write to /proc/self/uid_map that follows, which is what BitBake’s sanity check probes. Confirm it in one line:

unshare -r --user --net id -u
# unshare: write failed /proc/self/uid_map: Operation not permitted

The documented fix is a sysctl that weakens the whole machine. You do not need it. Running the build in Docker with seccomp off and AppArmor left on works with no root:

SettingResult
Docker defaultsunshare blocked by the seccomp profile
seccomp=unconfinedworks, because the docker-default AppArmor profile grants userns
seccomp=unconfined + apparmor=unconfinedfails again, back under the host restriction

Turning AppArmor off as well is the intuitive move and the wrong one: the container’s AppArmor profile is what grants the permission.

docker run --security-opt seccomp=unconfined \
  -v /mnt/build/yocto-krio:/mnt/build/yocto-krio \
  -w /mnt/build/yocto-krio -u "$(id -u):$(id -g)" krio-yocto:scarthgap bash

Mount the tree at its real host path, not at /work. BitBake bakes absolute paths into sstate signatures, so an identical path keeps the tree usable both inside the container and outside it.

Layers

Four repositories, all on scarthgap (Yocto 5.0 LTS):

for r in "git://git.yoctoproject.org/poky" \
         "https://github.com/Xilinx/meta-xilinx" \
         "https://github.com/openembedded/meta-openembedded" \
         "https://git.yoctoproject.org/meta-arm"; do
    git clone -b scarthgap "$r"
done

source poky/oe-init-build-env build
bitbake-layers add-layer ../meta-arm/meta-arm-toolchain
bitbake-layers add-layer ../meta-arm/meta-arm
bitbake-layers add-layer ../meta-xilinx/meta-xilinx-core
bitbake-layers add-layer ../meta-xilinx/meta-xilinx-bsp
bitbake-layers add-layer ../meta-openembedded/meta-oe
bitbake-layers add-layer ../meta-openembedded/meta-python
bitbake-layers add-layer ../meta-openembedded/meta-networking

meta-arm is not optional. From scarthgap onward meta-xilinx depends on it, and the error you get without it does not say so.

Define the machine

meta-xilinx ships no Kria machine. Its own conf/machine/README describes the intended chain (k26_kv → k26 → zynqmp-generic), but the middle is not in the layer, and zynqmp-ev-generic is marked obsolete. Define your own on top of zynqmp-generic, using the preamble and postamble that README specifies:

# meta-kria-custom/conf/machine/krio-k26.conf
MACHINEOVERRIDES =. "${@['', 'krio-k26:']['krio-k26' != '${MACHINE}']}"

require conf/machine/zynqmp-generic.conf

MACHINE_FEATURES += "mali400 vcu"
KERNEL_DEVICETREE = "xilinx/zynqmp-krio-k26.dtb"
SPL_BINARY = ""

PACKAGE_EXTRA_ARCHS:append = "${@['', ' krio_k26']['krio-k26' != "${MACHINE}"]}"

The preamble and postamble are what let sstate from a zynqmp-generic build carry over; only the kernel, bootloader and device-tree recipes rebuild.

Device tree from the kernel tree

linux-xlnx already ships the SOM. zynqmp-sm-k26-revA.dts describes the module (DDR, eMMC, QSPI, PMICs) and deliberately defines no GEM nodes, because those belong to the carrier.

Use sm-k26, not smk-k26: the starter-kit variant differs in exactly one respect, it disables sdhci0, and this SOM has that eMMC populated.

#include "zynqmp-sm-k26-revA.dts"

/ {
	model = "Kria K26 on KrIO carrier";
	compatible = "krio,k26-carrier", "xlnx,zynqmp-sm-k26-revA", "xlnx,zynqmp";
};

Add the Ethernet from the next section, then register the file with kbuild, because arm64 only builds device trees its directory Makefile lists:

# linux-xlnx_%.bbappend
do_configure:prepend() {
    dtsdir="${S}/arch/arm64/boot/dts/xilinx"
    install -m 0644 ${WORKDIR}/zynqmp-krio-k26.dts "$dtsdir/"
    if ! grep -q "zynqmp-krio-k26.dtb" "$dtsdir/Makefile"; then
        echo 'dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-krio-k26.dtb' >> "$dtsdir/Makefile"
    fi
}

Do not enable uart0. The console is uart1/ttyPS1, which the SOM file already enables. The PMU firmware in QSPI refuses uart0’s power domain, so enabling it buys two errors per boot and nothing else:

genpd_provider domain0: domain0 request failed for node 33: -13
xuartps ff000000.serial: probe with driver xuartps failed with error -13

Image configuration

Two settings in local.conf matter. The board boots from the BOOT.BIN already in QSPI, so Yocto must not try to build a boot image, which would need an FSBL and PMU firmware from the Xilinx tools:

EXTRA_IMAGEDEPENDS:remove = "virtual/boot-bin virtual/fsbl virtual/pmu-firmware"

And zynqmp-generic.conf appends wic.qemu-sd to IMAGE_FSTYPES with +=, parsing after local.conf. A plain assignment will not remove it:

IMAGE_FSTYPES = "tar.gz ext4 cpio.gz"
IMAGE_FSTYPES:remove = "wic.qemu-sd"

Check both before starting a long build. This takes minutes and saves hours:

bitbake -e kria-custom-image > env.txt
grep -E '^(EXTRA_IMAGEDEPENDS|IMAGE_FSTYPES|KERNEL_DEVICETREE|MACHINE)=' env.txt
bitbake kria-custom-image

Set BB_NUMBER_THREADS and PARALLEL_MAKE from your memory, not your core count. On a 24-core machine with 30 GB, six is right; matching the cores gets the build OOM-killed.

Booting an image without writing to the board

Whichever path you took, you can test an image entirely from RAM before committing it to eMMC or QSPI. Nothing on the board is modified, and recovery from a bad image is a power cycle. Build a cpio.gz rootfs and put the three files where TFTP can serve them, in a subdirectory so you do not overwrite images already being served. At the U-Boot prompt:

setenv serverip <your host>
setenv ipaddr 192.168.0.45
setenv bootargs 'console=ttyPS1,115200 root=/dev/ram0 rw earlycon'
tftpboot 0x00200000 yocto/Image
tftpboot 0x04000000 yocto/krio.dtb
tftpboot 0x06000000 yocto/rootfs.cpio.gz
booti 0x00200000 0x06000000:0x4202a2e 0x04000000

booti needs the ramdisk as address:size because a raw cpio.gz carries no uImage header. Use the size U-Boot prints as Bytes transferred, and check it against the file you staged. A truncated initramfs panics in a way that looks like a kernel bug.

Derive serverip, do not remember it. Your host’s wired address is a DHCP lease and it moves. U-Boot’s only symptom for a wrong serverip is TFTP server died; starting again, which reads like a server fault. ip route get <board-ip> gives you the address that actually routes.

5. Ethernet

Both ports work, but the device tree has to describe them correctly, and two independent mistakes each produce a silent failure.

&gem1 {
    phy-handle = <&phy0>;
    phy-mode = "rgmii-id";
    mdio {
        #address-cells = <1>;
        #size-cells = <0>;
        phy0: phy@0 {
            /* The ethernet-phy-id string MUST come first. Linux matches the
               first compatible entry, and a generic driver on a DP83867 leaves
               the RGMII delays unconfigured. */
            compatible = "ethernet-phy-id2000.a231", "ti,dp83867";
            reg = <0x0>;
            device_type = "ethernet-phy";
            ti,rx-internal-delay = <0x8>;
            ti,tx-internal-delay = <0x8>;
            ti,fifo-depth = <0x1>;
        };
        phy1: phy@5 { /* ... same, reg = <0x5> ... */ };
    };
};

The second PHY is at address 5, not 1. The schematic annotates it wrongly; the silicon straps to 0x5. The DP83867 uses four-level straps, so one divider does not encode one address bit.

PetaLinux path

This goes in project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi, which PetaLinux merges over the device tree generated from your XSA.

Yocto path

This goes in the carrier .dts from the previous section, alongside the #include "zynqmp-sm-k26-revA.dts". The SOM file defines no GEM nodes at all, so these are additions rather than overrides.

6. Building logic the board will accept

Your PL design’s PS block never configures anything, because the FSBL already did that before Linux started. What it must do is agree with the FSBL that ran. Disagree, and the fabric gets no clock, or decodes at an address with nothing routed to it.

SettingValue
PSU__CRL_APB__PL0_REF_CTRL__FREQMHZ100
PSU__FPGA_PL0_ENABLE1
PSU__USE__M_AXI_GP01
PSU__MAXIGP0__DATA_WIDTH128

Assert them in the build rather than trusting them:

dict for {k want} $PS_CONTRACT {
    if {[get_property CONFIG.$k [get_bd_cells ps]] ne $want} {
        error "PS setting $k disagrees with the fielded FSBL"
    }
}

PL peripherals live in the 0xA000_0000 aperture reached through M_AXI_HPM0_FPD.

Convert the bitstream for run-time loading:

printf 'all:\n{\n\t[destination_device = pl] design.bit\n}\n' > design.bif
bootgen -image design.bif -arch zynqmp -process_bitstream bin -w

7. Loading and switching firmware from Linux

sudo fpgautil -b /lib/firmware/krio/mydesign/design.bit.bin      # ~135 ms

The K26 holds one PL design at a time. If the board has more than one job, use krio-fw, which sequences the switch: stop the outgoing design’s services, swap the bitstream, start the incoming design’s. That ordering is the point, because a process holding a UIO node or a /dev/mem window on the outgoing design is pointing into fabric that is about to stop existing.

krio-fw list
sudo krio-fw load mydesign
sudo krio-fw default tdc         # what loads at next power-on

A slot is a directory under /lib/firmware/krio/ with four lines:

DESC="What this design is"
BIN=/lib/firmware/krio/mydesign/design.bit.bin
DTBO=
SERVICES=myservice.service

Anything written to the root filesystem needs an explicit sync before you cut power. There is no orderly shutdown on this board.

Rule Never touch a PL address when the fabric holds no matching design. It wedges the AXI bus and only a power cycle recovers. krio-fw status says what is loaded.

8. Talking to your logic

AXI4-Lite slaves need no driver and no device-tree node, just /dev/mem and a pointer:

int fd = open("/dev/mem", O_RDWR | O_SYNC);
volatile uint32_t *reg = mmap(NULL, 0x10000, PROT_READ | PROT_WRITE,
                              MAP_SHARED, fd, 0xA0040000);
reg[0] = 1;                       /* write a control register */

UIO is the alternative when you want interrupts or a named device. It needs a device-tree node and a kernel built with CONFIG_UIO_PDRV_GENIRQ, plus uio_pdrv_genirq.of_id=generic-uio,ui_pdrv on the kernel command line. Find devices by name rather than by number, because the PS performance monitors claim uio0 through uio3:

for d in /sys/class/uio/uio*; do
    printf '%s %s %s\n' "$(basename $d)" "$(cat $d/name)" "$(cat $d/maps/map0/addr)"
done

Measured cost of AXI4-Lite from userspace on this board, worth knowing before you design an interface: a posted write is about 7.6 ns, a blocking read about 200 ns. Writes are nearly free; reads, including polling a done flag, are not.

Troubleshooting

SymptomCause
Silent board, no console output at allWarm reset was attempted, or bootgen dropped the FSBL
Board boots but gets a new MAC every timesetenv ethaddr used instead of env set -f
Ethernet link comes up, no trafficPHY compatible string order, or PHY address 1 instead of 5
JTAG flash hangs partwayImage over ~2 MB; drop the bitstream or self-flash over TFTP
Everything hangs on first PL accessFabric holds no matching design; power cycle
sf reads garbage JEDEC IDsQSPI left in a non-power-on state; power cycle
Settings lost after power cutNo sync before power was removed

Next

With the board booting and loading your logic, put a neural network on it.