Digilent Arty Z7 (Zynq-7000)

Introduction

The Digilent Arty Z7 is a popular entry into the Zynq-7000 world, but as far as current software support goes, it does not exist: there is no devicetree for it in mainline Linux or U-Boot, no machine in meta-xilinx, and Digilent’s own Yocto layer dates back to the jethro era. The vendor-blessed path to a bootable image runs through Vivado, exporting a hardware description and building the FSBL from it. That is a multi-gigabyte tool installation for a board whose hardware configuration has not changed since 2016.

This tutorial takes the other path. We bring the board up on plain upstream Yocto scarthgap with the meta-xilinx zynq-generic machine, let U-Boot SPL replace the FSBL so that no Vivado output is needed anywhere in the build, and then add the standard Mender A/B integration on top. The result is a reproducible, commit-pinned build with exactly two board-specific inputs: the generated PS initialization code and the devicetree.

The board here is the Arty Z7-20. The approach applies to any Zynq-7000 board in the same situation, and the U-Boot environment pitfall in the Mender section applies to all of them.

The board


Image source: https://digilent.com/shop/arty-z7-zynq-7000-soc-development-board/

  • Vendor: Digilent Arty Z7
  • SoC: Zynq-7000 (xc7z020clg400-1), dual Cortex-A9 processing system plus Artix-7 programmable logic, 512 MB DDR3
  • Yocto MACHINE: arty-z7-20
  • Config in mender-community-images: yocto/scarthgap/{tagged,floating}/arty-z7-20.yml

Test results

Built and runtime-verified on the following Yocto Project release:

Yocto Project Build Runtime
scarthgap (5.0 LTS)

Build means the Yocto build completes and produces images. Runtime means a Mender deployment installs, reboots, commits, and rolls back on the board.

The verified component set:

Component Version
meta-xilinx scarthgap branch, XILINX_RELEASE_VERSION = "v2025.1" (u-boot-xlnx 2025.01, linux-xlnx 6.12)
meta-mender scarthgap branch, Mender client 6.0.0 (mender-update)
Board Digilent Arty Z7-20 (Zynq-7000 xc7z020clg400-1, 512 MB DDR3)
Build setup kas 5.1 (optional; the kas file below translates 1:1 to a bblayers.conf/local.conf setup)

Prerequisites

  • A Yocto-capable build host.
  • A Mender server; this tutorial uses hosted Mender.
  • The board facts, gathered from schematics or vendor sources: console UART, DDR configuration, PS clock, ethernet PHY. For the Arty Z7-20 these are collected below, for other boards this is the actual research work of the bring-up.

Goal

A close-to-upstream Yocto build that boots the Arty Z7-20 from SD card to a login prompt with working ethernet, built without any vendor tool. On top of it sits the standard Mender integration: A/B rootfs layout, redundant U-Boot environment, and verified rollback.

The starting point

What exists for this board:

  • Digilent’s Petalinux BSP, Petalinux-Arty-Z7-20, which contains the generated PS initialization files (ps7_init_gpl.c/.h, HSI 2017.4).
  • A devicetree in Digilent’s 2016 U-Boot fork, u-boot-digilent (arch/arm/dts/zynq-artyz7.dts).

A tempting shortcut is the in-tree sibling Zybo Z7, which shares the xc7z020. Its files are not reusable, though: the Zybo runs a 33.33 MHz PS clock where the Arty has 50 MHz, a 32-bit/1 GB DDR3 configuration where the Arty has 16-bit/512 MB, and its console is UART1 where the Arty uses UART0. Zynq PS initialization is board-level, not SoC-level. For the same reason, owners of the smaller Arty Z7-10 should take their ps7_init_gpl files from Digilent’s Petalinux-Arty-Z7-10 BSP rather than reusing the Z7-20 ones shown here; everything else in this tutorial carries over.

So the board facts, cross-checked between the two Digilent sources:

  • Console: UART0 (ttyPS0, 115200)
  • Memory: 512 MB DDR3, 16-bit
  • PS clock: 50 MHz
  • Ethernet: GEM0, RGMII-ID, Realtek RTL8211E or F (board-revision dependent) at MDIO address 1
  • No factory-programmed MAC address (more on that below)
  • USB0 host with a usb-nop-xceiv reset on GPIO 46

Booting a Zynq without the FSBL

