Skip to content

Commit 5f703da

Browse files
pi-anlJosverl
authored andcommitted
Implement pyOCD SWD/JTAG programming support with dynamic target detection
## Major Features Added ### pyOCD Integration - Add SWD/JTAG programming as alternative to serial bootloader methods - Support for debug probe discovery and management - Automated target chip selection using dynamic detection - Optional pyOCD dependency via `pyocd` extra ### Dynamic Target Detection - Replace hardcoded target mappings with dynamic API-based detection - Parse MCU info from `sys.implementation._machine` strings - Fuzzy matching algorithm for target selection - Direct probe-based target detection with fallback to fuzzy matching - Extensible architecture for future OpenOCD/J-Link support ### CLI Integration - Add `--method pyocd` option for explicit SWD/JTAG programming - Add `--probe-id` option for specific debug probe selection - Maintain existing serial bootloader behavior as default - Clean integration with existing flash method selection ### Architecture Improvements - Abstract debug probe layer for extensibility - Target detector abstraction with registry system - Proper error handling and fallback mechanisms - Performance optimized with caching and lazy loading ## Technical Details ### Files Added - `mpflash/flash/debug_probe.py` - Debug probe abstraction layer - `mpflash/flash/pyocd_probe.py` - pyOCD-specific probe implementation - `mpflash/flash/pyocd_flash.py` - pyOCD flash programming interface - `mpflash/flash/pyocd_targets.py` - Target detection wrapper functions - `mpflash/flash/dynamic_targets.py` - Dynamic target detection engine - `mpflash/cli_pyocd.py` - pyOCD-specific CLI commands (future) ### Files Modified - `mpflash/common.py` - Add FlashMethod enum for different programming methods - `mpflash/flash/__init__.py` - Integrate pyOCD into flash method selection - `mpflash/cli_flash.py` - Add CLI options for pyOCD method and probe selection - `pyproject.toml` - Add optional pyOCD dependency - `mpflash/cli_download.py` - Fix unused pytest import ### Key Benefits - **No hardware requirements change** - existing serial methods remain default - **Automated target selection** - no manual target configuration needed - **Extensible design** - easy to add OpenOCD, J-Link, etc. in future - **Performance optimized** - direct API calls instead of subprocess shells - **Maintainable** - eliminates hardcoded target mappings ## Usage ```bash # Existing behavior unchanged (serial bootloader methods) mpflash flash # Explicit pyOCD SWD/JTAG programming mpflash flash --method pyocd # Specific debug probe selection mpflash flash --method pyocd --probe-id stlink # Install with pyOCD support uv sync --extra pyocd ``` ## Breaking Changes None - all existing functionality preserved with same default behavior.
1 parent 89253a1 commit 5f703da

File tree

16 files changed

+3413
-24
lines changed

16 files changed

+3413
-24
lines changed

