- ping_ip returns bool, not tuple — fixed 2 unpack errors
- TftpSubStepFrame: _skipped set prevents set_phase from overwriting Skip
- set_expanded(True) before skip marking so UI shows it immediately
- After Step 3 success, fix IP to 192.168.100.11
- TftpSubStepFrame.set_step_skipped(): ◌ Skip in WARNING color
- _reload_config(): suppress callbacks to avoid duplicate log/status wipe
- _log_all_file_info(): removed clear_info(), updates in-place per-key
- Removed duplicate _log_file_info method
- main_frame grid: uniform col group for strict 7:3 ratio
- FileSelector browse title shows expected file type hint
- boot_wait_delay: default 10→30s
- Step 4 IP fallback: ping first, then UDP scan, extra boot_delay retry
- _relative_path(): replace Path.relative_to() with os.path.relpath()
(ValueError crossing filesystems silently returned absolute paths)
- _sync_config_from_ui(): always derive relative from get_path() absolute,
never trust stale FileSelector._relative_path after Browse
- validate_paths(): check all file paths exist on startup, clear missing
- Clear UI selectors after validation to prevent re-save of stale paths
- _load_config(): auto-create blank config.yaml when missing on disk
- .gitignore: add config.yaml (user-specific, must not be tracked)
Xilinx program_flash often outputs informational lines containing
'uboot' (mini u-boot image file, U-Boot version banner) which are
not errors. The 'failed' keyword already catches 'U-Boot failed'
and 'uboot failed' in actual failure scenarios.
Windows backslashes in TCL strings are interpreted as escape sequences
(\f → form feed, \t → tab). This caused 'dow "Files\fsbl..."' to
fail with garbled paths — TCL couldn't find the file.
Added _tcl_path() helper: replaces \ with / in any path before
embedding in TCL. Forward slashes work on all platforms including
Windows. Applied to all 7 path sites: source, fpga -file, dow.
program_flash can return exit code 0 while outputting errors like
'uboot failed' or 'cannot open'. Previously only returncode was
checked, so these silent failures caused the workflow to continue
past a failed erase/program step.
Now scans combined stdout+stderr for failure keywords
(uboot, failed, cannot, abort, invalid, unsupported, timeout)
and returns success=False. Also checks returncode != 0 as fallback.
Step 1 now compares detected part (e.g. XC7Z035) against config's
zynq_part. If mismatch: log error, return False to block subsequent
steps. Also returns False when no Zynq detected (was always True).
Vitis/Vivado on Windows uses .bat wrappers, not .exe. The hardcoded
fallback paths in _find_executable step 4 only looked for .exe files.
Also expanded to try both 2023.2 and 2022.2 + Vitis/Vivado combos.
Vivado batch mode creates vivado.jou, vivado.log, and backup files
in cwd by default. Use -tempDir to redirect all temp output to the
system temp directory, and clean up afterwards.
JTAG: Start hw_server explicitly (Windows needs it), then run Vivado
TCL with Popen + line-by-line stdout streaming via callback so user
can see Vivado progress in real-time (not just on failure).
COM: Remove normalize_port — pyserial >= 3.0 handles COM>=10 prefix
internally. The manual \\.\ prefix was causing PermissionError(13)
on Windows where pyserial's own handling conflicted.
xsct's [targets] returns unquoted TCL lists where multi-word names
(e.g. 'ARM Cortex-A9 MPCore #0') are split into individual words
by foreach, making it impossible to reconstruct device names.
Vivado TCL with open_hw_manager works reliably:
- Cold start (no hw_server): 7.1s on Linux
- hw_server already running: 6.8s on Linux
- Provides IDCODE (converted from Vivado binary to hex), device names
Smart hw_server lifecycle:
- Check port 3121 before starting — reuse if already running
- Start hw_server only when needed (poll port up to 3s)
- Don't kill hw_server that we didn't start
Output format parsed: DEVICE:arm_dap_0 IDCODE:010010111010...0111
→ arm_dap_0 idcode=4BA00477 PS detected
→ xc7z100_1 idcode=03736093 PL detected → XC7Z100
Tested on Linux 2023.2, cold + warm start, confirmed working.
The previous TCL used 'targets' which returns XSCT target IDs (numbers
like 1, 2, APU, xc7z100) — each word was parsed as a separate device,
producing '1 (Other)', 'APU (Other)', etc.
Switch to 'jtag targets' which outputs raw JTAG chain devices:
1 xc7z100 (idcode 03736093 irlen 4)
2 arm_dap (idcode 4BA00477 irlen 4)
Changes:
- New _JTAG_LINE_RE pattern for parsing jtag targets output
- Zynq pattern relaxed for xsct names (xc7z100 not xc7z100_1)
- ARM DAP pattern: arm_dap (not arm_dap_N)
- JtagDevice now carries idcode field
- ZynqDevice propagates idcode + is_programmable=True
- Removed unused tempfile/Path/os imports
**JTAG check**: Switch from Vivado to xsct
- xsct is already proven to work (JTAG reboot succeeds)
- Much lighter than Vivado (no GUI infrastructure / open_hw_manager)
- Uses -eval inline TCL: connect; targets; disconnect; quit
- Removes dead tempfile/Path/os imports
**Windows COM port**: Add normalize_port() to serial_monitor.py
- COM ports >= 10 require NT namespace prefix (\.\COM11)
- Without it: PermissionError(13, '拒绝访问。')
- Applied at all 7 call sites: serial_monitor (5), reboot_manager, widgets
Previously the TCL script relied on Vivado's connect_hw_server to
auto-start hw_server. On Windows this either never worked (hw_server
never shows in Task Manager) or hung indefinitely during Vivado's
slow batch-mode initialisation.
Root cause: hw_server was never started.
Changes:
- vitis_checker.py: add 'hw_server' to TOOL_ALIASES + get_hw_server_path()
- zynq_checker.py: start hw_server via Popen before Vivado, kill in finally
- 2s sleep for port binding after start
- timeout from config.step_timeouts.check_env (default bumped to 120s)
- clean terminate/kill sequence in finally
- config_manager.py: check_env default 30 → 120s
Step 4's _run_step_4_load_main_program() already marks each sub-step
individually via set_step_complete/set_step_error. But the outer
_run_steps_worker() called set_phase('Boot Verify') on success, which
re-entered the 'running' phase and started a fresh pulse animation
(because _stop_pulse cleared _pulse_job).
Fix: on success, do nothing (sub-steps are already green). Only set
error on failure.
Previously the boot wait loop broke on the first detection of EITHER
IP or version. The Zynq firmware outputs its version string early in
the boot sequence (e.g. firmware header), while the board IP arrives
in a later burst via 'Boot=, IP=192.168.100.13, Ver=...'. The loop
would break on the version-only signal, leaving IP empty.
Changes:
1. Phase 1: log boot signal (version) but keep polling for IP
2. Phase 2: break only when IP address is actually found
3. After timeout: final check of latest_info for late-arriving IP
4. UART buffer fallback: parse all accumulated lines for IP
(handles lines that arrived between the last poll and exit)
5. 3s pre-scan delay before UDP discover to let network stack settle
(board's UDP server starts at the very end of boot sequence)
/tmp/ doesn't exist on Windows — Path('/tmp/...') resolves to C:\tmp\ which
fails with 'No such file or directory'. Use tempfile.gettempdir() for
cross-platform temp directory resolution.
Also cleaned up the get_vivado_path() call (removed obsolete vitis_path kwarg).
Root cause (2 bugs):
1. check_zynq_jtag() used config.vitis_path which no longer exists
(config now uses xilinx_path) → always got empty path → 'Vivado not found'
2. _get_vivado_path() only checked hardcoded Linux paths, no version
scanning, no .bat support on Windows
Fix:
- Added get_vivado_path() in vitis_checker.py (consistent with
get_xsct_path, get_program_flash_path, etc.)
- Uses _find_executable() shared logic: scans Vivado/*/bin +
Vitis/*/bin, picks newest version, supports .bat on Windows
- zynq_checker.py now uses get_vivado_path() + build_tool_command()
for proper environment setup (settings64 sourcing on Linux)
- Removed obsolete _get_vivado_path() + unused shutil import
Root cause: When UART window is open, self._uart_monitor is None
(the window creates its own UartMonitor). Step 4 boot wait loop
only checked self._uart_monitor.latest_info → always saw empty data
→ fell through to IP scanning even though UART had detected the IP.
Fix:
- UartMonitorWindow.get_latest_info() exposes parsed BootInfo
- Step 4 checks both: self._uart_monitor first, then UART window
- Null guard on info before accessing .ip_address
After Step 4 reboot, the Zynq may obtain a different IP via DHCP.
IP discovery strategy:
1. UART available → parse IP from boot output (fast, zero network traffic)
2. UART unavailable → scan 192.168.100.11–30 via UDP ver() probe
(port = last_octet - 3, 0.5s timeout each, ~10s max for 20 IPs)
If IP changed → auto-update config.zynq_ip + UI field
Tool discovery:
- On Windows, scan .bat files in addition to .exe
(Xilinx Windows tools use .bat wrappers, not .exe)
- Prefer .exe over .bat when both exist (same version dir)
Environment setup:
- _find_settings_script(): walk up from bin/ to find settings64.sh/.bat
- build_tool_command(): wrap Linux tool invocation with 'source settings64.sh'
Windows .bat wrappers handle env internally, no extra wrapping needed
- reboot_manager.py now uses build_tool_command() for proper env
Example output (Linux):
build_tool_command('/data/xilinx/Vitis/2023.2/bin/xsct', 'script.tcl')
→ ['bash', '-c', 'source .../settings64.sh && .../xsct script.tcl']
_version discovery:
- _scan_xilinx_root() collects ALL versions under Vitis/*/bin/ and Vivado/*/bin/
- _parse_version_tuple() semantic version comparison (2023.2 > 2018.3)
- Defaults to newest version, Vitis preferred over Vivado for same version
- _find_executable() search order: PATH → legacy vitis_path → scan xilinx_root → common paths
_version reporting:
- _get_tool_version(): tries -version flag first, falls back to path parsing
- _version_from_path(): extracts version from install path (e.g., .../Vitis/2023.2/bin/xsct → 2023.2)
- Step 1 logs each tool with version: ✓ xsct: /path [v2023.2]
- If multiple versions found, lists all: xsct versions found: 2023.2 (Vitis), 2018.3 (Vivado)
Cross-platform:
- Handles Windows path separators via Path()
- Expanded common Windows install paths
rst -system inherently resumes ARM execution on Zynq.
The subsequent 'con' command fails with 'Already running'
because the core is already executing after system reset.
Fix: wrap 'con' in 'catch {}' — if core is already running
the error is silently caught; if stopped, con resumes it.
xsct exits 0 (success) in both cases.
Config:
- Add boot_wait_delay (default 10s) for post-reboot wait time
Layout:
- Restore row weights for proportional scaling when maximized
- CTkScrollableFrame handles overflow when window is small
Step 3:
- Log xsct/vivado tool output (last 20 lines per result)
for detailed diagnostics
Step 4:
- UART boot wait: monitor for IP/version via UART after reboot
- If UART unavailable, fall back to config.boot_wait_delay
- Reduces unnecessary wait time when boot is detected early
Serial:
- Real-time port validation on selection change (CTkComboBox command)
- Persistent UART unavailable warning until valid port selected
- Auto-restart UART monitor on port switch
- Graceful cross-platform handling
- Implement _tftp_get() for downloading files from Zynq via TFTP
- Add TftpSubStepFrame widget for Step 4 progress display
- Refactor Step 4 into 5 sub-steps: Upload, Download Verify, CRC Check, Reboot, Boot Verify
- Fix SubStepFrame.reset() bug that caused collapse after execution
- Fix layout overlap: Step 4 sub-step and progress bar now use separate rows
- Improve ProgressIndicator appearance with better styling and spacing
- Fix UART monitor to start regardless of _uart_available flag
- Add CRC32 verification between uploaded and downloaded files
- Add preemptive rst -system at Step 3 start to clean post-flash DAP state
- Replace ineffective SLCR write with targets 1 → rst -system recovery
- Exit with clear power-cycle instructions when DAP recovery fails
- Show prominent DAP error guidance in GUI for Step 3 and Run All mode
Verified: Step 1→2→3 now passes end-to-end (340s) without board power cycle.
- TCL now checks for ARM core visibility before attempting flow
- If ARM cores are not found (DAP APB transaction error):
* Logs WARNING with target list
* Attempts SLCR system reset (0xF8000200)
* Reconnects after recovery attempt
- DAP errors detected in xsct output and logged to GUI log
- Result message shows 'DAP error — board needs power cycle'
_reload_config(): re-reads config.yaml and updates all UI selectors
(FSBL, BIT, ELF, BIN, FW BIN) and IP/xilinx_path fields with latest
values from file.
Called at the top of both _execute_step() and _run_all_steps(),
so changing config.yaml externally is picked up before every run.
1. Boot mode parsing:
- BootInfo.boot_mode field added
- Matches 'Boot mode is JTAG', 'Boot Mode: QSPI', etc.
- Fixed false version match: 'Boot mode is' no longer
captures 'mode' as a version string
2. UART window cleanup:
- Parsed boot info (mode/IP/ver) now only goes to main log
- Raw serial lines still appear in UART window
3. Step 3 FSBL enforcement:
- BIT step: after fpga -file, CPU is immediately halted (stop)
to prevent Flash code from enabling MMU
- ELF step: always dow FSBL → con → stop → dow bootloader
4. UI reorder:
- FSBL ELF selector moved BEFORE Bootloader BIT
- New layout: FSBL → BIT → ELF → Bootloader BIN → Flash → FW BIN
- Fixed grid overlap (FSBL and Flash were both at row 6)
Problem: dow app.elf to DDR (0x100000) failed with MMU section
translation fault because the CPU had MMU enabled from previously
executed Flash code. ps7_init could reset this but user wants
FSBL-based flow.
Fix: run_elf() now downloads FSBL first (to OCM 0x0 — always
accessible), runs it to initialize clocks/DDR/MIO and force JTAG
boot mode, then downloads the app ELF to DDR.
New flow:
connect → targets 2 → dow fsbl.elf → con → after 3s → stop
→ dow app.elf → mwr 0xF800025C 0x1 → con
Fallback: if no FSBL configured, use direct dow (legacy path).
Removed: _run_elf_with_xsct (2-attempt ps7_init recovery) replaced
by _run_elf_via_fsbl and _run_elf_direct.
Issue 1 — FirmWare version not parsed:
Input: [Info] FirmWare:V1.2.1.2.1_11_07_pmu_log3
Fix: regex [Ff]irm[Ww]are:?\s* (was [Ff]irmware\s+ — no : or camelCase)
Plus broader version capture: [Vv]\d+\.\d+\.\d+[^\s;,\n]*
Issue 2 — ELF loaded from Flash instead of downloaded ELF:
Root cause: Zynq devcfg.CTRL (0xF800025C) PCAP_PROG_B may trigger
reconfiguration from Flash after fpga, causing CPU to jump to
Flash-loaded code instead of our dow'd ELF.
BIT step: add ps7_post_config after fpga (finalizes PL-PS interfaces)
add mwr 0xF800025C 0x1 (disables PCAP reconfig)
ELF step: add mwr 0xF800025C 0x1 after dow, before con
(both attempt-1 and attempt-2 fallback)
UartMonitorWindow now accepts on_log callback. When boot info
(IP, version) is parsed from serial output, it:
- Appears in the UART window's own log (prefix [UART])
- Forwards to main window's log panel via on_log callback
- Unified [UART] prefix across both windows (was [BOOT] in
UART window, [UART] Boot detected: in main log)
Flow execution's _start_uart_monitor() now checks if a UART window
is already monitoring before starting its own UartMonitor instance.
This prevents two UartMonitor threads from fighting over the same
serial port.
Changes:
- UartMonitorWindow: add is_monitoring property
- MainWindow._uart_window: store reference to open UART window
- _start_uart_monitor(): skip if UART window already monitoring
- _on_close(): destroy UART window on main window close
_on_open_success(): directly call _on_start() instead of just
enabling the button. User no longer needs to click Start —
monitoring begins automatically when the port is available.
Start/Stop buttons remain for manual control.
CTkToplevel grab_set() fails when called before the window is
fully realized on screen. Defer to after(50ms) so the window
has time to become viewable.
Replace 'Read Serial' button with 'Open UART' that opens a new
UART Monitor window (CTkToplevel) with:
- Title bar with status indicator (● connecting/ready/monitoring/error)
- Port info display + Start/Stop/Clear buttons
- Large scrollable log area (CTkTextbox) with monospace font
- Auto-scroll to bottom, line counter in status bar
- Boot info parsing (IP, version) shown as [BOOT] prefix
- Background serial monitoring via UartMonitor class
UART unavailable handling:
- On init, tries to open serial port in background thread
- If port can't open → shows 'UART 不可用' state with error message
- If port opens → shows 'Ready', user clicks Start to begin monitoring
Window features:
- Modal (grab_set) — focus stays on monitor window
- Clean up UartMonitor on close (WM_DELETE_WINDOW handler)
- Thread-safe UI updates via after(0, ...)
- Reuses existing UartMonitor from serial_monitor.py
Configuration panel now has a 'Xilinx Kit:' row at the top with:
- Entry field showing current path
- [Browse] button using system directory dialog (cross-platform)
- Select root directory (e.g. /data/xilinx or C:/Xilinx)
vitis_checker.py now scans xilinx_root subdirectories:
1. System PATH (shutil.which) — works on all platforms
2. Legacy vitis_path/bin
3. xilinx_root/Vitis/*/bin and Vivado/*/bin (all versions, newest first)
4. Common install paths (Windows: C:/Xilinx, Linux: /data/xilinx, /opt/xilinx)
Multi-version: scans all version directories under Vitis/ and Vivado/,
picks the newest version for each tool.
Config: xilinx_path replaces vitis_path (backward compat kept)
1. SubStepFrame defaults to expanded (▼), not collapsed
2. erase_all checkbox has command=_on_erase_toggle → immediate config update
3. _deferred_init runs _check_vitis/_refresh_ports in background thread
Fixes UI freeze during startup (subprocess.run for tool versions)
4. Sub-step colors now match StepHeader:
- pending: gray ACCENT_PENDING (#888888)
- running: blue CIRCLE_RUNNING (#0078D4) with pulse animation
- complete: green ACCENT_COMPLETE (#107C10)
- error: red ACCENT_ERROR (#D13438)
- Running sub-step pulses between light/dark blue like StepHeader