work / aurascope / step 02

STEP 02 · TAG v0.2-custom-recipe · DONE

Custom image & recipe

// phase 1 · foundation · Yocto 5.0 "Scarthgap" · meta-xilinx rel-v2025.2 · MACHINE=aurascope-arty-z7

The goal

Step 01 got a machine generated from the real .xsa, but nothing had actually built yet. meta-aurascope was still just a layer skeleton — conf/layer.conf and nothing else — and local.conf still defaulted MACHINE to qemux86-64. The goal here was bitbake aurascope-image succeeding for the real board, not the QEMU stand-in.

The image recipe

meta-aurascope/recipes-core/images/aurascope-image.bb, modeled directly on poky's core-image-minimal.bb:

SUMMARY = "AuraScope base image for the Arty Z7-20 (Zynq-7000)"
LICENSE = "MIT"
inherit core-image
IMAGE_INSTALL = "packagegroup-core-boot ${CORE_IMAGE_EXTRA_INSTALL}"
IMAGE_ROOTFS_SIZE ?= "8192"
IMAGE_ROOTFS_EXTRA_SPACE:append = "..."

Setting MACHINE = "aurascope-arty-z7" in build/conf/local.conf was enough on its own to get this building for QEMU. Pointing it at the real board is where it got interesting.

Bug: XILINX_WITH_ESW ordering in the generated machine conf

First real-board parse failed with virtual/dtb, fsbl, and virtual/bitstream all reporting "Nothing PROVIDES". The cause was inside the generated build/conf/machine/aurascope-arty-z7.conf itself:

# Required generic machine inclusion
require conf/machine/zynq-generic.conf     <- was BEFORE this

# This is an 'XSCT' based BSP
XILINX_WITH_ESW = "xsct"                   <- ...this

zynq-generic.conf's own comment says hardware-specific variables must be set before its require, because several PREFERRED_PROVIDER defaults in meta-xilinx-core/conf/machine/include/machine-xilinx-default.inc key off XILINX_WITH_ESW at parse time:

PREFERRED_PROVIDER_virtual/dtb ??= "${@'device-tree' if d.getVar('XILINX_WITH_ESW') else ''}"
Not a one-off typo: I confirmed this is an actual upstream bug by reading gen-machineconf's own source (meta-xilinx-core/gen-machine-conf/lib/yocto_machine.py, YoctoXsctConfigs()). Its own comment states the same ordering requirement — then the function emits the require line before XILINX_WITH_ESW = "xsct" anyway. It reappears every time gen-machineconf regenerates the file; I had to reapply the fix after re-running it.

Fix: move the XILINX_WITH_ESW line above the require line.

I re-ran gen-machineconf once meta-aurascope actually had a recipes-* directory to work with — the earlier run had warned "No bb files in default matched BBFILE_PATTERN_meta-aurascope", which is why it never dropped anything useful into the layer the first time:

./meta-xilinx/meta-xilinx-core/gen-machine-conf/gen-machineconf \
  --soc-family zynq \
  --hw-description aurascope/hw/design_2_wrapper.xsa \
  --machine-name aurascope-arty-z7 \
  --xsct-tool /opt/2025.2/Vitis

Device tree

device-tree.bb (in meta-xilinx-core) self-skips unless CONFIG_DTFILE or SYSTEM_DTFILE is set:

def check_devicetree_variables(d):
    if not d.getVar('CONFIG_DTFILE'):
        raise bb.parse.SkipRecipe(...)

Nothing in the "xsct" flow (used for Zynq-7000 — System Device Tree / lopper-based generation is ZynqMP+/Versal only) ever sets this. The YAML_CONSOLE_DEVICE_CONFIG / YAML_MAIN_MEMORY_CONFIG variables that gen-machineconf writes into the machine conf for "xsct" mode are, as far as I can tell, dead in this meta-xilinx release — I grepped the whole tree and nothing outside gen-machineconf itself reads them.

Rather than hand-write a full Zynq-7000 SoC-level device tree from scratch (real risk of subtly wrong clock/interrupt indices, hard to debug without hardware in hand), I added:

The bbappend re-enables devicetree.bbclass's normal KERNEL_INCLUDE default (device-tree.bb blanks it out by default — "Since we're not based on a kernel default to nothing"), so system-top.dts can #include "zynq-7000.dtsi" and pull in the real, maintained dtsi shipped by linux-xlnx instead of a hand-copied one. This also auto-adds a do_compile dependency on virtual/kernel:do_configure, so the kernel source is staged first.

system-top.dts itself is intentionally minimal for now: a memory node sized for the Arty Z7-20's 1 GiB DDR3L, chosen/stdout-path set to ttyPS0, uart0 forced on. No PL/AXI-GPIO nodes yet — see the bitstream section below.

Unverified assumption at the time: I assumed the dtsi filename zynq-7000.dtsi matches what linux-xlnx ships — it's the mainline/mostly-universal name across Xilinx forks, but I hadn't confirmed it against this specific branch yet. Update: it resolved cleanly — see the boot bring-up below for what the real build actually needed on top of this minimal file.

Bitstream — deferred, not solved

xilinx-bootbin (builds BOOT.bin) pulls in virtual/bitstream by default via BIF_BITSTREAM_ATTR, which defaults to "bitstream" unless MACHINE_FEATURES contains "fpga-overlay". Rather than sourcing an actual .bit file (would need Vivado to build/export one separately from the .xsa, or the .xsa to embed it), I set:

BIF_BITSTREAM_ATTR = ""

in build/conf/local.conf. Meaning:

Honest note: BOOT.bin ships FSBL + u-boot + device tree but does not configure the PL. Linux still boots fine — PS and PL are independent on Zynq-7000 — but AXI GPIO and anything else in the fabric won't be accessible until the bitstream is loaded, either at boot (revisit this) or later via fpga-manager. Fine for "first boot," but this will need revisiting for the audio-status LED step, since that's AXI GPIO in the PL.

FSBL — the hard part

fsbl.bb (in meta-xilinx-core) also self-skips unless it's handed a precompiled fsbl.elf via FSBL_FILE, or FSBL_DEPENDS / FSBL_MCDEPENDS point at something that builds one. Three things tried, in order.

a) meta-xilinx-standalone's "fsbl-fw" multiconfig. A ready-made template — conf/multiconfig/fsbl-fw.conf + recipes-bsp/embeddedsw/fsbl-fw-cfg.inc already exist: BBMULTICONFIG += "fsbl-fw", a separate TMPDIR, DISTRO=xilinx-standalone with a real arm-xilinx-eabi toolchain. Wired it up — the multiconfig parsed fine, mc:fsbl-fw context showed TARGET_SYS=arm-xilinx-eabi, DISTRO=xilinx-standalone, correct — but the task graph failed:

ERROR: Task mcdepends on non-existent task
mc:fsbl-fw:.../fsbl-firmware_2025.2.bb:do_deploy
Dead end. Read fsbl-firmware.inc and xlnx-embeddedsw.bbclass in full to confirm: in "xsct" ESW mode, this recipe only fetches the Xilinx embeddedsw git source. No do_compile, no do_install, no do_deploy at all — it genuinely does nothing beyond the source fetch. Verified with bitbake -c deploy mc:fsbl-fw:fsbl-firmware"Task do_deploy does not exist for target fsbl-firmware".

b) Switched XILINX_WITH_ESW from "xsct" to "generic". This mode's fsbl-firmware_generic.inc does have real do_install/do_deploy tasks — compiles zynq_fsbl from embeddedsw source via a Makefile and the multiconfig's cross-compiler. Looked promising, until:

SKIP_MSG:zynq = "Generic support for zynq is not available"
Dead end. Every version of fsbl-firmware_*+git-generic.inc has this — deliberately, permanently disabled for Zynq-7000 upstream. "generic" mode looks to be supported only for zynqmp/versal in this meta-xilinx release. Reverted XILINX_WITH_ESW back to "xsct".