mpflash/cli_download.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""CLI to Download MicroPython firmware for specific ports, boards and versions."""
22

3-
from pathlib import Path
43

5-
from pytest import param
64
import rich_click as click
75
from loguru import logger as log
86

@@ -16,7 +14,6 @@
1614
from .ask_input import ask_missing_params
1715
from .cli_group import cli
1816
from .common import DownloadParams
19-
from .config import config
2017
from .download import download
2118

2219

mpflash/cli_flash.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from mpflash.cli_download import connected_ports_boards_variants
1010
from mpflash.cli_group import cli
1111
from mpflash.cli_list import show_mcus
12-
from mpflash.common import BootloaderMethod, FlashParams, filtered_comports
12+
from mpflash.common import BootloaderMethod, FlashMethod, FlashParams, filtered_comports
1313
from mpflash.errors import MPFlashError
1414
from mpflash.flash import flash_list
1515
from mpflash.flash.worklist import WorkList, full_auto_worklist, manual_worklist, single_auto_worklist
@@ -112,6 +112,27 @@
112112
show_default=True,
113113
help="""How to enter the (MicroPython) bootloader before flashing.""",
114114
)
115+
@click.option(
116+
"--method",
117+
"--flash-method",
118+
"flash_method",
119+
type=click.Choice([e.value for e in FlashMethod]),
120+
default="auto",
121+
show_default=True,
122+
help="""Flash programming method. 'auto' uses serial bootloader methods (existing behavior). Use 'pyocd' for SWD/JTAG programming via debug probe.""",
123+
)
124+
@click.option(
125+
"--probe",
126+
"--probe-id", # Keep as alias for backwards compatibility
127+
"probe_id",
128+
help="""Specific pyOCD probe ID to use (partial match). Required when multiple probes are connected.""",
129+
metavar="PROBE_ID",
130+
)
131+
@click.option(
132+
"--auto-install-packs/--no-auto-install-packs",
133+
default=True,
134+
help="""Automatically install CMSIS packs for missing pyOCD targets. Default: enabled.""",
135+
)
115136
@click.option(
116137
"--force",
117138
"-f",
@@ -144,6 +165,14 @@ def cli_flash_board(**kwargs) -> int:
144165
kwargs.pop("board")
145166
else:
146167
kwargs["boards"] = [kwargs.pop("board")]
168+
169+
# Convert flash_method to method and convert to enum
170+
flash_method_str = kwargs.pop("flash_method", "auto")
171+
flash_method = FlashMethod(flash_method_str)
172+
173+
# Extract pyOCD options
174+
probe_id = kwargs.pop("probe_id", None)
175+
auto_install_packs = kwargs.pop("auto_install_packs", True)
147176

148177
params = FlashParams(**kwargs)
149178
params.versions = list(params.versions)
@@ -210,6 +239,7 @@ def cli_flash_board(**kwargs) -> int:
210239
board_id=board_id,
211240
version=params.versions[0],
212241
custom=params.custom,
242+
method=flash_method,
213243
)
214244
# if serial port == auto and there are one or more specified/detected boards
215245
elif params.serial == ["*"] and params.boards:
@@ -226,6 +256,7 @@ def cli_flash_board(**kwargs) -> int:
226256
version=params.versions[0],
227257
include=params.serial,
228258
ignore=params.ignore,
259+
method=flash_method,
229260
)
230261
elif params.versions[0] and params.boards[0] and params.serial:
231262
# A one or more serial port including the board / variant
@@ -238,19 +269,24 @@ def cli_flash_board(**kwargs) -> int:
238269
comports,
239270
board_id=params.boards[0],
240271
version=params.versions[0],
272+
method=flash_method,
241273
)
242274
else:
243275
# just this serial port on auto
244276
worklist = single_auto_worklist(
245277
serial=params.serial[0],
246278
version=params.versions[0],
279+
method=flash_method,
247280
)
248281
if not params.custom:
249282
jid.ensure_firmware_downloaded(worklist, version=params.versions[0], force=params.force)
250283
if flashed := flash_list(
251284
worklist,
252285
params.erase,
253286
params.bootloader,
287+
method=flash_method,
288+
probe_id=probe_id,
289+
auto_install_packs=auto_install_packs,
254290
flash_mode=params.flash_mode,
255291
):
256292
log.info(f"Flashed {len(flashed)} boards")

mpflash/cli_pyocd.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
"""
2+
CLI commands for pyOCD debug probe management and information.
3+
"""
4+
5+
import rich_click as click
6+
from rich.console import Console
7+
from rich.table import Table
8+
from loguru import logger as log
9+
10+
from mpflash.cli_group import cli
11+
from mpflash.errors import MPFlashError
12+
13+
try:
14+
from mpflash.flash.pyocd_flash import (
15+
list_pyocd_probes,
16+
pyocd_info,
17+
)
18+
from mpflash.flash.pyocd_core import (
19+
is_pyocd_available,
20+
get_pyocd_targets
21+
)
22+
PYOCD_AVAILABLE = True
23+
except ImportError:
24+
PYOCD_AVAILABLE = False
25+
26+
def list_supported_targets():
27+
"""Get supported targets for CLI display."""
28+
try:
29+
targets = get_pyocd_targets()
30+
return {name: info.get("part_number", name) for name, info in targets.items()}
31+
except Exception:
32+
return {}
33+
34+
console = Console()
35+
36+
@cli.command(
37+
"list-probes",
38+
short_help="List available pyOCD debug probes and their target information.",
39+
)
40+
@click.option(
41+
"--detect-targets/--no-detect-targets",
42+
default=True,
43+
show_default=True,
44+
help="Attempt to auto-detect target types connected to probes.",
45+
)
46+
def cli_list_probes(detect_targets: bool) -> int:
47+
"""
48+
List all connected pyOCD debug probes with their capabilities.
49+
50+
This command discovers debug probes (ST-Link, DAP-Link, etc.) that can be used
51+
for SWD/JTAG programming with the --method pyocd option.
52+
"""
53+
if not PYOCD_AVAILABLE:
54+
log.error("pyOCD is not installed. Install with: uv add pyocd")
55+
return 1
56+
57+
if not is_pyocd_available():
58+
log.error("pyOCD is installed but not functioning properly")
59+
return 1
60+
61+
try:
62+
probes = list_pyocd_probes()
63+
64+
if not probes:
65+
console.print("No pyOCD debug probes found.")
66+
console.print("\nMake sure your debug probe is connected and recognized by the system.")
67+
console.print("Common debug probes include ST-Link, DAP-Link, J-Link, etc.")
68+
return 1
69+
70+
table = Table(title="Available PyOCD Debug Probes")
71+
table.add_column("Probe ID", style="cyan", no_wrap=True)
72+
table.add_column("Description", style="white")
73+
table.add_column("Vendor", style="blue")
74+
table.add_column("Product", style="blue")
75+
table.add_column("Target Type", style="green")
76+
table.add_column("Status", style="yellow")
77+
78+
for probe in probes:
79+
# Optionally detect target type
80+
target_type = "Unknown"
81+
status = "Connected"
82+
83+
if detect_targets:
84+
try:
85+
detected = probe.detect_target_type()
86+
if detected:
87+
target_type = detected
88+
status = "Target Detected"
89+
else:
90+
status = "No Target"
91+
except Exception as e:
92+
target_type = "Detection Failed"
93+
status = f"Error: {str(e)[:30]}..."
94+
else:
95+
status = "Not Checked"
96+
97+
table.add_row(
98+
probe.unique_id,
99+
probe.description,
100+
probe.vendor_name,
101+
probe.product_name,
102+
target_type,
103+
status
104+
)
105+
106+
console.print(table)
107+
108+
console.print(f"\n[green]Found {len(probes)} debug probe(s)[/green]")
109+
console.print("\nTo use a specific probe with mpflash:")
110+
console.print(" mpflash flash --method pyocd --probe-id <PROBE_ID>")
111+
console.print("\nTo flash with automatic probe selection:")
112+
console.print(" mpflash flash --method pyocd")
113+
114+
return 0
115+
116+
except Exception as e:
117+
log.error(f"Failed to list pyOCD probes: {e}")
118+
return 1
119+
120+
@cli.command(
121+
"pyocd-info",
122+
short_help="Show pyOCD installation and target support information.",
123+
)
124+
def cli_pyocd_info() -> int:
125+
"""
126+
Display information about pyOCD installation, version, and supported targets.
127+
128+
This command shows the current pyOCD status, available debug probes,
129+
and information about target support for SWD/JTAG programming.
130+
"""
131+
info = pyocd_info() if PYOCD_AVAILABLE else {"available": False}
132+
133+
# PyOCD Installation Status
134+
console.print("[bold blue]PyOCD Installation Status[/bold blue]")
135+
if info["available"]:
136+
console.print(f"✅ pyOCD is installed (version: {info.get('version', 'unknown')})")
137+
else:
138+
console.print("❌ pyOCD is not installed")
139+
console.print(" Install with: uv add pyocd")
140+
return 1
141+
142+
# Debug Probes
143+
console.print(f"\n[bold blue]Connected Debug Probes[/bold blue]")
144+
probes = info.get("probes", [])
145+
if probes:
146+
for probe in probes:
147+
console.print(f"🔌 {probe['unique_id']}: {probe['description']}")
148+
if probe.get('target_type'):
149+
console.print(f" Target: {probe['target_type']}")
150+
else:
151+
console.print("No debug probes found")
152+
153+
# Supported Targets
154+
console.print(f"\n[bold blue]Built-in Target Support[/bold blue]")
155+
if PYOCD_AVAILABLE:
156+
targets = list_supported_targets()
157+
console.print(f"📋 {len(targets)} board mappings available")
158+
159+
# Group by target family
160+
stm32_boards = [bid for bid in targets.keys() if targets[bid].startswith("stm32")]
161+
rp2040_boards = [bid for bid in targets.keys() if targets[bid].startswith("rp20")]
162+
samd_boards = [bid for bid in targets.keys() if targets[bid].startswith("samd")]
163+
164+
console.print(f" STM32 boards: {len(stm32_boards)}")
165+
console.print(f" RP2040 boards: {len(rp2040_boards)}")
166+
console.print(f" SAMD boards: {len(samd_boards)}")
167+
168+
console.print(f"\n[dim]Note: ESP32/ESP8266 not supported (use esptool instead)[/dim]")
169+
170+
# Usage Examples
171+
console.print(f"\n[bold blue]Usage Examples[/bold blue]")
172+
console.print("Flash with pyOCD (auto-detect probe and target):")
173+
console.print(" mpflash flash --method pyocd")
174+
console.print("\nFlash with specific probe:")
175+
console.print(" mpflash flash --method pyocd --probe-id <PROBE_ID>")
176+
console.print("\nList available probes:")
177+
console.print(" mpflash list-probes")
178+
179+
return 0
180+
181+
@cli.command(
182+
"pyocd-targets",
183+
short_help="List supported pyOCD target mappings.",
184+
)
185+
@click.option(
186+
"--board-filter",
187+
"-b",
188+
help="Filter targets by board name (case-insensitive substring match)",
189+
metavar="PATTERN"
190+
)
191+
@click.option(
192+
"--target-filter",
193+
"-t",
194+
help="Filter by pyOCD target type (case-insensitive substring match)",
195+
metavar="PATTERN"
196+
)
197+
def cli_pyocd_targets(board_filter: str, target_filter: str) -> int:
198+
"""
199+
Display the mapping between MPFlash board IDs and pyOCD target types.
200+
201+
This shows which boards can be programmed using pyOCD SWD/JTAG interface
202+
and what target type pyOCD will use for each board.
203+
"""
204+
if not PYOCD_AVAILABLE:
205+
log.error("pyOCD is not installed. Install with: uv add pyocd")
206+
return 1
207+
208+
try:
209+
targets = list_supported_targets()
210+
211+
# Apply filters
212+
filtered_targets = targets
213+
if board_filter:
214+
filtered_targets = {
215+
board_id: target for board_id, target in targets.items()
216+
if board_filter.lower() in board_id.lower()
217+
}
218+
if target_filter:
219+
filtered_targets = {
220+
board_id: target for board_id, target in filtered_targets.items()
221+
if target_filter.lower() in target.lower()
222+
}
223+
224+
if not filtered_targets:
225+
console.print("No targets match the specified filters.")
226+
return 1
227+
228+
table = Table(title="PyOCD Target Mappings")
229+
table.add_column("Board ID", style="cyan", no_wrap=True)
230+
table.add_column("PyOCD Target", style="green", no_wrap=True)
231+
table.add_column("Family", style="blue")
232+
233+
# Sort by board ID for consistent output
234+
for board_id in sorted(filtered_targets.keys()):
235+
target = filtered_targets[board_id]
236+
237+
# Determine family
238+
if target.startswith("stm32"):
239+
family = "STM32"
240+
elif target.startswith("rp20"):
241+
family = "RP2040/RP2350"
242+
elif target.startswith("samd"):
243+
family = "SAMD"
244+
else:
245+
family = "Other"
246+
247+
table.add_row(board_id, target, family)
248+
249+
console.print(table)
250+
console.print(f"\n[green]Showing {len(filtered_targets)} of {len(targets)} supported targets[/green]")
251+
252+
if board_filter or target_filter:
253+
console.print(f"\nFilters applied:")
254+
if board_filter:
255+
console.print(f" Board: {board_filter}")
256+
if target_filter:
257+
console.print(f" Target: {target_filter}")
258+
259+
return 0
260+
261+
except Exception as e:
262+
log.error(f"Failed to list pyOCD targets: {e}")
263+
return 1

mpflash/common.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ class BootloaderMethod(Enum):
5656
NONE = "none"
5757

5858

59+
class FlashMethod(Enum):
60+
AUTO = "auto"
61+
SERIAL = "serial" # Traditional serial bootloader methods
62+
PYOCD = "pyocd" # SWD/JTAG programming via pyOCD
63+
UF2 = "uf2" # UF2 file copy method
64+
DFU = "dfu" # STM32 DFU method
65+
ESPTOOL = "esptool" # ESP32/ESP8266 esptool method
66+
67+
5968
@dataclass
6069
class FlashParams(Params):
6170
"""Parameters for flashing a board"""

0 commit comments

Comments
 (0)