"""Driver to program Xilinx FPGAs via JTAG using xsdb.
Supports both local execution (test runner == exporter) and remote
execution (a client acquiring the place through a coordinator runs xsdb
on the exporter host). Remote detection and ssh are unified in
:class:`~adi_lg_plugins.drivers._remote.RemoteExecMixin`, keyed off the
bound ``xilinxdevicejtag`` resource: when it carries exporter-host info
(``host`` or ``extra['proxy']``), the generated TCL script is staged to
the exporter and xsdb runs there over a single reused ssh connection;
otherwise xsdb runs locally.
Note: bitstream / kernel / ELF / ps7_init paths embedded in the TCL are
"as seen by the host that runs xsdb" — they are assumed to already exist
on the exporter and are NOT auto-staged by this driver.
"""
import os
import subprocess
import tempfile
import attr
from labgrid.driver.common import Driver
from labgrid.driver.exception import ExecutionError
from labgrid.factory import target_factory
from labgrid.step import step
from ._remote import RemoteExecMixin
[docs]
@target_factory.reg_driver
@attr.s(eq=False)
class XilinxJTAGDriver(RemoteExecMixin, Driver):
"""Program Xilinx FPGAs via JTAG using xsdb.
Bindings:
xilinxdevicejtag: XilinxDeviceJTAG resource (JTAG target IDs + bitstream/kernel paths
as seen by the host that runs xsdb).
xilinxvivado: XilinxVivadoTool resource (vivado_path / xsdb_path).
"""
bindings = {
"xilinxdevicejtag": {"XilinxDeviceJTAG"},
"xilinxvivado": {"XilinxVivadoTool"},
}
# RemoteExecMixin: the resource that locates the exporter host.
_remote_binding = "xilinxdevicejtag"
def __attrs_post_init__(self):
super().__attrs_post_init__()
self.logger.info("XilinxJTAGDriver initialized")
self.logger.debug(f"xsdb path: {self.xilinxvivado.xsdb_path}")
def _exporter_host(self, res):
if os.environ.get("LG_FORCE_LOCAL_XSDB", "").lower() in ("1", "true", "yes", "on"):
return None
return super()._exporter_host(res)
def _run_xsdb(self, tcl_script: str, timeout: int = 300):
"""Execute ``tcl_script`` through xsdb, locally or on the exporter.
The TCL script is staged to the host that runs xsdb (a no-op when
local), then xsdb is invoked over the mixin's reused connection.
Returns (stdout, stderr, returncode) as strings.
"""
xsdb = self.xilinxvivado.xsdb_path
with tempfile.NamedTemporaryFile(mode="w", suffix=".tcl", delete=False) as f:
f.write(tcl_script)
local_tcl = f.name
try:
remote_tcl = self._stage_file(local_tcl)
result = subprocess.run(
self._remote_prefix() + [xsdb, remote_tcl],
capture_output=True,
text=True,
timeout=timeout,
)
return result.stdout, result.stderr, result.returncode
finally:
try:
os.unlink(local_tcl)
except FileNotFoundError:
pass
[docs]
@Driver.check_active
@step()
def connect_jtag(self):
"""Connect to JTAG interface."""
self.logger.info("Connecting to JTAG")
tcl_script = """
connect
after 1000
puts "JTAG connected"
"""
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"JTAG connection failed: {stderr}")
self.logger.debug(f"JTAG connection output: {stdout}")
[docs]
@Driver.check_active
@step()
def flash_bitstream(self):
"""Flash the FPGA bitstream via JTAG."""
if not self.xilinxdevicejtag.bitstream_path:
raise ExecutionError("Bitstream path not configured in XilinxDeviceJTAG resource")
self.logger.info(f"Flashing bitstream: {self.xilinxdevicejtag.bitstream_path}")
tcl_script = f"""
connect
after 1000
targets {self.xilinxdevicejtag.root_target}
after 1000
fpga -f {self.xilinxdevicejtag.bitstream_path}
after 2000
puts "Bitstream flashed successfully"
"""
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"Bitstream flash failed: {stderr}")
self.logger.info("Bitstream flashed successfully")
self.logger.debug(f"Flash output: {stdout}")
[docs]
@Driver.check_active
@step()
def download_kernel(self):
"""Download Linux kernel image to Microblaze processor."""
if not self.xilinxdevicejtag.kernel_path:
raise ExecutionError("Kernel path not configured in XilinxDeviceJTAG resource")
self.logger.info(f"Downloading kernel: {self.xilinxdevicejtag.kernel_path}")
tcl_script = f"""
connect
after 1000
targets {self.xilinxdevicejtag.microblaze_target}
after 1000
dow {self.xilinxdevicejtag.kernel_path}
after 1000
puts "Kernel downloaded successfully"
"""
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"Kernel download failed: {stderr}")
self.logger.info("Kernel downloaded successfully")
self.logger.debug(f"Download output: {stdout}")
[docs]
@Driver.check_active
@step()
def start_execution(self):
"""Start kernel execution on Microblaze processor."""
self.logger.info("Starting kernel execution")
tcl_script = f"""
connect
after 1000
targets {self.xilinxdevicejtag.microblaze_target}
after 1000
con
after 500
puts "Kernel execution started"
"""
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"Kernel execution failed: {stderr}")
self.logger.info("Kernel execution started")
self.logger.debug(f"Execution output: {stdout}")
[docs]
@Driver.check_active
@step()
def load_bitstream_and_kernel_and_start(self):
"""Load bitstream + kernel, then run the Microblaze."""
tcl_script = f"""
connect
after 1000
targets {self.xilinxdevicejtag.root_target}
after 1000
fpga -f {self.xilinxdevicejtag.bitstream_path}
after 2000
targets {self.xilinxdevicejtag.microblaze_target}
after 1000
dow {self.xilinxdevicejtag.kernel_path}
after 1000
con
after 500
puts "System started"
"""
self.logger.debug(f"System start TCL script:\n{tcl_script}")
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"System start failed: {stderr}")
self.logger.debug(f"System start output: {stdout}")
[docs]
@Driver.check_active
@step()
def disconnect_jtag(self):
"""Disconnect from JTAG interface."""
self.logger.info("Disconnecting from JTAG")
tcl_script = """
disconnect
puts "JTAG disconnected"
"""
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
self.logger.warning(f"JTAG disconnect warning: {stderr}")
self.logger.debug(f"JTAG disconnect output: {stdout}")
[docs]
@Driver.check_active
@step()
def load_zynq_uboot(
self,
ps7_init_tcl: str,
uboot_elf: str,
a9_target_name: str = "*Cortex-A9 MPCore #0",
bitstream_path: str | None = None,
fsbl_elf: str | None = None,
) -> None:
"""JTAG-bootstrap U-Boot on a Zynq-7000 device.
The board can be in any boot state — xsdb will ``rst -system`` first
to clear residual DDR/PS state before sourcing the board-specific
``ps7_init.tcl``. Used for SD-card recovery when BootROM cannot load
FSBL from a corrupted card.
The ``a9_target_name`` filter is used instead of an integer target
index because Zynq-7000 xsdb target ordering shifts when the PL is
loaded; the name-pattern form matches Xilinx's generated wrappers
and is stable across Vivado versions.
"""
self.logger.info(f"JTAG-bootstrapping Zynq-7000 U-Boot from {uboot_elf}")
optional_lines = []
if bitstream_path:
optional_lines.append(f"fpga -f {bitstream_path}")
optional_lines.append("after 2000")
optional_lines.append(f"source {ps7_init_tcl}")
optional_lines.append("ps7_init")
optional_lines.append("ps7_post_config")
if fsbl_elf:
optional_lines.append(f"dow {fsbl_elf}")
optional_lines.append("con")
optional_lines.append("after 2000")
optional_lines.append("stop")
optional_lines.append(f"dow {uboot_elf}")
optional_lines.append("con")
optional_block = "\n ".join(optional_lines)
tcl_script = f"""
connect
after 1000
targets -set -filter {{name =~ "{a9_target_name}"}}
after 500
rst -system
after 2000
{optional_block}
puts "U-Boot started via JTAG"
"""
self.logger.debug(f"Zynq U-Boot bootstrap TCL:\n{tcl_script}")
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"Zynq U-Boot bootstrap failed: {stderr}")
self.logger.info("Zynq U-Boot bootstrap completed")
self.logger.debug(f"Bootstrap output: {stdout}")
[docs]
@Driver.check_active
@step()
def load_and_run_elf(
self,
elf_path: str,
a9_target_name: str = "*Cortex-A9 MPCore #0",
bitstream_path: str | None = None,
ps7_init_tcl: str | None = None,
) -> None:
"""JTAG-load and start an arbitrary bare-metal ELF (e.g. no-os firmware).
Generalizes :meth:`load_zynq_uboot` to any ELF that runs directly on a
Zynq core (no FSBL/U-Boot chain). The same xsdb sequence is used:
``connect → rst -system → [fpga] → [ps7_init] → dow elf → con``. The
optional ``bitstream_path`` programs the PL first (required when the
firmware touches FPGA-fabric peripherals), and ``ps7_init_tcl`` runs the
board PS init — both are produced by the no-os build's HDL ``.xsa``.
Paths are resolved to absolute before being embedded in the xsdb TCL:
xsdb runs the script from its own working directory (not the caller's),
so a relative ``dow``/``fpga -f`` path would fail to open.
"""
elf_path = os.path.abspath(elf_path)
if bitstream_path:
bitstream_path = os.path.abspath(bitstream_path)
if ps7_init_tcl:
ps7_init_tcl = os.path.abspath(ps7_init_tcl)
self.logger.info(f"JTAG-loading bare-metal ELF from {elf_path}")
lines = []
if bitstream_path:
lines.append(f"fpga -f {bitstream_path}")
lines.append("after 2000")
if ps7_init_tcl:
lines.append(f"source {ps7_init_tcl}")
lines.append("ps7_init")
lines.append("ps7_post_config")
lines.append(f"dow {elf_path}")
lines.append("con")
optional_block = "\n ".join(lines)
tcl_script = f"""
connect
after 1000
targets -set -filter {{name =~ "{a9_target_name}"}}
after 500
rst -system
after 2000
{optional_block}
puts "Bare-metal ELF started via JTAG"
"""
self.logger.debug(f"ELF load TCL:\n{tcl_script}")
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"Bare-metal ELF load failed: {stderr}")
self.logger.info("Bare-metal ELF load completed")
self.logger.debug(f"ELF load output: {stdout}")
[docs]
@Driver.check_active
@step()
def stop_zynq_cpu(self, a9_target_name: str = "*Cortex-A9 MPCore #0") -> None:
"""Halt the A9 #0 core — used between failed bootstrap attempts."""
self.logger.info(f"Stopping Zynq A9 CPU ({a9_target_name})")
tcl_script = f"""
connect
after 500
targets -set -filter {{name =~ "{a9_target_name}"}}
stop
puts "A9 CPU stopped"
"""
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
self.logger.warning(f"Stop CPU warning: {stderr}")
self.logger.debug(f"Stop CPU output: {stdout}")
[docs]
@Driver.check_active
@step()
def load_zynqmp_uboot(
self,
psu_init_tcl: str,
spl_elf: str,
bitstream_path: str | None = None,
a53_target_name: str = "*Cortex-A53*#0*",
apu_release_rst_value: str = "0x380E",
dcc_log_path: str | None = None,
settle_ms: int = 12000,
jtag_url: str = "TCP:127.0.0.1:3121",
) -> None:
"""JTAG-bootstrap Xilinx "mini" U-Boot SPL on a ZynqMP (UltraScale+).
This is the UltraScale+ counterpart of :meth:`load_zynq_uboot`. It is
required because ZynqMP differs fundamentally from Zynq-7000:
* In **JTAG boot mode** (``BOOT_MODE_USER == 0x0``) the BootROM does
not load PMU firmware and the MicroBlaze PMU is not a debug target,
so full U-Boot + Arm Trusted Firmware (BL31) cannot run -- BL31
spins forever in ``ipi_mb_notify`` waiting on the PMU IPI mailbox.
* The working recovery bootstrap is the Xilinx ``xilinx_zynqmp_mini_*``
SPL, which runs standalone in OCM at EL3 (no ATF, no PMU-FW) and
exposes an ARM DCC / JTAG-UART console readable directly with xsdb.
Sequence (proven on ADRV9009-ZU11EG / ADRV2CRR-FMC):
1. Release the APU from reset without PMU-FW: write an AArch64
``b .`` bootloop to RVBAR (OCM ``0xFFFF0000``) then poke
``CRF_APB.RST_FPD_APU`` (``0xFD1A0104``). ``apu_release_rst_value``
defaults to ``0x380E`` which releases A53 #0 while holding
A53 #1..3 and L2 in reset -- matching the generated ZU11EG flow.
2. (Optional) program the PL bitstream.
3. ``source`` the board's generated ``psu_init.tcl`` and run
``psu_init``/``psu_post_config``/``psu_ps_pl_reset_config``/
``psu_ps_pl_isolation_removal`` to bring up clocks, DDR and the
SD MIO mux. The psu_init path yields a clean core + DDR; an FSBL +
``rst -processor`` path breaks debugger DDR access (EDITR timeout).
4. Clean the A53 (``rst -processor -clear-registers``), ``dow`` the
SPL ELF and ``con``.
5. If ``dcc_log_path`` is given, capture the DCC console to that file
on the xsdb host via ``readjtaguart`` (headless; ``jtagterminal``
needs an X server and is avoided).
Args:
psu_init_tcl: Path (on the xsdb host) to the board ``psu_init.tcl``.
spl_elf: Path (on the xsdb host) to ``spl/u-boot-spl`` (mini SPL).
bitstream_path: Optional PL bitstream to program before psu_init.
a53_target_name: xsdb target filter for the boot A53 core.
apu_release_rst_value: value written to ``0xFD1A0104`` to release
the APU. Use ``0x0`` to release all four A53s (can destabilise
later debug); prefer the board's generated per-core value.
dcc_log_path: Optional path on the xsdb host to capture the DCC
console log. Leave ``None`` to skip console capture.
settle_ms: milliseconds to let the SPL run before stopping DCC
capture / returning.
"""
self.logger.info(f"JTAG-bootstrapping ZynqMP mini U-Boot SPL from {spl_elf}")
lines = [
f"connect -url {jtag_url}",
"after 1000",
"configparams force-mem-accesses 1",
'targets -set -nocase -filter {name =~ "PSU"}',
"mwr 0xffff0000 0x14000000",
f"mwr 0xFD1A0104 {apu_release_rst_value}",
"after 1000",
]
if bitstream_path:
lines.append(f"fpga -file {bitstream_path}")
lines.append("after 500")
lines.append('targets -set -nocase -filter {name =~ "PSU"}')
lines += [
f"source {psu_init_tcl}",
"psu_init",
"psu_post_config",
"psu_ps_pl_reset_config",
"psu_ps_pl_isolation_removal",
"after 1500",
f'targets -set -nocase -filter {{name =~ "{a53_target_name}"}}',
"catch {stop}",
"rst -processor -clear-registers",
"after 1000",
"catch {stop}",
f"dow {spl_elf}",
]
if dcc_log_path:
lines.append(f"catch {{readjtaguart -start -handle [open {dcc_log_path} w]}}")
lines.append("con")
lines.append('puts "ZynqMP mini U-Boot SPL launched via JTAG"')
lines.append(f"after {int(settle_ms)}")
if dcc_log_path:
lines.append("catch {readjtaguart -stop}")
lines.append("disconnect")
tcl_script = "\n ".join(lines)
tcl_script = f"\n {tcl_script}\n "
self.logger.debug(f"ZynqMP SPL bootstrap TCL:\n{tcl_script}")
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
raise ExecutionError(f"ZynqMP mini U-Boot bootstrap failed: {stderr}")
self.logger.info("ZynqMP mini U-Boot bootstrap completed")
self.logger.debug(f"Bootstrap output: {stdout}")
[docs]
@Driver.check_active
@step()
def load_zynqmp_production_uboot(
self,
psu_init_tcl: str,
pmufw_bin: str,
uboot_bin: str,
handoff_bin: str | None = None,
bitstream_path: str | None = None,
ddr_scrub_elf: str | None = None,
bl31_bin: str | None = None,
atf_handoff_bin: str | None = None,
pm_config_bin: str | None = None,
bl31_console_uart_base: str | None = None,
bl31_console_ref_ctrl_address: str | None = None,
bl31_console_reset_mask: str = "0x2",
a53_target_name: str = "*Cortex-A53*#0*",
apu_release_rst_value: str = "0x380E",
pmufw_address: str = "0xFFDC0000",
uboot_address: str = "0x08000000",
handoff_address: str = "0x00100000",
bl31_address: str = "0xFFFEA000",
pm_config_address: str = "0x00200000",
pmufw_timeout_ms: int = 10000,
ddr_scrub_settle_ms: int = 30000,
settle_ms: int = 12000,
jtag_url: str = "TCP:127.0.0.1:3121",
) -> None:
"""Start production U-Boot on a ZynqMP board strapped for JTAG boot.
This path is for a recovered SD card whose board cannot leave physical
JTAG boot mode remotely. It reconstructs the parts of the production
BootROM/FSBL chain which full U-Boot needs:
* initialize the PS and DDR with the board's generated ``psu_init``;
* optionally run an OCM-resident DDR ECC scrub before U-Boot relocates;
* load PMU firmware through the physical PSU/DAP target, wake the PMU
ROM, and require firmware to claim ``FW_IS_PRESENT``;
* optionally program a production PL bitstream already converted to
the byte order expected by xsdb;
* physically load raw U-Boot and EL3-to-EL2 handoff binaries, reset the
A53 translation state, and enter the handoff at EL3.
For production U-Boot builds which issue SMC calls, pass ``bl31_bin``
and ``atf_handoff_bin``. BL31 remains resident at EL3 and provides the
Xilinx PM runtime services before entering U-Boot at EL2. For simpler
U-Boot builds with no EL3 runtime dependency, ``handoff_bin`` may be a
raw one-way EL3-to-EL2 trampoline instead.
Loading every final payload through the PSU target avoids MMU
translation faults left by a previous Linux or BL31 session.
PMU ``FW_IS_PRESENT`` is a firmware-owned readiness indication. This
method deliberately sets only ``DONT_SLEEP`` and fails if firmware does
not assert readiness before the timeout.
"""
self.logger.info(f"JTAG-starting production ZynqMP U-Boot from {uboot_bin}")
use_bl31 = bl31_bin is not None or atf_handoff_bin is not None
if use_bl31 and not (bl31_bin and atf_handoff_bin):
raise ExecutionError("bl31_bin and atf_handoff_bin must be supplied together")
if not use_bl31 and not handoff_bin:
raise ExecutionError("either BL31 artifacts or handoff_bin are required")
poll_count = max(1, int(pmufw_timeout_ms) // 100)
lines = [
f"connect -url {jtag_url}",
"after 1000",
"configparams force-mem-accesses 1",
'targets -set -nocase -filter {name =~ "PSU"}',
"mwr 0xffff0000 0x14000000",
f"mwr 0xFD1A0104 {apu_release_rst_value}",
"after 1000",
f"source {psu_init_tcl}",
"psu_init",
"psu_post_config",
"psu_ps_pl_reset_config",
"psu_ps_pl_isolation_removal",
"after 1500",
f'targets -set -nocase -filter {{name =~ "{a53_target_name}"}}',
"catch {stop}",
"rst -processor -clear-registers",
"after 1000",
"catch {stop}",
]
if ddr_scrub_elf:
lines += [
f"dow {ddr_scrub_elf}",
"con",
f"after {int(ddr_scrub_settle_ms)}",
"catch {stop}",
f'targets -set -nocase -filter {{name =~ "{a53_target_name}"}}',
'puts "DDR_ECC_SCRUB_COMPLETE pc=[rrd pc]"',
"rst -processor -clear-registers",
"after 1000",
"catch {stop}",
]
lines += [
'targets -set -nocase -filter {name =~ "PSU"}',
"set pmu_control_addr 0xFFD80000",
"set pmu_sleep 0",
f"for {{set i 0}} {{$i < {poll_count}}} {{incr i}} {{",
" set pmu_control [mrd -force -value $pmu_control_addr]",
" if {($pmu_control & 0x10000) != 0} {set pmu_sleep 1; break}",
" after 100",
"}",
'if {!$pmu_sleep} {error "PMU ROM did not enter sleep"}',
f"dow -force -data {pmufw_bin} {pmufw_address}",
"set pmu_control [mrd -force -value $pmu_control_addr]",
"mwr $pmu_control_addr [expr {$pmu_control | 0x1}]",
"set pmufw_ready 0",
f"for {{set i 0}} {{$i < {poll_count}}} {{incr i}} {{",
" after 100",
" set pmu_control [mrd -force -value $pmu_control_addr]",
" if {($pmu_control & 0x10) != 0} {set pmufw_ready 1; break}",
"}",
'if {!$pmufw_ready} {error "PMU firmware did not claim FW_IS_PRESENT"}',
'puts [format "PMUFW_READY control=0x%08x" $pmu_control]',
]
if pm_config_bin:
lines.append(f"dow -force -data {pm_config_bin} {pm_config_address}")
if bitstream_path:
lines += [
'targets -set -filter {name == "PL"}',
f"fpga -file {bitstream_path}",
'puts "FPGA_STATE=[fpga -state]"',
]
lines += [
'targets -set -nocase -filter {name =~ "PSU"}',
f"dow -force -data {uboot_bin} {uboot_address}",
]
if use_bl31:
if bool(bl31_console_uart_base) != bool(bl31_console_ref_ctrl_address):
raise ExecutionError(
"BL31 console UART base and reference-clock address must be supplied together"
)
if bl31_console_uart_base:
lines += [
f"mwr {bl31_console_ref_ctrl_address} 0x01010F00",
"set iou_rst [lindex [mrd -value -force 0xFF5E0238] 0]",
f"mwr 0xFF5E0238 [expr {{$iou_rst & ~{bl31_console_reset_mask}}}]",
f"mwr [expr {{{bl31_console_uart_base} + 0x34}}] 0x00000006",
f"mwr [expr {{{bl31_console_uart_base} + 0x18}}] 0x0000007C",
f"mwr [expr {{{bl31_console_uart_base} + 0x04}}] 0x00000020",
f"mwr {bl31_console_uart_base} 0x00000114",
]
lines += [
f"dow -force -data {bl31_bin} {bl31_address}",
f"dow -force -data {atf_handoff_bin} {handoff_address}",
f'targets -set -nocase -filter {{name =~ "{a53_target_name}"}}',
"catch {stop}",
f"rwr r0 {handoff_address}",
"rwr r1 0",
"rwr r2 0",
"rwr r3 0",
f"rwr pc {bl31_address}",
"con",
'puts "PRODUCTION_BL31_LAUNCHED"',
]
else:
lines += [
f"dow -force -data {handoff_bin} {handoff_address}",
f'targets -set -nocase -filter {{name =~ "{a53_target_name}"}}',
"catch {stop}",
f"rwr pc {handoff_address}",
"con",
'puts "PRODUCTION_UBOOT_LAUNCHED"',
]
lines += [f"after {int(settle_ms)}", "disconnect"]
tcl_script = "\n ".join(lines)
tcl_script = f"\n {tcl_script}\n "
self.logger.debug(f"ZynqMP production U-Boot handoff TCL:\n{tcl_script}")
xsdb_timeout = max(300, int(ddr_scrub_settle_ms / 1000) + 300)
stdout, stderr, returncode = self._run_xsdb(tcl_script, timeout=xsdb_timeout)
if returncode != 0:
detail = stderr or stdout
raise ExecutionError(f"ZynqMP production U-Boot handoff failed: {detail}")
self.logger.info("ZynqMP production U-Boot handoff completed")
self.logger.debug(f"Production handoff output: {stdout}")
[docs]
@Driver.check_active
@step()
def load_zynqmp_recovery_linux(
self,
psu_init_tcl: str,
trampoline_elf: str,
kernel_image: str,
initramfs: str,
dtb: str,
ddr_scrub_elf: str | None = None,
bitstream_path: str | None = None,
a53_target_name: str = "*Cortex-A53*#0*",
apu_release_rst_value: str = "0x380E",
trampoline_address: str = "0x00100000",
kernel_address: str = "0x00200000",
initramfs_address: str = "0x10000000",
dtb_address: str = "0x20000000",
ddr_scrub_done_address: str | None = None,
ddr_scrub_settle_ms: int = 120000,
post_init_mask_writes: list | None = None,
settle_ms: int = 3000,
jtag_url: str = "TCP:127.0.0.1:3121",
) -> None:
"""JTAG-load a RAM-rooted ZynqMP recovery Linux without BL31 or PMUFW.
Kernel, initramfs and DTB are downloaded through the physical PSU/DAP
target, so stale A53 translation state cannot redirect writes. The
caller-provided EL3 trampoline performs the board-specific timer/GIC
setup and enters Linux at non-secure EL2.
"""
lines = [
f"connect -url {jtag_url}",
"after 1000",
"configparams force-mem-accesses 1",
'targets -set -nocase -filter {name =~ "PSU"}',
"mwr 0xffff0000 0x14000000",
f"mwr 0xFD1A0104 {apu_release_rst_value}",
]
if bitstream_path:
lines += [
f"fpga -file {bitstream_path}",
'puts "RECOVERY_FPGA_STATE=[fpga -state]"',
'targets -set -nocase -filter {name =~ "PSU"}',
]
lines += [
f"source {psu_init_tcl}",
"psu_init",
"psu_post_config",
"psu_ps_pl_reset_config",
"psu_ps_pl_isolation_removal",
]
for address, mask, value in post_init_mask_writes or []:
lines.append(f"mask_write {address} {mask} {value}")
lines.append("after 1500")
lines += [
f'targets -set -nocase -filter {{name =~ "{a53_target_name}"}}',
"catch {stop}",
"rst -processor -clear-registers",
"after 1000",
"catch {stop}",
]
if ddr_scrub_elf:
lines += [
f"dow {ddr_scrub_elf}",
"con",
f"after {int(ddr_scrub_settle_ms)}",
"stop",
]
if ddr_scrub_done_address:
lines += [
"set scrub_pc_text [lindex [rrd pc] 1]",
"scan $scrub_pc_text %llx scrub_pc",
f"set scrub_done [expr {{{ddr_scrub_done_address}}}]",
'if {$scrub_pc != $scrub_done && $scrub_pc != ($scrub_done + 4)} {error "DDR ECC scrub did not reach completion loop: $scrub_pc"}',
]
lines += [
'puts "RECOVERY_DDR_ECC_SCRUB_COMPLETE pc=[rrd pc]"',
"rst -processor -clear-registers",
"after 1000",
"catch {stop}",
]
lines += [
'targets -set -nocase -filter {name =~ "PSU"}',
f"dow -force -data {kernel_image} {kernel_address}",
f"dow -force -data {initramfs} {initramfs_address}",
f"dow -force -data {dtb} {dtb_address}",
f'targets -set -nocase -filter {{name =~ "{a53_target_name}"}}',
"catch {stop}",
"rst -processor -clear-registers",
f"dow {trampoline_elf}",
f"rwr pc {trampoline_address}",
"con",
'puts "RECOVERY_LINUX_LAUNCHED"',
f"after {int(settle_ms)}",
"disconnect",
]
tcl_script = "\n ".join(lines)
tcl_script = f"\n {tcl_script}\n "
self.logger.debug(f"ZynqMP recovery Linux TCL:\n{tcl_script}")
xsdb_timeout = max(300, int(ddr_scrub_settle_ms / 1000) + 300)
stdout, stderr, returncode = self._run_xsdb(tcl_script, timeout=xsdb_timeout)
if returncode != 0:
raise ExecutionError(f"ZynqMP recovery Linux launch failed: {stderr or stdout}")
self.logger.info("ZynqMP recovery Linux launched via JTAG")
[docs]
@Driver.check_active
@step()
def stop_zynqmp_cpu(self, a53_target_name: str = "*Cortex-A53*#0*") -> None:
"""Halt the ZynqMP A53 #0 core -- used between failed bootstrap attempts."""
self.logger.info(f"Stopping ZynqMP A53 CPU ({a53_target_name})")
tcl_script = f"""
connect -url TCP:127.0.0.1:3121
after 500
targets -set -nocase -filter {{name =~ "{a53_target_name}"}}
catch {{stop}}
puts "A53 CPU stopped"
disconnect
"""
stdout, stderr, returncode = self._run_xsdb(tcl_script)
if returncode != 0:
self.logger.warning(f"Stop CPU warning: {stderr}")
self.logger.debug(f"Stop CPU output: {stdout}")