The classic Zynq boot chain is BootROM → FSBL → U-Boot, where the FSBL is built from Vivado’s exported hardware description. U-Boot SPL can take the FSBL’s place entirely: it links the same generated ps7_init_gpl.c and produces boot.bin itself. The boot flow becomes:

BootROM -> boot.bin (U-Boot SPL + ps7_init) -> u-boot.img -> kernel + dtb

meta-xilinx supports this through the zynq-generic machine, it just needs to be told where the PS initialization comes from. The board support lives in a small custom layer with this layout:

meta-arty-z7/
├── conf/
│   ├── layer.conf
│   └── machine/arty-z7-20.conf
└── recipes-bsp/
    ├── device-tree/
    │   ├── device-tree.bbappend
    │   └── files/zynq-arty-z7-20.dts
    ├── platform-init/
    │   ├── platform-init.bbappend
    │   └── platform-init/arty-z7-20/ps7_init_gpl.{c,h}
    └── u-boot/
        ├── u-boot-xlnx_%.bbappend
        └── u-boot-xlnx/
            ├── 0001-arm-dts-add-zynq-arty-z7-20.patch
            └── arty-z7-20.cfg

The conf/layer.conf is entirely conventional:

BBPATH .= ":${LAYERDIR}"

BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
            ${LAYERDIR}/recipes-*/*/*.bbappend"

BBFILE_COLLECTIONS += "arty-z7"
BBFILE_PATTERN_arty-z7 = "^${LAYERDIR}/"
BBFILE_PRIORITY_arty-z7 = "7"

LAYERDEPENDS_arty-z7 = "core xilinx"
LAYERSERIES_COMPAT_arty-z7 = "scarthgap"

The machine configuration, conf/machine/arty-z7-20.conf:

#@TYPE: Machine
#@NAME: arty-z7-20
#@DESCRIPTION: Digilent Arty Z7-20 (Zynq-7000 xc7z020, 512 MB DDR3, console ttyPS0/UART0)

require conf/machine/zynq-generic.conf

# Boot flow: BootROM -> spl/boot.bin (U-Boot SPL + Arty ps7_init, replaces the
# FSBL) -> u-boot.img -> boot.scr -> uImage + system.dtb from the FAT partition.
SPL_BINARY = "spl/boot.bin"
# The weak default (xilinx-bootbin) needs a Vivado-built FSBL and silently
# clears SPL_BINARY otherwise; u-boot-xlnx must provide boot.bin itself.
PREFERRED_PROVIDER_virtual/boot-bin = "u-boot-xlnx"
# xilinx_zynq_virt_defconfig is listed in HAS_PLATFORM_INIT, so the ps7_init
# injection from virtual/xilinx-platform-init has to be forced.
FORCE_PLATFORM_INIT = "1"
# the u-boot build tree names its ELF plainly "u-boot"; the Xilinx include
# default (u-boot.elf) does not exist in this flow
UBOOT_ELF = "u-boot"
# SPL loads CONFIG_SPL_FS_LOAD_PAYLOAD_NAME = u-boot.img, not u-boot.bin
UBOOT_SUFFIX = "img"
# U-Boot control device tree, added to the u-boot-xlnx source by our patch
UBOOT_USER_SPECIFIED_DTS = "zynq-arty-z7-20"

# Runtime device tree from the out-of-tree device-tree recipe; the stock
# u-boot-xlnx-scr boot script loads /system.dtb from the boot partition.
PREFERRED_PROVIDER_virtual/dtb = "device-tree"
CONFIG_DTFILE = "zynq-arty-z7-20.dts"

IMAGE_BOOT_FILES += "boot.bin"

# Plain SD card image.
IMAGE_FSTYPES = "tar.gz wic wic.bmap"

(The Mender-specific additions to this file follow in the Mender section.)

The PS initialization files are taken verbatim from Digilent’s BSP, Arty-Z7-20/project-spec/hw-description/ps7_init_gpl.{c,h}, and handed to meta-xilinx’s platform-init recipe through a bbappend, with the files placed in platform-init/arty-z7-20/ next to it:

FILESEXTRAPATHS:prepend := "${THISDIR}/platform-init:"

# ps7_init_gpl.c/.h are taken verbatim from Digilent's own BSP:
# github.com/Digilent/Petalinux-Arty-Z7-20 @ 43dca7d (HSI 2017.4), file
# Arty-Z7-20/project-spec/hw-description/ps7_init_gpl.{c,h}.
COMPATIBLE_MACHINE:arty-z7-20 = "arty-z7-20"

# The base recipe points LIC_FILES_CHKSUM at common-licenses/GPL-2.0, which
# modern poky no longer ships (renamed to GPL-2.0-only/-or-later in honister).
# The shipped ps7_init_gpl.c/.h carry the Xilinx GPL-2.0-or-later header.
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-2.0-or-later;md5=fed54355545ffd980b814dab4a3b312c"

The license checksum override works around a real bug in meta-xilinx scarthgap: the recipe references a license file that poky removed several releases ago, so it fails do_populate_lic for everyone who actually uses it.

Two devicetrees

The build carries the board devicetree twice: once as the U-Boot control devicetree (added to the u-boot-xlnx source by a patch, since SPL and U-Boot proper are configured by it), and once for the kernel at runtime, compiled by meta-xilinx’s out-of-tree device-tree recipe as system.dtb. Both are derived from Digilent’s 2016 zynq-artyz7.dts, restructured after the mainline zynq-zybo-z7.dts. The runtime version:

// SPDX-License-Identifier: GPL-2.0+
/*
 * Digilent Arty Z7-20 board (Zynq-7000 xc7z020, 512 MB DDR3)
 *
 * Copyright (C) 2016 Digilent
 */
