openocd-python 2026.2.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openocd/__init__.py +64 -0
- openocd/breakpoints.py +234 -0
- openocd/cli.py +161 -0
- openocd/connection/__init__.py +7 -0
- openocd/connection/base.py +30 -0
- openocd/connection/tcl_rpc.py +259 -0
- openocd/connection/telnet.py +105 -0
- openocd/errors.py +43 -0
- openocd/events.py +112 -0
- openocd/flash.py +381 -0
- openocd/jtag/__init__.py +5 -0
- openocd/jtag/boundary.py +52 -0
- openocd/jtag/chain.py +218 -0
- openocd/jtag/scan.py +58 -0
- openocd/jtag/state.py +26 -0
- openocd/memory.py +243 -0
- openocd/process.py +148 -0
- openocd/py.typed +0 -0
- openocd/registers.py +186 -0
- openocd/rtt.py +233 -0
- openocd/session.py +330 -0
- openocd/svd/__init__.py +5 -0
- openocd/svd/decoder.py +54 -0
- openocd/svd/parser.py +128 -0
- openocd/svd/peripheral.py +186 -0
- openocd/target.py +164 -0
- openocd/transport.py +149 -0
- openocd/types.py +185 -0
- openocd_python-2026.2.12.dist-info/METADATA +75 -0
- openocd_python-2026.2.12.dist-info/RECORD +33 -0
- openocd_python-2026.2.12.dist-info/WHEEL +4 -0
- openocd_python-2026.2.12.dist-info/entry_points.txt +2 -0
- openocd_python-2026.2.12.dist-info/licenses/LICENSE +21 -0
openocd/svd/parser.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""SVD file loading and peripheral/register lookup.
|
|
2
|
+
|
|
3
|
+
Wraps cmsis_svd to parse CMSIS-SVD XML files and provide indexed access
|
|
4
|
+
to peripherals and their registers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from cmsis_svd import SVDParser
|
|
14
|
+
|
|
15
|
+
from openocd.errors import SVDError
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SVDParserWrapper:
|
|
21
|
+
"""Load and cache parsed SVD device data."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._device: Any = None
|
|
25
|
+
self._peripherals: dict[str, Any] = {}
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def loaded(self) -> bool:
|
|
29
|
+
"""Whether an SVD file has been parsed."""
|
|
30
|
+
return self._device is not None
|
|
31
|
+
|
|
32
|
+
def load(self, svd_path: Path) -> None:
|
|
33
|
+
"""Parse an SVD file and index peripherals/registers.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
svd_path: Path to the .svd file on disk.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
SVDError: If the file cannot be found or parsed.
|
|
40
|
+
"""
|
|
41
|
+
path = Path(svd_path)
|
|
42
|
+
if not path.exists():
|
|
43
|
+
raise SVDError(f"SVD file not found: {path}")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
parser = SVDParser.for_xml_file(str(path))
|
|
47
|
+
self._device = parser.get_device()
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
raise SVDError(f"Failed to parse SVD file {path}: {exc}") from exc
|
|
50
|
+
|
|
51
|
+
self._peripherals = {p.name: p for p in self._device.get_peripherals()}
|
|
52
|
+
log.info(
|
|
53
|
+
"Loaded SVD for %s — %d peripherals",
|
|
54
|
+
getattr(self._device, "name", "unknown"),
|
|
55
|
+
len(self._peripherals),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _require_loaded(self) -> None:
|
|
59
|
+
if not self.loaded:
|
|
60
|
+
raise SVDError("No SVD file loaded — call load() first")
|
|
61
|
+
|
|
62
|
+
def get_peripheral(self, name: str) -> Any:
|
|
63
|
+
"""Look up a peripheral by name.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
name: Peripheral name (case-sensitive, e.g. "GPIOA", "USART1").
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The cmsis_svd peripheral object.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
SVDError: If no SVD is loaded or the peripheral is not found.
|
|
73
|
+
"""
|
|
74
|
+
self._require_loaded()
|
|
75
|
+
periph = self._peripherals.get(name)
|
|
76
|
+
if periph is None:
|
|
77
|
+
raise SVDError(
|
|
78
|
+
f"Peripheral '{name}' not found. "
|
|
79
|
+
f"Available: {', '.join(sorted(self._peripherals))}"
|
|
80
|
+
)
|
|
81
|
+
return periph
|
|
82
|
+
|
|
83
|
+
def get_register(self, peripheral: str, register: str) -> Any:
|
|
84
|
+
"""Look up a register within a peripheral.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
peripheral: Peripheral name.
|
|
88
|
+
register: Register name (e.g. "CR1", "SR").
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The cmsis_svd register object.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
SVDError: If the peripheral or register is not found.
|
|
95
|
+
"""
|
|
96
|
+
periph = self.get_peripheral(peripheral)
|
|
97
|
+
registers = periph.registers or []
|
|
98
|
+
for reg in registers:
|
|
99
|
+
if reg.name == register:
|
|
100
|
+
return reg
|
|
101
|
+
|
|
102
|
+
available = [r.name for r in registers]
|
|
103
|
+
raise SVDError(
|
|
104
|
+
f"Register '{register}' not found in {peripheral}. "
|
|
105
|
+
f"Available: {', '.join(sorted(available))}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def list_peripherals(self) -> list[str]:
|
|
109
|
+
"""Return sorted names of all peripherals in the SVD.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
SVDError: If no SVD is loaded.
|
|
113
|
+
"""
|
|
114
|
+
self._require_loaded()
|
|
115
|
+
return sorted(self._peripherals.keys())
|
|
116
|
+
|
|
117
|
+
def list_registers(self, peripheral: str) -> list[str]:
|
|
118
|
+
"""Return sorted register names for a peripheral.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
peripheral: Peripheral name.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
SVDError: If the peripheral is not found.
|
|
125
|
+
"""
|
|
126
|
+
periph = self.get_peripheral(peripheral)
|
|
127
|
+
registers = periph.registers or []
|
|
128
|
+
return sorted(r.name for r in registers)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""SVDManager — combines SVD parsing, register decoding, and hardware reads.
|
|
2
|
+
|
|
3
|
+
This is the primary interface for SVD-based register inspection. It ties
|
|
4
|
+
the SVD parser, bitfield decoder, and the Memory subsystem together so
|
|
5
|
+
callers can do things like:
|
|
6
|
+
|
|
7
|
+
decoded = await svd.read_register("GPIOA", "ODR")
|
|
8
|
+
print(decoded)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from openocd.svd.decoder import decode_register
|
|
19
|
+
from openocd.svd.parser import SVDParserWrapper
|
|
20
|
+
from openocd.types import DecodedRegister
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from openocd.connection.base import Connection
|
|
24
|
+
from openocd.memory import Memory
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SVDManager:
|
|
30
|
+
"""High-level SVD register access: parse, read, decode."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, conn: Connection, memory: Memory) -> None:
|
|
33
|
+
self._conn = conn
|
|
34
|
+
self._memory = memory
|
|
35
|
+
self._parser = SVDParserWrapper()
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def loaded(self) -> bool:
|
|
39
|
+
"""Whether an SVD file has been loaded."""
|
|
40
|
+
return self._parser.loaded
|
|
41
|
+
|
|
42
|
+
async def load(self, svd_path: Path) -> None:
|
|
43
|
+
"""Parse an SVD file and make its peripherals available.
|
|
44
|
+
|
|
45
|
+
This is a synchronous file parse wrapped in the async interface
|
|
46
|
+
for consistency with the rest of the API.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
svd_path: Path to the .svd XML file.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
SVDError: If the file is missing or unparseable.
|
|
53
|
+
"""
|
|
54
|
+
await asyncio.to_thread(self._parser.load, svd_path)
|
|
55
|
+
|
|
56
|
+
def list_peripherals(self) -> list[str]:
|
|
57
|
+
"""Return sorted peripheral names from the loaded SVD.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
SVDError: If no SVD is loaded.
|
|
61
|
+
"""
|
|
62
|
+
return self._parser.list_peripherals()
|
|
63
|
+
|
|
64
|
+
def list_registers(self, peripheral: str) -> list[str]:
|
|
65
|
+
"""Return sorted register names for a peripheral.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
peripheral: Peripheral name (e.g. "GPIOA").
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
SVDError: If no SVD is loaded or peripheral not found.
|
|
72
|
+
"""
|
|
73
|
+
return self._parser.list_registers(peripheral)
|
|
74
|
+
|
|
75
|
+
async def read_register(self, peripheral: str, register: str) -> DecodedRegister:
|
|
76
|
+
"""Read a register from hardware and decode it using SVD metadata.
|
|
77
|
+
|
|
78
|
+
This is the primary method: it computes the register's memory-mapped
|
|
79
|
+
address from the SVD, reads 32 bits from the target, and returns
|
|
80
|
+
a fully decoded result with named bitfields.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
peripheral: Peripheral name (e.g. "GPIOA").
|
|
84
|
+
register: Register name (e.g. "ODR").
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
DecodedRegister with address, raw value, and decoded fields.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
SVDError: If peripheral/register not found.
|
|
91
|
+
TargetError: If the memory read fails.
|
|
92
|
+
"""
|
|
93
|
+
periph_obj = self._parser.get_peripheral(peripheral)
|
|
94
|
+
reg_obj = self._parser.get_register(peripheral, register)
|
|
95
|
+
address = periph_obj.base_address + reg_obj.address_offset
|
|
96
|
+
|
|
97
|
+
values = await self._memory.read_u32(address)
|
|
98
|
+
raw = values[0]
|
|
99
|
+
return decode_register(periph_obj, reg_obj, raw)
|
|
100
|
+
|
|
101
|
+
async def read_peripheral(self, peripheral: str) -> dict[str, DecodedRegister]:
|
|
102
|
+
"""Read and decode every register in a peripheral.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
peripheral: Peripheral name.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dict mapping register name to its DecodedRegister.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
SVDError: If peripheral not found.
|
|
112
|
+
TargetError: If any memory read fails.
|
|
113
|
+
"""
|
|
114
|
+
periph_obj = self._parser.get_peripheral(peripheral)
|
|
115
|
+
registers = periph_obj.registers or []
|
|
116
|
+
result: dict[str, DecodedRegister] = {}
|
|
117
|
+
|
|
118
|
+
for reg_obj in registers:
|
|
119
|
+
address = periph_obj.base_address + reg_obj.address_offset
|
|
120
|
+
try:
|
|
121
|
+
values = await self._memory.read_u32(address)
|
|
122
|
+
raw = values[0]
|
|
123
|
+
result[reg_obj.name] = decode_register(periph_obj, reg_obj, raw)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
log.warning(
|
|
126
|
+
"Failed to read %s.%s @ 0x%08X: %s",
|
|
127
|
+
peripheral,
|
|
128
|
+
reg_obj.name,
|
|
129
|
+
address,
|
|
130
|
+
exc,
|
|
131
|
+
)
|
|
132
|
+
# Skip unreadable registers (write-only, reserved, etc.)
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
def decode(self, peripheral: str, register: str, value: int) -> DecodedRegister:
|
|
137
|
+
"""Decode a raw value without reading hardware.
|
|
138
|
+
|
|
139
|
+
Useful when you already have the register value (from a log,
|
|
140
|
+
a previous read, or a known reset value).
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
peripheral: Peripheral name.
|
|
144
|
+
register: Register name.
|
|
145
|
+
value: Raw 32-bit register value.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
DecodedRegister with the decoded bitfields.
|
|
149
|
+
"""
|
|
150
|
+
periph_obj = self._parser.get_peripheral(peripheral)
|
|
151
|
+
reg_obj = self._parser.get_register(peripheral, register)
|
|
152
|
+
return decode_register(periph_obj, reg_obj, value)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SyncSVDManager:
|
|
156
|
+
"""Synchronous wrapper around SVDManager."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, manager: SVDManager, loop: asyncio.AbstractEventLoop) -> None:
|
|
159
|
+
self._manager = manager
|
|
160
|
+
self._loop = loop
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def loaded(self) -> bool:
|
|
164
|
+
return self._manager.loaded
|
|
165
|
+
|
|
166
|
+
def load(self, svd_path: Path) -> None:
|
|
167
|
+
self._loop.run_until_complete(self._manager.load(svd_path))
|
|
168
|
+
|
|
169
|
+
def list_peripherals(self) -> list[str]:
|
|
170
|
+
return self._manager.list_peripherals()
|
|
171
|
+
|
|
172
|
+
def list_registers(self, peripheral: str) -> list[str]:
|
|
173
|
+
return self._manager.list_registers(peripheral)
|
|
174
|
+
|
|
175
|
+
def read_register(self, peripheral: str, register: str) -> DecodedRegister:
|
|
176
|
+
return self._loop.run_until_complete(
|
|
177
|
+
self._manager.read_register(peripheral, register)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def read_peripheral(self, peripheral: str) -> dict[str, DecodedRegister]:
|
|
181
|
+
return self._loop.run_until_complete(
|
|
182
|
+
self._manager.read_peripheral(peripheral)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def decode(self, peripheral: str, register: str, value: int) -> DecodedRegister:
|
|
186
|
+
return self._manager.decode(peripheral, register, value)
|
openocd/target.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Target state control — halt, resume, step, reset, and state queries.
|
|
2
|
+
|
|
3
|
+
Wraps the OpenOCD target commands: halt, resume, step, reset,
|
|
4
|
+
wait_halt, and targets (for state inspection).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from openocd.connection.tcl_rpc import TclRpcConnection
|
|
15
|
+
from openocd.errors import TargetError, TimeoutError
|
|
16
|
+
from openocd.types import TargetState
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Matches a target row from "targets" output, e.g.:
|
|
21
|
+
# " 0* stm32f1x.cpu cortex_m little stm32f1x.cpu halted"
|
|
22
|
+
_TARGET_ROW_RE = re.compile(
|
|
23
|
+
r"^\s*\d+\*?\s+" # index, optional current marker
|
|
24
|
+
r"(\S+)\s+" # target name
|
|
25
|
+
r"\S+\s+" # type
|
|
26
|
+
r"\S+\s+" # endian
|
|
27
|
+
r"\S+\s+" # tap name
|
|
28
|
+
r"(\S+)" # state
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Target:
|
|
33
|
+
"""Target execution control — halt, resume, step, reset."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, conn: TclRpcConnection) -> None:
|
|
36
|
+
self._conn = conn
|
|
37
|
+
|
|
38
|
+
async def halt(self) -> TargetState:
|
|
39
|
+
"""Halt the target and return the resulting state."""
|
|
40
|
+
resp = await self._conn.send("halt")
|
|
41
|
+
if "error" in resp.lower() and "already" not in resp.lower():
|
|
42
|
+
raise TargetError(f"halt failed: {resp}")
|
|
43
|
+
return await self._parse_state()
|
|
44
|
+
|
|
45
|
+
async def resume(self, address: int | None = None) -> None:
|
|
46
|
+
"""Resume execution, optionally from a specific address."""
|
|
47
|
+
cmd = "resume"
|
|
48
|
+
if address is not None:
|
|
49
|
+
cmd = f"resume 0x{address:x}"
|
|
50
|
+
resp = await self._conn.send(cmd)
|
|
51
|
+
if "error" in resp.lower():
|
|
52
|
+
raise TargetError(f"resume failed: {resp}")
|
|
53
|
+
|
|
54
|
+
async def step(self, address: int | None = None) -> TargetState:
|
|
55
|
+
"""Single-step and return the resulting state."""
|
|
56
|
+
cmd = "step"
|
|
57
|
+
if address is not None:
|
|
58
|
+
cmd = f"step 0x{address:x}"
|
|
59
|
+
resp = await self._conn.send(cmd)
|
|
60
|
+
if "error" in resp.lower():
|
|
61
|
+
raise TargetError(f"step failed: {resp}")
|
|
62
|
+
return await self._parse_state()
|
|
63
|
+
|
|
64
|
+
async def reset(self, mode: Literal["run", "halt", "init"] = "halt") -> None:
|
|
65
|
+
"""Reset the target.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
mode: Reset mode — "run" resumes after reset, "halt" stops at
|
|
69
|
+
the reset vector, "init" runs init scripts after reset.
|
|
70
|
+
"""
|
|
71
|
+
resp = await self._conn.send(f"reset {mode}")
|
|
72
|
+
if "error" in resp.lower():
|
|
73
|
+
raise TargetError(f"reset failed: {resp}")
|
|
74
|
+
|
|
75
|
+
async def wait_halt(self, timeout_ms: int = 5000) -> TargetState:
|
|
76
|
+
"""Block until the target halts or the timeout expires.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
timeout_ms: Maximum wait time in milliseconds.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
TimeoutError: Target did not halt within the deadline.
|
|
83
|
+
"""
|
|
84
|
+
resp = await self._conn.send(f"wait_halt {timeout_ms}")
|
|
85
|
+
if "timed out" in resp.lower() or "time out" in resp.lower():
|
|
86
|
+
raise TimeoutError(f"Target did not halt within {timeout_ms}ms")
|
|
87
|
+
if "error" in resp.lower():
|
|
88
|
+
raise TargetError(f"wait_halt failed: {resp}")
|
|
89
|
+
return await self._parse_state()
|
|
90
|
+
|
|
91
|
+
async def state(self) -> TargetState:
|
|
92
|
+
"""Query and return the current target state."""
|
|
93
|
+
return await self._parse_state()
|
|
94
|
+
|
|
95
|
+
async def _parse_state(self) -> TargetState:
|
|
96
|
+
"""Parse the ``targets`` command output into a TargetState.
|
|
97
|
+
|
|
98
|
+
The output looks like::
|
|
99
|
+
|
|
100
|
+
TargetName Type Endian TapName State
|
|
101
|
+
-- ------------------ ---------- ------ ------------------ ------------
|
|
102
|
+
0* stm32f1x.cpu cortex_m little stm32f1x.cpu halted
|
|
103
|
+
|
|
104
|
+
If the target is halted, also reads the program counter via ``reg pc``.
|
|
105
|
+
"""
|
|
106
|
+
resp = await self._conn.send("targets")
|
|
107
|
+
|
|
108
|
+
name = "unknown"
|
|
109
|
+
raw_state = "unknown"
|
|
110
|
+
|
|
111
|
+
for line in resp.splitlines():
|
|
112
|
+
m = _TARGET_ROW_RE.match(line)
|
|
113
|
+
if m:
|
|
114
|
+
name = m.group(1)
|
|
115
|
+
raw_state = m.group(2).lower()
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
# Normalize to our known state literals
|
|
119
|
+
if raw_state not in ("running", "halted", "reset", "debug-running"):
|
|
120
|
+
raw_state = "unknown"
|
|
121
|
+
|
|
122
|
+
pc: int | None = None
|
|
123
|
+
if raw_state == "halted":
|
|
124
|
+
try:
|
|
125
|
+
pc = await self._read_pc()
|
|
126
|
+
except Exception:
|
|
127
|
+
log.debug("Could not read PC while halted", exc_info=True)
|
|
128
|
+
|
|
129
|
+
return TargetState(name=name, state=raw_state, current_pc=pc)
|
|
130
|
+
|
|
131
|
+
async def _read_pc(self) -> int:
|
|
132
|
+
"""Read the program counter from the halted target."""
|
|
133
|
+
resp = await self._conn.send("reg pc")
|
|
134
|
+
# Output: "pc (/32): 0x08001234"
|
|
135
|
+
m = re.search(r":\s*(0x[0-9a-fA-F]+)", resp)
|
|
136
|
+
if not m:
|
|
137
|
+
raise TargetError(f"Cannot parse PC from: {resp}")
|
|
138
|
+
return int(m.group(1), 16)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class SyncTarget:
|
|
142
|
+
"""Synchronous wrapper around Target."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, target: Target, loop: asyncio.AbstractEventLoop) -> None:
|
|
145
|
+
self._target = target
|
|
146
|
+
self._loop = loop
|
|
147
|
+
|
|
148
|
+
def halt(self) -> TargetState:
|
|
149
|
+
return self._loop.run_until_complete(self._target.halt())
|
|
150
|
+
|
|
151
|
+
def resume(self, address: int | None = None) -> None:
|
|
152
|
+
self._loop.run_until_complete(self._target.resume(address))
|
|
153
|
+
|
|
154
|
+
def step(self, address: int | None = None) -> TargetState:
|
|
155
|
+
return self._loop.run_until_complete(self._target.step(address))
|
|
156
|
+
|
|
157
|
+
def reset(self, mode: Literal["run", "halt", "init"] = "halt") -> None:
|
|
158
|
+
self._loop.run_until_complete(self._target.reset(mode))
|
|
159
|
+
|
|
160
|
+
def wait_halt(self, timeout_ms: int = 5000) -> TargetState:
|
|
161
|
+
return self._loop.run_until_complete(self._target.wait_halt(timeout_ms))
|
|
162
|
+
|
|
163
|
+
def state(self) -> TargetState:
|
|
164
|
+
return self._loop.run_until_complete(self._target.state())
|
openocd/transport.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Transport selection and debug adapter configuration.
|
|
2
|
+
|
|
3
|
+
OpenOCD supports multiple debug transports (JTAG, SWD, SWIM, etc.)
|
|
4
|
+
and various adapter interfaces (CMSIS-DAP, ST-Link, J-Link, etc.).
|
|
5
|
+
This module provides access to transport and adapter state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from openocd.errors import OpenOCDError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from openocd.connection.base import Connection
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Transport:
|
|
22
|
+
"""Query and configure the debug transport and adapter.
|
|
23
|
+
|
|
24
|
+
Usage::
|
|
25
|
+
|
|
26
|
+
transport = Transport(conn)
|
|
27
|
+
current = await transport.select() # e.g. "swd"
|
|
28
|
+
available = await transport.list() # e.g. ["jtag", "swd"]
|
|
29
|
+
speed = await transport.adapter_speed() # current kHz
|
|
30
|
+
await transport.adapter_speed(4000) # set to 4 MHz
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, conn: Connection) -> None:
|
|
34
|
+
self._conn = conn
|
|
35
|
+
|
|
36
|
+
async def select(self) -> str:
|
|
37
|
+
"""Get the currently selected transport.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Transport name string (e.g. "jtag", "swd", "swim").
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
OpenOCDError: If the command fails.
|
|
44
|
+
"""
|
|
45
|
+
response = await self._conn.send("transport select")
|
|
46
|
+
response = response.strip()
|
|
47
|
+
if not response:
|
|
48
|
+
raise OpenOCDError("Empty response from 'transport select'")
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
async def list(self) -> list[str]:
|
|
52
|
+
"""List transports available for the current adapter.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of transport name strings.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
OpenOCDError: If the command fails.
|
|
59
|
+
"""
|
|
60
|
+
response = await self._conn.send("transport list")
|
|
61
|
+
response = response.strip()
|
|
62
|
+
if not response:
|
|
63
|
+
raise OpenOCDError("Empty response from 'transport list'")
|
|
64
|
+
|
|
65
|
+
# OpenOCD may return a Tcl list like "jtag swd" or one per line
|
|
66
|
+
transports: list[str] = []
|
|
67
|
+
for line in response.splitlines():
|
|
68
|
+
for token in line.split():
|
|
69
|
+
cleaned = token.strip("{}")
|
|
70
|
+
if cleaned:
|
|
71
|
+
transports.append(cleaned)
|
|
72
|
+
return transports
|
|
73
|
+
|
|
74
|
+
async def adapter_info(self) -> str:
|
|
75
|
+
"""Get adapter/interface information.
|
|
76
|
+
|
|
77
|
+
Tries ``adapter name`` first (newer OpenOCD), falls back to
|
|
78
|
+
``adapter info`` for older versions.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Adapter description string.
|
|
82
|
+
"""
|
|
83
|
+
# "adapter name" is the preferred command in OpenOCD >= 0.12
|
|
84
|
+
response = await self._conn.send("adapter name")
|
|
85
|
+
response = response.strip()
|
|
86
|
+
|
|
87
|
+
if not response or "invalid" in response.lower() or "error" in response.lower():
|
|
88
|
+
response = await self._conn.send("adapter info")
|
|
89
|
+
response = response.strip()
|
|
90
|
+
|
|
91
|
+
if not response:
|
|
92
|
+
raise OpenOCDError("Could not determine adapter info")
|
|
93
|
+
return response
|
|
94
|
+
|
|
95
|
+
async def adapter_speed(self, khz: int | None = None) -> int:
|
|
96
|
+
"""Get or set the adapter clock speed.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
khz: If provided, set the adapter speed to this value in kHz.
|
|
100
|
+
If None, just query the current speed.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The current (or newly set) adapter speed in kHz.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
OpenOCDError: If the command fails or response is not parseable.
|
|
107
|
+
"""
|
|
108
|
+
cmd = f"adapter speed {khz}" if khz is not None else "adapter speed"
|
|
109
|
+
|
|
110
|
+
response = await self._conn.send(cmd)
|
|
111
|
+
response = response.strip()
|
|
112
|
+
|
|
113
|
+
# OpenOCD response is typically just a number, or
|
|
114
|
+
# "adapter speed: 4000 kHz" depending on the interface
|
|
115
|
+
speed = _parse_speed(response)
|
|
116
|
+
if speed is None:
|
|
117
|
+
raise OpenOCDError(f"Cannot parse adapter speed from: {response!r}")
|
|
118
|
+
|
|
119
|
+
if khz is not None:
|
|
120
|
+
log.info("Adapter speed set to %d kHz", speed)
|
|
121
|
+
return speed
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Helpers
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def _parse_speed(response: str) -> int | None:
|
|
129
|
+
"""Extract a numeric kHz value from an adapter speed response.
|
|
130
|
+
|
|
131
|
+
Handles formats like:
|
|
132
|
+
"4000"
|
|
133
|
+
"adapter speed: 4000 kHz"
|
|
134
|
+
"4000 kHz"
|
|
135
|
+
"""
|
|
136
|
+
# Try the whole thing as a plain integer
|
|
137
|
+
try:
|
|
138
|
+
return int(response)
|
|
139
|
+
except ValueError:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
# Pull out the first integer-looking token
|
|
143
|
+
for token in response.replace(":", " ").split():
|
|
144
|
+
try:
|
|
145
|
+
return int(token)
|
|
146
|
+
except ValueError:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
return None
|