Compare commits
10
Commits
2695180c8f
...
5ce544cd3c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce544cd3c | ||
|
|
73b8225a27 | ||
|
|
0568315b83 | ||
|
|
7067ce273f | ||
|
|
9a7a625c46 | ||
|
|
47569a0e43 | ||
|
|
8b2fac931c | ||
|
|
09be20791f | ||
|
|
b294dfe58a | ||
|
|
a47e3b78ae |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin
|
bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin
|
||||||
bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit
|
bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit
|
||||||
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf
|
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/tftp_app/tftp.elf
|
||||||
erase_all: false
|
erase_all: false
|
||||||
firmware_bin_path: /home/ly0kos/work/Verify/FW/V1.3.1.3.3_06_01_pmu_cpld_check/V1.3.1.3.3_06_01_pmu_cpld_check.bin
|
firmware_bin_path: /home/ly0kos/work/Verify/FW/V1.3.1.3.3_06_01_pmu_cpld_check/V1.3.1.3.3_06_01_pmu_cpld_check.bin
|
||||||
flash_model: s25fl256s1
|
flash_model: s25fl256s1
|
||||||
|
|||||||
+324
-53
@@ -214,7 +214,7 @@ def _program_with_xsct(
|
|||||||
if callback:
|
if callback:
|
||||||
callback("progress", "Initializing PS then programming FPGA...")
|
callback("progress", "Initializing PS then programming FPGA...")
|
||||||
|
|
||||||
# Build TCL: PS init first, then FPGA config
|
# Build TCL: PS init → FPGA config → post-config → halt CPU → disable Flash boot
|
||||||
lines = ["connect"]
|
lines = ["connect"]
|
||||||
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
||||||
lines.append("targets 1")
|
lines.append("targets 1")
|
||||||
@@ -223,6 +223,17 @@ def _program_with_xsct(
|
|||||||
if callback:
|
if callback:
|
||||||
callback("progress", "PS initialized (clocks, DDR, MIO)")
|
callback("progress", "PS initialized (clocks, DDR, MIO)")
|
||||||
lines.append(f'fpga -file "{bit_path}"')
|
lines.append(f'fpga -file "{bit_path}"')
|
||||||
|
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
||||||
|
lines.append("ps7_post_config")
|
||||||
|
if callback:
|
||||||
|
callback("progress", "PS post-config (PL-PS interfaces)")
|
||||||
|
# After FPGA config, CPU may be reset — halt it to prevent Flash code
|
||||||
|
# from enabling MMU before the ELF step can download FSBL
|
||||||
|
lines.append("targets 2")
|
||||||
|
lines.append("after 100")
|
||||||
|
lines.append("stop")
|
||||||
|
# Prevent boot from Flash by disabling PCAP reconfiguration
|
||||||
|
lines.append("mwr 0xF800025C 0x00000001")
|
||||||
lines.append("exit")
|
lines.append("exit")
|
||||||
tcl_script = "\n".join(lines)
|
tcl_script = "\n".join(lines)
|
||||||
|
|
||||||
@@ -323,7 +334,9 @@ def run_elf(
|
|||||||
) -> BitstreamResult:
|
) -> BitstreamResult:
|
||||||
"""Download and run a .elf executable on the Zynq PS ARM core.
|
"""Download and run a .elf executable on the Zynq PS ARM core.
|
||||||
|
|
||||||
Uses xsct's `dow` (download) command, with Vivado as fallback.
|
First downloads FSBL (First Stage Bootloader) to OCM (0x0) to
|
||||||
|
initialize clocks, DDR, MIO and force JTAG boot mode. Then
|
||||||
|
downloads the target ELF to DDR.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -341,10 +354,15 @@ def run_elf(
|
|||||||
message=f"ELF file not found: {elf_path}",
|
message=f"ELF file not found: {elf_path}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fsbl_path = config.fsbl_elf_path if hasattr(config, 'fsbl_elf_path') else ""
|
||||||
|
if fsbl_path and not Path(fsbl_path).exists():
|
||||||
|
if callback:
|
||||||
|
callback("warning", f"FSBL not found: {fsbl_path}, will try direct dow")
|
||||||
|
fsbl_path = ""
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("start", f"Running ELF: {elf_path.name}...")
|
callback("start", f"Running ELF: {elf_path.name}...")
|
||||||
|
|
||||||
# Use xsct for CPU operations (Vivado HM cannot program PS CPU)
|
|
||||||
xsct = get_xsct_path(
|
xsct = get_xsct_path(
|
||||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
||||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
||||||
@@ -356,91 +374,168 @@ def run_elf(
|
|||||||
message="xsct not found in PATH (required for CPU operations)",
|
message="xsct not found in PATH (required for CPU operations)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Auto-detect ps7_init.tcl from config or BIT file directory
|
|
||||||
ps7_tcl = _find_ps7_init_tcl(config)
|
|
||||||
timeout = config.step_timeouts.get("elf_download", 60)
|
timeout = config.step_timeouts.get("elf_download", 60)
|
||||||
return _run_elf_with_xsct(xsct, elf_path, callback, ps7_tcl, timeout)
|
|
||||||
|
if fsbl_path:
|
||||||
|
return _run_elf_via_fsbl(xsct, elf_path, fsbl_path, callback, timeout)
|
||||||
|
else:
|
||||||
|
return _run_elf_direct(xsct, elf_path, callback, timeout)
|
||||||
|
|
||||||
|
|
||||||
def _run_elf_with_xsct(
|
def _run_elf_via_fsbl(
|
||||||
xsct_path: str,
|
xsct_path: str,
|
||||||
elf_path: Path,
|
elf_path: Path,
|
||||||
|
fsbl_path: str,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
ps7_init_tcl: str = "",
|
|
||||||
timeout: int = 60,
|
timeout: int = 60,
|
||||||
) -> BitstreamResult:
|
) -> BitstreamResult:
|
||||||
"""Download and run ELF using xsct's dow command.
|
"""Download and run ELF via FSBL bootloader.
|
||||||
|
|
||||||
Assumes PS was already initialized (by a prior BIT download step).
|
FSBL loads to OCM (0x0) which is always accessible even when
|
||||||
Falls back to PS init if DAP is broken.
|
MMU is enabled. FSBL initializes clocks/DDR/MIO and forces
|
||||||
|
JTAG boot mode, making DDR accessible for the app ELF.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xsct_path: Path to xsct executable.
|
xsct_path: Path to xsct executable.
|
||||||
elf_path: Path to .elf file.
|
elf_path: Path to target .elf file.
|
||||||
|
fsbl_path: Path to FSBL .elf file.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
ps7_init_tcl: Optional ps7_init.tcl path for recovery.
|
timeout: Subprocess timeout in seconds.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
BitstreamResult with status.
|
BitstreamResult with status.
|
||||||
"""
|
"""
|
||||||
if callback:
|
|
||||||
callback("progress", "Downloading ELF...")
|
|
||||||
|
|
||||||
lines = ["connect"]
|
|
||||||
# Recovery: if DAP is broken, re-init PS
|
|
||||||
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
|
||||||
lines.append("targets 1")
|
|
||||||
lines.append(f'source "{ps7_init_tcl}"')
|
|
||||||
lines.append("ps7_init")
|
|
||||||
lines.append("targets 2")
|
|
||||||
lines.append(f'dow "{elf_path}"')
|
|
||||||
lines.append("con")
|
|
||||||
lines.append("exit")
|
|
||||||
tcl_script = "\n".join(lines)
|
|
||||||
|
|
||||||
import tempfile, os
|
import tempfile, os
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.tcl', delete=False) as f:
|
|
||||||
f.write(tcl_script)
|
if callback:
|
||||||
tcl_file = f.name
|
callback("progress", "Downloading FSBL to OCM...")
|
||||||
|
|
||||||
|
tcl = "\n".join([
|
||||||
|
"connect",
|
||||||
|
"targets 2",
|
||||||
|
"dow \"%s\"" % fsbl_path,
|
||||||
|
"puts \"FSBL downloaded\"",
|
||||||
|
"con",
|
||||||
|
"after 3000",
|
||||||
|
"stop",
|
||||||
|
"puts \"FSBL done, CPU halted\"",
|
||||||
|
"dow \"%s\"" % elf_path,
|
||||||
|
"mwr 0xF800025C 0x00000001",
|
||||||
|
"con",
|
||||||
|
"puts \"App running\"",
|
||||||
|
"exit",
|
||||||
|
])
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".tcl", delete=False
|
||||||
|
) as tf:
|
||||||
|
tf.write(tcl)
|
||||||
|
tcl_file = tf.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[xsct_path, tcl_file],
|
[xsct_path, tcl_file],
|
||||||
capture_output=True,
|
capture_output=True, text=True, timeout=timeout,
|
||||||
text=True,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
)
|
||||||
os.unlink(tcl_file)
|
os.unlink(tcl_file)
|
||||||
|
|
||||||
success = result.returncode == 0
|
|
||||||
output = (result.stdout + result.stderr).strip()
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
|
||||||
# xsct may return non-zero even if download succeeded
|
success = result.returncode == 0 or any(
|
||||||
if any(phrase in output for phrase in ("Download successful", "Done", "done")):
|
p in output for p in (
|
||||||
success = True
|
"App running", "Download successful",
|
||||||
|
"Successfully downloaded",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback(
|
callback(
|
||||||
"complete" if success else "error",
|
"complete" if success else "error",
|
||||||
"ELF " + ("executed" if success else "failed"),
|
"ELF " + ("executed via FSBL" if success else "failed"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
step="elf",
|
step="elf",
|
||||||
success=success,
|
success=success,
|
||||||
message="ELF executed successfully" if success else "ELF execution failed",
|
message="ELF executed via FSBL" if success else "ELF execution failed",
|
||||||
output=output[:4000],
|
output=output[:4000],
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
os.unlink(tcl_file)
|
os.unlink(tcl_file)
|
||||||
return BitstreamResult(
|
return BitstreamResult(step="elf", success=False,
|
||||||
step="elf",
|
message="ELF download timed out")
|
||||||
success=False,
|
|
||||||
message="xsct ELF download timed out",
|
|
||||||
|
def _run_elf_direct(
|
||||||
|
xsct_path: str,
|
||||||
|
elf_path: Path,
|
||||||
|
callback: ProgressCallback = None,
|
||||||
|
timeout: int = 60,
|
||||||
|
) -> BitstreamResult:
|
||||||
|
"""Download and run ELF directly (no FSBL, assumes DAP alive).
|
||||||
|
|
||||||
|
Used when no FSBL ELF is configured. Attempts direct dow
|
||||||
|
which only works if the CPU is in a clean state with MMU off.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xsct_path: Path to xsct executable.
|
||||||
|
elf_path: Path to .elf file.
|
||||||
|
callback: Optional progress callback.
|
||||||
|
timeout: Subprocess timeout in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BitstreamResult with status.
|
||||||
|
"""
|
||||||
|
import tempfile, os
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback("progress", "Downloading ELF directly...")
|
||||||
|
|
||||||
|
tcl = "\n".join([
|
||||||
|
"connect",
|
||||||
|
"targets 2",
|
||||||
|
"dow \"%s\"" % elf_path,
|
||||||
|
"mwr 0xF800025C 0x00000001",
|
||||||
|
"con",
|
||||||
|
"exit",
|
||||||
|
])
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".tcl", delete=False
|
||||||
|
) as tf:
|
||||||
|
tf.write(tcl)
|
||||||
|
tcl_file = tf.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[xsct_path, tcl_file],
|
||||||
|
capture_output=True, text=True, timeout=timeout,
|
||||||
|
)
|
||||||
|
os.unlink(tcl_file)
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
|
||||||
|
success = result.returncode == 0 or any(
|
||||||
|
p in output for p in (
|
||||||
|
"Download successful", "Successfully downloaded",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback("complete" if success else "error",
|
||||||
|
"ELF " + ("executed" if success else "failed"))
|
||||||
|
|
||||||
# ── Full Workflow ───────────────────────────────────────────────────
|
return BitstreamResult(
|
||||||
|
step="elf",
|
||||||
|
success=success,
|
||||||
|
message="ELF executed" if success else "ELF execution failed",
|
||||||
|
output=output[:4000],
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.unlink(tcl_file)
|
||||||
|
return BitstreamResult(step="elf", success=False,
|
||||||
|
message="ELF download timed out")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Full Workflow (combined BIT+ELF in one xsct session) ──────────
|
||||||
|
|
||||||
|
|
||||||
def full_bitstream_program(
|
def full_bitstream_program(
|
||||||
@@ -449,7 +544,13 @@ def full_bitstream_program(
|
|||||||
elf_path: str | Path,
|
elf_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> list[BitstreamResult]:
|
) -> list[BitstreamResult]:
|
||||||
"""Execute full bootloader workflow: program bitstream → run ELF.
|
"""Execute full bootloader workflow in one xsct session.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. rst -processor (bypass BootROM, block Flash boot)
|
||||||
|
2. dow fsbl → con → stop (FSBL initializes clocks/DDR/MIO)
|
||||||
|
3. fpga -f bit (configure PL)
|
||||||
|
4. dow app → con (run bootloader)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -460,11 +561,181 @@ def full_bitstream_program(
|
|||||||
Returns:
|
Returns:
|
||||||
List of BitstreamResult for each step.
|
List of BitstreamResult for each step.
|
||||||
"""
|
"""
|
||||||
results: list[BitstreamResult] = []
|
import tempfile, os
|
||||||
|
|
||||||
results.append(program_bitstream(config, bit_path, callback))
|
bit_path = Path(bit_path)
|
||||||
if not results[-1].success:
|
elf_path = Path(elf_path)
|
||||||
return results
|
fsbl_path = config.fsbl_elf_path if hasattr(config, 'fsbl_elf_path') else ""
|
||||||
|
|
||||||
results.append(run_elf(config, elf_path, callback))
|
# Validate files
|
||||||
return results
|
if not bit_path.exists():
|
||||||
|
return [BitstreamResult(step="bitstream", success=False,
|
||||||
|
message=f"BIT not found: {bit_path}")]
|
||||||
|
if not elf_path.exists():
|
||||||
|
return [BitstreamResult(step="elf", success=False,
|
||||||
|
message=f"ELF not found: {elf_path}")]
|
||||||
|
if not fsbl_path or not Path(fsbl_path).exists():
|
||||||
|
return [BitstreamResult(step="elf", success=False,
|
||||||
|
message=f"FSBL not found: {fsbl_path or '(not configured)'}")]
|
||||||
|
|
||||||
|
xsct = get_xsct_path(
|
||||||
|
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
||||||
|
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
||||||
|
)
|
||||||
|
if not xsct:
|
||||||
|
return [BitstreamResult(step="bitstream", success=False,
|
||||||
|
message="xsct not found")]
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback("start", "Initializing Zynq via JTAG (FSBL override)...")
|
||||||
|
|
||||||
|
# Build TCL with preemptive system reset + DAP error recovery
|
||||||
|
tcl_lines = [
|
||||||
|
'puts "INFO: Starting Zynq initialization via JTAG override..."',
|
||||||
|
"connect",
|
||||||
|
"after 500",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Preemptive system reset (safe on clean board; may help after flash)
|
||||||
|
tcl_lines.extend([
|
||||||
|
'# Preemptive system reset to clean up any leftover state',
|
||||||
|
'if {[catch {',
|
||||||
|
' targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||||
|
' rst -system',
|
||||||
|
' after 1000',
|
||||||
|
' stop',
|
||||||
|
' puts "INFO: Preemptive system reset OK"',
|
||||||
|
'}]} { puts "INFO: Preemptive system reset skipped (no ARM target)" }',
|
||||||
|
'',
|
||||||
|
])
|
||||||
|
|
||||||
|
# DAP error check — if ARM cores not visible, try recovery via DAP
|
||||||
|
tcl_lines.extend([
|
||||||
|
'# Check for DAP errors and attempt recovery',
|
||||||
|
'set target_list [targets]',
|
||||||
|
'set has_arm 0',
|
||||||
|
'foreach t $target_list { if {[regexp -nocase {arm|cortex} $t]} { set has_arm 1 } }',
|
||||||
|
'if {!$has_arm} {',
|
||||||
|
' puts "WARNING: ARM cores not found — DAP may be in error state"',
|
||||||
|
' # Show DAP status for logging',
|
||||||
|
' foreach t $target_list { puts " TARGET: $t" }',
|
||||||
|
' # Attempt recovery: select DAP (target 1) and issue system reset',
|
||||||
|
' puts "INFO: Attempting DAP recovery via system reset..."',
|
||||||
|
' catch {',
|
||||||
|
' targets 1',
|
||||||
|
' rst -system',
|
||||||
|
' after 1000',
|
||||||
|
' stop',
|
||||||
|
' }',
|
||||||
|
' # Disconnect and reconnect to refresh JTAG chain',
|
||||||
|
' catch { disconnect; after 1000; connect; after 500 }',
|
||||||
|
' # Check if ARM cores reappeared',
|
||||||
|
' set target_list2 [targets]',
|
||||||
|
' set has_arm2 0',
|
||||||
|
' foreach t $target_list2 {',
|
||||||
|
' puts " AFTER RECOVERY: $t"',
|
||||||
|
' if {[regexp -nocase {arm|cortex} $t]} { set has_arm2 1 }',
|
||||||
|
' }',
|
||||||
|
' if {!$has_arm2} {',
|
||||||
|
' puts "FATAL: DAP recovery failed — board requires power cycle"',
|
||||||
|
' puts "FATAL: Unplug/replug power, then retry Step 3"',
|
||||||
|
' exit 1',
|
||||||
|
' }',
|
||||||
|
' puts "INFO: ARM cores recovered after DAP reset"',
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
])
|
||||||
|
|
||||||
|
# Core flow
|
||||||
|
tcl_lines.extend([
|
||||||
|
'targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||||
|
'puts "INFO: Resetting processor (Bypassing BootROM Flash boot)..."',
|
||||||
|
"rst -processor",
|
||||||
|
"after 500",
|
||||||
|
'puts "INFO: Downloading FSBL..."',
|
||||||
|
'dow "%s"' % fsbl_path,
|
||||||
|
'puts "INFO: Running FSBL for hardware initialization..."',
|
||||||
|
"con",
|
||||||
|
"after 2000",
|
||||||
|
"stop",
|
||||||
|
'puts "INFO: Hardware initialization completed."',
|
||||||
|
'puts "INFO: Downloading Bitstream..."',
|
||||||
|
'fpga -f "%s"' % bit_path,
|
||||||
|
"after 1000",
|
||||||
|
'targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||||
|
'puts "INFO: Downloading Application ELF..."',
|
||||||
|
'dow "%s"' % elf_path,
|
||||||
|
'puts "INFO: Executing Application..."',
|
||||||
|
"con",
|
||||||
|
'puts "SUCCESS: Zynq target is running the specified application."',
|
||||||
|
"exit",
|
||||||
|
])
|
||||||
|
|
||||||
|
tcl = "\n".join(tcl_lines)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".tcl", delete=False
|
||||||
|
) as tf:
|
||||||
|
tf.write(tcl)
|
||||||
|
tcl_file = tf.name
|
||||||
|
|
||||||
|
timeout = config.step_timeouts.get("bitstream", 60) + \
|
||||||
|
config.step_timeouts.get("elf_download", 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if callback:
|
||||||
|
callback("progress", "Executing combined JTAG flow...")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[xsct, tcl_file],
|
||||||
|
capture_output=True, text=True, timeout=timeout,
|
||||||
|
)
|
||||||
|
os.unlink(tcl_file)
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
|
||||||
|
success = result.returncode == 0 and "SUCCESS" in output
|
||||||
|
|
||||||
|
# Detect DAP errors for logging
|
||||||
|
dap_errors = [l for l in output.split("\n") if "DAP" in l and "error" in l.lower()]
|
||||||
|
if dap_errors:
|
||||||
|
if callback:
|
||||||
|
for e in dap_errors:
|
||||||
|
callback("error", f"DAP: {e.strip()[:120]}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback(
|
||||||
|
"complete" if success else "error",
|
||||||
|
"Bootloader " + ("running" if success else "failed"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
BitstreamResult(
|
||||||
|
step="bitstream",
|
||||||
|
success=success,
|
||||||
|
message=(
|
||||||
|
"DAP error — board needs power cycle"
|
||||||
|
if dap_errors else
|
||||||
|
"Bitstream loaded" if success else "Combined flow failed"
|
||||||
|
),
|
||||||
|
output=output[:4000],
|
||||||
|
),
|
||||||
|
BitstreamResult(
|
||||||
|
step="elf",
|
||||||
|
success=success,
|
||||||
|
message=(
|
||||||
|
"DAP error — board needs power cycle"
|
||||||
|
if dap_errors else
|
||||||
|
"ELF executed" if success else "Combined flow failed"
|
||||||
|
),
|
||||||
|
output=output[:4000],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.unlink(tcl_file)
|
||||||
|
return [
|
||||||
|
BitstreamResult(step="bitstream", success=False,
|
||||||
|
message="Combined JTAG flow timed out"),
|
||||||
|
BitstreamResult(step="elf", success=False,
|
||||||
|
message="Combined JTAG flow timed out"),
|
||||||
|
]
|
||||||
|
|||||||
+91
-22
@@ -82,6 +82,7 @@ class MainWindow(ctk.CTk):
|
|||||||
self._zynq_jtag_present: bool = False
|
self._zynq_jtag_present: bool = False
|
||||||
self._zynq_jtag_info = None
|
self._zynq_jtag_info = None
|
||||||
self._uart_monitor: UartMonitor | None = None
|
self._uart_monitor: UartMonitor | None = None
|
||||||
|
self._uart_window: UartMonitorWindow | None = None
|
||||||
self._flash_phase: str = ""
|
self._flash_phase: str = ""
|
||||||
|
|
||||||
# StringVar references for config sync
|
# StringVar references for config sync
|
||||||
@@ -157,21 +158,46 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config_path:
|
if self._config_path:
|
||||||
try:
|
try:
|
||||||
self._config = Config.from_file(self._config_path)
|
self._config = Config.from_file(self._config_path)
|
||||||
# If loaded from default_config.yaml, set save path to config.yaml
|
|
||||||
if self._config_path.name == "default_config.yaml":
|
if self._config_path.name == "default_config.yaml":
|
||||||
user_config_path = self._get_user_config_path()
|
user_config_path = self._get_user_config_path()
|
||||||
self._config._config_path = user_config_path
|
self._config._config_path = user_config_path
|
||||||
except Exception:
|
except Exception:
|
||||||
self._config = Config.from_default()
|
self._config = Config.from_default()
|
||||||
# Set save path to config.yaml for new configs
|
|
||||||
user_config_path = self._get_user_config_path()
|
user_config_path = self._get_user_config_path()
|
||||||
self._config._config_path = user_config_path
|
self._config._config_path = user_config_path
|
||||||
else:
|
else:
|
||||||
self._config = Config.from_default()
|
self._config = Config.from_default()
|
||||||
# Set save path to config.yaml for new configs
|
|
||||||
user_config_path = self._get_user_config_path()
|
user_config_path = self._get_user_config_path()
|
||||||
self._config._config_path = user_config_path
|
self._config._config_path = user_config_path
|
||||||
|
|
||||||
|
def _reload_config(self) -> None:
|
||||||
|
"""Re-read config.yaml and update UI selectors with latest paths."""
|
||||||
|
user_path = self._get_user_config_path()
|
||||||
|
if not user_path.exists():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
fresh = Config.from_file(user_path)
|
||||||
|
self._config = fresh
|
||||||
|
# Update UI selectors
|
||||||
|
for attr, selector in [
|
||||||
|
("bootloader_bit_path", self._bit_selector),
|
||||||
|
("bootloader_elf_path", self._elf_selector),
|
||||||
|
("bootloader_bin_path", self._bootloader_bin_selector),
|
||||||
|
("fsbl_elf_path", self._fsbl_selector),
|
||||||
|
("firmware_bin_path", self._firmware_bin_selector),
|
||||||
|
]:
|
||||||
|
val = getattr(fresh, attr, "")
|
||||||
|
if val:
|
||||||
|
selector.set_path(str(fresh.resolve_path(val)))
|
||||||
|
# Update IP
|
||||||
|
if self._ip_string_var:
|
||||||
|
self._ip_string_var.set(fresh.zynq_ip)
|
||||||
|
# Update xilinx path
|
||||||
|
if hasattr(fresh, 'xilinx_path') and self._xilinx_path_var:
|
||||||
|
self._xilinx_path_var.set(fresh.xilinx_path)
|
||||||
|
except Exception:
|
||||||
|
pass # Keep existing config on error
|
||||||
|
|
||||||
def _sync_config_from_ui(self) -> None:
|
def _sync_config_from_ui(self) -> None:
|
||||||
"""Read current UI values and update the Config object.
|
"""Read current UI values and update the Config object.
|
||||||
|
|
||||||
@@ -432,7 +458,7 @@ class MainWindow(ctk.CTk):
|
|||||||
row=0, column=0, sticky="nsew",
|
row=0, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=PADDING,
|
padx=PADDING, pady=PADDING,
|
||||||
)
|
)
|
||||||
panel.grid_rowconfigure(8, weight=1)
|
panel.grid_rowconfigure(9, weight=1)
|
||||||
|
|
||||||
title = ctk.CTkLabel(
|
title = ctk.CTkLabel(
|
||||||
panel,
|
panel,
|
||||||
@@ -465,6 +491,17 @@ class MainWindow(ctk.CTk):
|
|||||||
self._ip_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
|
self._ip_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
|
||||||
self._ip_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
self._ip_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
||||||
|
|
||||||
|
# FSBL ELF path (First Stage Bootloader — initialises PS, forces JTAG boot)
|
||||||
|
self._fsbl_selector = FileSelector(
|
||||||
|
panel,
|
||||||
|
label_text="FSBL ELF (.elf):",
|
||||||
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||||
|
)
|
||||||
|
if self._config.fsbl_elf_path:
|
||||||
|
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
|
||||||
|
self._fsbl_selector.set_path(str(resolved))
|
||||||
|
self._fsbl_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# BIT path (Bootloader)
|
# BIT path (Bootloader)
|
||||||
self._bit_selector = FileSelector(
|
self._bit_selector = FileSelector(
|
||||||
panel,
|
panel,
|
||||||
@@ -474,7 +511,7 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config.bootloader_bit_path:
|
if self._config.bootloader_bit_path:
|
||||||
resolved = self._config.resolve_path(self._config.bootloader_bit_path)
|
resolved = self._config.resolve_path(self._config.bootloader_bit_path)
|
||||||
self._bit_selector.set_path(str(resolved))
|
self._bit_selector.set_path(str(resolved))
|
||||||
self._bit_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# ELF path (Bootloader)
|
# ELF path (Bootloader)
|
||||||
self._elf_selector = FileSelector(
|
self._elf_selector = FileSelector(
|
||||||
@@ -485,7 +522,7 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config.bootloader_elf_path:
|
if self._config.bootloader_elf_path:
|
||||||
resolved = self._config.resolve_path(self._config.bootloader_elf_path)
|
resolved = self._config.resolve_path(self._config.bootloader_elf_path)
|
||||||
self._elf_selector.set_path(str(resolved))
|
self._elf_selector.set_path(str(resolved))
|
||||||
self._elf_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# Bootloader BIN path (Flash programming)
|
# Bootloader BIN path (Flash programming)
|
||||||
self._bootloader_bin_selector = FileSelector(
|
self._bootloader_bin_selector = FileSelector(
|
||||||
@@ -496,22 +533,11 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config.bootloader_bin_path:
|
if self._config.bootloader_bin_path:
|
||||||
resolved = self._config.resolve_path(self._config.bootloader_bin_path)
|
resolved = self._config.resolve_path(self._config.bootloader_bin_path)
|
||||||
self._bootloader_bin_selector.set_path(str(resolved))
|
self._bootloader_bin_selector.set_path(str(resolved))
|
||||||
self._bootloader_bin_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._bootloader_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# FSBL ELF path (required by program_flash for QSPI programming)
|
|
||||||
self._fsbl_selector = FileSelector(
|
|
||||||
panel,
|
|
||||||
label_text="FSBL ELF (.elf):",
|
|
||||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
|
||||||
)
|
|
||||||
if self._config.fsbl_elf_path:
|
|
||||||
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
|
|
||||||
self._fsbl_selector.set_path(str(resolved))
|
|
||||||
self._fsbl_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
||||||
|
|
||||||
# Flash options
|
# Flash options
|
||||||
flash_frame = ctk.CTkFrame(panel)
|
flash_frame = ctk.CTkFrame(panel)
|
||||||
flash_frame.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
flash_frame.grid(row=7, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
flash_frame.grid_columnconfigure(0, weight=1)
|
flash_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_all)
|
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_all)
|
||||||
@@ -845,9 +871,22 @@ class MainWindow(ctk.CTk):
|
|||||||
self._bitstream_callback,
|
self._bitstream_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dap_fail = False
|
||||||
for result in results:
|
for result in results:
|
||||||
status = "✓" if result.success else "✗"
|
status = "✓" if result.success else "✗"
|
||||||
self._log_message(f" {status} {result.step}: {result.message}")
|
self._log_message(f" {status} {result.step}: {result.message}")
|
||||||
|
if "DAP" in result.message and not result.success:
|
||||||
|
dap_fail = True
|
||||||
|
|
||||||
|
if dap_fail:
|
||||||
|
self._log_message("")
|
||||||
|
self._log_message(" ╔══════════════════════════════════════════╗")
|
||||||
|
self._log_message(" ║ DAP ERROR — JTAG debug port hung ║")
|
||||||
|
self._log_message(" ║ This is caused by prior flash programming║")
|
||||||
|
self._log_message(" ║ → Power-cycle the board (unplug/replug) ║")
|
||||||
|
self._log_message(" ║ → Then re-run Step 3 ║")
|
||||||
|
self._log_message(" ╚══════════════════════════════════════════╝")
|
||||||
|
self._log_message("")
|
||||||
|
|
||||||
return all(r.success for r in results)
|
return all(r.success for r in results)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -988,6 +1027,9 @@ class MainWindow(ctk.CTk):
|
|||||||
if index >= len(self._steps):
|
if index >= len(self._steps):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Always re-read config.yaml before running
|
||||||
|
self._reload_config()
|
||||||
|
|
||||||
self._is_running = True
|
self._is_running = True
|
||||||
self._run_btn.configure(state="disabled", text="Running...")
|
self._run_btn.configure(state="disabled", text="Running...")
|
||||||
self._progress.reset()
|
self._progress.reset()
|
||||||
@@ -1021,6 +1063,9 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._is_running:
|
if self._is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Always re-read config.yaml before running
|
||||||
|
self._reload_config()
|
||||||
|
|
||||||
self._is_running = True
|
self._is_running = True
|
||||||
self._run_btn.configure(state="disabled", text="Running...")
|
self._run_btn.configure(state="disabled", text="Running...")
|
||||||
self._progress.reset()
|
self._progress.reset()
|
||||||
@@ -1106,6 +1151,17 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
# Stop on first failure when running all steps
|
# Stop on first failure when running all steps
|
||||||
if not single_step and not success:
|
if not single_step and not success:
|
||||||
|
# Special case: Step 2 pass → Step 3 DAP fail = board needs power cycle
|
||||||
|
if i == 2 and results[0] and not success:
|
||||||
|
self._log_message("")
|
||||||
|
self._log_message(" ┌─────────────────────────────────────────┐")
|
||||||
|
self._log_message(" │ Step 3 failed after flash programming. │")
|
||||||
|
self._log_message(" │ This is normal — program_flash leaves │")
|
||||||
|
self._log_message(" │ the JTAG DAP in error state. │")
|
||||||
|
self._log_message(" │ → Power-cycle the board, then re-run │")
|
||||||
|
self._log_message(" │ Step 3 (Load Bootloader) alone. │")
|
||||||
|
self._log_message(" └─────────────────────────────────────────┘")
|
||||||
|
self._log_message("")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Inter-step delay (skip after last step)
|
# Inter-step delay (skip after last step)
|
||||||
@@ -1148,6 +1204,12 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
def _start_uart_monitor(self) -> None:
|
def _start_uart_monitor(self) -> None:
|
||||||
"""Start background serial monitoring if UART is available."""
|
"""Start background serial monitoring if UART is available."""
|
||||||
|
# If UART window is already monitoring, don't start a second instance
|
||||||
|
if (self._uart_window
|
||||||
|
and self._uart_window.winfo_exists()
|
||||||
|
and self._uart_window.is_monitoring):
|
||||||
|
self._log_message(" [UART] Already monitoring via UART window")
|
||||||
|
return
|
||||||
if not self._uart_available or not self._config:
|
if not self._uart_available or not self._config:
|
||||||
return
|
return
|
||||||
port = self._config.serial_port
|
port = self._config.serial_port
|
||||||
@@ -1157,8 +1219,9 @@ class MainWindow(ctk.CTk):
|
|||||||
self._uart_monitor = UartMonitor(port, self._config.serial_baudrate)
|
self._uart_monitor = UartMonitor(port, self._config.serial_baudrate)
|
||||||
self._uart_monitor.on_line = lambda line: self._log_message(f" [UART] {line}")
|
self._uart_monitor.on_line = lambda line: self._log_message(f" [UART] {line}")
|
||||||
self._uart_monitor.on_boot_info = lambda info: (
|
self._uart_monitor.on_boot_info = lambda info: (
|
||||||
self._log_message(f" [UART] Boot detected: IP={info.ip_address}, Ver={info.version}")
|
self._log_message(
|
||||||
if info.ip_address or info.version else None
|
f" [UART] Boot={info.boot_mode}, IP={info.ip_address}, Ver={info.version}"
|
||||||
|
) if any([info.boot_mode, info.ip_address, info.version]) else None
|
||||||
)
|
)
|
||||||
self._uart_monitor.on_error = lambda e: self._log_message(f" [UART] Error: {e}")
|
self._uart_monitor.on_error = lambda e: self._log_message(f" [UART] Error: {e}")
|
||||||
if self._uart_monitor.start():
|
if self._uart_monitor.start():
|
||||||
@@ -1191,6 +1254,9 @@ class MainWindow(ctk.CTk):
|
|||||||
def _on_close(self) -> None:
|
def _on_close(self) -> None:
|
||||||
"""Handle window close event: save config and cleanup."""
|
"""Handle window close event: save config and cleanup."""
|
||||||
self._stop_uart_monitor()
|
self._stop_uart_monitor()
|
||||||
|
if self._uart_window and self._uart_window.winfo_exists():
|
||||||
|
self._uart_window.destroy()
|
||||||
|
self._uart_window = None
|
||||||
if self._config:
|
if self._config:
|
||||||
self._sync_config_from_ui() # Sync UI values first
|
self._sync_config_from_ui() # Sync UI values first
|
||||||
# Save to user config file (config.yaml)
|
# Save to user config file (config.yaml)
|
||||||
@@ -1206,7 +1272,10 @@ class MainWindow(ctk.CTk):
|
|||||||
"""Open the UART Monitor window."""
|
"""Open the UART Monitor window."""
|
||||||
port = self._port_var.get()
|
port = self._port_var.get()
|
||||||
baudrate = self._config.serial_baudrate if self._config else 115200
|
baudrate = self._config.serial_baudrate if self._config else 115200
|
||||||
UartMonitorWindow(self, port=port, baudrate=baudrate)
|
self._uart_window = UartMonitorWindow(
|
||||||
|
self, port=port, baudrate=baudrate,
|
||||||
|
on_log=self._log_message,
|
||||||
|
)
|
||||||
|
|
||||||
def _test_serial_version(self) -> None:
|
def _test_serial_version(self) -> None:
|
||||||
"""Test serial port by sending 'ver()' command and parsing response."""
|
"""Test serial port by sending 'ver()' command and parsing response."""
|
||||||
|
|||||||
+15
-1
@@ -786,6 +786,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
master: ctk.CTk | ctk.CTkToplevel,
|
master: ctk.CTk | ctk.CTkToplevel,
|
||||||
port: str,
|
port: str,
|
||||||
baudrate: int = 115200,
|
baudrate: int = 115200,
|
||||||
|
on_log: Callable[[str], None] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the UART monitor window.
|
"""Initialize the UART monitor window.
|
||||||
|
|
||||||
@@ -793,6 +794,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
master: Parent window (MainWindow).
|
master: Parent window (MainWindow).
|
||||||
port: Serial port device path (e.g., '/dev/ttyUSB0').
|
port: Serial port device path (e.g., '/dev/ttyUSB0').
|
||||||
baudrate: Baud rate for serial communication.
|
baudrate: Baud rate for serial communication.
|
||||||
|
on_log: Optional callback to forward parsed events to main log.
|
||||||
"""
|
"""
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
self.title("UART Monitor")
|
self.title("UART Monitor")
|
||||||
@@ -800,6 +802,8 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
self.minsize(480, 360)
|
self.minsize(480, 360)
|
||||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
|
|
||||||
|
self._on_log = on_log
|
||||||
|
|
||||||
self._port = port
|
self._port = port
|
||||||
self._baudrate = baudrate
|
self._baudrate = baudrate
|
||||||
self._monitor: UartMonitor | None = None
|
self._monitor: UartMonitor | None = None
|
||||||
@@ -991,12 +995,17 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
|
|
||||||
def _on_boot_info(self, info) -> None:
|
def _on_boot_info(self, info) -> None:
|
||||||
parts = []
|
parts = []
|
||||||
|
if info.boot_mode:
|
||||||
|
parts.append(f"Boot={info.boot_mode}")
|
||||||
if info.ip_address:
|
if info.ip_address:
|
||||||
parts.append(f"IP={info.ip_address}")
|
parts.append(f"IP={info.ip_address}")
|
||||||
if info.version:
|
if info.version:
|
||||||
parts.append(f"Ver={info.version}")
|
parts.append(f"Ver={info.version}")
|
||||||
if parts:
|
if parts:
|
||||||
self.after(0, lambda: self._log(f"[BOOT] {' '.join(parts)}\n"))
|
# Forward to main log only (not UART window)
|
||||||
|
if self._on_log:
|
||||||
|
msg = f"[UART] {' '.join(parts)}"
|
||||||
|
self.after(0, lambda: self._on_log(msg))
|
||||||
|
|
||||||
def _on_error(self, error: str) -> None:
|
def _on_error(self, error: str) -> None:
|
||||||
self.after(0, lambda: self._log_error(error))
|
self.after(0, lambda: self._log_error(error))
|
||||||
@@ -1024,6 +1033,11 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
|
|
||||||
# ── Window Close ───────────────────────────────────────────
|
# ── Window Close ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_monitoring(self) -> bool:
|
||||||
|
"""True if the UART monitor thread is currently running."""
|
||||||
|
return self._monitor is not None and self._monitor.is_running()
|
||||||
|
|
||||||
def _on_close(self) -> None:
|
def _on_close(self) -> None:
|
||||||
"""Cleanup UartMonitor and destroy window."""
|
"""Cleanup UartMonitor and destroy window."""
|
||||||
if self._monitor:
|
if self._monitor:
|
||||||
|
|||||||
+31
-6
@@ -23,6 +23,7 @@ class BootInfo:
|
|||||||
ip_address: str = ""
|
ip_address: str = ""
|
||||||
version: str = ""
|
version: str = ""
|
||||||
boot_message: str = ""
|
boot_message: str = ""
|
||||||
|
boot_mode: str = ""
|
||||||
raw_lines: list[str] | None = None
|
raw_lines: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -73,10 +74,19 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
|||||||
ip_pattern = re.compile(r"\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b")
|
ip_pattern = re.compile(r"\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b")
|
||||||
version_patterns = [
|
version_patterns = [
|
||||||
re.compile(r"[Vv]ersion[:\s]+([^\s;,\n]+)", re.IGNORECASE),
|
re.compile(r"[Vv]ersion[:\s]+([^\s;,\n]+)", re.IGNORECASE),
|
||||||
re.compile(r"[Bb]oot\s+([^\s;,\n]+)", re.IGNORECASE),
|
re.compile(r"[Bb]oot\s+(?:version\s+)?([Vv]?\d+\.\d+[^\s;,\n]*)", re.IGNORECASE),
|
||||||
re.compile(r"[Ff]irmware\s+([^\s;,\n]+)", re.IGNORECASE),
|
re.compile(r"[Ff]irm[Ww]are:?\s*([^\s;,\n]+)", re.IGNORECASE),
|
||||||
re.compile(r"(?:^|[\s:=])(v\d+\.\d+\.\d+)(?:\s|$)", re.IGNORECASE),
|
re.compile(r"(?:^|[\s:=])([Vv]\d+\.\d+\.\d+[^\s;,\n]*)", re.IGNORECASE),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _is_valid_version(v: str) -> bool:
|
||||||
|
"""Filter out noise like '3.1', 'mode', 'update'."""
|
||||||
|
if len(v) < 4:
|
||||||
|
return False
|
||||||
|
has_digit = any(c.isdigit() for c in v)
|
||||||
|
has_dot = "." in v or "_" in v
|
||||||
|
noise = {"mode", "upate", "update", "done", "error", "loaded", "running"}
|
||||||
|
return has_digit and has_dot and v.lower() not in noise
|
||||||
boot_complete_patterns = [
|
boot_complete_patterns = [
|
||||||
re.compile(r"boot\s+complete", re.IGNORECASE),
|
re.compile(r"boot\s+complete", re.IGNORECASE),
|
||||||
re.compile(r"system\s+ready", re.IGNORECASE),
|
re.compile(r"system\s+ready", re.IGNORECASE),
|
||||||
@@ -84,6 +94,10 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
|||||||
re.compile(r"Linux\s+booted", re.IGNORECASE),
|
re.compile(r"Linux\s+booted", re.IGNORECASE),
|
||||||
re.compile(r"QSYS\s+ready", re.IGNORECASE),
|
re.compile(r"QSYS\s+ready", re.IGNORECASE),
|
||||||
]
|
]
|
||||||
|
boot_mode_patterns = [
|
||||||
|
re.compile(r"[Bb]oot\s*[Mm]ode\s*(?:is)?\s*[:\[=\s]*(\w+)"),
|
||||||
|
re.compile(r"BootMode\s*[:\[=\s]*(\w+)", re.IGNORECASE),
|
||||||
|
]
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
# Extract IP address
|
# Extract IP address
|
||||||
@@ -96,7 +110,7 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
|||||||
if not info.version:
|
if not info.version:
|
||||||
for pattern in version_patterns:
|
for pattern in version_patterns:
|
||||||
match = pattern.search(line)
|
match = pattern.search(line)
|
||||||
if match:
|
if match and _is_valid_version(match.group(1)):
|
||||||
info.version = match.group(1)
|
info.version = match.group(1)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -107,6 +121,14 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
|||||||
info.boot_message = line.strip()
|
info.boot_message = line.strip()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Extract boot mode
|
||||||
|
if not info.boot_mode:
|
||||||
|
for pattern in boot_mode_patterns:
|
||||||
|
match = pattern.search(line)
|
||||||
|
if match:
|
||||||
|
info.boot_mode = match.group(1)
|
||||||
|
break
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
@@ -332,6 +354,7 @@ class UartMonitor:
|
|||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._all_lines: list[str] = []
|
self._all_lines: list[str] = []
|
||||||
self._latest_info = BootInfo(raw_lines=[])
|
self._latest_info = BootInfo(raw_lines=[])
|
||||||
|
self._last_emitted: tuple[str, str, str] = ("", "", "")
|
||||||
|
|
||||||
# Callbacks — set these before calling start()
|
# Callbacks — set these before calling start()
|
||||||
self.on_line: Callable[[str], None] | None = None
|
self.on_line: Callable[[str], None] | None = None
|
||||||
@@ -428,8 +451,10 @@ class UartMonitor:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
self._latest_info = info
|
self._latest_info = info
|
||||||
|
|
||||||
# Notify if we have new boot info
|
# Notify only when parsed info changes (dedup)
|
||||||
if info.boot_message or info.ip_address or info.version:
|
current = (info.boot_mode, info.ip_address, info.version)
|
||||||
|
if current != self._last_emitted and any(current):
|
||||||
|
self._last_emitted = current
|
||||||
if self.on_boot_info:
|
if self.on_boot_info:
|
||||||
self.on_boot_info(info)
|
self.on_boot_info(info)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+104
-154
@@ -1,16 +1,14 @@
|
|||||||
"""TFTP file transfer manager for Zynq Flasher GUI.
|
"""TFTP file transfer manager for Zynq Flasher GUI.
|
||||||
|
|
||||||
Handles uploading, downloading, and CRC verification of files
|
Handles uploading, downloading, and CRC verification of files
|
||||||
via TFTP to the Zynq device.
|
via TFTP to the Zynq device using a pure-Python TFTP client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import os
|
import socket
|
||||||
import platform
|
import struct
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -35,18 +33,17 @@ class TftpResult:
|
|||||||
|
|
||||||
ProgressCallback = Callable[[str, str], None] | None
|
ProgressCallback = Callable[[str, str], None] | None
|
||||||
|
|
||||||
|
# TFTP opcodes
|
||||||
|
_TFTP_RRQ = 1
|
||||||
|
_TFTP_WRQ = 2
|
||||||
|
_TFTP_DATA = 3
|
||||||
|
_TFTP_ACK = 4
|
||||||
|
_TFTP_ERROR = 5
|
||||||
|
_TFTP_BLOCK_SIZE = 512
|
||||||
|
|
||||||
|
|
||||||
def _compute_crc32(file_path: str | Path) -> int:
|
def _compute_crc32(file_path: str | Path) -> int:
|
||||||
"""Compute CRC32 checksum of a file.
|
"""Compute CRC32 checksum of a file."""
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Path to the file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
CRC32 value as unsigned integer.
|
|
||||||
"""
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
crc = 0
|
crc = 0
|
||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
while True:
|
while True:
|
||||||
@@ -57,15 +54,66 @@ def _compute_crc32(file_path: str | Path) -> int:
|
|||||||
return crc & 0xFFFFFFFF
|
return crc & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
def _find_tftp_client() -> str | None:
|
def _tftp_put(host: str, file_path: str, remote_name: str,
|
||||||
"""Find the tftp client executable.
|
timeout: float = 10.0) -> tuple[bool, str]:
|
||||||
|
"""Upload a file to a TFTP server using pure Python.
|
||||||
|
|
||||||
Uses shutil.which() for cross-platform compatibility.
|
Args:
|
||||||
|
host: TFTP server IP address.
|
||||||
|
file_path: Local file path.
|
||||||
|
remote_name: Remote filename.
|
||||||
|
timeout: Socket timeout in seconds.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to tftp executable, or None if not found.
|
(success, message).
|
||||||
"""
|
"""
|
||||||
return shutil.which("tftp")
|
data = Path(file_path).read_bytes()
|
||||||
|
total = len(data)
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ── Send WRQ ──
|
||||||
|
wrq = struct.pack("!H", _TFTP_WRQ)
|
||||||
|
wrq += remote_name.encode("ascii") + b"\x00"
|
||||||
|
wrq += b"octet\x00"
|
||||||
|
sock.sendto(wrq, (host, 69))
|
||||||
|
|
||||||
|
# ── Wait for ACK(0) to get server's data port ──
|
||||||
|
ack_data, server_addr = sock.recvfrom(516)
|
||||||
|
if len(ack_data) < 4:
|
||||||
|
return False, "Invalid ACK from server"
|
||||||
|
opcode = struct.unpack("!H", ack_data[:2])[0]
|
||||||
|
if opcode == _TFTP_ERROR:
|
||||||
|
err_msg = ack_data[4:].decode("ascii", errors="replace").rstrip("\x00")
|
||||||
|
return False, f"TFTP error: {err_msg}"
|
||||||
|
if opcode != _TFTP_ACK:
|
||||||
|
return False, f"Expected ACK, got opcode {opcode}"
|
||||||
|
|
||||||
|
# ── Send data blocks ──
|
||||||
|
block = 1
|
||||||
|
offset = 0
|
||||||
|
while offset < total:
|
||||||
|
chunk = data[offset:offset + _TFTP_BLOCK_SIZE]
|
||||||
|
offset += len(chunk)
|
||||||
|
data_pkt = struct.pack("!HH", _TFTP_DATA, block) + chunk
|
||||||
|
sock.sendto(data_pkt, server_addr)
|
||||||
|
|
||||||
|
# Wait for ACK
|
||||||
|
try:
|
||||||
|
ack_data, _ = sock.recvfrom(516)
|
||||||
|
ack_op = struct.unpack("!H", ack_data[:2])[0]
|
||||||
|
ack_block = struct.unpack("!H", ack_data[2:4])[0]
|
||||||
|
if ack_op != _TFTP_ACK or ack_block != block:
|
||||||
|
return False, f"Bad ACK: op={ack_op} block={ack_block}"
|
||||||
|
except socket.timeout:
|
||||||
|
return False, f"Timeout waiting for ACK block {block}"
|
||||||
|
block += 1
|
||||||
|
|
||||||
|
return True, f"Uploaded {total} bytes in {block - 1} blocks"
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
def tftp_upload(
|
def tftp_upload(
|
||||||
@@ -74,7 +122,7 @@ def tftp_upload(
|
|||||||
remote_name: str | None = None,
|
remote_name: str | None = None,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> TftpResult:
|
) -> TftpResult:
|
||||||
"""Upload a file to the Zynq device via TFTP.
|
"""Upload a file to the Zynq device via TFTP (pure Python).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -90,83 +138,42 @@ def tftp_upload(
|
|||||||
|
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return TftpResult(
|
return TftpResult(
|
||||||
step="upload",
|
step="upload", success=False,
|
||||||
success=False,
|
|
||||||
message=f"File not found: {file_path}",
|
message=f"File not found: {file_path}",
|
||||||
local_path=str(file_path),
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("start", f"Uploading {file_path.name} -> {remote_name}")
|
callback("start",
|
||||||
|
f"Uploading {file_path.name} -> {remote_name}")
|
||||||
|
|
||||||
# Verify device is reachable first
|
# Verify device is reachable
|
||||||
if not ping_ip(config.zynq_ip, config.ping_count, config.ping_timeout):
|
if not ping_ip(config.zynq_ip, config.ping_count, config.ping_timeout):
|
||||||
return TftpResult(
|
return TftpResult(
|
||||||
step="upload",
|
step="upload", success=False,
|
||||||
success=False,
|
|
||||||
message=f"Device {config.zynq_ip} not reachable",
|
message=f"Device {config.zynq_ip} not reachable",
|
||||||
local_path=str(file_path),
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tftp_cmd = _find_tftp_client()
|
try:
|
||||||
if not tftp_cmd:
|
ok, msg = _tftp_put(config.zynq_ip, str(file_path), remote_name)
|
||||||
|
except OSError as e:
|
||||||
return TftpResult(
|
return TftpResult(
|
||||||
step="upload",
|
step="upload", success=False,
|
||||||
success=False,
|
message=f"Socket error: {e}",
|
||||||
message="tftp client not found",
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
local_path=str(file_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("progress", f"Running: {tftp_cmd}")
|
callback("complete" if ok else "error",
|
||||||
|
"Upload " + ("succeeded" if ok else "failed"))
|
||||||
|
|
||||||
# Build TFTP command
|
return TftpResult(
|
||||||
cmd = [tftp_cmd, "-m", "binary", config.zynq_ip, "put", str(file_path), remote_name]
|
step="upload", success=ok,
|
||||||
|
message=msg,
|
||||||
try:
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
result = subprocess.run(
|
crc_local=_compute_crc32(file_path) if ok else 0,
|
||||||
cmd,
|
)
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
success = result.returncode == 0
|
|
||||||
output = (result.stdout + result.stderr).strip()
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
callback("complete" if success else "error",
|
|
||||||
"Upload " + ("succeeded" if success else "failed"))
|
|
||||||
|
|
||||||
return TftpResult(
|
|
||||||
step="upload",
|
|
||||||
success=success,
|
|
||||||
message=output or ("Upload succeeded" if success else "Upload failed"),
|
|
||||||
local_path=str(file_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
crc_local=_compute_crc32(file_path),
|
|
||||||
)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
if callback:
|
|
||||||
callback("error", "Upload timed out")
|
|
||||||
return TftpResult(
|
|
||||||
step="upload",
|
|
||||||
success=False,
|
|
||||||
message="Upload timed out",
|
|
||||||
local_path=str(file_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
crc_local=_compute_crc32(file_path),
|
|
||||||
)
|
|
||||||
except OSError as e:
|
|
||||||
return TftpResult(
|
|
||||||
step="upload",
|
|
||||||
success=False,
|
|
||||||
message=f"OS error: {e}",
|
|
||||||
local_path=str(file_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def tftp_download(
|
def tftp_download(
|
||||||
@@ -175,85 +182,28 @@ def tftp_download(
|
|||||||
local_dir: str | Path | None = None,
|
local_dir: str | Path | None = None,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> TftpResult:
|
) -> TftpResult:
|
||||||
"""Download a file from the Zynq device via TFTP.
|
"""Download a file from the Zynq device via TFTP (pure Python).
|
||||||
|
|
||||||
|
Note: This requires the Zynq to act as a TFTP client (U-Boot tftpboot),
|
||||||
|
which typically means we need a TFTP server on the PC. For simplicity,
|
||||||
|
this operation is not natively supported — use ping-based verification
|
||||||
|
or UART log analysis instead.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
remote_name: Remote filename on the TFTP server.
|
remote_name: Remote filename on the TFTP server.
|
||||||
local_dir: Local directory to save the file (default: temp directory).
|
local_dir: Local directory to save the file.
|
||||||
callback: Optional progress callback.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
TftpResult with download status.
|
TftpResult with status (always not-implemented).
|
||||||
"""
|
"""
|
||||||
local_dir = Path(local_dir or tempfile.gettempdir())
|
return TftpResult(
|
||||||
local_path = local_dir / remote_name
|
step="download",
|
||||||
|
success=False,
|
||||||
if callback:
|
message="TFTP download requires PC-side server (not implemented)",
|
||||||
callback("start", f"Downloading {remote_name} -> {local_path}")
|
remote_path=remote_name,
|
||||||
|
)
|
||||||
# Verify device is reachable
|
|
||||||
if not ping_ip(config.zynq_ip, config.ping_count, config.ping_timeout):
|
|
||||||
return TftpResult(
|
|
||||||
step="download",
|
|
||||||
success=False,
|
|
||||||
message=f"Device {config.zynq_ip} not reachable",
|
|
||||||
local_path=str(local_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
tftp_cmd = _find_tftp_client()
|
|
||||||
if not tftp_cmd:
|
|
||||||
return TftpResult(
|
|
||||||
step="download",
|
|
||||||
success=False,
|
|
||||||
message="tftp client not found",
|
|
||||||
local_path=str(local_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd = [tftp_cmd, "-m", "binary", config.zynq_ip, "get", remote_name, str(local_path)]
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=60,
|
|
||||||
)
|
|
||||||
success = result.returncode == 0
|
|
||||||
output = (result.stdout + result.stderr).strip()
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
callback("complete" if success else "error",
|
|
||||||
"Download " + ("succeeded" if success else "failed"))
|
|
||||||
|
|
||||||
crc = _compute_crc32(local_path) if success and local_path.exists() else 0
|
|
||||||
|
|
||||||
return TftpResult(
|
|
||||||
step="download",
|
|
||||||
success=success,
|
|
||||||
message=output or ("Download succeeded" if success else "Download failed"),
|
|
||||||
local_path=str(local_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
crc_remote=crc,
|
|
||||||
)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return TftpResult(
|
|
||||||
step="download",
|
|
||||||
success=False,
|
|
||||||
message="Download timed out",
|
|
||||||
local_path=str(local_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
|
||||||
except OSError as e:
|
|
||||||
return TftpResult(
|
|
||||||
step="download",
|
|
||||||
success=False,
|
|
||||||
message=f"OS error: {e}",
|
|
||||||
local_path=str(local_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def tftp_upload_verify(
|
def tftp_upload_verify(
|
||||||
|
|||||||
Reference in New Issue
Block a user