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:
Jeremy Shen
2026-06-10 13:39:11 +08:00
parent 2c48224aea
commit ea3e3b8ac7
6 changed files with 101 additions and 17 deletions
+7 -6
View File
@@ -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
+4
View File
@@ -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: ""
+6
View File
@@ -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,
+16 -5
View File
@@ -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)
results.append(wipe_flash(config, callback)) if getattr(config, 'erase_before_program', True):
if not results[-1].success: results.append(wipe_flash(config, callback))
return results if not results[-1].success:
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
View File
@@ -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
View File
@@ -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: