David Farrell

← Field Notes

Field note · 28 September 2026

Adrift In U-Boot Shell.

Taking the bootloader model from a book to the actual cold-boot transcript of my EDF-built KV260 synthesizer system.

Goal

To supplement my reading of Mastering Embedded Linux Programming and validate my cumulative learning against real hardware, I decided to dissect the boot log of the resultant image from the prior field note, EDF103.

Tmux are better than Omux

I began with the same two-terminal method as the earlier embedded-Linux notes. In one terminal, Codex had the website and synthesizer documentation available, including the provenance of the exact EDF image in the board. In the other, I would operate the serial console and interrogate the real system.

Before booting anything, I realized the usual exchange—run a command, copy its output, paste it back—was a poor fit for a long, live boot transcript. I asked whether I could have an ordinary shell which I controlled while Codex read the same terminal. tmux was the obvious answer, except that it was not installed.

So the experiment began by improving the instrument used to observe it. I retained the keyboard and the physical board; Codex could read the same terminal without impersonating my actions. That was a better version of the two-terminal workflow, built in real time because this particular investigation needed it.

The cold boot

Recommended listening.

I powered the board with picocom watching the UART. The logs were, of course, much too fast to read in real time, so instinct kicked in and I scrolled the mouse wheel, trying to review what had already disappeared above the terminal. That's as long as it took for something to go wrong.

U-Boot's countdown stopped and left me at:

ZynqMP>

I connected the speaker and played a few notes. Nothing happened. Between the silent synthesizer and a prompt I had never used before, I had successfully verified that I was in entirely unfamiliar territory and briefly wondered whether I had broken the device.