c) Built fsbl.elf directly with Vitis, outside bitbake. Vitis 2025.2 is the "unified" IDE; found a working example at /opt/2025.2/Vitis/cli/examples/embedded/platform_uc4_zynq.py and adapted it — headless, via vitis -s script.py:

client = vitis.create_client()
client.set_workspace(workspace)
platform = client.create_platform_component(
    name='aurascope_platform',
    hw_design='/path/to/aurascope/hw/design_2_wrapper.xsa')
platform.add_domain(name='standalone_a9_0',
                     cpu='ps7_cortexa9_0', os='standalone')
platform.build()
Worked. Compiled a real fsbl.elf (91345 text / 10904 data / 76300 bss), using ps7_init.c generated fresh from the .xsa's hardware description — a good sanity check that the .xsa itself is valid and complete. Output landed at <workspace>/aurascope_platform/export/aurascope_platform/sw/boot/fsbl.elf.

Copied it into the repo and wired it into the recipe:

meta-aurascope/recipes-bsp/embeddedsw/files/fsbl.elf
meta-aurascope/recipes-bsp/embeddedsw/fsbl.bbappend
  FSBL_FILE := "${THISDIR}/files/fsbl"
The := vs = bug: first attempt used = instead of := for that assignment — silently wrong. THISDIR only resolves to the bbappend's own directory if captured immediately (:=) at parse time; with a plain = it's re-evaluated later, when the anonymous python() check actually reads FSBL_FILE — by which point THISDIR had reverted to fsbl.bb's own directory (meta-xilinx-core/recipes-bsp/embeddedsw), not the bbappend's. This is exactly why the standard FILESEXTRAPATHS:prepend := "${THISDIR}/files:" idiom always uses := — good one to remember.

One more red herring along the way: meta-xilinx-standalone ships its own competing fsbl.bbappend, at higher layer priority than meta-aurascope (7 vs 6), which redefines check_fsbl_variables() with a slightly different error message but the same underlying logic. Not a conflict — just made the error text look like it was coming from somewhere other than expected.

The real build, and a silent board

bitbake -n aurascope-image (dry run) had confirmed the plan: 4637 tasks, 0 missing providers. Running it for real — then flashing to SD and powering up the actual board — was a different story. The board was completely silent on serial for several days.

Everything I could think to suspect turned out correct: SD cards, QSPI, BootROM, PS7 clocks, DDR, MIO routing, the FSBL binary's integrity. The real cause was two missing properties in system-top.dts — the file from the section above.

Root causeMechanismSymptom
Missing serial0 alias chosen/stdout-path referenced serial0, but the aliases zynq-7000.dtsi provides are only uart0/uart1 (Xilinx naming). U-Boot couldn't resolve the console node. No serial driver found → CONFIG_REQUIRE_SERIAL_CONSOLE=y forced a reset → continuous boot loop.
Missing ps-clk-frequency zynq-7000.dtsi defaults to 33.333 MHz. The Arty Z7's PS clock is 50 MHz, so every derived clock was off by a factor of 1.5. UART transmitted at ~172800 baud while the terminal listened at 115200 — consistent, non-random character corruption.

A third issue wasn't a bug but the single biggest obstacle to diagnosing the first two: the committed FSBL had debug printing compiled out entirely, so "no output" carried no information about how far boot had actually gotten. More on that below.

Fix 1 — the serial0 alias

zynq-7000.dtsi's aliases node defines uart0 and uart1 — the Xilinx convention. Both U-Boot and Linux resolve stdout-path and console=ttyPS0 through the standard names serial0/serial1. Those aliases didn't exist, so no console device was ever bound. Confirmed by decompiling the generated u-boot.dtb: its aliases block listed uart0, uart1, spi0, qspi, gem0, sdhci0, clkc … — no serial0 — while chosen still referenced stdout-path = "serial0:115200n8".

/ {
	aliases {
		serial0 = &uart0;
		serial1 = &uart1;
	};
};
Placement matters: the aliases node has to sit inside the root / { … } braces. Aliases merge rather than replace, so uart0/uart1 and everything else inherited from the .dtsi survive. Putting it at the top level, between the #include and / {, produced system-top.dts:3.1-8 syntax error — Unable to parse input tree.

