✨ feat: flash phase tracking, erase checkbox, flash_model config
Step 2 UI now shows real-time flash operation phases parsed from program_flash output: - Erase: "Performing Erase Operation..." → sf erase → Erased: OK - Program: "Performing Program Operation..." → write blocks 0%→100% - Verify: "Performing Verify Operation..." → read+cmp 0%→100% Configuration additions: - erase_before_program checkbox: controls whether to pre-erase before programming - flash_model: chip type for capacity reference (s25fl256s1 = 32 MiB) subprocess_utils.py now emits 'phase' callbacks (erase/program/verify/done) detected from program_flash output transitions.
This commit is contained in:
+7
-6
@@ -3,6 +3,8 @@ bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrap
|
|||||||
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf
|
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf
|
||||||
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_type: qspi-x4-single
|
flash_type: qspi-x4-single
|
||||||
|
flash_model: s25fl256s1
|
||||||
|
erase_before_program: true
|
||||||
fsbl_elf_path: /home/ly0kos/work/Verify/FPGA/Xilinx/Z100/fsbl_qspi_bypass.elf
|
fsbl_elf_path: /home/ly0kos/work/Verify/FPGA/Xilinx/Z100/fsbl_qspi_bypass.elf
|
||||||
inter_step_delay: 2
|
inter_step_delay: 2
|
||||||
ping_count: 3
|
ping_count: 3
|
||||||
@@ -10,15 +12,14 @@ ping_timeout: 5
|
|||||||
reboot_timeout: 30
|
reboot_timeout: 30
|
||||||
serial_baudrate: 115200
|
serial_baudrate: 115200
|
||||||
serial_port: /dev/ttyUSB0
|
serial_port: /dev/ttyUSB0
|
||||||
tftp_upload_name: z7bin
|
|
||||||
uart_delay: 3
|
|
||||||
inter_step_delay: 2
|
|
||||||
step_timeouts:
|
step_timeouts:
|
||||||
|
bitstream: 60
|
||||||
check_env: 30
|
check_env: 30
|
||||||
|
elf_download: 60
|
||||||
flash_erase: 120
|
flash_erase: 120
|
||||||
flash_program: 600
|
flash_program: 600
|
||||||
bitstream: 60
|
|
||||||
elf_download: 60
|
|
||||||
tftp_reboot: 120
|
tftp_reboot: 120
|
||||||
vitis_path: ""
|
tftp_upload_name: z7bin
|
||||||
|
uart_delay: 3
|
||||||
|
vitis_path: ''
|
||||||
zynq_ip: 192.168.100.11
|
zynq_ip: 192.168.100.11
|
||||||
|
|||||||
@@ -22,9 +22,13 @@ bootloader_elf_path: ""
|
|||||||
# bootloader_bin_path is the bootloader image to program into QSPI flash
|
# bootloader_bin_path is the bootloader image to program into QSPI flash
|
||||||
# fsbl_elf_path is the FSBL (First Stage Boot Loader) required by program_flash
|
# fsbl_elf_path is the FSBL (First Stage Boot Loader) required by program_flash
|
||||||
# flash_type sets the QSPI interface mode (qspi-x4-single, qspi-x8-dual_parallel, etc.)
|
# flash_type sets the QSPI interface mode (qspi-x4-single, qspi-x8-dual_parallel, etc.)
|
||||||
|
# flash_model determines chip capacity: s25fl256s1=32MiB, s25fl128s=16MiB, etc.
|
||||||
|
# erase_before_program: if true, erase flash sectors before programming
|
||||||
bootloader_bin_path: ""
|
bootloader_bin_path: ""
|
||||||
fsbl_elf_path: ""
|
fsbl_elf_path: ""
|
||||||
flash_type: "qspi-x4-single"
|
flash_type: "qspi-x4-single"
|
||||||
|
flash_model: "s25fl256s1"
|
||||||
|
erase_before_program: true
|
||||||
|
|
||||||
# Firmware upload (Step 4: Load Main Program via TFTP)
|
# Firmware upload (Step 4: Load Main Program via TFTP)
|
||||||
firmware_bin_path: ""
|
firmware_bin_path: ""
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||||||
"bootloader_bin_path": "",
|
"bootloader_bin_path": "",
|
||||||
"fsbl_elf_path": "",
|
"fsbl_elf_path": "",
|
||||||
"flash_type": "qspi-x4-single",
|
"flash_type": "qspi-x4-single",
|
||||||
|
"flash_model": "s25fl256s1",
|
||||||
|
"erase_before_program": True,
|
||||||
"firmware_bin_path": "",
|
"firmware_bin_path": "",
|
||||||
"reboot_timeout": 30,
|
"reboot_timeout": 30,
|
||||||
"ping_timeout": 5,
|
"ping_timeout": 5,
|
||||||
@@ -60,6 +62,8 @@ class Config:
|
|||||||
bootloader_bin_path: str = ""
|
bootloader_bin_path: str = ""
|
||||||
fsbl_elf_path: str = ""
|
fsbl_elf_path: str = ""
|
||||||
flash_type: str = "qspi-x4-single"
|
flash_type: str = "qspi-x4-single"
|
||||||
|
flash_model: str = "s25fl256s1"
|
||||||
|
erase_before_program: bool = True
|
||||||
firmware_bin_path: str = ""
|
firmware_bin_path: str = ""
|
||||||
reboot_timeout: int = 30
|
reboot_timeout: int = 30
|
||||||
ping_timeout: int = 5
|
ping_timeout: int = 5
|
||||||
@@ -166,6 +170,8 @@ class Config:
|
|||||||
"bootloader_bin_path": self._relative_path(self.bootloader_bin_path),
|
"bootloader_bin_path": self._relative_path(self.bootloader_bin_path),
|
||||||
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
|
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
|
||||||
"flash_type": self.flash_type,
|
"flash_type": self.flash_type,
|
||||||
|
"flash_model": self.flash_model,
|
||||||
|
"erase_before_program": self.erase_before_program,
|
||||||
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
||||||
"reboot_timeout": self.reboot_timeout,
|
"reboot_timeout": self.reboot_timeout,
|
||||||
"ping_timeout": self.ping_timeout,
|
"ping_timeout": self.ping_timeout,
|
||||||
|
|||||||
+13
-2
@@ -279,7 +279,11 @@ def full_flash_program(
|
|||||||
bin_path: str | Path,
|
bin_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> list[FlashResult]:
|
) -> list[FlashResult]:
|
||||||
"""Execute full flash workflow: wipe → program → verify.
|
"""Execute full flash workflow: (optional wipe) → program → verify.
|
||||||
|
|
||||||
|
If config.erase_before_program is True, erases flash sectors before
|
||||||
|
programming. Otherwise, program_flash handles sector erase during
|
||||||
|
programming (per-sector, not full chip).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -291,10 +295,17 @@ def full_flash_program(
|
|||||||
"""
|
"""
|
||||||
results: list[FlashResult] = []
|
results: list[FlashResult] = []
|
||||||
|
|
||||||
# Step 1: Erase the entire flash
|
# Step 1: Erase (optional — controlled by config.erase_before_program)
|
||||||
|
if getattr(config, 'erase_before_program', True):
|
||||||
results.append(wipe_flash(config, callback))
|
results.append(wipe_flash(config, callback))
|
||||||
if not results[-1].success:
|
if not results[-1].success:
|
||||||
return results
|
return results
|
||||||
|
else:
|
||||||
|
results.append(FlashResult(
|
||||||
|
step="wipe",
|
||||||
|
success=True,
|
||||||
|
message="Erase skipped (handled by program_flash)",
|
||||||
|
))
|
||||||
|
|
||||||
# Step 2: Program and verify (program_flash -verify)
|
# Step 2: Program and verify (program_flash -verify)
|
||||||
results.append(program_flash_bin(config, bin_path, callback))
|
results.append(program_flash_bin(config, bin_path, callback))
|
||||||
|
|||||||
+41
-4
@@ -80,6 +80,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._flash_phase: str = ""
|
||||||
|
|
||||||
# StringVar references for config sync
|
# StringVar references for config sync
|
||||||
self._ip_string_var: ctk.StringVar | None = None
|
self._ip_string_var: ctk.StringVar | None = None
|
||||||
@@ -186,6 +187,7 @@ class MainWindow(ctk.CTk):
|
|||||||
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
||||||
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
||||||
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
||||||
|
self._config.erase_before_program = self._erase_cb_var.get()
|
||||||
self._config.firmware_bin_path = files["firmware_bin_path"]
|
self._config.firmware_bin_path = files["firmware_bin_path"]
|
||||||
|
|
||||||
def _save_config(self) -> None:
|
def _save_config(self) -> None:
|
||||||
@@ -398,7 +400,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(6, weight=1)
|
panel.grid_rowconfigure(7, weight=1)
|
||||||
|
|
||||||
title = ctk.CTkLabel(
|
title = ctk.CTkLabel(
|
||||||
panel,
|
panel,
|
||||||
@@ -461,6 +463,27 @@ class MainWindow(ctk.CTk):
|
|||||||
self._fsbl_selector.set_path(str(resolved))
|
self._fsbl_selector.set_path(str(resolved))
|
||||||
self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
|
# Flash options
|
||||||
|
flash_frame = ctk.CTkFrame(panel)
|
||||||
|
flash_frame.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
flash_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_before_program)
|
||||||
|
self._erase_cb = ctk.CTkCheckBox(
|
||||||
|
flash_frame,
|
||||||
|
text="Erase flash before programming",
|
||||||
|
variable=self._erase_cb_var,
|
||||||
|
font=FONT_SMALL,
|
||||||
|
)
|
||||||
|
self._erase_cb.grid(row=0, column=0, sticky="w", padx=PADDING_SMALL)
|
||||||
|
|
||||||
|
flash_model_label = ctk.CTkLabel(
|
||||||
|
flash_frame,
|
||||||
|
text=f"Flash: {self._config.flash_model} (32 MiB)",
|
||||||
|
font=FONT_SMALL,
|
||||||
|
)
|
||||||
|
flash_model_label.grid(row=1, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
||||||
|
|
||||||
# Firmware BIN path (TFTP upload)
|
# Firmware BIN path (TFTP upload)
|
||||||
self._firmware_bin_selector = FileSelector(
|
self._firmware_bin_selector = FileSelector(
|
||||||
panel,
|
panel,
|
||||||
@@ -470,7 +493,7 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config.firmware_bin_path:
|
if self._config.firmware_bin_path:
|
||||||
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
||||||
self._firmware_bin_selector.set_path(str(resolved))
|
self._firmware_bin_selector.set_path(str(resolved))
|
||||||
self._firmware_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._firmware_bin_selector.grid(row=7, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
# Save button
|
# Save button
|
||||||
save_btn = ctk.CTkButton(
|
save_btn = ctk.CTkButton(
|
||||||
@@ -479,7 +502,7 @@ class MainWindow(ctk.CTk):
|
|||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
command=self._save_config,
|
command=self._save_config,
|
||||||
)
|
)
|
||||||
save_btn.grid(row=7, column=0, padx=PADDING, pady=PADDING)
|
save_btn.grid(row=8, column=0, padx=PADDING, pady=PADDING)
|
||||||
|
|
||||||
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
|
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
|
||||||
"""Build the serial monitor panel."""
|
"""Build the serial monitor panel."""
|
||||||
@@ -725,7 +748,21 @@ class MainWindow(ctk.CTk):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _flash_callback(self, status: str, message: str) -> None:
|
def _flash_callback(self, status: str, message: str) -> None:
|
||||||
"""Callback for flash programming progress."""
|
"""Callback for flash programming progress — tracks phases."""
|
||||||
|
if status == "phase":
|
||||||
|
# Track current flash phase for UI
|
||||||
|
self._flash_phase = message # 'erase', 'program', 'verify', 'done'
|
||||||
|
phase_names = {"erase": "Erasing", "program": "Programming", "verify": "Verifying", "done": "Done"}
|
||||||
|
label = phase_names.get(message, message)
|
||||||
|
self._log_message(f" ▸ {label}")
|
||||||
|
return
|
||||||
|
if status == "progress":
|
||||||
|
pct = message.rstrip('%')
|
||||||
|
# Show progress within current phase
|
||||||
|
phase_label = {"erase": "Erase", "program": "Program", "verify": "Verify"}.get(
|
||||||
|
self._flash_phase, "Flash")
|
||||||
|
self._log_message(f" [{phase_label}] {pct}%")
|
||||||
|
return
|
||||||
self._log_message(f" [{status}] {message}")
|
self._log_message(f" [{status}] {message}")
|
||||||
|
|
||||||
def _jtag_callback(self, status: str, message: str) -> None:
|
def _jtag_callback(self, status: str, message: str) -> None:
|
||||||
|
|||||||
+27
-2
@@ -1,7 +1,8 @@
|
|||||||
"""Non-blocking subprocess execution with real-time output parsing.
|
"""Non-blocking subprocess execution with real-time output parsing.
|
||||||
|
|
||||||
Provides run_streaming() which streams stdout/stderr to callbacks
|
Provides run_streaming() which streams stdout/stderr to callbacks
|
||||||
in real time, parsing for warnings, errors, and progress indicators.
|
in real time, parsing for warnings, errors, progress indicators,
|
||||||
|
and flash operation phase transitions (erase/program/verify).
|
||||||
Used by flash_programmer and bitstream_programmer.
|
Used by flash_programmer and bitstream_programmer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ from typing import Callable
|
|||||||
|
|
||||||
StreamCallback = Callable[[str, str], None] | None
|
StreamCallback = Callable[[str, str], None] | None
|
||||||
"""Callback type: (category, message) where category is one of:
|
"""Callback type: (category, message) where category is one of:
|
||||||
'out', 'err', 'warn', 'error', 'progress', 'done'"""
|
'out', 'err', 'warn', 'error', 'progress', 'phase', 'done'"""
|
||||||
|
|
||||||
# Regex patterns for parsing tool output
|
# Regex patterns for parsing tool output
|
||||||
_WARNING_RE = re.compile(r"WARNING\s*[:\[].*", re.IGNORECASE)
|
_WARNING_RE = re.compile(r"WARNING\s*[:\[].*", re.IGNORECASE)
|
||||||
@@ -24,6 +25,12 @@ _PROGRESS_RE = re.compile(r"(\d{1,3})\s*%")
|
|||||||
_INFO_KEYWORDS = ["connected", "downloading", "running", "initialization",
|
_INFO_KEYWORDS = ["connected", "downloading", "running", "initialization",
|
||||||
"verif", "write", "erase", "flash operation"]
|
"verif", "write", "erase", "flash operation"]
|
||||||
|
|
||||||
|
# Phase transition patterns for program_flash output
|
||||||
|
_PHASE_ERASE_RE = re.compile(r"Performing Erase Operation", re.IGNORECASE)
|
||||||
|
_PHASE_PROGRAM_RE = re.compile(r"Performing Program Operation", re.IGNORECASE)
|
||||||
|
_PHASE_VERIFY_RE = re.compile(r"Performing Verify Operation", re.IGNORECASE)
|
||||||
|
_PHASE_DONE_RE = re.compile(r"(Erase|Program|Verify) Operation successful", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def run_streaming(
|
def run_streaming(
|
||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
@@ -133,6 +140,24 @@ def _parse_and_callback(
|
|||||||
callback("warn", line)
|
callback("warn", line)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check for phase transitions (program_flash output)
|
||||||
|
if _PHASE_ERASE_RE.search(line):
|
||||||
|
callback("phase", "erase")
|
||||||
|
callback("out", line)
|
||||||
|
return
|
||||||
|
if _PHASE_PROGRAM_RE.search(line):
|
||||||
|
callback("phase", "program")
|
||||||
|
callback("out", line)
|
||||||
|
return
|
||||||
|
if _PHASE_VERIFY_RE.search(line):
|
||||||
|
callback("phase", "verify")
|
||||||
|
callback("out", line)
|
||||||
|
return
|
||||||
|
if _PHASE_DONE_RE.search(line):
|
||||||
|
callback("phase", "done")
|
||||||
|
callback("out", line)
|
||||||
|
return
|
||||||
|
|
||||||
# Check for progress percentage
|
# Check for progress percentage
|
||||||
m = _PROGRESS_RE.search(line)
|
m = _PROGRESS_RE.search(line)
|
||||||
if m:
|
if m:
|
||||||
|
|||||||
Reference in New Issue
Block a user