diff --git a/platform.py b/platform.py index 234415bd0..6aa99dc29 100644 --- a/platform.py +++ b/platform.py @@ -48,6 +48,7 @@ import os import requests import shutil +import struct import subprocess from pathlib import Path from typing import Optional, Dict, List, Any, Union @@ -629,7 +630,18 @@ def _needs_debug_tools(self, variables: Dict, targets: List[str]) -> bool: def _configure_mcu_toolchains( self, mcu: str, variables: Dict, targets: List[str] ) -> None: - """Configure MCU-specific toolchains with optimized installation.""" + """ + Install toolchains and debugging packages required for the specified MCU. + + Installs the MCU's base toolchains from the MCU configuration. If an "ulp" directory exists, + installs the ULP toolchain entries. When build variables or targets indicate debugging is required, + installs debug-related tools (including OpenOCD and ROM-ELF helper). + + Parameters: + mcu (str): MCU identifier (e.g., "esp32", "esp32c3"). + variables (Dict): Build variables used to determine debugging requirements. + targets (List[str]): Build targets that may trigger installation of debug tooling. + """ mcu_config = self._get_mcu_config(mcu) if not mcu_config: logger.warning(f"Unknown MCU: {mcu}") @@ -649,9 +661,16 @@ def _configure_mcu_toolchains( for debug_tool in mcu_config["debug_tools"]: self.install_tool(debug_tool) self.install_tool("tool-openocd-esp32") + self.install_tool("tool-esp-rom-elfs") def _configure_installer(self) -> None: - """Configure the ESP-IDF tools installer with proper version checking.""" + """ + Ensure the ESP-IDF tools installer is present and up to date. + + Verifies and installs the tool-esp_install package when necessary, removes a legacy + PlatformIO install marker to avoid conflicts, and marks the installer package as + optional if idf_tools.py is available. Logs a warning if idf_tools.py cannot be found. + """ # Check version - installs only when needed if not self._check_tl_install_version(): @@ -798,7 +817,21 @@ def get_boards(self, id_=None): return result def _add_dynamic_options(self, board): - """Add dynamic board options for upload protocols and debug tools.""" + """ + Add dynamic upload protocol and debug-tool entries to a board manifest. + + Ensures upload.protocols and upload.protocol defaults, auto-adds supported debug tools + (and MCU-specific builtin/ftdi entries), sets an SVD path when available, and + populates debug.tools with OpenOCD server configurations, init commands, and + per-tool metadata. Returns the updated board object. + + Parameters: + board: Board object whose manifest will be modified. + + Returns: + The same Board instance with its manifest updated to include dynamic upload + protocols and debug tool configurations. + """ # Upload protocols if not board.get("upload.protocols", []): board.manifest["upload"]["protocols"] = ["esptool", "espota"] @@ -849,7 +882,6 @@ def _add_dynamic_options(self, board): if "tools" not in debug: debug["tools"] = {} - # Debug tool configuration for link in upload_protocols: if link in non_debug_protocols or link in debug["tools"]: continue @@ -857,6 +889,22 @@ def _add_dynamic_options(self, board): openocd_interface = self._get_openocd_interface(link, board) server_args = self._get_debug_server_args(openocd_interface, debug) + init_cmds = [ + "define pio_reset_halt_target", + " monitor reset halt", + " maintenance flush register-cache", + "end", + "define pio_reset_run_target", + " monitor reset", + "end", + ] + init_cmds.extend([ + "target extended-remote $DEBUG_PORT", + "$LOAD_CMDS", + "pio_reset_halt_target", + "$INIT_BREAK", + ]) + debug["tools"][link] = { "server": { "package": "tool-openocd-esp32", @@ -864,19 +912,7 @@ def _add_dynamic_options(self, board): "arguments": server_args, }, "init_break": "thb app_main", - "init_cmds": [ - "define pio_reset_halt_target", - " monitor reset halt", - " flushregs", - "end", - "define pio_reset_run_target", - " monitor reset", - "end", - "target extended-remote $DEBUG_PORT", - "$LOAD_CMDS", - "pio_reset_halt_target", - "$INIT_BREAK", - ], + "init_cmds": init_cmds, "onboard": link in debug.get("onboard_tools", []), "default": link == debug.get("default_tool"), } @@ -887,14 +923,186 @@ def _add_dynamic_options(self, board): board.manifest["debug"] = debug return board + def _gdb_has_python(self, mcu: str) -> bool: + """ + Determine whether the GDB executable for the given MCU supports embedding Python. + + Returns: + True if a GDB binary for the MCU accepts Python commands, + False otherwise (including when no matching tool/package is found or the probe fails). + """ + mcu_config = self._get_mcu_config(mcu) + if not mcu_config: + return False + for tool_pkg in mcu_config["debug_tools"]: + pkg_dir = self.get_package_dir(tool_pkg) + if not pkg_dir: + continue + toolchain_arch = "xtensa-esp-elf" if mcu in MCU_TOOLCHAIN_CONFIG["xtensa"]["mcus"] else "riscv32-esp-elf" + candidates = [Path(pkg_dir) / "bin" / f"{toolchain_arch}-gdb"] + if IS_WINDOWS: + candidates.insert(0, Path(pkg_dir) / "bin" / f"{toolchain_arch}-gdb.exe") + gdb_path = next((path for path in candidates if path.is_file()), None) + if not gdb_path: + continue + try: + result = subprocess.run( + [str(gdb_path), "--batch-silent", "--ex", "python import os"], + capture_output=True, timeout=10, + ) + return result.returncode == 0 + except (OSError, subprocess.TimeoutExpired): + logger.debug("GDB Python support probe failed for %s", gdb_path) + return False + return False + + @staticmethod + def _get_freertos_gdb_cmds() -> List[str]: + """ + Generate GDB commands to load FreeRTOS thread-awareness extension. + + Returns: + list[str]: GDB command strings that attempt to import the `freertos_gdb` Python + extension and print a warning if it is not available. + """ + return [ + "python", + "try:", + " import freertos_gdb", + "except ModuleNotFoundError:", + " print('warning: python extension \"freertos_gdb\" not found.')", + "end", + ] + + def _get_rom_elf_gdb_cmds(self, mcu: str) -> List[str]: + """ + Generate a GDB command sequence that automatically selects and loads ROM ELF symbols for the given MCU. + + Builds a `target hookpost-extended-remote` hook using ROM metadata (from misc/roms.json) and installed + ROM ELF artifacts (tool-esp-rom-elfs) so the appropriate ROM symbol file is loaded after connecting to the target. + + Parameters: + mcu (str): MCU identifier used to look up ROM entries in misc/roms.json. + + Returns: + A list of GDB command strings that implement the ROM selection and loading hook; an empty list + if ROM metadata or ROM ELF package is not available. + """ + rom_elfs_dir = self.get_package_dir("tool-esp-rom-elfs") + if not rom_elfs_dir or not Path(rom_elfs_dir).is_dir(): + return [] + + roms_json = Path(self.get_dir()) / "misc" / "roms.json" + if not roms_json.is_file(): + return [] + + try: + with open(roms_json, encoding="utf-8") as f: + roms = json.load(f) + except (json.JSONDecodeError, OSError): + return [] + + if mcu not in roms: + return [] + + rom_elfs_path = to_unix_path(str(Path(rom_elfs_dir).resolve())) + if not rom_elfs_path.endswith("/"): + rom_elfs_path += "/" + + entries = roms[mcu] + cmds = [ + "define target hookpost-extended-remote", + "set confirm off", + ] + cmds.extend( + self._build_rom_elf_conditions(entries, mcu, rom_elfs_path, depth=1) + ) + cmds.extend([ + "set confirm on", + "end", + ]) + return cmds + + @staticmethod + def _rom_date_condition(date_addr: int, date_str: str) -> str: + """ + Constructs a GDB conditional expression that compares 32-bit memory words + starting at a given address to a provided build-date string. + + Parameters: + date_addr (int): Base memory address where the build-date string is stored. + date_str (str): Build-date string to match; compared in 4-byte little-endian chunks. + + Returns: + condition (str): A GDB `if` expression like `if (*(int*)0xADDR) == 0xVALUE && ...` + that tests each 4-byte chunk of `date_str` against memory at `date_addr`. + """ + parts = [] + for i in range(0, len(date_str), 4): + chunk = date_str[i:i + 4] + value = hex(struct.unpack(' List[str]: + """ + Build a list of GDB conditional command strings that load ROM ELF symbols based on ROM revision. + + Parameters: + entries (list): Ordered list of ROM metadata dicts, each containing at least + "build_date_str_addr" (hex string), "build_date_str" (string), and "rev" (revision identifier). + mcu (str): MCU identifier used to form ROM ELF filenames. + rom_dir (str): Directory path (may include trailing slash) where ROM ELF files reside. + depth (int): Current recursion depth used to compute indentation for nested blocks. + + Returns: + List[str]: A sequence of GDB command lines forming nested if/else/end blocks that + evaluate ROM build-date memory values and call `add-symbol-file` for the matching ROM ELF. + """ + if not entries: + return [] + indent = " " * depth + entry = entries[0] + addr = int(entry["build_date_str_addr"], 16) + rom_file = f"{mcu}_rev{entry['rev']}_rom.elf" + rom_path = f"{rom_dir}{rom_file}" + lines = [ + f"{indent}{cls._rom_date_condition(addr, entry['build_date_str'])}", + f'{indent} add-symbol-file "{rom_path}"', + ] + if len(entries) > 1: + lines.append(f"{indent}else") + lines.extend( + cls._build_rom_elf_conditions(entries[1:], mcu, rom_dir, depth + 1) + ) + else: + lines.append(f"{indent}else") + lines.append( + f"{indent} echo Warning: Unknown {mcu} ROM revision.\\n" + ) + lines.append(f"{indent}end") + return lines + def _get_openocd_interface(self, link: str, board) -> str: - """Determine OpenOCD interface configuration for debug link.""" + """ + Resolve the OpenOCD interface identifier for a given debug link and board. + + Parameters: + link (str): Debug link name. + board: Board object whose `id` may affect the chosen interface. + + Returns: + str: OpenOCD interface string (for example "jlink", "ftdi/esp_ftdi", or "esp_usb_jtag"). + """ if link in ("jlink", "cmsis-dap"): return link if link in ("esp-prog", "ftdi"): if board.id == "esp32-s2-kaluga-1": return "ftdi/esp32s2_kaluga_v1" - return "ftdi/esp32_devkitj_v1" + return "ftdi/esp_ftdi" if link == "esp-bridge": return "esp_usb_bridge" if link == "esp-builtin": @@ -916,7 +1124,31 @@ def _get_debug_server_args(self, openocd_interface: str, debug: Dict) -> List[st ] def configure_debug_session(self, debug_config): - """Configure debug session with flash image loading.""" + """ + Configure debug session to inject debug extensions and prepare GDB load commands for flashing. + + This updates the provided debug_config in-place: + - Injects additional GDB init commands and ROM/FreeRTOS extensions via _inject_debug_extensions. + - If the debug server is OpenOCD, appends an adapter speed argument derived from debug_config.speed. + - If debug_config.load_cmds is the default ["load"] and valid flash image metadata is present in + build_data["extra"]["flash_images"], replaces load_cmds with a sequence of `monitor program_esp + "" verify` entries for each flash image and the application binary + (using build_data["prog_path"] and application_offset if available; + falls back to DEFAULT_APP_OFFSET and logs a warning). + - If flash image metadata is missing or invalid, leaves load_cmds unchanged and logs a warning. + + Parameters: + debug_config: object + Debug session configuration object that must provide (at least) the attributes: + - build_data (dict): build metadata including an "extra" dict with "flash_images" + (list of { "path", "offset" }) and optional "application_offset". + - server (dict | None): server configuration; if server["executable"] + contains "openocd", server["arguments"] (list) will be extended. + - load_cmds (list): current GDB load commands; may be replaced. + - speed (str | None): optional adapter speed value used when configuring OpenOCD. + """ + self._inject_debug_extensions(debug_config) + build_extra_data = debug_config.build_data.get("extra", {}) flash_images = build_extra_data.get("flash_images", []) @@ -960,3 +1192,35 @@ def configure_debug_session(self, debug_config): f'{app_offset} verify' ) debug_config.load_cmds = load_cmds + + def _inject_debug_extensions(self, debug_config): + """ + Inject FreeRTOS thread-awareness and ROM ELF GDB commands into the debug tool's init_cmds. + + This inserts additional GDB initialization commands (FreeRTOS Python-based helpers when available + and ROM-ELF symbol loading commands) into debug_config.tool_settings["init_cmds"] at the position + immediately before the "target extended-remote" command. + + Parameters: + debug_config: An object representing the debug session configuration. It must provide: + - board_config: a mapping containing "build.mcu". + - tool_settings: a mapping containing "init_cmds", a list of GDB init command strings. + """ + mcu = debug_config.board_config.get("build.mcu", "") + if not mcu: + return + tool_init_cmds = debug_config.tool_settings.get("init_cmds") + if tool_init_cmds is None: + return + # Find insertion point: just before "target extended-remote" + insert_idx = next( + (i for i, cmd in enumerate(tool_init_cmds) + if "target extended-remote" in cmd), + len(tool_init_cmds), + ) + extra_cmds = [] + if self._gdb_has_python(mcu): + extra_cmds.extend(self._get_freertos_gdb_cmds()) + extra_cmds.extend(self._get_rom_elf_gdb_cmds(mcu)) + for i, cmd in enumerate(extra_cmds): + tool_init_cmds.insert(insert_idx + i, cmd)