EDF 102.
Continuing the EDF journey: moving from individual packages to building the full bootable image for the KV260 synthesizer.
Goal
Build the complete bootable disk image (synth-image) under EDF 25.11, containing the synthesizer runtime, ALSA Sequencer support, automatic boot services and the packaged programmable logic payload.
Methodology
Picking up from yesterday's package-level validation. I am connected via SSH from the office into DDRiver. The long-running container from yesterday is still alive on the workstation, with our persistent workspace intact. The agent continues providing documentation lookups, build inspection, and live drafting as I drive the tools.
Starting point
Yesterday established the EDF 25.11 workspace, the meta-synth layer, and proved that both synth-live and synth-firmware compile and package cleanly (2,600 tasks completed). Today's task was to carry those packages into a complete image and then boot it on hardware.
I was working from the office over SSH, while the disposable build container I had entered at home was still running on DDRiver. A fresh SSH shell did not inherit that interactive shell's environment, but the container and bind-mounted workspace had survived. I entered the existing container with podman exec and sourced edf-init-build-env again, restoring the same build directory, layers and caches.
Before building the image, the agent inspected the kernel configuration produced during package dependency work. Userspace ALSA and aconnect were present, but the kernel had CONFIG_SND_SEQUENCER disabled. We added a one-line Kconfig fragment through a linux-xlnx_%.bbappend, forced the target kernel's configure task, and verified that the generated .config now contained CONFIG_SND_SEQUENCER=y.
That led into the next question: which target context should own the complete image? During the prior day we had used one machine name to inspect the Kria image and another after BitBake warned against the first. The agent now proposed changing recipe compatibility and switching back for the full image. Before allowing another string substitution, I needed to understand how those names participated in BitBake's configuration model.
Typechecking the machines
I was now in unfamiliar territory, with several obviously meaningful strings appearing in command arguments: kria-zynqmp-generic, amd-cortexa53-mali-common, k26-smk-kv-sdt. They were plainly names for different hardware/build targets. What I did not know was where each name entered the configuration system, which actors interpreted it, what metadata it selected, and how that selection propagated into recipes and image composition.
The agent made that worse by making and unmaking decisions around them. We started with kria-zynqmp-generic. After BitBake warned that it “should not be used by itself,” the agent changed the package and kernel commands to amd-cortexa53-mali-common. Later it proposed switching the full image back to kria-zynqmp-generic and changing a recipe compatibility declaration to match.
“Did we not yesterday start with ‘kria’ and then switch to ‘...-mali’?”
The answer explained that one was for package and kernel validation while the other was for a complete Kria disk image. It contained true-looking distinctions, but still left the actual mechanism hidden. My response was:
“uh”
This was the point at which I stopped the flow and restated the methodology. I was not there merely to accept whichever string the agent most recently recommended. I wanted to understand the system well enough to drive it. An earlier explanation of the kernel configuration fragment had made the same mistake in another form: it spent time motivating why fragments exist when the interesting questions were what form the fragment takes, which other files and systems see it, and how it enters the global build configuration.
I clarified how I needed unfamiliar software machinery explained. In ordinary computing, much of the motivation is obvious once the mechanism is visible; save motivation-heavy exposition for genuinely non-obvious choices and difficult intellectual material. I am a category theorist. Show me the objects, their types, the relations and transformations between them, who owns each representation, and where composition occurs.
“Typecheck. What are the domains and codomains. Who sees these machine names, where do they live, who consumes them, where are they wrong, where are we changing them?”
That produced the first explanation which made the situation transparent. The machine names were not interchangeable labels, and the proposed edit was not another switch of target. There was one selected nominal machine, a lookup from that name to a configuration file, an inherited ordered set of machine-classification overrides, and a compatibility predicate over those overrides. Our image and firmware recipes had accidentally been declared over disjoint domains.
Assistant / GPT-5.6 Sol
Let's typecheck the machinery precisely. There are several different things that have all been called “machine,” and I blurred them.
1. Selected machine name
Type:
MachineName := String
Examples:
"amd-cortexa53-mali-common"
"kria-zynqmp-generic"
"k26-smk-kv-sdt"
The build has exactly one selected MACHINE value at a time:
MACHINE = "..."
We currently supply it on the command line:
MACHINE=amd-cortexa53-mali-common bitbake ...
or:
MACHINE=kria-zynqmp-generic bitbake ...
The shell exports MACHINE; BitBake imports it into its metadata environment. We have not permanently changed MACHINE in local.conf.
selectedMachine : BuildInvocation → MachineName
2. Resolving a machine name to a file
BitBake searches the configured layers for:
conf/machine/${MACHINE}.conf
So there is a partial lookup:
resolveMachine :
MachineName ⇀ MachineConfigurationFile
For the two relevant names:
"amd-cortexa53-mali-common"
↦ sources/meta-amd-edf/meta-amd-edf-bsp/
conf/machine/amd-cortexa53-mali-common.conf
"kria-zynqmp-generic"
↦ sources/meta-kria/
conf/machine/kria-zynqmp-generic.conf
These are not aliases for each other. They are different configuration files.
3. Machine configuration files compose other files
A machine configuration is metadata, not an object file or target package. It can import other configuration files with require.
Common Cortex-A53/Mali machine
# amd-cortexa53-mali-common.conf
DEFAULTTUNE = "cortexa72-cortexa53"
require conf/machine/zynqmp-generic.conf
MACHINE_FEATURES += "mali400 vcu fpga-overlay efi"
Its composition graph is roughly:
amd-cortexa53-mali-common.conf
└── zynqmp-generic.conf
├── soc-zynqmp.inc
├── machine-xilinx-default.inc
└── machine-xilinx-qemu.inc
It describes a common software/filesystem target for ZynqMP devices with Cortex-A53 application processors, a Mali-400 GPU, VCU, FPGA-overlay support and EFI support. It intentionally clears several board/QEMU-specific variables after importing the generic ZynqMP base.
Generic Kria image machine
# kria-zynqmp-generic.conf
require conf/machine/zynqmp-generic.conf
require conf/machine/include/kria-common.inc
MACHINE_FEATURES += "mali400 vcu fpga-overlay"
Its graph is:
kria-zynqmp-generic.conf
├── zynqmp-generic.conf
│ ├── soc-zynqmp.inc
│ ├── machine-xilinx-default.inc
│ └── machine-xilinx-qemu.inc
└── kria-common.inc
kria-common.inc adds Kria-specific image semantics:
MACHINEOVERRIDES =. "kria:"
IMAGE_CLASSES += "kria-image"
SERIAL_CONSOLES = "115200;ttyPS1"
IMAGE_FSTYPES += "ext4 wic wic.xz wic.qemu-sd"
UBOOT_MACHINE = "xilinx_zynqmp_kria_defconfig"
So the Kria machine is not merely “the same CPU with another name.” It adds a Kria classification and image behavior.
4. Machine overrides
This is the key missing type.
MachineOverride := String
MachineOverrides := ordered list of MachineOverride
BitBake begins with:
MACHINEOVERRIDES ?= "${MACHINE}"
Then imported machine configuration files prepend additional classification tags.
classifyMachine :
MachineConfigurationFile → MachineOverrides
For kria-zynqmp-generic, the effective list includes tags conceptually like:
[
"kria",
"zynqmp-generic",
"zynqmp",
...,
"kria-zynqmp-generic"
]
For amd-cortexa53-mali-common, it includes tags conceptually like:
[
"zynqmp-generic",
"zynqmp",
...,
"amd-cortexa53-mali-common"
]
Critically, the common Cortex-A53 machine does not import kria-common.inc, so it does not acquire the kria override.
These tags are inserted into BitBake's global override namespace:
OVERRIDES = "...:${MACHINEOVERRIDES}:${DISTROOVERRIDES}:..."
5. Override syntax
A statement such as:
COMPATIBLE_MACHINE:kria = "${MACHINE}"
does not compare MACHINE to the literal string "kria". Its type is more like:
overrideAssignment :
OverrideTag × VariableName × Value
It means: when kria is present in the active override set, this is the effective value of COMPATIBLE_MACHINE.
if "kria" ∈ MACHINEOVERRIDES:
COMPATIBLE_MACHINE := selected MACHINE string
else:
COMPATIBLE_MACHINE := "^$"
6. Recipe compatibility
COMPATIBLE_MACHINE has type:
CompatibleMachinePattern := RegularExpression
BitBake's actual compatibility test is in base.bbclass:
compat_machines = MACHINEOVERRIDES.split(":")
for m in compat_machines:
if re.match(COMPATIBLE_MACHINE, m):
recipe_is_compatible
Formally:
isCompatible :
CompatibleMachinePattern × MachineOverrides → Bool
isCompatible(regex, overrides)
:= ∃ tag ∈ overrides . regex matches tag
BitBake tests the regex against every machine classification tag, not only against the selected MACHINE string.
7. Our current firmware recipe
It currently says:
COMPATIBLE_MACHINE = "^$"
COMPATIBLE_MACHINE:amd-cortexa53-mali-common = "${MACHINE}"
Under the common machine
Input:
MACHINE = "amd-cortexa53-mali-common"
The corresponding override is active, so the effective value becomes:
COMPATIBLE_MACHINE =
"amd-cortexa53-mali-common"
That regex matches one of the active tags. The recipe is available. This is why yesterday's package build worked.
Under the Kria machine
Input:
MACHINE = "kria-zynqmp-generic"
The amd-cortexa53-mali-common override is not active. Therefore:
COMPATIBLE_MACHINE = "^$"
^$ matches only an empty string. None of the machine tags are empty, so BitBake skips synth-firmware.
8. Our image recipe
Our image recipe says:
require recipes-core/images/kria-image-full-cmdline.bb
That imported recipe contains:
COMPATIBLE_MACHINE = "^$"
COMPATIBLE_MACHINE:kria = "${MACHINE}"
Therefore synth-image is itself only compatible when the selected machine has the kria override.
Under amd-cortexa53-mali-common
"kria" ∉ MACHINEOVERRIDES
Therefore the imported image recipe remains disabled.
Under kria-zynqmp-generic
"kria" ∈ MACHINEOVERRIDES
Therefore the image recipe becomes enabled.
9. The actual type error
The current declarations have disjoint domains:
synth-image:
buildable on machines classified as kria
synth-firmware:
buildable only on amd-cortexa53-mali-common
But synth-image includes synth-firmware:
IMAGE_INSTALL:append = " synth-firmware synth-live"
So we currently have:
imageDomain ∩ firmwareDomain = ∅
More explicitly:
synth-image("kria-zynqmp-generic")
requires
synth-firmware("kria-zynqmp-generic")
but
synth-firmware("kria-zynqmp-generic")
is undefined/skipped
That is the type error.
10. What we are changing
We are not changing any machine name. We are not editing:
kria-zynqmp-generic.conf
amd-cortexa53-mali-common.conf
local.conf
MACHINE
We are changing only the declared domain of our firmware recipe.
Current:
COMPATIBLE_MACHINE:amd-cortexa53-mali-common = "${MACHINE}"
Proposed:
COMPATIBLE_MACHINE:kria = "${MACHINE}"
This changes:
domain(synth-firmware)
from:
{ machines carrying amd-cortexa53-mali-common override }
to:
{ machines carrying kria override }
Then:
domain(synth-image)
⊆ domain(synth-firmware)
for our intended build.
11. Where the warning is wrong
The warning is generated by this generic predicate:
if MACHINE.endswith("-generic"):
warn("intended to be included by other machines")
It examines only the spelling of the selected machine name. It does not inspect whether meta-kria deliberately specializes that machine, whether the image recipe has explicit overrides for it, whether AMD publishes artifacts built from it, or whether the machine is valid for this particular image.
Thus it reports:
kria-zynqmp-generic → warning
because the string ends with -generic. But meta-kria contains explicit direct-use behavior for that exact nominal machine, and AMD publishes:
kria-image-full-cmdline-kria-zynqmp-generic...
So the warning is syntactically triggered but semantically overbroad for this curated image flow.
12. Complete composition
BuildInvocation
│
│ MACHINE=kria-zynqmp-generic
▼
kria-zynqmp-generic.conf
│
├── zynqmp-generic.conf
└── kria-common.inc
│
▼
MACHINEOVERRIDES includes "kria"
│
├── enables kria-image-full-cmdline
├── should enable synth-firmware
└── selects Kria-specific metadata
│
▼
synth-image
= kria-image-full-cmdline
+ synth-live
+ synth-firmware
│
▼
root filesystem and WIC image
That is the precise reason for the one-line compatibility edit. No machine name changes; only our recipe's domain changes so its domain agrees with the image that consumes it.
After changing only the firmware recipe's compatibility domain, I dry-ran the complete image. BitBake resolved 13,624 tasks without a missing provider or incompatible package. It spent 11 minutes 46 seconds checking AMD's shared-state mirror, then reported 98% coverage and a successful task plan. The generic-machine warning appeared twice, exactly as the typecheck predicted.
MACHINE=kria-zynqmp-generic bitbake -n synth-image
Sstate summary: Wanted 6433 Local 101 Mirrors 6242 Missed 90 Current 196
NOTE: Tasks Summary: Attempted 13624 tasks of which 13422 didn't need to be rerun and all succeeded.
Summary: There were 2 WARNING messages.
From packages to a system
The userspace recipe already requested alsa-lib and alsa-utils-aconnect, but inspecting the kernel produced during the package work found CONFIG_SND=y and USB audio support with CONFIG_SND_SEQUENCER disabled. That would let the application compile while leaving it without the kernel interface behind /dev/snd/seq.
We added a one-line Kconfig fragment, CONFIG_SND_SEQUENCER=y, through a linux-xlnx_%.bbappend in meta-synth. The append makes the fragment an input to AMD's existing kernel recipe; Kconfig merges it with the machine and distribution configuration to produce the final .config. A forced target-machine configure completed all 769 tasks, and the generated configuration contained the requested symbol.
CONFIG_SND_SEQUENCER=y
The full synth-image build ran for 50 minutes. BitBake printed 532 errors while failing to restore shared-state objects, then fell back to the real tasks. Its final summary reported all 13,624 tasks successful, but the cache-fetch errors still made the command return nonzero.
NOTE: Tasks Summary: Attempted 13624 tasks of which 12703 didn't need to be rerun and all succeeded.
Summary: There were 833 WARNING messages.
Summary: There were 532 ERROR messages, returning a non-zero exit code.
real 50m16.156s
The artifacts settled the apparent contradiction. The build produced a 1.5 GB WIC image and a 164 MB compressed image. The manifest contained synth-live, synth-firmware, alsa-utils-aconnect, Bash, Python and dfx-mgr. The root filesystem contained the AArch64 application linked to libasound.so.2, both enabled synth services, the complete firmware triplet and a default-firmware file selecting synth. The Kria-context kernel configuration contained CONFIG_SND_SEQUENCER=y.
synth-image-kria-zynqmp-generic.rootfs-20260923022458.wic.xz
SHA256 d49f11217adb11dd1a47721cb4007595a6b5e32b845a2b2a2662382c0417b4d3
This is a completed image build, not yet a completed port. The next evidence comes from writing separate removable media, booting the KV260 and checking the actual device nodes, firmware load, register identity, FC25 recovery and physical audio.
First boot, first real failure
I wrote the WIC image to a fresh 16 GB card, verified the bytes against the uncompressed image hash, preserved the known-good Ubuntu card, and captured the first EDF boot over the KV260 serial console. The board reached the amd-edf login and forced me to establish a password. Nothing synthy appeared to remain running.
The service logs showed that most of the system had actually worked. The default firmware loader found the packaged synth bundle, loaded it into slot 0, and exited successfully. The synth service started and waited for the FC25. When the keyboard appeared, synth_live validated the register bank and reported ready. One second later the reconnect watcher restarted synth.service, and the system fell over:
May 29 18:48:32 xlnx-firmware-load: synth: Loaded with slot_handle 0
May 29 18:48:32 systemd: Finished dfx-mgrd Default Firmware Load Service.
May 29 18:48:33 synth-start: synth: waiting for Carry-on FC25 MIDI 1
May 29 18:48:34 synth-midi-watch: synth-midi-watch: FC25 absent
Sep 23 06:33:21 synth-start: synth: FC25 source 16:0
Sep 23 06:33:21 synth_live: ready, source 16:0, master gain 1.0
Sep 23 06:33:22 synth-midi-watch: FC25 returned; restarting synth
Sep 23 06:33:22 xlnx-firmware-load: Load Error: -3: No empty slot for synth
Sep 23 06:33:22 systemd: Dependency failed for KV260 FC25 polyphonic synthesizer.
The absurd one-year timestamp jump was the board clock moving from its uncorrected image/RTC time to network-corrected time during the same boot, not a year of uptime.
The failure was in the service-state model. dfx-mgr-fw-load.service is a successful Type=oneshot service, but without RemainAfterExit=yes it returns to the inactive state after loading PL. synth.service had a hard Requires= dependency on that one-shot. Therefore every later restart of the MIDI process also started the apparently inactive firmware service again. The second attempt found the only flat-PL slot already occupied, failed, and systemd stopped the dependent synth. The watcher then raised CalledProcessError because its systemctl restart synth.service command failed, and systemd restarted the watcher.
successful firmware load
→ oneshot becomes inactive
→ FC25 watcher restarts synth.service
→ Requires= starts firmware loader again
→ no empty PL slot
→ firmware loader fails
→ synth dependency fails
→ watcher command raises
The smallest intended correction is to keep the successful firmware one-shot logically active with RemainAfterExit=yes. The initial boot still fails closed when firmware loading fails, but later FC25 recovery sees the dependency already active and restarts only the Linux control process.
I installed that property as a systemd drop-in, deliberately unloaded slot handle 0, and started the corrected one-shot exactly once. Systemd then reported the loader as active (exited), FPGA Manager reported operating, and synth again occupied 0->0. Starting synth.service produced a live synth_live 16:0 /dev/mem 93 91 71 74 process while the loader remained active and the slot handle did not change. The application could now start without replaying the hardware load.
Then I played it. The synthesizer worked through the EDF image: FC25 MIDI reached synth_live, the packaged PL generated audio, and the speakers supplied the final proof that the port was alive.
The reboot test was stronger. Automatic pre-login operation had always been the requirement, but the agent's test procedure began with “Log in as amd-edf,” making it sound as though authentication might still be necessary before the instrument could work. I objected, then simply tried the keyboard before logging in. It was already playing. The EDF image had booted Linux, loaded the packaged PL payload, started the MIDI application and watcher, found the FC25 and reached audible end-to-end operation without human intervention.
I pulled power, restored it, and tried the instrument again. It returned without login or manual intervention and played perfectly. The EDF port was complete.
□