EDF 103.
The first image worked only after one small live correction. Today, Codex and I teach the build that correction so its next output is already right.
Goal
Modify the EDF build metadata before building so that an ordinary bitbake synth-image produces the service-state correction which currently exists only as a manual drop-in on the working card. The flashable build artifact itself should be complete: after writing it to clean media, the synthesizer should boot and survive later FC25 reconnects without post-processing the image or repairing the board.
Methodology: two terminals
As usual, I booted up my dual-terminal workflow: one with an AI agent CLI and the necessary project information at its fingertips, and one in which I would implement the workflow myself. In the first terminal, Codex could inspect source and build artifacts, explain what it found and propose the next bounded check. In the second, I read the files, made the edits, ran BitBake and handled the hardware.
If you are just joining us, that is the established rhythm of these notes. I ask from the mental model I actually have; Codex answers against the specific system in front of us; I challenge weak abstractions and implement the steps; and the files, tools and hardware answer back. Hardware-in-the-loop has been fundamental throughout the synthesizer bring-up. Today's physical endpoint is the KV260, the FC25 and the sound coming out of the speakers.
What counts as the product?
We hit a snag before touching the build. I told Codex that the working image needed a manual service tweak and that today's goal was to make future images come out correct. Codex described the task as “move that into the Yocto image.” I objected: that sounded like modifying an image after the build, when the whole point was to correct the inputs which produce it.
“Surely the goal is that, you hit build, and the product at the end is what you wanted.”
Codex corrected the boundary. The manual drop-in had been useful as a diagnostic: it tested the smallest suspected correction directly on the running system and established that the service model then behaved correctly. It was not the final product definition. The intended composition was source file plus Yocto recipe metadata, transformed by BitBake into a package, root filesystem and complete flashable image. Nothing should have to edit that image or repair the board between building and flashing.
That sharpened the terminology on both sides. The correction was build-time configuration in the broad sense, but not a private setting for this particular build directory's local.conf. Codex identified it as a property of the product's firmware package, so I opened meta-synth/recipes-synth/synth-firmware/synth-firmware_1.0.bb.
Reading the recipe
I listed meta-synth, followed it into recipes-synth, and opened synth-firmware_1.0.bb. My first useful question was not how to edit it, but what kind of object I was looking at.
“They really call them recipes, huh? .bb I assume is BitBake? Is this a DSL, or is it literally shell code?”
Codex's answer was: both, at different levels. A .bb file is a BitBake recipe written in BitBake's metadata language. Assignments such as S = "${WORKDIR}", overrides such as RDEPENDS:${PN}, and declarations such as SRC_URI belong to that DSL. A task body such as do_compile() { ... } or do_install() { ... } is shell unless the recipe explicitly declares a Python task. BitBake evaluates the metadata, constructs the task graph and then invokes those bodies inside a prepared per-recipe environment.
I began sorting the punctuation by role. ${...} is variable expansion. ${PN} is the recipe's output package name. The colon in RDEPENDS:${PN} introduces an override-qualified value; in FILESEXTRAPATHS:prepend it selects an operation which prepends to the eventual search path. The recipe was not a shell script with unusually enthusiastic environment variables. It was a declarative object whose evaluated values would later parameterize shell tasks.
The dependency names also described two different worlds. DEPENDS = "dtc-native" makes a host-native device-tree compiler available while the recipe builds. RDEPENDS:${PN} = "dfx-mgr" says the resulting target package needs dfx-mgr when it runs on the board. One edge belongs to the build graph; the other belongs to the installed system.
That distinction made the familiar-looking fragments less deceptive. 0644 was indeed a Unix file mode. install -d creates a directory; install -m 0644 copies a file while assigning its installed permissions. I noticed that ${S} was defined in the recipe and made a first guess about the next symbol.
“I assume${WORKDIR}=pwd.”
Not quite. Codex separated the namespaces: ${WORKDIR} is BitBake's isolated workspace for this recipe, version and machine context. ${S} names the source directory within that workspace, ${B} the build directory, and ${D} the destination staging root which will be packaged—not the board's live root filesystem.
The compile task contained another small vocabulary lesson:
dtc -@ -I dts -O dtb -o synth.dtbo ${S}/synth.dtso
dtc is the device-tree compiler. It transforms the textual device-tree overlay source, .dtso, into the binary overlay blob, .dtbo. I noticed the family resemblance before Codex finished explaining it: all those dts were, indeed, “device tree.” The task reads its input from ${S}, writes the relative output into the task's build directory ${B}, and do_install later copies ${B}/synth.dtbo. The bitstream and shell.json pass through without compilation. Together the three files become the firmware bundle installed below /lib/firmware/xilinx/synth.
The final compatibility lines were another compact use of the override system:
COMPATIBLE_MACHINE = "^$"
COMPATIBLE_MACHINE:kria = "${MACHINE}"
The impossible-looking base regular expression disables the recipe by default. When the active machine carries the kria override, BitBake replaces it with the selected machine name and the recipe becomes eligible. A previous EDF session had already made machine compatibility painfully concrete; here I could recognize the same mechanism in its resting form.
Only after establishing those objects did I ask what about this recipe led to the reconnect failure. Codex initially answered as though I had asked what line was wrong. I corrected the question: a file's causal contribution includes what it omits. The recipe installed the firmware payload and selected it as dfx-mgr's default, but installed no service-state override. That absence connected the build metadata to the runtime failure.
Why the reconnect failed
Codex pulled the failure model directly from the repository's retained EDF 102 knowledge. The packaged PL loader, dfx-mgr-fw-load.service, successfully loaded the synthesizer into the only flat-PL slot and then became inactive because it is a Type=oneshot unit. When the FC25 reconnect watcher later restarted synth.service, systemd started that required but apparently inactive loader again. The second load found no empty slot and the dependent synthesizer service failed.
I initially extrapolated a slightly different story: perhaps the control loop lost its MIDI friend, exited, and systemd declined to revive it. Codex pointed out that the watcher and systemd did cooperate; the restart failed because it pulled the apparently inactive firmware loader back in. Then the actual role of the correction clicked.
“Systemd does indeed cooperate, but the synth tries to re-program the PL, which is rejected. Similar start, different failure.”
“Oh—it's the loader that needs to remain active.”
The live correction was a systemd drop-in containing:
[Service]
RemainAfterExit=yes
With that property, a successful firmware load remains logically active (exited). A reconnect can restart the Linux MIDI application without replaying the FPGA load. The corrected card has already survived reboot and cold power cycling and produced audible FC25-controlled audio before login.
That raised the next question immediately: what exactly was a “slot” here? I reasoned aloud that there probably was not a simple xil_pl_load(bit_path) hiding underneath; requests appeared to go through dfx-mgr, and there was probably one slot unless the platform had been configured with multiple independently programmable regions.
Codex separated the layers. The current PL_FLAT platform exposes one full-fabric configuration slot. dfx-mgr owns its Linux-visible lifecycle, but it cannot invent independently reconfigurable regions. Those partitions, interfaces and boundaries must exist in the hardware design which becomes the XSA; Linux can then manage what the platform actually exposes.
“Oh—so this happens at .xsa level?”
Yes. That gave the service failure a physical shape: the second request was not colliding with an arbitrary software limit. It was asking the manager to fill a full-fabric slot which the first successful load already occupied.
The working card still had not proved reproducibility. Its drop-in was installed manually after the image had been written. The next question was where the correction belonged in meta-synth, how BitBake would carry it into the root filesystem, and what we had to rebuild to show that the next image was born correct.
Closing the recipe triangle
Codex proposed the smallest source change and I implemented it by hand. I created a recipe-local retain-state.conf containing the two-line systemd override. A copied indentation briefly came along for the ride; I removed it in gedit and remembered, slightly after using it, that >> appends while > truncates. Do not tell my dad.
Next I added the new files directory to FILESEXTRAPATHS and the filename to SRC_URI. “URI” is Uniform Resource Identifier. Here file://retain-state.conf does not name an absolute source-tree path; it tells BitBake's local-file fetcher to search the directories in FILESPATH.
At that point the spelling itself was not teaching me much. Create a two-line marker file, list its directory, list its name: those were conventions which could have been called almost anything.
“The only thing I am gaining is knowledge of the conventions. Put a ‘yes’ marker, list its location, list its name.”
Codex adjusted its explanation accordingly. The interesting structure was the system of actors underneath: which component declares an input, which mechanism stages it, which task transforms it, which package owns the resulting path, which service performs the one-time action, which daemon manages PL lifecycle, and which kernel subsystem programs the device.
Codex then told me to install ${S}/retain-state.conf. I caught what looked like a missing component: if the source file lived below files/, why was that not ${S}/files/retain-state.conf? The answer exposed the machinery between the declarations and the shell task. BitBake searches FILESPATH for each file:// input and stages what it finds into WORKDIR. This recipe defines S = "${WORKDIR}", so by the time do_install runs, the fetched file really is at ${S}/retain-state.conf. The recipe does not reach back into its own source directory during installation.
“The machinery ofA:Bandfile://combine to resolve the missing/filesand insert that, via BitBake's doing, appropriately so that the file is found.”
That was the right composition, with one vocabulary refinement from Codex: :prepend modifies the search space, file:// asks the fetcher to resolve a member of that space, and BitBake stages the resolved object so the later shell task sees a flat path below ${S}. The missing /files was no longer magic. It had been consumed by an earlier morphism.
The install task created ${D}${systemd_system_unitdir}/dfx-mgr-fw-load.service.d/ and copied the drop-in there with mode 0644. Then Codex had me add the same installed pathname to FILES:${PN}.
“Right, so I gotta add retain-state.conf twice. Sure. Why not thrice?”
It was, in fact, three appearances once the install command was counted. The repetition represented three different boundaries rather than three ways to say the same thing:
SRC_URI source dependency
do_install staged-filesystem transformation
FILES:${PN} output-package ownership
SRC_URI makes the file an input to the recipe. do_install transforms that staged input into a target-filesystem path below ${D}. FILES:${PN} assigns the installed path to the output package. None of the three substitutes for another. The arbitrary names had resolved into an actual composition.
I wondered aloud whether recipes were normally generated by scripts. Codex clarified that these small declarative recipes are intended to be maintained source; BitBake generates the enormous work directories, task graphs, package trees and images around them. This earned me the “manually edited a BitBake recipe file” badge, under heavy supervision.
The movement from “is this a DSL or shell?” to independently predicting the fetch, stage, install and package boundaries took about 25 minutes of poking at one recipe and challenging each unexplained transition.
I recreated the disposable EDF container around the persistent workspace and live synthesizer checkout, then sourced the existing build environment. Before spending time on a build, Codex asked me to run bitbake -e synth-firmware. The -e emits BitBake's fully evaluated environment after parsing, overrides and variable expansion. In 16 seconds we could see that the machinery had understood the edit: SRC_URI contained the new file, S was the recipe work directory, D was the target-image staging root, and the package path expanded to /usr/lib/systemd/system/dfx-mgr-fw-load.service.d/retain-state.conf.
SRC_URI="... file://retain-state.conf "
S=".../synth-firmware/1.0"
D=".../synth-firmware/1.0/image"
FILES:synth-firmware="... /usr/lib/systemd/system/dfx-mgr-fw-load.service.d/retain-state.conf "
No build task had run yet. I had produced the edit; Codex had given me a cheap way to interrogate BitBake's parsed object model; and the output answered before either of us needed to infer what the build would do.
From package to image
I ran the focused synth-firmware build; it completed successfully in 2 minutes 10 seconds. Codex then inspected both the recipe's staged target root and its split package and found the exact 30-byte drop-in at /usr/lib/systemd/system/dfx-mgr-fw-load.service.d/retain-state.conf, root-owned with mode 0644. The build result was not “exit zero”; it was the intended file, in the intended package, with the intended content and permissions.
I asked how much of the full build we were about to repeat and whether the previous known-good image was safe. Codex explained that BitBake would reuse almost all unchanged artifacts but regenerate the image assembly, and copied the accepted 23 September compressed image outside the disposable build/tmp tree before we continued. I ran bitbake synth-image. Six minutes later it had produced a new compressed WIC with SHA-256 fc773892ceb48fb1f93c3343b08872d25d30275a721ce1a2d5d2c99749e6afbe. Codex opened the deployed ext4 artifact directly, found the same drop-in, and confirmed that the manifest retained dfx-mgr, synth-firmware and synth-live.
I plugged in the only spare card. Codex examined it read-only and identified my Ubuntu 24.04 rollback deployment from its partition labels, service definition and firmware hash. I authorized its destruction with appropriate ceremony.
“Annihilate it.”
Codex's first privileged write attempt correctly stopped at sudo authentication, although the retained workstation knowledge already described the graphical authorization path. I ran the write myself. Codex then launched the same write through the proper mechanism before learning that mine had already finished, so the card received the identical 1.59 GB WIC twice. The mistake remained in the account rather than being edited into competence. The second pass was redundant but harmless; Codex compared the entire device byte-for-byte with the source WIC, confirmed the expected FAT boot and ext4 root partitions, and powered the reader off cleanly.
I moved the card to the KV260 and supplied the final kind of evidence. The board booted from the newly produced image and immediately made audible FC25-controlled sound. The artifact emitted by the build worked without recreating the manual board-side fix.
“It booted—and it's making them #sounds.”
Then I killed and de-killed the FC25. The instrument survived. The watcher restarted the synthesizer while the successful firmware-loader oneshot remained logically active, so systemd did not ask dfx-mgr to program the already occupied PL slot again. The physical test answered the exact question which had started the session.
Result: the image is the product
The correction now lives in the build inputs, travels through the firmware package, appears in the assembled root filesystem, and survives the final transition onto hardware. A normal bitbake synth-image produced a card which booted, played, lost its MIDI controller, found it again and kept playing. There was no post-build image edit and no repair on the board.
The source change was tiny. The other product was a working model of BitBake source discovery, staging, package ownership, systemd state and PL lifecycle, acquired conversationally and checked at every boundary against something real. Codex adapted the explanation to the system and to how I think; I challenged it, implemented it and supplied the hardware judgement it could not. The two-terminal method did exactly what it was there to do.
□