Verification:

dtc -I dtb -O dts \
  tmp/deploy/images/aurascope-arty-z7/u-boot.dtb 2>/dev/null \
  | grep -A25 aliases | grep serial
# expect: serial0 = "/axi/serial@e0000000";

Fix 2 — the PS clock frequency

The clkc@100 node carried no ps-clk-frequency property, so the Zynq clock driver fell back to its built-in default of 33,333,333 Hz — correct for most Xilinx reference boards, but not the Arty Z7's 50 MHz PS clock. Every derived clock, including uart_ref_clk, was 1.5× off, and the UART baud divisors were computed from the wrong base.

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

Corroborated by reading the divisors live off the hardware, rather than just trusting the fix:

StepValue
Observed on hardwareUART0 CD = 124, BDIV = 6 (via mrd 0xE0000018 / 0xE0000034)
Implied reference clock115200 × 124 × 7 = 99,993,600 Hz ≈ 100 MHz
SLCR divisor0xF8000154 bits [13:8] = 0x0A = 10
Implied IO PLL100 MHz × 10 = 1000 MHz
Implied PS clock1000 MHz ÷ 20 = 50 MHz — matches the fix
Kernel confirmationttyPS0 … base_baud = 6250000 = 100 MHz ÷ 16

Fix 3 — enabling SDHCI0

With the console working, U-Boot reached its prompt but reported MMC: (empty) and No MMC device available — it couldn't read the very card it had just been loaded from. zynq-7000.dtsi defaults sdhci0 to status = "disabled".

&sdhci0 {
	status = "okay";
};

After this, U-Boot found /boot.scr on mmc 0:1 and the full boot chain completed. Ethernet (gem0) came up in the same rebuild.

The final system-top.dts

All three fixes landed with comments explaining why, since neither is self-evident from the symptom:

/dts-v1/;
#include "zynq-7000.dtsi"

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

	/* REQUIRED: zynq-7000.dtsi provides only the Xilinx-style
	 * uart0/uart1 aliases. U-Boot and Linux resolve the console
	 * through the standard serial0/serial1 names. Without these,
	 * stdout-path cannot resolve, no console binds, and
	 * CONFIG_REQUIRE_SERIAL_CONSOLE=y resets the board in a loop. */
	aliases {
		serial0 = &uart0;
		serial1 = &uart1;
	};

	/* Arty Z7-20: 1 GiB DDR3L. */
	memory@0 {
		device_type = "memory";
		reg = <0x0 0x40000000>;
	};

	chosen {
		bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait";
		stdout-path = "serial0:115200n8";
	};
};

&uart0 {
	status = "okay";
};

/* REQUIRED: zynq-7000.dtsi defaults ps-clk-frequency to 33.333 MHz.
 * The Arty Z7 PS clock is 50 MHz. Without this override every derived
 * clock is 1.5x low and the UART transmits at ~172800 baud. */
&clkc {
	ps-clk-frequency = <50000000>;
};

&sdhci0 {
	status = "okay";
};

The real obstacle: an FSBL that couldn't tell me anything

Not a boot bug, but the single largest reason this took days instead of hours: the committed fsbl.elf (from the Vitis build in the section above) had debug printing compiled out.

$ strings fsbl.elf | grep -i fsbl_dbg
fsbl_printf(type,...) if (((type) & fsbl_dbg_current_types)) { xil_printf(...); }
fsbl_dbg_current_types 0
Silence said nothing. Every fsbl_printf call was optimized away and the format strings don't appear in the binary at all. A perfectly healthy FSBL and a completely broken one look identical on the wire until this is fixed — which meant the first few days of debugging were aimed at the wrong layer entirely.

Rebuilt with FSBL_DEBUG_INFO defined (Vitis: project → C/C++ Build Settings → ARM v7 gcc compiler → Symbols; equivalent headless flow):

xsct
platform create -name arty_fsbl -hw hw/design_2_wrapper.xsa \
  -os standalone -proc ps7_cortexa9_0
app create -name fsbl -template {Zynq FSBL} -platform arty_fsbl \
  -domain standalone_ps7_cortexa9_0
app config -name fsbl -add define-compiler-symbols FSBL_DEBUG_INFO
app build -name fsbl
# verify before installing:
strings fsbl.elf | grep -i "first stage"   # must return the banner string
What it revealed: Xilinx First Stage Boot Loader · Boot mode is SD · SD: rc= 0 · SD Init Done · Handoff Address: 0x04000000 · SUCCESSFUL_HANDOFF · FSBL Status = 0xE0000000. FSBL had been working correctly the entire time — this immediately eliminated FSBL, SD hardware, BootROM, and the boot image layout, and moved the investigation to U-Boot, which is where the serial0 alias fix actually lived.
Recommendation: keep FSBL_DEBUG_INFO enabled for development builds. A boot loader that can't report its own progress turns a one-line configuration bug into a multi-day investigation.

U-Boot notes

A temporary debug-UART config fragment bypassed U-Boot's driver model and device-tree probing entirely, to get an early console. It's what produced the decisive No serial driver found message that pointed at the alias problem in the first place. Removed once the real fix landed, but worth keeping in the layer, commented out, for future early-boot debugging:

# meta-aurascope/recipes-bsp/u-boot/files/debug-uart.cfg  (removed)
CONFIG_DEBUG_UART=y
CONFIG_DEBUG_UART_ZYNQ=y
CONFIG_DEBUG_UART_BASE=0xe0000000
CONFIG_DEBUG_UART_CLOCK=100000000
CONFIG_DEBUG_UART_ANNOUNCE=y

Everything else in the U-Boot config was already correct and didn't need touching:

ConfigValue / note
UBOOT_MACHINExilinx_zynq_virt_defconfig
CONFIG_TEXT_BASE0x4000000 — matches the FSBL handoff address
CONFIG_XILINX_OF_BOARD_DTB_ADDR0x100000 — matches the FSBL partition 2 load address
CONFIG_REQUIRE_SERIAL_CONSOLEy — this is what turned the console-bind failure into a hard reset
CONFIG_DEFAULT_DEVICE_TREE"zynq-zc706" — unused, overridden by the external DTB at 0x100000. A red herring during diagnosis, not a bug.

Diagnostic pitfalls worth not repeating

Several observations were actively misleading during the investigation:

ObservationAssumedActually
Serial silence on early testsBoard not bootingA stale minicom held /dev/ttyUSB1 in an unwatched window; output went to an invisible buffer.
Boot mode jumper labelled SDBoard booting from SDJumper was on QSPI. Confirmed only via BOOT_MODE (0xF800025C = 0x5 = SD). Every prior "SD test" had actually been a QSPI boot.
PC = 0x5490 when haltedStuck in BootROMXGlobalTimer_ModifyInterval inside FSBL's polled SD read — normal polling, caught mid-transfer.
PC = 0x0409cae8 after runRunaway / crashProgram had completed; main() returned and the CPU ran into unwritten DDR.
Repeated stack in Xil_WaitForEventsSD transfer hangPolled-mode read spends nearly all its time there; halting at random lands in that loop with near-certainty.
xsct "Invalid context"Flaky JTAG cableThe CPU was resetting between commands — the reset loop itself, surfacing as a tooling error.
General lesson: silence is not evidence. Before concluding a stage has failed, confirm that a working version of that stage would actually produce output — that debug printing is compiled in, the terminal is attached and visible, and the hardware is in the mode you believe it's in.

Result

Board boots end to end: FSBL → U-Boot → boot.scruImage + system.dtb → Linux → login prompt. Root filesystem mounts r/w on mmcblk0p2, Ethernet links at 100 Mbps, console on ttyPS0. Two device-tree properties were responsible for the entire multi-day failure; everything else investigated (SD cards, QSPI, BootROM, PS7 clocks, DDR, MIO routing, FSBL integrity, bare-metal toolchain) was ruled out by direct evidence, not inference, and didn't need fixing.