/dts-v1/;
#include "zynq-7000.dtsi"
#include <dt-bindings/gpio/gpio.h>

/ {
	model = "Digilent Arty Z7-20 board";
	compatible = "digilent,zynq-artyz7", "xlnx,zynq-7000";

	aliases {
		ethernet0 = &gem0;
		serial0 = &uart0;
		spi0 = &qspi;
		mmc0 = &sdhci0;
	};

	memory@0 {
		device_type = "memory";
		reg = <0x0 0x20000000>;
	};

	chosen {
		stdout-path = "serial0:115200n8";
	};

	usb_phy0: phy0 {
		#phy-cells = <0>;
		compatible = "usb-nop-xceiv";
		reset-gpios = <&gpio0 46 GPIO_ACTIVE_LOW>;
	};
};

&clkc {
	ps-clk-frequency = <50000000>;
};

&gem0 {
	status = "okay";
	phy-mode = "rgmii-id";
	phy-handle = <&ethernet_phy>;
	/* No factory MAC on the board: fixed locally administered address */
	local-mac-address = [02 44 5a 37 32 30];

	ethernet_phy: ethernet-phy@1 { /* RTL8211E or F, board-revision dependent */
		reg = <1>;
	};
};

&qspi {
	status = "okay";
};

&sdhci0 {
	status = "okay";
};

&uart0 {
	status = "okay";
};

&usb0 {
	status = "okay";
	dr_mode = "host";
	usb-phy = <&usb_phy0>;
};

The U-Boot copy is identical except for bootph-all markers on &uart0 and &sdhci0, which keep those nodes in the SPL devicetree; the SoC-level nodes SPL also needs (slcr, clock controller, timer) already carry their markers in U-Boot’s zynq-7000.dtsi, so two board-level markers are all it takes. If you maintain both copies by hand as done here, put a comment in each pointing at the other: the two values that must never diverge are the MAC address and the PHY address.

The MAC address deserves a word. The Arty Z7 has no factory-programmed MAC. With nothing set, every boot invents a random one, which means a new DHCP lease on every boot and, once Mender enters the picture, a new device identity on every boot. A fixed locally administered address (second-least-significant bit of the first octet set, 02:...) in the devicetree solves both: U-Boot picks it up from its control devicetree, the kernel from system.dtb, and it doubles as a stable Mender device identity.

Two glue pieces complete the picture. The device-tree recipe defaults its kernel include path to empty, so a bbappend restores it. The dts above includes zynq-7000.dtsi and dt-bindings headers from the kernel source:

FILESEXTRAPATHS:prepend := "${THISDIR}/files:"

KERNEL_INCLUDE:arty-z7-20 = " \
    ${STAGING_KERNEL_DIR}/arch/${ARCH}/boot/dts \
    ${STAGING_KERNEL_DIR}/arch/${ARCH}/boot/dts/* \
    ${STAGING_KERNEL_DIR}/scripts/dtc/include-prefixes \
    "

