diff --git a/config.yaml b/config.yaml index 3f4bc4e..9649708 100644 --- a/config.yaml +++ b/config.yaml @@ -1,10 +1,10 @@ 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_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 -flash_type: qspi-x4-single -flash_model: s25fl256s1 erase_before_program: true +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_type: qspi-x4-single fsbl_elf_path: /home/ly0kos/work/Verify/FPGA/Xilinx/Z100/fsbl_qspi_bypass.elf inter_step_delay: 2 ping_count: 3 diff --git a/config/default_config.yaml b/config/default_config.yaml index 039fdd7..61330aa 100644 --- a/config/default_config.yaml +++ b/config/default_config.yaml @@ -22,13 +22,12 @@ bootloader_elf_path: "" # 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 # 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 +# erase_all: if true, erase entire flash chip; if false, only erase sectors needed bootloader_bin_path: "" fsbl_elf_path: "" flash_type: "qspi-x4-single" flash_model: "s25fl256s1" -erase_before_program: true +erase_all: false # Firmware upload (Step 4: Load Main Program via TFTP) firmware_bin_path: "" diff --git a/src/config_manager.py b/src/config_manager.py index d997b1a..e8267b4 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -26,7 +26,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "fsbl_elf_path": "", "flash_type": "qspi-x4-single", "flash_model": "s25fl256s1", - "erase_before_program": True, + "erase_all": False, "firmware_bin_path": "", "reboot_timeout": 30, "ping_timeout": 5, @@ -63,7 +63,7 @@ class Config: fsbl_elf_path: str = "" flash_type: str = "qspi-x4-single" flash_model: str = "s25fl256s1" - erase_before_program: bool = True + erase_all: bool = False firmware_bin_path: str = "" reboot_timeout: int = 30 ping_timeout: int = 5 @@ -171,7 +171,7 @@ class Config: "fsbl_elf_path": self._relative_path(self.fsbl_elf_path), "flash_type": self.flash_type, "flash_model": self.flash_model, - "erase_before_program": self.erase_before_program, + "erase_all": self.erase_all, "firmware_bin_path": self._relative_path(self.firmware_bin_path), "reboot_timeout": self.reboot_timeout, "ping_timeout": self.ping_timeout, diff --git a/src/flash_programmer.py b/src/flash_programmer.py index 76707d9..199fc88 100644 --- a/src/flash_programmer.py +++ b/src/flash_programmer.py @@ -98,11 +98,11 @@ def wipe_flash( config: Config, callback: ProgressCallback = None, ) -> FlashResult: - """Erase QSPI flash sectors matching the bootloader image size. + """Erase QSPI flash before programming. - Uses -erase_only to erase only the sectors covered by the BIN file. - Avoids -erase_all which fails on some flash chips (e.g. s25fl256s1) - that don't report memory density information. + Uses config.erase_all to determine erase scope: + - True: -erase_all -erase_only (erase entire chip, may fail on some) + - False: -erase_only (erase only sectors matching the BIN file, safer) Args: config: Application configuration (needs fsbl_elf_path). @@ -131,7 +131,10 @@ def wipe_flash( callback("start", "Erasing QSPI flash...") cmd = _build_program_flash_cmd_base(flash_tool, config) - cmd += ["-erase_only"] # Erase sectors matching the BIN size (safer than -erase_all) + if getattr(config, 'erase_all', False): + cmd += ["-erase_all", "-erase_only"] # Full chip erase + else: + cmd += ["-erase_only"] # Erase sectors matching BIN size try: result = _run_command(cmd, timeout=config.step_timeouts.get("flash_erase", 120), callback=callback) @@ -279,11 +282,11 @@ def full_flash_program( bin_path: str | Path, callback: ProgressCallback = None, ) -> list[FlashResult]: - """Execute full flash workflow: (optional wipe) → program → verify. + """Execute full flash workflow: 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). + Erase scope is controlled by config.erase_all: + - True: entire flash chip (uses -erase_all) + - False: sectors matching BIN file size (uses -erase_only) Args: config: Application configuration. @@ -295,17 +298,10 @@ def full_flash_program( """ results: list[FlashResult] = [] - # Step 1: Erase (optional — controlled by config.erase_before_program) - if getattr(config, 'erase_before_program', True): - results.append(wipe_flash(config, callback)) - if not results[-1].success: - return results - else: - results.append(FlashResult( - step="wipe", - success=True, - message="Erase skipped (handled by program_flash)", - )) + # Step 1: Erase (scope determined by config.erase_all) + results.append(wipe_flash(config, callback)) + if not results[-1].success: + return results # Step 2: Program and verify (program_flash -verify) results.append(program_flash_bin(config, bin_path, callback)) diff --git a/src/gui/main_window.py b/src/gui/main_window.py index a3c78a6..f4d369d 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -52,6 +52,7 @@ from gui.widgets import ( StatusDisplay, FileSelector, LogDisplay, + SubStepFrame, ) @@ -187,7 +188,7 @@ class MainWindow(ctk.CTk): self._config.bootloader_elf_path = files["bootloader_elf_path"] self._config.bootloader_bin_path = files["bootloader_bin_path"] self._config.fsbl_elf_path = files["fsbl_elf_path"] - self._config.erase_before_program = self._erase_cb_var.get() + self._config.erase_all = self._erase_cb_var.get() self._config.firmware_bin_path = files["firmware_bin_path"] def _save_config(self) -> None: @@ -338,6 +339,16 @@ class MainWindow(ctk.CTk): pady=(0, 2), ) + # Step 2: add collapsible sub-step frame + if i == 1: + self._sub_step_frame = SubStepFrame(panel) + self._sub_step_frame.grid( + row=i * 2 + 2, column=0, + sticky="nsew", + padx=PADDING_SMALL + 16, + pady=(0, 2), + ) + # ── Progress indicator ── self._progress = ProgressIndicator(panel) self._progress.grid( @@ -468,10 +479,10 @@ class MainWindow(ctk.CTk): 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_var = ctk.BooleanVar(value=self._config.erase_all) self._erase_cb = ctk.CTkCheckBox( flash_frame, - text="Erase flash before programming", + text="整片Flash擦除 (Erase entire flash)", variable=self._erase_cb_var, font=FONT_SMALL, ) @@ -748,20 +759,25 @@ class MainWindow(ctk.CTk): return False def _flash_callback(self, status: str, message: str) -> None: - """Callback for flash programming progress — tracks phases.""" + """Callback for flash programming — drives sub-step UI.""" if status == "phase": - # Track current flash phase for UI - self._flash_phase = message # 'erase', 'program', 'verify', 'done' + self._flash_phase = message phase_names = {"erase": "Erasing", "program": "Programming", "verify": "Verifying", "done": "Done"} label = phase_names.get(message, message) self._log_message(f" ▸ {label}") + if hasattr(self, '_sub_step_frame'): + self._sub_step_frame.set_phase(message) return if status == "progress": - pct = message.rstrip('%') - # Show progress within current phase + try: + pct = int(message.rstrip('%')) + except ValueError: + pct = -1 phase_label = {"erase": "Erase", "program": "Program", "verify": "Verify"}.get( self._flash_phase, "Flash") self._log_message(f" [{phase_label}] {pct}%") + if hasattr(self, '_sub_step_frame'): + self._sub_step_frame.set_phase(self._flash_phase, pct) return self._log_message(f" [{status}] {message}") @@ -1018,6 +1034,12 @@ class MainWindow(ctk.CTk): step_idx = i - start_index self._set_step_status(i, "running") + # Step 2: expand sub-step frame and reset + if i == 1 and hasattr(self, '_sub_step_frame'): + self._sub_step_frame.reset() + self._sub_step_frame.set_expanded(True) + self._flash_phase = "" + # Log step timeout estimate timeout_key, default_timeout = step_timeout_keys[i] if i < len(step_timeout_keys) else ("", 30) step_timeout = self._config.step_timeouts.get(timeout_key, default_timeout) if self._config else default_timeout @@ -1032,6 +1054,10 @@ class MainWindow(ctk.CTk): self._set_step_status(i, "complete" if success else "error") elapsed = time.time() - t_step_start self._log_message(f" ⏱ Step {i+1} completed in {elapsed:.0f}s") + + # Step 2: mark all sub-steps done + if i == 1 and hasattr(self, '_sub_step_frame'): + self._sub_step_frame.set_phase("done") except Exception as e: results.append(False) self._set_step_status(i, "error") diff --git a/src/gui/widgets.py b/src/gui/widgets.py index e61ac63..c3925bb 100644 --- a/src/gui/widgets.py +++ b/src/gui/widgets.py @@ -623,3 +623,135 @@ class LogDisplay(ctk.CTkFrame): self._text_widget.delete("1.0", "end") self._text_widget.insert("1.0", text) self._text_widget.configure(state="disabled") + + +# ════════════════════════════════════════════════════════════════════ +# SubStepFrame — collapsible sub-step progress display +# ════════════════════════════════════════════════════════════════════ + + +class SubStepFrame(ctk.CTkFrame): + """Collapsible frame showing flash operation sub-steps with status. + + Three sub-steps: Erase → Program → Verify. + Each shows a status indicator (dot + label) updated via set_phase(). + Collapsed by default, expands when a workflow step starts. + """ + + def __init__(self, parent: ctk.CTkFrame) -> None: + """Initialize the sub-step frame. + + Args: + parent: Parent CTkFrame widget. + """ + super().__init__(parent, fg_color="transparent") + self.grid_columnconfigure(1, weight=1) + + # Collapse toggle + self._collapsed = True + self._toggle_btn = ctk.CTkButton( + self, + text="▶", + width=24, + height=24, + font=FONT_SMALL, + command=self._toggle, + ) + self._toggle_btn.grid(row=0, column=0, padx=(PADDING_SMALL, 4), pady=(2, 0), sticky="nw") + + self._header_label = ctk.CTkLabel( + self, + text="Sub-steps", + font=FONT_SMALL, + ) + self._header_label.grid(row=0, column=1, padx=(0, PADDING), pady=(2, 0), sticky="w") + + # Sub-items (hidden initially) + self._sub_items: list[dict] = [] + sub_names = ["Erase", "Program", "Verify"] + for i, name in enumerate(sub_names): + dot = ctk.CTkLabel(self, text="○", font=(FONT_SMALL[0], 10), text_color=CONNECTOR_COLOR) + label = ctk.CTkLabel(self, text=name, font=FONT_SMALL, text_color=CONNECTOR_COLOR) + dot.grid(row=i + 1, column=0, padx=(PADDING_SMALL + 14, 4), pady=(1, 1), sticky="e") + label.grid(row=i + 1, column=1, padx=(4, PADDING), pady=(1, 1), sticky="w") + dot.grid_remove() + label.grid_remove() + self._sub_items.append({"dot": dot, "label": label, "name": name}) + + def _toggle(self) -> None: + """Toggle collapse/expand.""" + self._collapsed = not self._collapsed + self._toggle_btn.configure(text="▶" if self._collapsed else "▼") + for item in self._sub_items: + if self._collapsed: + item["dot"].grid_remove() + item["label"].grid_remove() + else: + item["dot"].grid() + item["label"].grid() + + def set_expanded(self, expanded: bool) -> None: + """Set expanded state. + + Args: + expanded: True to expand, False to collapse. + """ + if expanded == self._collapsed: + self._toggle() + + def set_phase(self, phase: str, pct: int = -1) -> None: + """Update a sub-step's status. + + Args: + phase: One of 'erase', 'program', 'verify', 'done'. + pct: Progress percentage, -1 to show "running", 100 for "done". + """ + phase_map = {"erase": 0, "program": 1, "verify": 2} + colors = { + "pending": CONNECTOR_COLOR, + "running": PRIMARY_COLOR, + "complete": SUCCESS_COLOR, + "error": DANGER_COLOR, + } + + if phase == "done": + # Mark all as complete + for item in self._sub_items: + item["dot"].configure(text="●", text_color=SUCCESS_COLOR) + item["label"].configure(text_color=SUCCESS_COLOR) + return + + idx = phase_map.get(phase, -1) + if idx < 0: + return + + # Determine state + if pct >= 100: + state = "complete" + dot_text = "●" + elif pct >= 0: + state = "running" + dot_text = "◉" + else: + state = "running" + dot_text = "◉" + + # Update current and previous + for i, item in enumerate(self._sub_items): + if i < idx: + item["dot"].configure(text="●", text_color=SUCCESS_COLOR) + item["label"].configure(text_color=SUCCESS_COLOR) + elif i == idx: + item["dot"].configure(text=dot_text, text_color=colors[state]) + item["label"].configure(text_color=colors[state]) + else: + item["dot"].configure(text="○", text_color=colors["pending"]) + item["label"].configure(text_color=colors["pending"]) + + def reset(self) -> None: + """Reset all sub-steps to pending state.""" + for item in self._sub_items: + item["dot"].configure(text="○", text_color=CONNECTOR_COLOR) + item["label"].configure(text_color=CONNECTOR_COLOR) + if not self._collapsed: + self._toggle()