The next command arrived as [Aprintenv. That gave us the mechanism: with tmux's mouse handling disabled, scrolling over its alternate screen had produced the terminal sequence for an Up arrow, ESC [ A. The escape byte was enough to satisfy “Hit any key to stop autoboot”; the remaining [A waited in U-Boot's input and attached itself to what I typed next. Codex enabled tmux mouse handling before we continued.

“Wait. Where's my synth?” Codex read the shared terminal and found a less exciting explanation. Linux had simply never started, so neither had the userspace service which loads the programmable logic and runs the instrument. My synth had not vanished; we were sitting at a useful accidental breakpoint in the boot.

I looked back through the messages. The ZynqMP first-stage boot loader had announced itself, followed by PMU firmware and BL31. Then came U-Boot 2023.01, its inventory of the CPU, board, memory and devices, the interrupted countdown and the prompt.

Scrolling through the saved log was already paying off. I recognized the FSBL vocabulary from AMD's training material: the multiboot offset, the processing-system handoff, and the firmware for the triple-redundant MicroBlaze PMU. The FSBL itself did not have much to say. It established the platform, reported the boot mode and handed control onward quickly.

I paused at BL31 until Codex connected it to something I did remember: Arm Trusted Firmware. More precisely, BL31 was the EL3 runtime-firmware component supplied here by Trusted Firmware-A, not a synonym for the whole firmware project. The naming was BL3-1, followed conceptually by an optional BL3-2 secure payload and a BL3-3 non-secure payload, rather than the more exciting thirty-first boot loader. Its own message named the next boundary: Non secure code at 0x8000000. Once I knew what BL31 was, I could read that as the handoff to the non-secure U-Boot image waiting in DDR.

“Usually, U-Boot offers a command-line interface over a serial port. It provides a command prompt that is customized for each board.”

That was exactly what I was looking at. ZynqMP> was not a Linux shell or a generic serial monitor. It was the board's U-Boot command prompt. The accidental interruption had turned a fast-moving transcript into the first concrete boundary I could inspect.

I began with printenv bootcmd boot_targets. My first attempt arrived as the malformed [Aprintenv; the second revealed that printenv was doing exactly what it said, and that bootcmd was an environment variable whose value happened to be a U-Boot program. It selected between K24 and K26 starter-kit identifiers before running another named variable. I initially laughed that U-Boot apparently did not know which board it was on, then parsed the expression properly: the first match was a K24 attempt, the second a K26 attempt, and the detected board_name was unambiguously SMK-K26-XCL2G.

Codex had also guessed that the generic U-Boot variable boot_targets would already describe the search order. U-Boot replied that no such variable was defined. One layer later the apparent miss became the mechanism: som_cc_boot recognized the KV260 carrier and created boot_targets=mmc1 usb0 usb1 usb2 usb3 pxe dhcp at runtime. distro_bootcmd then iterated over those targets and expanded bootcmd_${target}. For mmc1, that became:

bootcmd_mmc1=devnum=1; run mmc_boot

I asked whether bootcmd_mmc1 was a binary or another script and whether I could inspect it. help run settled the type question: “run commands in an environment variable.” mmc_boot selected the device and delegated to another variable, which listed bootable partitions, identified their filesystems, and searched each one for Extlinux configuration, boot scripts or EFI. I followed several layers before announcing that I was bored of the onion. The important model was no longer opaque: compiled U-Boot commands supplied the hardware and filesystem machinery, while small programs stored in its environment expressed the board's boot policy.

The environment introduced itself with the unimprovable Loading Environment from nowhere... OK. In this build, U-Boot was using its compiled default environment rather than loading one from the SD card; board-specific values were then established from the hardware it discovered.

Leaving the shell

I finally ran run bootcmd. U-Boot followed the policy I had just unfolded, found the first bootable MMC target and handed the machine onward. By the time I recognized the familiar flood of systemd[1] messages, the kernel had already initialized, mounted the real ext4 root filesystem from mmcblk1p2, and executed its first userspace process.

Before that handoff, the messages gave me another small modelling problem:

switch to partitions #0, OK
mmc1 is current device
Scanning mmc 1:1...
Found U-Boot script /boot.scr

U-Boot was looking at the card, selecting partitions and gathering boot images. I reached for the name of the little packed structure which describes a disk's partitions and briefly could not remeMBR it. Right: the MBR. Then I noticed that U-Boot had found a 2.6 KB script there. That did not sniff. An MBR is tiny binary boot and partition metadata; it does not contain a filesystem shell script. I had compressed two operations into one. U-Boot used the partition table to locate a partition, understood that partition's filesystem, and found /boot.scr as an ordinary file within it.

I then put U-Boot itself on the SD card in my mental model: an understandable association between the second-stage boot loader and the Linux image, rather like GRUB living on the operating-system disk. Codex said the board had reported QSPI_MODE and that Kria's primary boot firmware, including U-Boot, lived in QSPI. I made it go back and retrieve AMD's documentation rather than letting a plausible inference harden into fact. The exact KV260 guide settled it: its QSPI BOOT.BIN contains the FSBL, PMU firmware, Arm Trusted Firmware and U-Boot. My general model of Zynq UltraScale+ flexibility was not wrong; this carrier had chosen a specific, deliberately isolated architecture. The stable board firmware lives in redundant QSPI images, while the replaceable SD card holds the operating system.

The saved transcript made that transition unusually explicit. U-Boot first read roughly 31.4 MiB, then another 12.6 MiB. I guessed that the first was the kernel and recognized that the second matched the initramfs artifact. U-Boot then removed the remaining ambiguity by naming it kria-image-initramfs-kria-zynqmp and classifying it as a “RAMDisk Image.” In this context, ramdisk was the broader boot-image category and this particular ramdisk was the initramfs.

U-Boot loaded that pair and a flattened device tree into DDR before printing Starting kernel .... Linux received root=LABEL=root on its command line, freed the initial ramdisk after using it, printed Run /init as init process, discovered the SD card and mounted mmcblk1p2. The initramfs had bridged the kernel to the persistent root filesystem.

The remaining output connected this general boot process back to the specimen. Systemd started dfx-mgrd, ran the default firmware-load service, then reported:

[  OK  ] Started KV260 FC25 polyphonic synthesizer.
[  OK  ] Started Watch for FC25 MIDI reconnection.

Only afterward did the serial login prompt appear. I watched the familiar green [ OK ]s fly past, then verified—by measuring changes in air pressure—that the synthesizer was exhibiting its familiar “working as intended” behaviour.

□