And the u-boot-xlnx bbappend adds the control devicetree patch plus a config fragment. The patch must add two things: the dts as arch/arm/dts/zynq-arty-z7-20.dts, and its entry in the dts Makefile:

dtb-$(CONFIG_ARCH_ZYNQ) += \
	zynq-arty-z7-20.dtb \

That Makefile line is easy to forget. Without it the dtb is never built, and the build dies with a kbuild “no rule to make target” error that does not exactly shout “you forgot a Makefile hunk”.

xilinx_zynq_virt_defconfig builds a multi-dtb U-Boot with seventeen boards in CONFIG_OF_LIST and defaults to the zc706; the config fragment prunes the list to the one devicetree we actually have:

FILESEXTRAPATHS:prepend := "${THISDIR}/u-boot-xlnx:"

SRC_URI:append:arty-z7-20 = " \
    file://0001-arm-dts-add-zynq-arty-z7-20.patch \
    file://arty-z7-20.cfg \
    "

# With UBOOT_SUFFIX = "img" (SPL payload) no /boot/u-boot*.bin exists, which
# leaves ${PN}-bin empty while ${PN} RDEPENDS on it; let it take the .img.
FILES:${PN}-bin:append:arty-z7-20 = " /boot/u-boot*.img"
CONFIG_DEFAULT_DEVICE_TREE="zynq-arty-z7-20"
CONFIG_OF_LIST="zynq-arty-z7-20"

With this in place, bitbake core-image-minimal produces a .wic image that boots the board from SD to a login prompt: BootROM loads SPL from boot.bin, SPL runs the Digilent ps7_init, loads u-boot.img, and U-Boot boots the kernel with system.dtb. Not a single Vivado-generated artifact appears anywhere in the chain.

Adding Mender

The Mender integration follows the standard Yocto setup with MENDER_FEATURES_ENABLE = "mender-uboot mender-image-sd": an A/B rootfs pair on the SD card, a data partition, and the redundant U-Boot environment at raw offsets. Kept in a separate layer so the plain bring-up build stays untouched, the configuration is unspectacular:

INIT_MANAGER = "systemd"
INHERIT += "mender-full"
MENDER_FEATURES_ENABLE:append = " mender-uboot mender-image-sd"
MENDER_FEATURES_DISABLE:append = " mender-grub mender-image-uefi"
# meta-mender depends on "u-boot"; our fork provides it (bbappend adds PROVIDES)
PREFERRED_PROVIDER_u-boot = "u-boot-xlnx"
MENDER_ARTIFACT_NAME = "arty-z7-20-v1"

Because the bootloader is a fork (u-boot-xlnx) rather than upstream U-Boot, meta-mender’s bbappend does not attach to it; the fork integration is a bbappend of our own, following the pattern of the meta-mender-community board layers:

# Mender U-Boot integration for u-boot-xlnx (meta-mender only appends to the
# plain "u-boot" recipe). Pattern per meta-mender-community fork integrations.
require recipes-bsp/u-boot/u-boot-mender.inc

# meta-mender depends on "u-boot" in several places (EXTRA_IMAGEDEPENDS,
# part-image deps, libubootenv do_compile); make the fork satisfy them.
PROVIDES += "u-boot"
RPROVIDES:${PN} += "u-boot"

FILESEXTRAPATHS:prepend := "${THISDIR}/u-boot-xlnx:"

# meta-mender's 0002 patch only applies with fuzz against u-boot-xlnx
# 2025.01 (patch-fuzz QA is fatal); carry a rebased copy instead. The env
# patch is numbered 0004: meta-mender's 0001 and 0003 apply before it.
SRC_URI:remove:arty-z7-20 = "file://0002-Integration-of-Mender-boot-code-into-U-Boot.patch"

SRC_URI:append:arty-z7-20 = " \
    file://0002-Integration-of-Mender-boot-code-into-U-Boot-xlnx-2025.01.patch \
    file://0004-zynq-env-get-location-mmc.patch \
    file://mender-bootargs.cfg \
    "

The rebased integration patch is nothing exotic: it is meta-mender’s own boot-code patch, refreshed. Apply meta-mender-core/recipes-bsp/u-boot/patches/0002-Integration-of-Mender-boot-code-into-U-Boot.patch to a u-boot-xlnx checkout, fix up the offset-shifted hunks, and export the result with git format-patch. The remaining two files are where the Zynq-specific substance is.

The environment pitfall

This is the part that will bite anyone doing Mender on a Zynq-7000, and it bites silently. Mender’s A/B mechanism lives in the U-Boot environment: mender_boot_part, upgrade_available and the boot counter are written by the client through libubootenv and read by U-Boot on the next boot. With mender-uboot, that environment sits at raw MMC offsets (CONFIG_ENV_IS_IN_MMC), redundantly.

The Zynq board code, however, overrides env_get_location(), and its SD-boot case only ever considers FAT and EXT4 environment backends. CONFIG_ENV_IS_IN_MMC is silently ignored and the function falls through to ENVL_NOWHERE, so U-Boot happily boots with its built-in default environment on every single boot. The failure mode is nasty precisely because everything else works: the image boots, the client connects, a deployment installs and reboots the device, which comes back on the old rootfs, because U-Boot never saw upgrade_available=1. The deployment reports a rollback that was never supposed to happen, and nothing in any log says why.

The fix is a two-line patch to board/xilinx/zynq/board.c:

--- a/board/xilinx/zynq/board.c
+++ b/board/xilinx/zynq/board.c
@@ -147,6 +147,8 @@ enum env_location env_get_location(enum env_operation op, int prio)
 			return ENVL_FAT;
 		if (IS_ENABLED(CONFIG_ENV_IS_IN_EXT4))
 			return ENVL_EXT4;
+		if (IS_ENABLED(CONFIG_ENV_IS_IN_MMC))
+			return ENVL_MMC;
 		return ENVL_NOWHERE;
 	case ZYNQ_BM_NAND:
 		if (IS_ENABLED(CONFIG_ENV_IS_IN_NAND))

Be careful how you verify this: fw_printenv proves nothing. It reads the raw offsets directly through /etc/fw_env.config and works perfectly on a broken build. Since the image is created with mender_boot_part=2 and an unpatched U-Boot boots the first slot from its built-in defaults, the values even agree. The conclusive check is a single line in the U-Boot console output. Broken:

Loading Environment from nowhere... OK

Fixed:

Loading Environment from MMC... OK

For a second opinion, set a scratch variable from Linux with fw_setenv, reboot, and interrupt autoboot: printenv at the U-Boot prompt must show it.

The kernel command line

The stock meta-xilinx boot flow sets the kernel command line in boot.scr; the Mender boot code replaces the boot script entirely and constructs the root device itself, so the rest of the command line has to come from the U-Boot configuration:

CONFIG_USE_BOOTARGS=y
CONFIG_BOOTARGS="console=ttyPS0,115200 rootwait"

Machine configuration additions

The Mender image types change where files live: the kernel and system.dtb move into the rootfs /boot of each A/B slot, and the FAT boot partition carries only SPL and its payload. Gated behind the Mender overrides, in the same machine conf as above:

IMAGE_BOOT_FILES:mender-uboot = "boot.bin u-boot.img"
# The runtime dtb is not kernel-built (KERNEL_DEVICETREE unset): name it for
# the Mender boot code and ship the device-tree package in the rootfs.
MENDER_DTB_NAME_FORCE:mender-uboot = "system.dtb"
MACHINE_ESSENTIAL_EXTRA_RDEPENDS:append:mender-image = " device-tree"
# mender-image would add kernel-devicetree, which is never emitted here
MACHINE_ESSENTIAL_EXTRA_RDEPENDS:remove:mender-image = "kernel-devicetree"
IMAGE_FSTYPES:remove:mender-image = "wic wic.bmap"
# the Mender boot code replaces boot.scr entirely; stop building the stock one
EXTRA_IMAGEDEPENDS:remove:mender-image = "u-boot-xlnx-scr"
# bootimg populates from DEPLOY_DIR_IMAGE
do_image_bootimg[depends] += "virtual/boot-bin:do_deploy virtual/bootloader:do_deploy"

The overrides only exist when the Mender features are enabled, so the plain bring-up build is unaffected by this block.

Pinning it down with kas

Reproducibility is cheap to add and expensive to skip. The whole setup is driven by a kas configuration with every upstream repository pinned to a commit; the essential parts:

header:
  version: 14

machine: arty-z7-20
distro: poky
target: core-image-minimal