StageExpected outputMeaning
FSBLSUCCESSFUL_HANDOFF / FSBL Status = 0xE0000000Both partitions loaded; jumped to U-Boot at 0x04000000
U-BootModel: AuraScope Arty Z7-20 · DRAM: 1 GiB · MMC: mmc@e0100000: 0Console bound, DDR sized, SD controller probed
ScriptFound U-Boot script /boot.scrPartition 1 readable; boot script executing
KernelttyPS0 … base_baud = 6250000Clock tree correct — confirms ps-clk-frequency
RootfsEXT4-fs (mmcblk0p2) … re-mounted r/wRoot filesystem mounted; init running

Still outstanding

Nothing here blocks the next step, but all of it is real and worth tracking rather than rediscovering later:

ItemDetailSuggested action
Random MAC addresseth0 gets a new random MAC every boot, so the DHCP lease changes too.Add local-mac-address to &gem0; reserve the address on the router.
DHCP raceudhcpc gives up before the PHY finishes autonegotiation; link comes up after the login prompt.Wait for carrier in /etc/network/interfaces, or move to systemd-networkd.
No SSH daemonNeither sshd nor dropbear is installed. Resolved — dropbear added to IMAGE_INSTALL and confirmed working over the network. See below.Done.
U-Boot environmentNo Valid Environment Area foundsaveenv won't persist.Configure CONFIG_ENV_IS_IN_FAT for partition 1.
Prebuilt FSBLfsbl.bbappend supplies a committed ELF with an empty DEPENDS, so the .xsa isn't consumed by the build and can silently drift from it.Re-evaluate the meta-xilinx-standalone build path, or add a CI check comparing the FSBL's ps7_init against the current .xsa.
No RTChwclock: can't open '/dev/misc/rtc' — expected on this board.Add NTP, or accept a fixed epoch at boot.
Recommendation: commit system-top.dts with the explanatory comments intact. Neither serial0 nor ps-clk-frequency is self-evident, and both would be easy to lose in a future refactor of the device tree.

SSH is in. USB isn't — yet

With the board on the network, dropbear came up cleanly — no separate debug session needed anymore, just SSH from a normal terminal:

$ ssh root@192.168.1.155
The authenticity of host '192.168.1.155' can't be established.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.155' (ED25519) to the list of known hosts.

WARNING: Poky is a reference Yocto Project distribution that should be used for
testing and development purposes only. It is recommended that you create your
own distribution for production use.

root@aurascope-arty-z7:~#
Terminal showing a successful SSH connection to root@aurascope-arty-z7, followed by lsusb not found and an ls /dev/tty* listing with no ttyUSB or ttyACM device present.
First successful SSH session on the real board — dropbear, no debug UART needed.

Next thing to check was whether the nRF5340 audio receiver — plugged into the board's USB host port — was visible at all. It wasn't, and the first attempt didn't even get far enough to say so properly:

root@aurascope-arty-z7:~# lsusb
-sh: lsusb: not found
root@aurascope-arty-z7:~# ls /dev/tty*
/dev/tty    /dev/tty18  /dev/tty28  /dev/tty38  /dev/tty48  /dev/tty58
... (tty0-tty63, all virtual consoles) ...
/dev/ttyPS0
Two separate gaps, not one: lsusb is missing because usbutils was never added to IMAGE_INSTALL — a real gap, but a minor one-line fix for a future rebuild. The more important signal is in the second command: only the expected virtual consoles and ttyPS0 show up. No ttyUSB*, no ttyACM*, nothing at all from the USB host controller — the nRF5340 isn't enumerating.

The likely cause isn't software: the board was still running powered from the JTAG/prog USB connector during this test, which is enough to boot Linux but not enough current to also power a downstream USB host port and whatever the nRF5340 dev kit draws while acting as a UAC device. Two things to change before re-testing:

With real power behind the USB host port, the next check is whether the nRF5340 receiver's UAC firmware actually enumerates as a USB sound card — that's the real goal of the wireless audio phase, and the natural next step once power is sorted.

Code

Tagged at v0.2-custom-recipe · code: meta-aurascope