"""Strategy to recover a Zynq-7000 board with a corrupted SD card.
Bootstraps U-Boot directly into DDR over JTAG, TFTP-loads a recovery Linux
(kernel + DTB + initramfs — rootfs in RAM), then streams a fresh SD-card
image over HTTP and ``dd``s it to ``/dev/mmcblk0``.
The strategy automates the host-side wiring by default. Minimal caller
responsibilities:
- ``ps7_init.tcl`` + ``u-boot.elf`` (+ optional FSBL) at host paths readable
by xsdb. Extract these from a known-good ``BOOT.BIN`` with
``bootgen -arch zynq -read BOOT.BIN``.
- **FPGA bitstream** at ``bitstream_path``. Required when the recovery kernel
has device-tree nodes for FPGA-fabric peripherals (``axi_clkgen``,
``axi_jesd204_*``, ``axi_adxcvr``, custom IPs). With an unprogrammed FPGA,
the kernel's AXI probe of those addresses hangs indefinitely. The driver
re-flashes the bitstream over JTAG before downloading U-Boot.
- Recovery ``kernel`` + ``dtb`` inside the ``TFTPServerResource.root``
directory.
- The SD image to flash, either as a local path (``sd_image_path``, served
automatically) or a pre-existing URL (``sd_image_url``).
Per-board file setup:
- ADI's "Kuiper-full" SD image is multi-board and ships BOOT.BIN /
uImage / devicetree under named subdirectories (one per board); the
FAT root is empty of those files. The Zynq-7000 BootROM only reads
``BOOT.BIN`` at the root, so a bare ``dd`` of Kuiper-full produces a
silent boot failure.
- Setting ``board_variant=<subdir-name>`` (e.g.
``zynq-zc706-adv7511-adrv937x``) tells the strategy to mount FAT
partition 1 after the dd, copy the per-board files up to the root,
sync, and unmount — all from inside the recovery initramfs.
- For images that already have ``BOOT.BIN`` at root, leave
``board_variant=None``.
- For tweaks that don't fit the board-variant pattern, add entries to
``post_flash_commands`` (a list of shell snippets run sequentially).
Automation defaults (``auto_build_initramfs=True``, ``auto_serve_http=True``):
- The recovery ``uInitrd.recovery`` is auto-built into the TFTP root on
first run via :func:`adi_lg_plugins.recovery.build_recovery_initramfs`
(cross-compiling a static busybox once and caching it under
``recovery_cache_dir``; supply ``busybox_static_path`` to skip the
compile entirely).
- The SD image at ``sd_image_path`` is served over HTTP on
``http_serve_port`` (ephemeral by default) for the lifetime of the
``sd_flash_done`` phase; the URL is composed using the local routable
IP and substituted into the recovery command.
Set either knob to ``False`` for fully-explicit mode where the caller
hand-stages every artifact and runs their own HTTP server.
See ``examples/zynq7000_recovery/`` for a working per-board YAML.
"""
import enum
import os
import time
import attr
from labgrid.factory import target_factory
from labgrid.step import step
from labgrid.strategy import Strategy, StrategyError, never_retry
# Pulled in lazily inside the auto-build path to keep import cost low.
# Re-exported here as a module-level constant only because the strategy
# attribute uses it as a default sentinel.
_DEFAULT_BUSYBOX_URL = "https://busybox.net/downloads/busybox-1.36.1.tar.bz2"
[docs]
class Status(enum.Enum):
"""Boot strategy state machine states for the SD recovery flow."""
unknown = 0
powered_off = 1
powered_on = 2
jtag_bootstrap = 3
uboot_prompt = 4
tftp_recovery_kernel = 5
linux_recovery = 6
sd_flash_done = 7
soft_off = 8
# Optional post-flash check: cold-cycle, let BootROM read the
# freshly-flashed SD, wait for the normal Kuiper login prompt.
sd_boot_verified = 9
[docs]
@target_factory.reg_driver
@attr.s(eq=False)
class BootZynq7000JTAGRecovery(Strategy):
"""Recover a Zynq-7000 board with a corrupted SD card.
Workflow:
1. Cold-cycle power.
2. JTAG-bootstrap U-Boot into DDR via xsdb (``ps7_init.tcl`` +
optional FPGA ``bitstream`` + ``u-boot.elf``).
3. Interrupt U-Boot autoboot on serial.
4. TFTP-load recovery kernel/DTB/initramfs; ``bootm`` into RAM-rooted
Linux.
5. From Linux userspace, ``wget <sd_image_url> | dd of=/dev/mmcblk0``.
6. (optional) ``sd_boot_verified``: cold-cycle and confirm the
freshly-flashed SD boots all the way to a normal login prompt —
no boot-mode switches change, BootROM just reads the
freshly-written ``BOOT.BIN`` this time.
Stop at ``sd_flash_done`` if you just want the flash, or transition all
the way to ``sd_boot_verified`` to also assert the resulting SD is
bootable. ``soft_off`` leaves the board powered down regardless.
Generic across Zynq-7000 boards; board-specific values (memory addresses,
DTB filename, ``ps7_init.tcl`` path) are all attributes.
"""
bindings = {
"power": "PowerProtocol",
"jtag": "XilinxJTAGDriver",
"shell": "ADIShellDriver",
"tftp_server": "TFTPServerResource",
"tftp_driver": "TFTPServerDriver",
"ssh": {"SSHDriver", None},
}
status = attr.ib(default=Status.unknown)
# JTAG bootstrap inputs (paths on the host that runs xsdb)
ps7_init_tcl = attr.ib(default=None)
uboot_elf = attr.ib(default=None)
fsbl_elf = attr.ib(default=None)
bitstream_path = attr.ib(default=None)
a9_target_name = attr.ib(default="*Cortex-A9 MPCore #0")
# Recovery image inputs (filenames inside tftp_root_folder)
recovery_kernel = attr.ib(default=None)
recovery_dtb = attr.ib(default=None)
recovery_initramfs = attr.ib(default="uInitrd.recovery")
recovery_login_marker = attr.ib(default="recovery login:")
# SD flash inputs
sd_image_url = attr.ib(default=None)
sd_device = attr.ib(default="/dev/mmcblk0")
download_cmd_template = attr.ib(default='wget -q -O - "{url}"')
# Post-flash board-variant copy. ADI's "Kuiper-full" SD image ships per
# board BOOT.BIN/uImage/devicetree under named subdirectories
# (zynq-zc706-adv7511-adrv937x, zynqmp-zcu102-rev10-adrv937x, ...);
# BootROM only reads BOOT.BIN at the FAT root.
#
# Two ways to drive the post-dd copy:
#
# simple — all files live under one subdir:
# board_variant: "zynq-zc706-adv7511-adrv9371"
# board_variant_files: ("BOOT.BIN", "uImage", "devicetree.dtb") # default
#
# complex — files span multiple subdirs (Kuiper-full ZC706, where
# uImage is shared in zynq-common/ and devicetree.dtb is one level
# deeper than BOOT.BIN):
# board_variant_paths:
# BOOT.BIN: "zynq-zc706-adv7511-adrv937x/BOOT.BIN"
# uImage: "zynq-common/uImage"
# devicetree.dtb: "zynq-zc706-adv7511-adrv937x/zynq-zc706-adv7511-adrv9371/devicetree.dtb"
#
# When ``board_variant_paths`` is set it takes precedence and the
# ``board_variant`` / ``board_variant_files`` pair is ignored.
# When neither is set the post-dd copy is skipped entirely (use for
# pre-cooked per-board images where BOOT.BIN is already at FAT root).
board_variant = attr.ib(default=None)
board_variant_files = attr.ib(default=("BOOT.BIN", "uImage", "devicetree.dtb"))
board_variant_paths = attr.ib(default=None) # dict[target, fat_source] | None
sd_boot_partition = attr.ib(default=1)
# Mount point inside the recovery initramfs — must exist as an empty
# dir (the strategy ``mkdir -p`` it first to be safe).
sd_mount_point = attr.ib(default="/mnt")
# Arbitrary post-flash shell commands executed in the recovery
# initramfs after the dd + board_variant copy succeed (one
# ``shell.run`` per entry, in order). Use for one-off image-shape
# tweaks that don't fit the board_variant pattern.
post_flash_commands = attr.ib(factory=list)
post_flash_timeout = attr.ib(default=120)
# --- Automation knobs --------------------------------------------------
# When True (default), the strategy will:
# - build the recovery initramfs (cross-compiling busybox once,
# cached at ``recovery_cache_dir``) into ``tftp_server.root/
# recovery_initramfs`` before the tftp_recovery_kernel phase, if
# that file doesn't already exist.
# - serve ``sd_image_path`` over HTTP for the lifetime of the
# sd_flash_done phase and substitute the URL into the recovery
# command, if ``sd_image_url`` isn't already set.
# Set False for pure-explicit mode where the caller hand-stages the
# initramfs in the TFTP root and runs their own HTTP server.
auto_build_initramfs = attr.ib(default=True)
auto_serve_http = attr.ib(default=True)
# Used by auto_build_initramfs. Pre-built static ARM busybox if the
# caller already has one (skips the cross-compile entirely); otherwise
# the strategy invokes ensure_busybox_static() which compiles + caches.
busybox_static_path = attr.ib(default=None)
busybox_source_url = attr.ib(default=None) # None = library default
cross_compile = attr.ib(default=None) # None = autodetect on PATH
recovery_cache_dir = attr.ib(default=None) # None = ~/.cache/...
# Used by auto_serve_http. Local filesystem path to the SD image; the
# strategy serves its parent directory over HTTP on ``http_serve_port``
# (0 picks a free ephemeral port). The board reaches back through
# ``http_serve_address`` — autodetected by routing to the target's
# IP if None.
sd_image_path = attr.ib(default=None)
http_serve_port = attr.ib(default=0)
http_serve_address = attr.ib(default=None)
# U-Boot env (Zynq-7000 = arm32: bootm + zImage + uInitrd)
uboot_prompt = attr.ib(default="zynq-uboot>|U-Boot>|=>")
kernel_addr = attr.ib(default="0x2080000")
dtb_addr = attr.ib(default="0x2000000")
initramfs_addr = attr.ib(default="0x4000000")
# Default uses ``rdinit=/init`` (the cpio's own ``/init`` script) rather
# than ``rdinit=/sbin/init``: with an initramfs rootfs the kernel uses
# ``rootfs`` directly and ``/sbin/init`` symlinks are inconsistent
# across busybox configurations. No ``root=`` because the kernel uses
# the unpacked initramfs as rootfs.
bootargs = attr.ib(default="console=ttyPS0,115200 earlyprintk loglevel=8 rdinit=/init")
# Robustness
jtag_bootstrap_retries = attr.ib(default=2)
wait_for_uboot_prompt_timeout = attr.ib(default=60)
wait_for_recovery_linux_timeout = attr.ib(default=180)
wait_for_sd_flash_timeout = attr.ib(default=1800)
# sd_boot_verified: drive the freshly-flashed SD through a real
# boot sequence and assert it reaches userspace.
#
# The strategy JTAG-loads U-Boot, interrupts autoboot, then has
# U-Boot ``fatload`` the kernel+dtb from the SD's FAT boot
# partition and ``bootm`` them. This avoids two real-world snags
# on Zynq-7000 dev boards:
#
# - boot-mode pins set to JTAG: BootROM doesn't try SD, so a
# cold-power-only test produces no UART output regardless of
# SD content.
# - U-Boot environment configured for TFTP boot: the default
# autoboot script doesn't fatload from SD; we drive it
# explicitly.
#
# The end state still depends on the SD's content (kernel + dtb +
# rootfs) being correct — the SD is what's actually booted.
verify_kernel_name = attr.ib(default="uImage")
verify_dtb_name = attr.ib(default="devicetree.dtb")
verify_bootargs = attr.ib(
default=(
"console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlyprintk rootfstype=ext4 rootwait"
)
)
verify_boot_login_marker = attr.ib(default="(analog|raspberrypi|kuiper).*login:")
wait_for_verify_boot_timeout = attr.ib(default=180)
def __attrs_post_init__(self):
super().__attrs_post_init__()
# Stash for the HTTP-serve context manager; populated when the
# sd_flash_done phase opens its own server (auto_serve_http path).
self._http_ctx = None
self.logger.info("BootZynq7000JTAGRecovery strategy initialized")
def _ensure_recovery_initramfs(self) -> None:
"""Auto-build uInitrd.recovery into the TFTP root if absent.
Skipped when ``auto_build_initramfs`` is False or the destination
file already exists. Cross-compiles busybox on first run (cached
in ``recovery_cache_dir``); subsequent runs are a few seconds.
"""
if not self.auto_build_initramfs:
return
tftp_root = self.tftp_server.root
target = os.path.join(tftp_root, self.recovery_initramfs)
if os.path.exists(target):
self.logger.info("recovery initramfs already staged at %s", target)
return
# Local import keeps module load cheap and avoids dragging
# urllib/tarfile in for non-auto users.
from adi_lg_plugins.recovery import build_recovery_initramfs
from adi_lg_plugins.recovery.busybox import ensure_busybox_static
busybox = self.busybox_static_path or ensure_busybox_static(
cache_dir=self.recovery_cache_dir,
source_url=self.busybox_source_url or _DEFAULT_BUSYBOX_URL,
cross_compile=self.cross_compile,
)
self.logger.info("building recovery initramfs at %s (busybox=%s)", target, busybox)
sizes = build_recovery_initramfs(busybox=busybox, output=target)
self.logger.info(
"recovery initramfs ready: cpio=%dB gz=%dB uimage=%dB",
sizes["cpio"],
sizes["gz"],
sizes.get("uimage", 0),
)
def _ensure_sd_image_url(self) -> None:
"""Spin up the HTTP server for the local SD image, if needed.
No-op when ``sd_image_url`` is already set explicitly, or when
``auto_serve_http`` is False. Stores the running server in
``self._http_ctx`` so ``_teardown_http_server`` can close it on
soft_off / failure.
"""
if self.sd_image_url or not self.auto_serve_http:
return
if not self.sd_image_path:
raise StrategyError(
"BootZynq7000JTAGRecovery: set sd_image_url or sd_image_path, "
"or disable auto_serve_http"
)
if not os.path.isfile(self.sd_image_path):
raise StrategyError(f"sd_image_path does not exist: {self.sd_image_path}")
from adi_lg_plugins.recovery.http import local_ip_for, serve_directory
directory = os.path.dirname(os.path.abspath(self.sd_image_path))
filename = os.path.basename(self.sd_image_path)
ctx = serve_directory(directory, port=self.http_serve_port)
# Entering the context starts the daemon thread; we keep it alive
# by stashing the ctx manager and __exit__-ing it in teardown.
_bind, port = ctx.__enter__()
self._http_ctx = ctx
# Figure out the IP the board reaches us at. The TFTP server
# binding gives the lab-side IP already; reuse it so we don't
# surprise users with a different interface.
host = self.http_serve_address or self.tftp_server.get_ip() or local_ip_for("8.8.8.8")
self.sd_image_url = f"http://{host}:{port}/{filename}"
self.logger.info("serving %s as %s", self.sd_image_path, self.sd_image_url)
def _teardown_http_server(self) -> None:
ctx = self._http_ctx
if ctx is None:
return
self._http_ctx = None
try:
ctx.__exit__(None, None, None)
except Exception as e: # pragma: no cover - cleanup is best-effort
self.logger.warning("HTTP server teardown raised: %s", e)
def _resolve_board_variant_paths(self) -> dict[str, str] | None:
"""Return {target_filename: fat_source_path} or None if no copy needed.
Explicit ``board_variant_paths`` wins. Otherwise expand
``board_variant`` + ``board_variant_files`` into a flat
``{f: f"{board_variant}/{f}"}`` mapping. Returns ``None`` when
neither is configured so the caller can skip the copy step.
"""
if self.board_variant_paths:
return dict(self.board_variant_paths)
if self.board_variant:
return {f: f"{self.board_variant}/{f}" for f in self.board_variant_files}
return None
def _build_board_variant_cmd(self, paths: dict[str, str]) -> str:
"""Compose the post-dd Kuiper-multi → root copy one-liner.
Mounts the FAT boot partition, copies each ``paths[target]``
source file up to the partition root as ``target``, syncs, and
unmounts. Emits ``BOARD_VARIANT_COPY_OK`` on success so the
caller can assert against output rather than just exit code.
"""
partition = f"{self.sd_device}p{self.sd_boot_partition}"
mount = self.sd_mount_point
copies = " && ".join(f'cp "{mount}/{src}" "{mount}/{dst}"' for dst, src in paths.items())
# The kernel keeps a per-block-device page cache. The post-dd
# whole-disk write (to ``sd_device``) doesn't invalidate the
# partition's own cache (``{sd_device}p{N}``) — they're distinct
# device entities. So even after sync + drop_caches +
# ``blockdev --flushbufs sd_device``, mounting the partition
# still sees the pre-dd FAT, ``cp`` updates only that cache,
# and umount discards everything.
#
# The fix: invalidate BOTH the whole-disk and the partition
# caches, re-read the partition table (which also drops the
# partitions' buffers), then mount with ``-o sync`` so writes
# land synchronously regardless of any remaining cache games.
return (
f"mkdir -p {mount} && "
"sync && "
"echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true && "
f"blockdev --flushbufs {self.sd_device} 2>/dev/null || true && "
f"blockdev --flushbufs {partition} 2>/dev/null || true && "
f"blockdev --rereadpt {self.sd_device} 2>/dev/null || "
f"partprobe {self.sd_device} 2>/dev/null || true && "
"sleep 1 && "
f"mount -t vfat -o rw,sync {partition} {mount} && "
f"{copies} && "
"sync && "
f"umount {mount} && "
"sync && "
f"blockdev --flushbufs {partition} 2>/dev/null || true && "
f"blockdev --flushbufs {self.sd_device} 2>/dev/null || true && "
"echo BOARD_VARIANT_COPY_OK"
)
def _run_post_flash(self) -> None:
"""Run board-variant copy (if configured) + post_flash_commands."""
paths = self._resolve_board_variant_paths()
if paths is not None:
cmd = self._build_board_variant_cmd(paths)
self.logger.info(
"Copying %d board-variant file(s) to FAT root: %s",
len(paths),
", ".join(f"{src} -> {dst}" for dst, src in paths.items()),
)
stdout, stderr, returncode = self.shell.run(cmd, timeout=self.post_flash_timeout)
stdout_str = "\n".join(stdout) if isinstance(stdout, list) else str(stdout)
stderr_str = "\n".join(stderr) if isinstance(stderr, list) else str(stderr)
if returncode != 0 or "BOARD_VARIANT_COPY_OK" not in stdout_str:
raise StrategyError(
f"board_variant copy failed (rc={returncode}): {stderr_str or stdout_str}"
)
for extra in self.post_flash_commands:
self.logger.info(f"Post-flash command: {extra}")
stdout, stderr, returncode = self.shell.run(extra, timeout=self.post_flash_timeout)
if returncode != 0:
stderr_str = "\n".join(stderr) if isinstance(stderr, list) else str(stderr)
stdout_str = "\n".join(stdout) if isinstance(stdout, list) else str(stdout)
raise StrategyError(
f"post_flash_commands entry failed (rc={returncode}): "
f"{extra}\n{stderr_str or stdout_str}"
)
def _require(self, name: str) -> str:
"""Fetch a required attr or raise StrategyError naming the field."""
value = getattr(self, name)
if not value:
raise StrategyError(f"BootZynq7000JTAGRecovery requires '{name}' to be configured")
return value
def _cold_cycle(self) -> None:
"""Off → settle → on. Clears residual board state."""
self.target.activate(self.power)
self.power.off()
time.sleep(5)
self.power.on()
def _build_sd_flash_cmd(self) -> str:
"""Compose the streaming download | dd one-liner for the recovery shell."""
sd_image_url = self._require("sd_image_url")
download_cmd = self.download_cmd_template.format(url=sd_image_url)
return (
f'test -b "{self.sd_device}" && '
f'{download_cmd} | dd of="{self.sd_device}" bs=4M conv=fsync && '
f"sync && echo SD_FLASH_OK"
)
[docs]
@never_retry
@step()
def transition(self, status, *, step):
if not isinstance(status, Status):
status = Status[status]
self.logger.info(f"Transitioning to {status} (Current: {self.status})")
if status == Status.unknown:
raise StrategyError(f"can not transition to {status}")
if status == self.status:
step.skip("nothing to do")
return
if status == Status.powered_off:
self.target.deactivate(self.shell)
if self.tftp_driver:
self.target.deactivate(self.tftp_driver)
self._teardown_http_server()
self.target.activate(self.power)
self.power.off()
self.logger.info("Device powered off")
elif status == Status.powered_on:
self.transition(Status.powered_off)
self.logger.info("Cold-cycling power...")
self._cold_cycle()
self.logger.info("Device powered on")
elif status == Status.jtag_bootstrap:
self.transition(Status.powered_on)
ps7_init_tcl = self._require("ps7_init_tcl")
uboot_elf = self._require("uboot_elf")
self.target.activate(self.jtag)
attempts = int(self.jtag_bootstrap_retries) + 1
last_error: Exception | None = None
for attempt in range(1, attempts + 1):
try:
self.logger.info(f"JTAG bootstrap attempt {attempt}/{attempts}...")
self.jtag.load_zynq_uboot(
ps7_init_tcl=ps7_init_tcl,
uboot_elf=uboot_elf,
a9_target_name=self.a9_target_name,
bitstream_path=self.bitstream_path,
fsbl_elf=self.fsbl_elf,
)
break
except Exception as e:
last_error = e
self.logger.error(f"JTAG bootstrap attempt {attempt}/{attempts} failed: {e}")
if attempt >= attempts:
raise StrategyError(f"JTAG bootstrap exhausted retries: {e}") from e
self.logger.info("Cold-cycling power before retry...")
self._cold_cycle()
else: # pragma: no cover - loop always breaks or raises
raise StrategyError(f"JTAG bootstrap exhausted retries: {last_error}")
self.logger.info("U-Boot bootstrapped via JTAG")
elif status == Status.uboot_prompt:
self.transition(Status.jtag_bootstrap)
self.target.activate(self.tftp_driver)
self.shell.bypass_login = True
self.target.activate(self.shell)
attempts = 2
for attempt in range(1, attempts + 1):
try:
self.logger.info("Waiting for U-Boot autoboot prompt...")
self.shell.console.expect(
"Hit any key to stop autoboot",
timeout=self.wait_for_uboot_prompt_timeout,
)
break
except Exception as e:
captured = b""
try:
captured = self.shell.console._expect.before or b""
except Exception:
pass
self.logger.error(
"Attempt %d/%d: no autoboot prompt within %ss (%d bytes captured).",
attempt,
attempts,
self.wait_for_uboot_prompt_timeout,
len(captured),
)
if captured:
self.logger.error("Captured UART tail: %r", captured[-400:])
if attempt >= attempts or len(captured) > 0:
raise e
self.logger.info("Re-bootstrapping U-Boot via JTAG before retry...")
self.target.deactivate(self.shell)
self._cold_cycle()
self.jtag.load_zynq_uboot(
ps7_init_tcl=self.ps7_init_tcl,
uboot_elf=self.uboot_elf,
a9_target_name=self.a9_target_name,
bitstream_path=self.bitstream_path,
fsbl_elf=self.fsbl_elf,
)
self.shell.bypass_login = True
self.target.activate(self.shell)
self.logger.info("Stopping autoboot...")
self.shell.console.sendline(" ")
time.sleep(2)
self._original_prompt = self.shell.prompt
self.shell.prompt = self.uboot_prompt
self.shell.console.sendline("\n")
self.shell._check_prompt_uboot()
self.logger.info("U-Boot prompt reached")
elif status == Status.tftp_recovery_kernel:
self.transition(Status.uboot_prompt)
kernel = self._require("recovery_kernel")
dtb = self._require("recovery_dtb")
initramfs = self._require("recovery_initramfs")
# Auto-build the initramfs into the TFTP root on first run.
# No-op when the file already exists or auto_build is off.
self._ensure_recovery_initramfs()
commands = [
"setenv autoload no",
"dhcp",
f"setenv serverip {self.tftp_server.get_ip()}",
f"setenv tftpdstport {self.tftp_driver.resource.port}",
f"setenv tftpport {self.tftp_driver.resource.port}",
f"setenv bootargs {self.bootargs}",
f"tftpboot {self.kernel_addr} {kernel}",
f"tftpboot {self.dtb_addr} {dtb}",
f"tftpboot {self.initramfs_addr} {initramfs}",
]
self.logger.info("Configuring U-Boot for recovery TFTP boot...")
for cmd in commands:
self.logger.info(f"U-Boot: {cmd}")
self.shell.run_uboot(f"{cmd}\n", timeout=60)
self.shell._check_prompt_uboot()
bootm = f"bootm {self.kernel_addr} {self.initramfs_addr} {self.dtb_addr}"
self.logger.info(f"Launching recovery kernel: {bootm}")
self.shell.console.sendline(bootm)
elif status == Status.linux_recovery:
self.transition(Status.tftp_recovery_kernel)
self.logger.info(f"Waiting for recovery login marker '{self.recovery_login_marker}'...")
self.shell.console.expect(
self.recovery_login_marker,
timeout=self.wait_for_recovery_linux_timeout,
)
# Restore original prompt and re-activate shell with login.
if hasattr(self, "_original_prompt"):
self.shell.prompt = self._original_prompt
self.shell.bypass_login = False
self.target.deactivate(self.shell)
self.target.activate(self.shell)
self.logger.info("Recovery Linux shell ready")
elif status == Status.sd_flash_done:
self.transition(Status.linux_recovery)
# Stand up an HTTP server for sd_image_path if the caller
# didn't pre-set sd_image_url. No-op for explicit mode.
self._ensure_sd_image_url()
# Inline ``shell.run`` rather than ``shell.run_script`` — the latter
# pushes the script via XMODEM, which expects a stable post-transfer
# prompt return that busybox ``rx`` over a raw initramfs console
# didn't deliver reliably in testing.
cmd = self._build_sd_flash_cmd()
self.logger.info(
f"Streaming SD image to {self.sd_device} (timeout {self.wait_for_sd_flash_timeout}s)..."
)
try:
stdout, stderr, returncode = self.shell.run(
cmd, timeout=self.wait_for_sd_flash_timeout
)
finally:
# Always tear down the HTTP server; leaving it dangling
# would hold the port and block subsequent runs.
self._teardown_http_server()
stdout_str = "\n".join(stdout) if isinstance(stdout, list) else str(stdout)
stderr_str = "\n".join(stderr) if isinstance(stderr, list) else str(stderr)
if returncode != 0 or "SD_FLASH_OK" not in stdout_str:
raise StrategyError(
f"SD flash failed (rc={returncode}): {stderr_str or stdout_str}"
)
# Run board-variant copy + any user post-flash commands. Must
# happen after the dd succeeds so the freshly-written FAT is
# what we're mounting / editing.
self._run_post_flash()
self.logger.info("SD card reflashed successfully")
elif status == Status.soft_off:
self.transition(Status.sd_flash_done)
try:
self.logger.info("Triggering soft power off...")
self.shell.run("poweroff")
self.shell.console.expect("Power down", timeout=30)
self.target.deactivate(self.shell)
time.sleep(10)
except Exception as e:
self.logger.debug(f"Soft off failed: {e}")
time.sleep(5)
self.target.deactivate(self.shell)
self._teardown_http_server()
self.target.activate(self.power)
self.power.off()
self.logger.info("Device powered off")
elif status == Status.sd_boot_verified:
self.transition(Status.soft_off)
# Cold-cycle from soft_off; chip is otherwise already off.
self.logger.info("Cold-cycling for SD-boot verification...")
self._cold_cycle()
# JTAG-load U-Boot. Many ADI dev-bench setups leave the
# boot-mode pins on JTAG, so a cold-power-only test
# produces zero UART output even with a perfect SD. By
# using JTAG we always reach a known U-Boot state, and
# the actual boot below still depends on the SD content.
self.target.activate(self.jtag)
self.jtag.load_zynq_uboot(
ps7_init_tcl=self._require("ps7_init_tcl"),
uboot_elf=self._require("uboot_elf"),
a9_target_name=self.a9_target_name,
bitstream_path=self.bitstream_path,
fsbl_elf=self.fsbl_elf,
)
self.shell.bypass_login = True
self.target.activate(self.shell)
self.logger.info("Waiting for U-Boot autoboot prompt...")
self.shell.console.expect(
"Hit any key to stop autoboot",
timeout=self.wait_for_uboot_prompt_timeout,
)
self.shell.console.sendline(" ")
time.sleep(2)
self._original_prompt = self.shell.prompt
self.shell.prompt = self.uboot_prompt
self.shell.console.sendline("\n")
self.shell._check_prompt_uboot()
# Drive U-Boot to fatload from the SD and bootm. This is
# what proves the freshly-flashed SD content is correct:
# the kernel + dtb come from the SD, and root= points at
# /dev/mmcblk0p2 so the rootfs comes from there too.
partition = f"0:{self.sd_boot_partition}"
commands = [
"mmc rescan",
f"fatload mmc {partition} {self.kernel_addr} {self.verify_kernel_name}",
f"fatload mmc {partition} {self.dtb_addr} {self.verify_dtb_name}",
f"setenv bootargs {self.verify_bootargs}",
]
self.logger.info("Configuring U-Boot for SD-boot verification...")
for cmd in commands:
self.logger.info(f"U-Boot: {cmd}")
self.shell.run_uboot(f"{cmd}\n", timeout=60)
self.shell._check_prompt_uboot()
bootm = f"bootm {self.kernel_addr} - {self.dtb_addr}"
self.logger.info(f"Booting from SD: {bootm}")
self.shell.console.sendline(bootm)
self.logger.info(
f"Waiting for SD-boot login marker '{self.verify_boot_login_marker}' "
f"(timeout {self.wait_for_verify_boot_timeout}s)..."
)
try:
self.shell.console.expect(
self.verify_boot_login_marker,
timeout=self.wait_for_verify_boot_timeout,
)
except Exception as e:
captured = b""
try:
captured = self.shell.console._expect.before or b""
except Exception:
pass
self.logger.error(
"SD-boot verification timed out (%d bytes captured).",
len(captured),
)
if captured:
self.logger.error("UART tail: %r", captured[-1000:])
raise StrategyError(
"Freshly-flashed SD did not reach the expected login "
f"prompt within {self.wait_for_verify_boot_timeout}s"
) from e
self.logger.info("SD card boots normally — recovery verified")
else:
raise StrategyError(f"no transition found from {self.status} to {status}")
self.status = status