repos:
  workspace:
    layers:
      meta-arty-z7:

  poky:
    url: https://git.yoctoproject.org/poky
    branch: scarthgap
    commit: e146cbbc73e10fecfb7f1c26e8567a5a2873c51b
    layers:
      meta:
      meta-poky:

  # hard LAYERDEPENDS of meta-xilinx-core
  meta-arm:
    url: https://git.yoctoproject.org/meta-arm
    branch: scarthgap
    commit: 23b572c40ea6709ab3f8bd7abbb5a795fc3631c2
    layers:
      meta-arm:
      meta-arm-toolchain:

  meta-xilinx:
    url: https://github.com/Xilinx/meta-xilinx
    branch: scarthgap
    commit: 936845b97ab1ff035108f219f4e7cfef5401eb02
    layers:
      meta-xilinx-core:

local_conf_header:
  base: |
    # Pin the xlnx component set (linux-xlnx 6.12, u-boot-xlnx 2025.01) so a
    # future meta-xilinx default bump cannot change component versions.
    XILINX_RELEASE_VERSION = "v2025.1"

One kas construct here deserves a warning: the workspace repository has no url, which tells kas to use the enclosing git repository, the one the kas file itself lives in. This is what lets the kas configuration and meta-arty-z7 travel together in a single repository, but it only works when kas is invoked from inside a git checkout; in a bare directory it fails with a rather unhelpful error about the repository path.

The Mender pieces live in a second kas file, whose local_conf_header carries exactly the configuration block from the Mender section:

header:
  version: 14

repos:
  workspace:
    layers:
      meta-arty-z7-mender:

  meta-mender:
    url: https://github.com/mendersoftware/meta-mender.git
    branch: scarthgap
    commit: de0f577c0e612afeed22c06015d590ade6ea0b96
    layers:
      meta-mender-core:

  # the C++ mender client DEPENDS on lmdb and RDEPENDS on jq, both meta-oe
  meta-openembedded:
    url: https://git.openembedded.org/meta-openembedded
    branch: scarthgap
    commit: 29a044218285fdc7fcdd63d5f0929cb3a27b6fed
    layers:
      meta-oe:

local_conf_header:
  mender: |
    # ... the INIT_MANAGER / mender-full block shown above ...

With that,

kas build kas/arty-z7-20.yml

builds the plain bring-up image and

kas build kas/arty-z7-20.yml:kas/mender.yml

builds the Mender-enabled one, producing core-image-minimal-arty-z7-20.sdimg for the initial flash and .mender as the deployable artifact.

Seeing it work

After flashing the sdimg and booting, the device requests authorization on the Mender server with the fixed MAC as its identity. Accept it, and the inventory reports device_type: arty-z7-20 with client 6.0.0. From there it is the standard flow: upload the .mender artifact of the next build (with a bumped MENDER_ARTIFACT_NAME), create the deployment, and watch the device switch rootfs slots: install to the inactive partition, reboot, commit. On this board the full cycle takes about three minutes, and pulling power mid-update leaves the device booting the old rootfs, which is the entire point of the exercise.

Conclusion

Bringing up a Zynq-7000 board with no current vendor support turned out to require exactly two board-specific inputs, the generated ps7_init_gpl files and a devicetree, both extractable from old vendor sources, plus one genuinely non-obvious fix. That fix is the env_get_location() patch, without which the Mender integration appears to work right up until the first real deployment quietly rolls back.

With the A/B foundation in place, the same board can update more than its rootfs: building an FPGA bitstream for the programmable logic with the open source openXC7 toolchain and packaging it as a Mender Artifact is covered in the follow-up tutorial “Building and packaging FPGA bitstreams for the Zynq-7000 without Vivado”.

References

Known issues

  • Board support is out-of-tree and experimental: there is no mainline or meta-xilinx machine for the Arty Z7, so the machine, devicetree and PS initialization live in the meta-mender-xilinx layer rather than upstream.
  • The ethernet PHY is board-revision dependent (Realtek RTL8211E on earlier boards, RTL8211F on later ones). Both answer at MDIO address 1 and work with the single rgmii-id devicetree node shown here, so no revision-specific handling is needed, but the PHY marking on the board will differ between revisions.
  • The locally administered MAC address is baked into the image, so every unit flashed from the same build shares it. For more than one board on a network, override local-mac-address per device or provision a MAC at first boot.