fix: use xsct (not Vivado) for JTAG check + normalize COM>=10 ports on Windows

**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
This commit is contained in:
2026-06-11 16:29:32 +08:00
parent 91417f609e
commit 7716171aeb
4 changed files with 56 additions and 38 deletions
+2 -1
View File
@@ -1075,7 +1075,8 @@ class UartMonitorWindow(ctk.CTkToplevel):
def _bg_open() -> None: def _bg_open() -> None:
try: try:
import serial import serial
ser = serial.Serial(self._port, self._baudrate, timeout=1) from serial_monitor import normalize_port
ser = serial.Serial(normalize_port(self._port), self._baudrate, timeout=1)
ser.close() ser.close()
self.after(0, self._on_open_success) self.after(0, self._on_open_success)
except Exception as e: except Exception as e:
+2 -1
View File
@@ -125,9 +125,10 @@ def reboot_via_serial(
try: try:
import serial import serial
from serial_monitor import normalize_port
with serial.Serial( with serial.Serial(
config.serial_port, normalize_port(config.serial_port),
config.serial_baudrate, config.serial_baudrate,
timeout=5, timeout=5,
) as ser: ) as ser:
+27 -6
View File
@@ -7,8 +7,10 @@ Provides UartMonitor for continuous background monitoring.
from __future__ import annotations from __future__ import annotations
import platform
import re import re
import threading import threading
import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable from typing import Callable
@@ -16,6 +18,26 @@ import serial
import serial.tools.list_ports import serial.tools.list_ports
def normalize_port(port: str) -> str:
"""Normalize a serial port name for the current platform.
On Windows, COM ports numbered >= 10 require the NT namespace
prefix (\\\\.\\COM11). Without it, CreateFile fails with
'Access denied' or 'file not found'.
Args:
port: Raw port name from config or detection.
Returns:
Normalized port name suitable for serial.Serial().
"""
if platform.system() == "Windows":
match = re.match(r"^COM(\d+)$", port, re.IGNORECASE)
if match and int(match.group(1)) >= 10:
return rf"\\.\{port}"
return port
@dataclass @dataclass
class BootInfo: class BootInfo:
"""Parsed information extracted from Zynq boot output.""" """Parsed information extracted from Zynq boot output."""
@@ -168,7 +190,7 @@ def check_uart_available(
# 3. Try to open and read # 3. Try to open and read
try: try:
with serial.Serial(port, baudrate, timeout=timeout) as ser: with serial.Serial(normalize_port(port), baudrate, timeout=timeout) as ser:
ser.reset_input_buffer() ser.reset_input_buffer()
lines: list[str] = [] lines: list[str] = []
while ser.in_waiting: while ser.in_waiting:
@@ -206,7 +228,7 @@ def read_serial_stream(
serial.SerialException: If the port cannot be opened. serial.SerialException: If the port cannot be opened.
""" """
lines: list[str] = [] lines: list[str] = []
with serial.Serial(port, baudrate, timeout=timeout) as ser: with serial.Serial(normalize_port(port), baudrate, timeout=timeout) as ser:
# Clear any pending data # Clear any pending data
ser.reset_input_buffer() ser.reset_input_buffer()
while ser.in_waiting: while ser.in_waiting:
@@ -242,8 +264,7 @@ def test_serial_version(
import serial import serial
try: try:
with serial.Serial(port, baudrate, timeout=timeout) as ser: with serial.Serial(normalize_port(port), baudrate, timeout=timeout) as ser:
# Clear buffers
ser.reset_input_buffer() ser.reset_input_buffer()
ser.reset_output_buffer() ser.reset_output_buffer()
@@ -384,7 +405,7 @@ class UartMonitor:
# Quick port check before starting thread # Quick port check before starting thread
try: try:
test_ser = serial.Serial(self._port, self._baudrate, timeout=1) test_ser = serial.Serial(normalize_port(self._port), self._baudrate, timeout=1)
test_ser.close() test_ser.close()
except serial.SerialException as e: except serial.SerialException as e:
if self.on_error: if self.on_error:
@@ -411,7 +432,7 @@ class UartMonitor:
"""Main read loop running in background thread.""" """Main read loop running in background thread."""
try: try:
ser = serial.Serial( ser = serial.Serial(
self._port, normalize_port(self._port),
self._baudrate, self._baudrate,
timeout=self._timeout, timeout=self._timeout,
) )
+25 -30
View File
@@ -8,17 +8,14 @@ recognition on the JTAG chain.
from __future__ import annotations from __future__ import annotations
import os
import re import re
import subprocess import subprocess
import tempfile
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable from typing import Callable
from config_manager import Config from config_manager import Config
from vitis_checker import get_vivado_path, get_hw_server_path, build_tool_command from vitis_checker import get_xsct_path, get_hw_server_path, build_tool_command
@dataclass @dataclass
@@ -102,35 +99,32 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
hw_proc: subprocess.Popen | None = None hw_proc: subprocess.Popen | None = None
try: try:
# Start hw_server in the background (it daemonises own I/O) # Start hw_server in the background (needed for xsct connect)
hw_proc = subprocess.Popen( hw_proc = subprocess.Popen(
build_tool_command(hw_server_path), build_tool_command(hw_server_path),
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
# Give hw_server a moment to bind its port time.sleep(2) # let hw_server bind its port
time.sleep(2)
# ── 2. Find vivado ──────────────────────────────────── # ── 2. Find xsct (much lighter than Vivado for JTAG ops) ──
vivado_path = get_vivado_path(xilinx_root=xilinx_root) xsct_path = get_xsct_path(xilinx_root=xilinx_root)
if not vivado_path: if not xsct_path:
return ZynqCheckResult( return ZynqCheckResult(
devices=[], jtag_chain=[], is_present=False, devices=[], jtag_chain=[], is_present=False,
ps_detected=False, pl_detected=False, ps_detected=False, pl_detected=False,
hw_server_url=hw_server_url, hw_server_url=hw_server_url,
error="Vivado not found", error="xsct not found",
) )
# ── 3. Create and run JTAG query TCL ────────────────── # ── 3. Run JTAG query TCL via xsct ────────────────────
tcl_content = _build_jtag_query_tcl() tcl_content = _build_jtag_query_tcl()
tcl_path = Path(tempfile.gettempdir()) / f"zynq_jtag_check_{os.getpid()}.tcl"
tcl_path.write_text(tcl_content)
if callback: if callback:
callback("progress", "Scanning JTAG chain...") callback("progress", "Scanning JTAG chain...")
cmd = build_tool_command( cmd = build_tool_command(
vivado_path, "-mode", "batch", "-source", str(tcl_path), xsct_path, "-eval", tcl_content,
) )
python_timeout = config.step_timeouts.get("check_env", 120) python_timeout = config.step_timeouts.get("check_env", 120)
result = subprocess.run( result = subprocess.run(
@@ -139,7 +133,6 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
text=True, text=True,
timeout=python_timeout, timeout=python_timeout,
) )
tcl_path.unlink(missing_ok=True)
output = result.stdout + result.stderr output = result.stdout + result.stderr
jtag_chain = _parse_jtag_chain(output) jtag_chain = _parse_jtag_chain(output)
@@ -193,23 +186,25 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
def _build_jtag_query_tcl() -> str: def _build_jtag_query_tcl() -> str:
"""Build TCL script for JTAG device query. """Build TCL script for xsct JTAG device query.
Uses xsct (not Vivado) because:
- xsct is much lighter (no GUI infrastructure)
- xsct JTAG reboot already works on this system
- connect + targets lists all devices on the JTAG chain
Returns: Returns:
TCL script content as string. TCL script content as string (semicolon-delimited for -eval mode).
""" """
return """ return (
open_hw_manager "connect; "
connect_hw_server 'puts "===JTAG_START==="; '
open_hw_target "set all [targets]; "
puts "===JTAG_START===" 'foreach t $all { puts "DEVICE:$t" }; '
foreach dev [get_hw_devices] { 'puts "===JTAG_END==="; '
set name [get_property NAME $dev] "disconnect; "
puts "DEVICE:$name" "quit"
} )
puts "===JTAG_END==="
quit
"""
def _parse_jtag_chain(output: str) -> list[JtagDevice]: def _parse_jtag_chain(output: str) -> list[JtagDevice]: