pyGdbToolkit 0.1.0__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.
- pyGdbToolkit/__init__.py +55 -0
- pyGdbToolkit/arch/__init__.py +79 -0
- pyGdbToolkit/arch/arm/__init__.py +17 -0
- pyGdbToolkit/arch/arm/coresight.py +427 -0
- pyGdbToolkit/arch/arm/cortex_m.py +504 -0
- pyGdbToolkit/arch/arm/fault.py +629 -0
- pyGdbToolkit/arch/arm/models.py +60 -0
- pyGdbToolkit/arch/arm/mpu.py +556 -0
- pyGdbToolkit/arch/arm/probe.py +10 -0
- pyGdbToolkit/arch/arm/providers/__init__.py +16 -0
- pyGdbToolkit/arch/arm/providers/base.py +77 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/__init__.py +4 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/stm32.py +680 -0
- pyGdbToolkit/arch/arm/sau.py +253 -0
- pyGdbToolkit/arch/arm/security.py +816 -0
- pyGdbToolkit/arch/arm/session_state.py +96 -0
- pyGdbToolkit/arch/arm/target.py +32 -0
- pyGdbToolkit/arch/base.py +143 -0
- pyGdbToolkit/arch/diagnostics.py +300 -0
- pyGdbToolkit/arch/registry.py +43 -0
- pyGdbToolkit/cmd_faultinfo.py +99 -0
- pyGdbToolkit/cmd_lscpu.py +116 -0
- pyGdbToolkit/cmd_profile.py +1402 -0
- pyGdbToolkit/cmd_rtos.py +298 -0
- pyGdbToolkit/cmd_secscan.py +466 -0
- pyGdbToolkit/cmd_svd.py +1208 -0
- pyGdbToolkit/diagnostic_runtime.py +52 -0
- pyGdbToolkit/rtos/__init__.py +8 -0
- pyGdbToolkit/rtos/camelot.py +513 -0
- pyGdbToolkit/session.py +233 -0
- pyGdbToolkit/svd.py +1194 -0
- pyGdbToolkit/target_memory.py +190 -0
- pygdbtoolkit-0.1.0.dist-info/METADATA +586 -0
- pygdbtoolkit-0.1.0.dist-info/RECORD +36 -0
- pygdbtoolkit-0.1.0.dist-info/WHEEL +5 -0
- pygdbtoolkit-0.1.0.dist-info/top_level.txt +1 -0
pyGdbToolkit/svd.py
ADDED
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""CMSIS-SVD download, parsing, and dictionary mapping primitives.
|
|
5
|
+
|
|
6
|
+
This module provides facilities to:
|
|
7
|
+
- Resolve and download SVD (System View Description) XML files from the
|
|
8
|
+
cmsis-svd-data repository (https://github.com/cmsis-svd/cmsis-svd-data).
|
|
9
|
+
- Automatically detect the active SoC/MCU using cmd_lscpu target inspection
|
|
10
|
+
and select the appropriate SVD file using a multi-vendor matching engine.
|
|
11
|
+
- Provide primitives to load SVD files explicitly by vendor/name or file path.
|
|
12
|
+
- Parse SVD XML definitions into rich immutable data models and plain Python
|
|
13
|
+
dictionaries for peripheral, register, and bit-field inspection.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import re
|
|
22
|
+
from typing import Any
|
|
23
|
+
import xml.etree.ElementTree as ET
|
|
24
|
+
|
|
25
|
+
import requests
|
|
26
|
+
|
|
27
|
+
from .arch.arm.coresight import discover_rom_tables
|
|
28
|
+
from .arch.arm.cortex_m import CPUID_ADDRESS, CortexMTargetDescription, decode_cpuid
|
|
29
|
+
from .arch.arm.models import DeviceReport
|
|
30
|
+
from .arch.arm.providers import DEFAULT_PROVIDER_REGISTRY
|
|
31
|
+
from .target_memory import TargetMemory, TargetMemoryReader, TargetReadError
|
|
32
|
+
|
|
33
|
+
CMSIS_SVD_DATA_RAW_URL = "https://raw.githubusercontent.com/cmsis-svd/cmsis-svd-data/main/data"
|
|
34
|
+
|
|
35
|
+
# Normalization mapping from CPU/Device vendor names to cmsis-svd-data folder names
|
|
36
|
+
VENDOR_DIR_MAPPING: dict[str, str] = {
|
|
37
|
+
"stmicroelectronics": "STMicro",
|
|
38
|
+
"stmicro": "STMicro",
|
|
39
|
+
"stm": "STMicro",
|
|
40
|
+
"st": "STMicro",
|
|
41
|
+
"nxp semiconductors": "NXP",
|
|
42
|
+
"nxp": "NXP",
|
|
43
|
+
"freescale": "Freescale",
|
|
44
|
+
"atmel": "Atmel",
|
|
45
|
+
"microchip": "Atmel",
|
|
46
|
+
"nordic semiconductor": "Nordic",
|
|
47
|
+
"nordic": "Nordic",
|
|
48
|
+
"raspberry pi": "RaspberryPi",
|
|
49
|
+
"raspberrypi": "RaspberryPi",
|
|
50
|
+
"raspberry": "RaspberryPi",
|
|
51
|
+
"silicon labs": "SiliconLabs",
|
|
52
|
+
"silabs": "SiliconLabs",
|
|
53
|
+
"espressif": "Espressif",
|
|
54
|
+
"texas instruments": "TexasInstruments",
|
|
55
|
+
"ti": "TexasInstruments",
|
|
56
|
+
"infineon": "Infineon",
|
|
57
|
+
"gigadevice": "GigaDevice",
|
|
58
|
+
"renesas": "Renesas",
|
|
59
|
+
"cypress": "Cypress",
|
|
60
|
+
"fujitsu": "Fujitsu",
|
|
61
|
+
"holtek": "Holtek",
|
|
62
|
+
"nuvoton": "Nuvoton",
|
|
63
|
+
"toshiba": "Toshiba",
|
|
64
|
+
"alifsemi": "AlifSemi",
|
|
65
|
+
"arterytek": "ArteryTek",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SvdError(Exception):
|
|
70
|
+
"""Base exception for all SVD related operations."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SvdNotFoundError(SvdError):
|
|
74
|
+
"""Raised when an SVD file cannot be located for the target device."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SvdDownloadError(SvdError):
|
|
78
|
+
"""Raised when downloading an SVD file fails."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SvdParseError(SvdError):
|
|
82
|
+
"""Raised when parsing an SVD XML document fails."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class SvdField:
|
|
87
|
+
"""Description of a register bit-field."""
|
|
88
|
+
|
|
89
|
+
name: str
|
|
90
|
+
description: str
|
|
91
|
+
bit_offset: int
|
|
92
|
+
bit_width: int
|
|
93
|
+
access: str | None = None
|
|
94
|
+
reset_value: int | None = None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def bit_mask(self) -> int:
|
|
98
|
+
"""Return the bitmask corresponding to this field."""
|
|
99
|
+
return ((1 << self.bit_width) - 1) << self.bit_offset
|
|
100
|
+
|
|
101
|
+
def extract_value(self, reg_value: int) -> int:
|
|
102
|
+
"""Extract this field's integer value from a raw register word.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
reg_value : int
|
|
107
|
+
The full integer value of the parent register.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
int
|
|
112
|
+
The extracted and shifted bitfield value.
|
|
113
|
+
"""
|
|
114
|
+
return (reg_value >> self.bit_offset) & ((1 << self.bit_width) - 1)
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> dict[str, Any]:
|
|
117
|
+
"""Convert the field model to a plain dictionary.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
dict[str, Any]
|
|
122
|
+
Dictionary representation of the field.
|
|
123
|
+
"""
|
|
124
|
+
return {
|
|
125
|
+
"name": self.name,
|
|
126
|
+
"description": self.description,
|
|
127
|
+
"bit_offset": self.bit_offset,
|
|
128
|
+
"bit_width": self.bit_width,
|
|
129
|
+
"bit_mask": self.bit_mask,
|
|
130
|
+
"access": self.access,
|
|
131
|
+
"reset_value": self.reset_value,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class SvdRegister:
|
|
137
|
+
"""Description of a memory-mapped peripheral register."""
|
|
138
|
+
|
|
139
|
+
name: str
|
|
140
|
+
display_name: str | None
|
|
141
|
+
description: str
|
|
142
|
+
address_offset: int
|
|
143
|
+
size: int
|
|
144
|
+
access: str | None
|
|
145
|
+
reset_value: int | None
|
|
146
|
+
reset_mask: int | None
|
|
147
|
+
fields: tuple[SvdField, ...] = ()
|
|
148
|
+
|
|
149
|
+
def get_field(self, name: str) -> SvdField | None:
|
|
150
|
+
"""Find a field by case-insensitive name.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
name : str
|
|
155
|
+
The name of the field to find.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
SvdField | None
|
|
160
|
+
The matching field, or None if not found.
|
|
161
|
+
"""
|
|
162
|
+
target = name.upper()
|
|
163
|
+
for field_obj in self.fields:
|
|
164
|
+
if field_obj.name.upper() == target:
|
|
165
|
+
return field_obj
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def decode(self, value: int) -> list[dict[str, Any]]:
|
|
169
|
+
"""Decode a numeric register value into its constituent bitfields.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
value : int
|
|
174
|
+
The raw numeric value read from target memory.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
list[dict[str, Any]]
|
|
179
|
+
List of decoded field dictionaries containing extracted values.
|
|
180
|
+
"""
|
|
181
|
+
results: list[dict[str, Any]] = []
|
|
182
|
+
for field_obj in self.fields:
|
|
183
|
+
extracted = field_obj.extract_value(value)
|
|
184
|
+
results.append(
|
|
185
|
+
{
|
|
186
|
+
"name": field_obj.name,
|
|
187
|
+
"description": field_obj.description,
|
|
188
|
+
"bit_offset": field_obj.bit_offset,
|
|
189
|
+
"bit_width": field_obj.bit_width,
|
|
190
|
+
"bit_mask": field_obj.bit_mask,
|
|
191
|
+
"value": extracted,
|
|
192
|
+
"hex_value": hex(extracted),
|
|
193
|
+
"access": field_obj.access,
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
return results
|
|
197
|
+
|
|
198
|
+
def to_dict(self) -> dict[str, Any]:
|
|
199
|
+
"""Convert the register model to a plain dictionary.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
dict[str, Any]
|
|
204
|
+
Dictionary representation of the register and all fields.
|
|
205
|
+
"""
|
|
206
|
+
return {
|
|
207
|
+
"name": self.name,
|
|
208
|
+
"display_name": self.display_name,
|
|
209
|
+
"description": self.description,
|
|
210
|
+
"address_offset": self.address_offset,
|
|
211
|
+
"size": self.size,
|
|
212
|
+
"access": self.access,
|
|
213
|
+
"reset_value": self.reset_value,
|
|
214
|
+
"reset_mask": self.reset_mask,
|
|
215
|
+
"fields": [f.to_dict() for f in self.fields],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class SvdPeripheral:
|
|
221
|
+
"""Description of a memory-mapped peripheral."""
|
|
222
|
+
|
|
223
|
+
name: str
|
|
224
|
+
description: str
|
|
225
|
+
group_name: str | None
|
|
226
|
+
base_address: int
|
|
227
|
+
derived_from: str | None = None
|
|
228
|
+
registers: tuple[SvdRegister, ...] = ()
|
|
229
|
+
|
|
230
|
+
def get_register(self, name_or_offset: str | int) -> SvdRegister | None:
|
|
231
|
+
"""Find a register by case-insensitive name or address offset.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
name_or_offset : str | int
|
|
236
|
+
Register name (str) or offset in bytes (int).
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
SvdRegister | None
|
|
241
|
+
The matching register, or None if not found.
|
|
242
|
+
"""
|
|
243
|
+
if isinstance(name_or_offset, int):
|
|
244
|
+
for reg in self.registers:
|
|
245
|
+
if reg.address_offset == name_or_offset:
|
|
246
|
+
return reg
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
target = name_or_offset.upper()
|
|
250
|
+
for reg in self.registers:
|
|
251
|
+
if reg.name.upper() == target:
|
|
252
|
+
return reg
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def to_dict(self) -> dict[str, Any]:
|
|
256
|
+
"""Convert the peripheral model to a plain dictionary.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
dict[str, Any]
|
|
261
|
+
Dictionary representation of the peripheral and all registers.
|
|
262
|
+
"""
|
|
263
|
+
return {
|
|
264
|
+
"name": self.name,
|
|
265
|
+
"description": self.description,
|
|
266
|
+
"group_name": self.group_name,
|
|
267
|
+
"base_address": self.base_address,
|
|
268
|
+
"derived_from": self.derived_from,
|
|
269
|
+
"registers": [r.to_dict() for r in self.registers],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass(frozen=True)
|
|
274
|
+
class SvdDevice:
|
|
275
|
+
"""Complete description of an MCU peripheral and register map."""
|
|
276
|
+
|
|
277
|
+
name: str
|
|
278
|
+
version: str | None
|
|
279
|
+
description: str
|
|
280
|
+
vendor: str | None
|
|
281
|
+
address_unit_bits: int
|
|
282
|
+
width: int
|
|
283
|
+
size: int
|
|
284
|
+
reset_value: int
|
|
285
|
+
reset_mask: int
|
|
286
|
+
peripherals: tuple[SvdPeripheral, ...] = ()
|
|
287
|
+
|
|
288
|
+
def get_peripheral(self, name_or_address: str | int) -> SvdPeripheral | None:
|
|
289
|
+
"""Find a peripheral by case-insensitive name or base address.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
name_or_address : str | int
|
|
294
|
+
Peripheral name (str) or base address (int).
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
SvdPeripheral | None
|
|
299
|
+
The matching peripheral, or None if not found.
|
|
300
|
+
"""
|
|
301
|
+
if isinstance(name_or_address, int):
|
|
302
|
+
for periph in self.peripherals:
|
|
303
|
+
if periph.base_address == name_or_address:
|
|
304
|
+
return periph
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
target = name_or_address.upper()
|
|
308
|
+
for periph in self.peripherals:
|
|
309
|
+
if periph.name.upper() == target:
|
|
310
|
+
return periph
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def find_peripheral_by_address(
|
|
314
|
+
self, address: int
|
|
315
|
+
) -> tuple[SvdPeripheral, SvdRegister | None, int] | None:
|
|
316
|
+
"""Find a peripheral and matching register containing a given memory address.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
address : int
|
|
321
|
+
Target memory address.
|
|
322
|
+
|
|
323
|
+
Returns
|
|
324
|
+
-------
|
|
325
|
+
tuple[SvdPeripheral, SvdRegister | None, int] | None
|
|
326
|
+
Tuple of (peripheral, register_or_none, offset_within_peripheral),
|
|
327
|
+
or None if no peripheral spans the given address.
|
|
328
|
+
"""
|
|
329
|
+
for periph in self.peripherals:
|
|
330
|
+
if periph.base_address <= address < periph.base_address + 0x10000:
|
|
331
|
+
offset = address - periph.base_address
|
|
332
|
+
reg = periph.get_register(offset)
|
|
333
|
+
return periph, reg, offset
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
def to_dict(self) -> dict[str, Any]:
|
|
337
|
+
"""Convert the entire device model to a plain nested dictionary.
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
dict[str, Any]
|
|
342
|
+
Dictionary representation of the entire device hierarchy.
|
|
343
|
+
"""
|
|
344
|
+
return {
|
|
345
|
+
"name": self.name,
|
|
346
|
+
"version": self.version,
|
|
347
|
+
"description": self.description,
|
|
348
|
+
"vendor": self.vendor,
|
|
349
|
+
"address_unit_bits": self.address_unit_bits,
|
|
350
|
+
"width": self.width,
|
|
351
|
+
"size": self.size,
|
|
352
|
+
"reset_value": self.reset_value,
|
|
353
|
+
"reset_mask": self.reset_mask,
|
|
354
|
+
"peripherals": {p.name: p.to_dict() for p in self.peripherals},
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def detect_target_device(reader: TargetMemory | None = None) -> DeviceReport:
|
|
359
|
+
"""Detect the active SoC/MCU by reading CPUID and inspecting providers.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
reader : TargetMemory | None
|
|
364
|
+
Typed reader for target memory. When None, a new
|
|
365
|
+
TargetMemoryReader is used.
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
DeviceReport
|
|
370
|
+
The detected target device report.
|
|
371
|
+
|
|
372
|
+
Raises
|
|
373
|
+
------
|
|
374
|
+
SvdError
|
|
375
|
+
If reading target memory fails.
|
|
376
|
+
"""
|
|
377
|
+
if reader is None:
|
|
378
|
+
reader = TargetMemoryReader()
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
raw_cpuid = reader.read_uint32(CPUID_ADDRESS)
|
|
382
|
+
except TargetReadError as error:
|
|
383
|
+
raise SvdError(f"Cannot read target CPUID register: {error}") from error
|
|
384
|
+
|
|
385
|
+
cpuid = decode_cpuid(raw_cpuid)
|
|
386
|
+
discovery = discover_rom_tables(reader)
|
|
387
|
+
return DEFAULT_PROVIDER_REGISTRY.inspect(reader, cpuid, discovery)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def resolve_svd_vendor(vendor_name: str) -> str:
|
|
391
|
+
"""Normalize a vendor string to match cmsis-svd-data directory structure.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
vendor_name : str
|
|
396
|
+
Raw vendor name (e.g. STMicroelectronics, Nordic Semiconductor).
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
str
|
|
401
|
+
Corresponding directory name in the cmsis-svd-data/data repository.
|
|
402
|
+
"""
|
|
403
|
+
normalized = vendor_name.strip().lower()
|
|
404
|
+
return VENDOR_DIR_MAPPING.get(normalized, vendor_name.strip())
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _clean_soc_name(raw_name: str) -> list[str]:
|
|
408
|
+
"""Clean and extract potential SoC model tokens from a string.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
raw_name : str
|
|
413
|
+
Raw SoC or product line string.
|
|
414
|
+
|
|
415
|
+
Returns
|
|
416
|
+
-------
|
|
417
|
+
list[str]
|
|
418
|
+
Cleaned tokens suitable for candidate generation.
|
|
419
|
+
"""
|
|
420
|
+
cleaned = re.sub(r"\(.*?\)", "", raw_name).strip()
|
|
421
|
+
raw_parts = re.split(r"[/,]", cleaned)
|
|
422
|
+
tokens: list[str] = []
|
|
423
|
+
|
|
424
|
+
for part in raw_parts:
|
|
425
|
+
token = part.strip()
|
|
426
|
+
token = re.sub(
|
|
427
|
+
r"(product line|series|line|medium-density|low-density|high-density|connectivity)",
|
|
428
|
+
"",
|
|
429
|
+
token,
|
|
430
|
+
flags=re.IGNORECASE,
|
|
431
|
+
).strip()
|
|
432
|
+
if token and token not in tokens:
|
|
433
|
+
tokens.append(token)
|
|
434
|
+
|
|
435
|
+
return tokens
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def get_candidate_svd_filenames(
|
|
439
|
+
product_line: str | None = None,
|
|
440
|
+
part_number: str | None = None,
|
|
441
|
+
) -> list[str]:
|
|
442
|
+
"""Generate prioritized SVD filename candidates from SoC naming info.
|
|
443
|
+
|
|
444
|
+
Parameters
|
|
445
|
+
----------
|
|
446
|
+
product_line : str | None
|
|
447
|
+
Product line identifier from device report (e.g. STM32F401, nRF52840).
|
|
448
|
+
part_number : str | None
|
|
449
|
+
Optional detailed part number.
|
|
450
|
+
|
|
451
|
+
Returns
|
|
452
|
+
-------
|
|
453
|
+
list[str]
|
|
454
|
+
List of candidate SVD filenames in order of preference.
|
|
455
|
+
"""
|
|
456
|
+
raw_inputs: list[str] = []
|
|
457
|
+
if part_number:
|
|
458
|
+
raw_inputs.extend(_clean_soc_name(part_number))
|
|
459
|
+
if product_line:
|
|
460
|
+
raw_inputs.extend(_clean_soc_name(product_line))
|
|
461
|
+
|
|
462
|
+
candidates: list[str] = []
|
|
463
|
+
|
|
464
|
+
def add_candidate(name: str) -> None:
|
|
465
|
+
if not name.lower().endswith(".svd"):
|
|
466
|
+
name = f"{name}.svd"
|
|
467
|
+
if name not in candidates:
|
|
468
|
+
candidates.append(name)
|
|
469
|
+
|
|
470
|
+
for token in raw_inputs:
|
|
471
|
+
base = token.strip()
|
|
472
|
+
if not base:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
add_candidate(base)
|
|
476
|
+
add_candidate(base.lower())
|
|
477
|
+
add_candidate(base.upper())
|
|
478
|
+
|
|
479
|
+
trimmed_x = re.sub(r"[xX]+$", "", base)
|
|
480
|
+
if trimmed_x:
|
|
481
|
+
add_candidate(f"{trimmed_x}xx")
|
|
482
|
+
add_candidate(f"{trimmed_x}x")
|
|
483
|
+
add_candidate(f"{trimmed_x}xx".lower())
|
|
484
|
+
add_candidate(f"{trimmed_x}xx".upper())
|
|
485
|
+
add_candidate(f"{trimmed_x}x".lower())
|
|
486
|
+
add_candidate(f"{trimmed_x}x".upper())
|
|
487
|
+
add_candidate(trimmed_x)
|
|
488
|
+
|
|
489
|
+
m = re.match(r"^([A-Za-z]+)(\d+)([A-Za-z0-9]*)", base)
|
|
490
|
+
if m:
|
|
491
|
+
prefix, digits, _ = m.group(1), m.group(2), m.group(3)
|
|
492
|
+
add_candidate(f"{prefix}{digits}")
|
|
493
|
+
add_candidate(f"{prefix}{digits}x")
|
|
494
|
+
add_candidate(f"{prefix}{digits}xx")
|
|
495
|
+
add_candidate(f"{prefix}{digits}".lower())
|
|
496
|
+
add_candidate(f"{prefix}{digits}".upper())
|
|
497
|
+
|
|
498
|
+
if len(digits) >= 3:
|
|
499
|
+
short_digits = digits[:2]
|
|
500
|
+
add_candidate(f"{prefix}{short_digits}")
|
|
501
|
+
add_candidate(f"{prefix}{short_digits}xx")
|
|
502
|
+
add_candidate(f"{prefix}{short_digits}x")
|
|
503
|
+
add_candidate(f"{prefix}{short_digits}".lower())
|
|
504
|
+
|
|
505
|
+
return candidates
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def resolve_svd_candidates(report: DeviceReport) -> tuple[str, list[str]]:
|
|
509
|
+
"""Determine the vendor folder and candidate SVD filenames for a device report.
|
|
510
|
+
|
|
511
|
+
Parameters
|
|
512
|
+
----------
|
|
513
|
+
report : DeviceReport
|
|
514
|
+
The device report obtained from cmd_lscpu target inspection.
|
|
515
|
+
|
|
516
|
+
Returns
|
|
517
|
+
-------
|
|
518
|
+
tuple[str, list[str]]
|
|
519
|
+
Tuple containing (vendor_dir, list_of_candidates).
|
|
520
|
+
|
|
521
|
+
Raises
|
|
522
|
+
------
|
|
523
|
+
SvdNotFoundError
|
|
524
|
+
If the vendor is unknown or no product line is available.
|
|
525
|
+
"""
|
|
526
|
+
if not report.vendor or report.vendor == "Unknown":
|
|
527
|
+
raise SvdNotFoundError("Cannot determine SVD: vendor is unknown or unsupported.")
|
|
528
|
+
|
|
529
|
+
vendor_dir = resolve_svd_vendor(report.vendor)
|
|
530
|
+
product_line = report.product_line.value if report.product_line.is_available else ""
|
|
531
|
+
part_number = report.part_number.value if report.part_number.is_available else None
|
|
532
|
+
|
|
533
|
+
if not product_line and not part_number:
|
|
534
|
+
raise SvdNotFoundError(
|
|
535
|
+
f"Cannot determine SVD: no product line or part number identified for {report.vendor}."
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
candidates = get_candidate_svd_filenames(product_line, part_number)
|
|
539
|
+
if not candidates:
|
|
540
|
+
raise SvdNotFoundError(
|
|
541
|
+
f"Cannot find candidate SVD filenames for {report.vendor} {product_line}."
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
return vendor_dir, candidates
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def get_cache_dir(custom_path: Path | str | None = None) -> Path:
|
|
548
|
+
"""Return the base local directory used to cache downloaded SVD files.
|
|
549
|
+
|
|
550
|
+
Parameters
|
|
551
|
+
----------
|
|
552
|
+
custom_path : Path | str | None
|
|
553
|
+
Optional explicit cache path override.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
Path
|
|
558
|
+
Path to the local SVD cache directory.
|
|
559
|
+
"""
|
|
560
|
+
if custom_path is not None:
|
|
561
|
+
cache_path = Path(custom_path)
|
|
562
|
+
else:
|
|
563
|
+
xdg_cache = os.environ.get("XDG_CACHE_HOME")
|
|
564
|
+
base = Path(xdg_cache) if xdg_cache else Path.home() / ".cache"
|
|
565
|
+
cache_path = base / "pyGdbToolkit" / "svd"
|
|
566
|
+
|
|
567
|
+
cache_path.mkdir(parents=True, exist_ok=True)
|
|
568
|
+
return cache_path
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def download_file(url: str, destination: Path, timeout: float = 15.0) -> Path:
|
|
572
|
+
"""Download a remote file over HTTP/HTTPS to a destination path.
|
|
573
|
+
|
|
574
|
+
Parameters
|
|
575
|
+
----------
|
|
576
|
+
url : str
|
|
577
|
+
Remote URL of the file to fetch.
|
|
578
|
+
destination : Path
|
|
579
|
+
Local destination path.
|
|
580
|
+
timeout : float
|
|
581
|
+
Network timeout in seconds.
|
|
582
|
+
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
Path
|
|
586
|
+
The destination file path.
|
|
587
|
+
|
|
588
|
+
Raises
|
|
589
|
+
------
|
|
590
|
+
SvdDownloadError
|
|
591
|
+
If the download fails due to network error, HTTP error, or timeout.
|
|
592
|
+
"""
|
|
593
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
594
|
+
try:
|
|
595
|
+
response = requests.get(
|
|
596
|
+
url,
|
|
597
|
+
headers={
|
|
598
|
+
"User-Agent": "pyGdbToolkit-SvdFetcher/1.0",
|
|
599
|
+
"Accept": "application/xml,text/xml,*/*",
|
|
600
|
+
},
|
|
601
|
+
timeout=timeout,
|
|
602
|
+
)
|
|
603
|
+
response.raise_for_status()
|
|
604
|
+
destination.write_bytes(response.content)
|
|
605
|
+
return destination
|
|
606
|
+
except (requests.RequestException, OSError) as error:
|
|
607
|
+
raise SvdDownloadError(f"Failed to download {url}: {error}") from error
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def fetch_svd_file(
|
|
611
|
+
vendor_dir: str,
|
|
612
|
+
filename: str,
|
|
613
|
+
cache_dir: Path | None = None,
|
|
614
|
+
force_download: bool = False,
|
|
615
|
+
timeout: float = 15.0,
|
|
616
|
+
) -> Path:
|
|
617
|
+
"""Fetch an SVD file from local cache or download it from cmsis-svd-data.
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
vendor_dir : str
|
|
622
|
+
Vendor directory name in cmsis-svd-data (e.g. STMicro, Nordic).
|
|
623
|
+
filename : str
|
|
624
|
+
SVD filename (e.g. STM32F401.svd, nrf52840.svd).
|
|
625
|
+
cache_dir : Path | None
|
|
626
|
+
Local cache directory. Defaults to standard user cache.
|
|
627
|
+
force_download : bool
|
|
628
|
+
Whether to bypass existing cache and re-download.
|
|
629
|
+
timeout : float
|
|
630
|
+
HTTP request timeout in seconds.
|
|
631
|
+
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
Path
|
|
635
|
+
Path to the locally cached SVD file.
|
|
636
|
+
|
|
637
|
+
Raises
|
|
638
|
+
------
|
|
639
|
+
SvdDownloadError
|
|
640
|
+
If download fails.
|
|
641
|
+
"""
|
|
642
|
+
if not filename.lower().endswith(".svd"):
|
|
643
|
+
filename = f"{filename}.svd"
|
|
644
|
+
|
|
645
|
+
cache = get_cache_dir(cache_dir)
|
|
646
|
+
local_path = cache / vendor_dir / filename
|
|
647
|
+
|
|
648
|
+
if local_path.is_file() and not force_download:
|
|
649
|
+
return local_path
|
|
650
|
+
|
|
651
|
+
url = f"{CMSIS_SVD_DATA_RAW_URL}/{vendor_dir}/{filename}"
|
|
652
|
+
return download_file(url, local_path, timeout=timeout)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def resolve_and_fetch_svd(
|
|
656
|
+
report: DeviceReport,
|
|
657
|
+
cache_dir: Path | None = None,
|
|
658
|
+
force_download: bool = False,
|
|
659
|
+
timeout: float = 15.0,
|
|
660
|
+
) -> tuple[Path, str]:
|
|
661
|
+
"""Resolve and fetch the best matching SVD file for a device report.
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
report : DeviceReport
|
|
666
|
+
Device report from target inspection.
|
|
667
|
+
cache_dir : Path | None
|
|
668
|
+
Local cache directory.
|
|
669
|
+
force_download : bool
|
|
670
|
+
Whether to force re-downloading even if cached.
|
|
671
|
+
timeout : float
|
|
672
|
+
Network timeout in seconds.
|
|
673
|
+
|
|
674
|
+
Returns
|
|
675
|
+
-------
|
|
676
|
+
tuple[Path, str]
|
|
677
|
+
Tuple of (local_file_path, matched_filename).
|
|
678
|
+
|
|
679
|
+
Raises
|
|
680
|
+
------
|
|
681
|
+
SvdNotFoundError
|
|
682
|
+
If no candidate SVD file could be located or downloaded.
|
|
683
|
+
"""
|
|
684
|
+
vendor_dir, candidates = resolve_svd_candidates(report)
|
|
685
|
+
cache = get_cache_dir(cache_dir)
|
|
686
|
+
|
|
687
|
+
if not force_download:
|
|
688
|
+
for candidate in candidates:
|
|
689
|
+
cached_file = cache / vendor_dir / candidate
|
|
690
|
+
if cached_file.is_file():
|
|
691
|
+
return cached_file, candidate
|
|
692
|
+
|
|
693
|
+
last_error: Exception | None = None
|
|
694
|
+
for candidate in candidates:
|
|
695
|
+
try:
|
|
696
|
+
downloaded = fetch_svd_file(
|
|
697
|
+
vendor_dir,
|
|
698
|
+
candidate,
|
|
699
|
+
cache_dir=cache,
|
|
700
|
+
force_download=force_download,
|
|
701
|
+
timeout=timeout,
|
|
702
|
+
)
|
|
703
|
+
return downloaded, candidate
|
|
704
|
+
except SvdDownloadError as err:
|
|
705
|
+
last_error = err
|
|
706
|
+
continue
|
|
707
|
+
|
|
708
|
+
raise SvdNotFoundError(
|
|
709
|
+
f"Could not locate or download SVD for {report.vendor} "
|
|
710
|
+
f"candidates {candidates}. Last error: {last_error}"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _parse_int(text: str | None, default: int = 0) -> int:
|
|
715
|
+
"""Parse an integer from an SVD XML string (hex, decimal, or binary).
|
|
716
|
+
|
|
717
|
+
Parameters
|
|
718
|
+
----------
|
|
719
|
+
text : str | None
|
|
720
|
+
String text from XML element.
|
|
721
|
+
default : int
|
|
722
|
+
Default value if text is empty or None.
|
|
723
|
+
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
int
|
|
727
|
+
Parsed integer value.
|
|
728
|
+
"""
|
|
729
|
+
if text is None:
|
|
730
|
+
return default
|
|
731
|
+
text = text.strip()
|
|
732
|
+
if not text:
|
|
733
|
+
return default
|
|
734
|
+
if text.startswith("#"):
|
|
735
|
+
return int(text[1:], 16)
|
|
736
|
+
if text.startswith(("0x", "0X")):
|
|
737
|
+
return int(text, 16)
|
|
738
|
+
if text.startswith(("0b", "0B")):
|
|
739
|
+
return int(text, 2)
|
|
740
|
+
return int(text, 10)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _parse_bit_range(field_elem: ET.Element) -> tuple[int, int]:
|
|
744
|
+
"""Parse bit offset and width from an SVD field element.
|
|
745
|
+
|
|
746
|
+
Parameters
|
|
747
|
+
----------
|
|
748
|
+
field_elem : ET.Element
|
|
749
|
+
XML Element representing an SVD <field>.
|
|
750
|
+
|
|
751
|
+
Returns
|
|
752
|
+
-------
|
|
753
|
+
tuple[int, int]
|
|
754
|
+
Tuple of (bit_offset, bit_width).
|
|
755
|
+
"""
|
|
756
|
+
offset_elem = field_elem.find("bitOffset")
|
|
757
|
+
width_elem = field_elem.find("bitWidth")
|
|
758
|
+
if offset_elem is not None and offset_elem.text:
|
|
759
|
+
bit_offset = _parse_int(offset_elem.text)
|
|
760
|
+
bit_width = (
|
|
761
|
+
_parse_int(width_elem.text, default=1)
|
|
762
|
+
if width_elem is not None and width_elem.text
|
|
763
|
+
else 1
|
|
764
|
+
)
|
|
765
|
+
return bit_offset, bit_width
|
|
766
|
+
|
|
767
|
+
range_elem = field_elem.find("bitRange")
|
|
768
|
+
if range_elem is not None and range_elem.text:
|
|
769
|
+
match = re.search(r"\[\s*(\d+)\s*:\s*(\d+)\s*\]", range_elem.text)
|
|
770
|
+
if match:
|
|
771
|
+
msb = int(match.group(1))
|
|
772
|
+
lsb = int(match.group(2))
|
|
773
|
+
return lsb, (msb - lsb + 1)
|
|
774
|
+
|
|
775
|
+
lsb_elem = field_elem.find("lsb")
|
|
776
|
+
msb_elem = field_elem.find("msb")
|
|
777
|
+
if lsb_elem is not None and lsb_elem.text and msb_elem is not None and msb_elem.text:
|
|
778
|
+
lsb = _parse_int(lsb_elem.text)
|
|
779
|
+
msb = _parse_int(msb_elem.text)
|
|
780
|
+
return lsb, (msb - lsb + 1)
|
|
781
|
+
|
|
782
|
+
return 0, 1
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _parse_field(field_elem: ET.Element) -> SvdField:
|
|
786
|
+
"""Parse a single <field> XML element.
|
|
787
|
+
|
|
788
|
+
Parameters
|
|
789
|
+
----------
|
|
790
|
+
field_elem : ET.Element
|
|
791
|
+
The XML element representing a register field.
|
|
792
|
+
|
|
793
|
+
Returns
|
|
794
|
+
-------
|
|
795
|
+
SvdField
|
|
796
|
+
Parsed immutable field model.
|
|
797
|
+
"""
|
|
798
|
+
name = (field_elem.findtext("name") or "").strip()
|
|
799
|
+
description = (field_elem.findtext("description") or "").strip()
|
|
800
|
+
access = field_elem.findtext("access")
|
|
801
|
+
reset_text = field_elem.findtext("resetValue")
|
|
802
|
+
reset_val = _parse_int(reset_text) if reset_text is not None else None
|
|
803
|
+
bit_offset, bit_width = _parse_bit_range(field_elem)
|
|
804
|
+
|
|
805
|
+
return SvdField(
|
|
806
|
+
name=name,
|
|
807
|
+
description=description,
|
|
808
|
+
bit_offset=bit_offset,
|
|
809
|
+
bit_width=bit_width,
|
|
810
|
+
access=access.strip() if access else None,
|
|
811
|
+
reset_value=reset_val,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _parse_register(
|
|
816
|
+
reg_elem: ET.Element, default_size: int, default_reset: int, default_mask: int
|
|
817
|
+
) -> SvdRegister:
|
|
818
|
+
"""Parse a single <register> XML element.
|
|
819
|
+
|
|
820
|
+
Parameters
|
|
821
|
+
----------
|
|
822
|
+
reg_elem : ET.Element
|
|
823
|
+
The XML element representing a register.
|
|
824
|
+
default_size : int
|
|
825
|
+
Default register size in bits.
|
|
826
|
+
default_reset : int
|
|
827
|
+
Default reset value.
|
|
828
|
+
default_mask : int
|
|
829
|
+
Default reset mask.
|
|
830
|
+
|
|
831
|
+
Returns
|
|
832
|
+
-------
|
|
833
|
+
SvdRegister
|
|
834
|
+
Parsed immutable register model.
|
|
835
|
+
"""
|
|
836
|
+
name = (reg_elem.findtext("name") or "").strip()
|
|
837
|
+
display_name = reg_elem.findtext("displayName")
|
|
838
|
+
description = (reg_elem.findtext("description") or "").strip()
|
|
839
|
+
offset = _parse_int(reg_elem.findtext("addressOffset"))
|
|
840
|
+
|
|
841
|
+
size_text = reg_elem.findtext("size")
|
|
842
|
+
size = _parse_int(size_text, default=default_size) if size_text else default_size
|
|
843
|
+
|
|
844
|
+
access = reg_elem.findtext("access")
|
|
845
|
+
|
|
846
|
+
reset_val_text = reg_elem.findtext("resetValue")
|
|
847
|
+
reset_value = (
|
|
848
|
+
_parse_int(reset_val_text, default=default_reset) if reset_val_text else default_reset
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
reset_mask_text = reg_elem.findtext("resetMask")
|
|
852
|
+
reset_mask = (
|
|
853
|
+
_parse_int(reset_mask_text, default=default_mask) if reset_mask_text else default_mask
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
fields_elem = reg_elem.find("fields")
|
|
857
|
+
fields: list[SvdField] = []
|
|
858
|
+
if fields_elem is not None:
|
|
859
|
+
for f_elem in fields_elem.findall("field"):
|
|
860
|
+
fields.append(_parse_field(f_elem))
|
|
861
|
+
|
|
862
|
+
return SvdRegister(
|
|
863
|
+
name=name,
|
|
864
|
+
display_name=display_name.strip() if display_name else None,
|
|
865
|
+
description=description,
|
|
866
|
+
address_offset=offset,
|
|
867
|
+
size=size,
|
|
868
|
+
access=access.strip() if access else None,
|
|
869
|
+
reset_value=reset_value,
|
|
870
|
+
reset_mask=reset_mask,
|
|
871
|
+
fields=tuple(fields),
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def parse_svd_xml(xml_content: str | bytes) -> SvdDevice:
|
|
876
|
+
"""Parse SVD XML content into a structured SvdDevice object hierarchy.
|
|
877
|
+
|
|
878
|
+
Parameters
|
|
879
|
+
----------
|
|
880
|
+
xml_content : str | bytes
|
|
881
|
+
Raw SVD XML string or bytes.
|
|
882
|
+
|
|
883
|
+
Returns
|
|
884
|
+
-------
|
|
885
|
+
SvdDevice
|
|
886
|
+
Decoded device and peripheral mapping.
|
|
887
|
+
|
|
888
|
+
Raises
|
|
889
|
+
------
|
|
890
|
+
SvdParseError
|
|
891
|
+
If XML syntax is invalid or required fields are missing.
|
|
892
|
+
"""
|
|
893
|
+
try:
|
|
894
|
+
if isinstance(xml_content, str):
|
|
895
|
+
root = ET.fromstring(xml_content)
|
|
896
|
+
else:
|
|
897
|
+
root = ET.fromstring(xml_content.decode("utf-8", errors="replace"))
|
|
898
|
+
except ET.ParseError as error:
|
|
899
|
+
raise SvdParseError(f"XML parse error: {error}") from error
|
|
900
|
+
|
|
901
|
+
dev_name = (root.findtext("name") or "").strip()
|
|
902
|
+
version = root.findtext("version")
|
|
903
|
+
description = (root.findtext("description") or "").strip()
|
|
904
|
+
vendor = root.findtext("vendor")
|
|
905
|
+
address_unit_bits = _parse_int(root.findtext("addressUnitBits"), default=8)
|
|
906
|
+
width = _parse_int(root.findtext("width"), default=32)
|
|
907
|
+
dev_size = _parse_int(root.findtext("size"), default=32)
|
|
908
|
+
dev_reset_val = _parse_int(root.findtext("resetValue"), default=0)
|
|
909
|
+
dev_reset_mask = _parse_int(root.findtext("resetMask"), default=0xFFFFFFFF)
|
|
910
|
+
|
|
911
|
+
periphs_elem = root.find("peripherals")
|
|
912
|
+
if periphs_elem is None:
|
|
913
|
+
raise SvdParseError("SVD XML document is missing <peripherals> root element.")
|
|
914
|
+
|
|
915
|
+
raw_peripherals: dict[str, tuple[ET.Element, int, str, str | None, str | None]] = {}
|
|
916
|
+
for p_elem in periphs_elem.findall("peripheral"):
|
|
917
|
+
p_name = (p_elem.findtext("name") or "").strip()
|
|
918
|
+
p_desc = (p_elem.findtext("description") or "").strip()
|
|
919
|
+
p_group = p_elem.findtext("groupName")
|
|
920
|
+
p_base = _parse_int(p_elem.findtext("baseAddress"))
|
|
921
|
+
p_derived = p_elem.attrib.get("derivedFrom") or p_elem.findtext("derivedFrom")
|
|
922
|
+
raw_peripherals[p_name] = (
|
|
923
|
+
p_elem,
|
|
924
|
+
p_base,
|
|
925
|
+
p_desc,
|
|
926
|
+
p_group.strip() if p_group else None,
|
|
927
|
+
p_derived.strip() if p_derived else None,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
peripherals_list: list[SvdPeripheral] = []
|
|
931
|
+
registers_cache: dict[str, tuple[SvdRegister, ...]] = {}
|
|
932
|
+
|
|
933
|
+
def get_peripheral_registers(name: str) -> tuple[SvdRegister, ...]:
|
|
934
|
+
if name in registers_cache:
|
|
935
|
+
return registers_cache[name]
|
|
936
|
+
|
|
937
|
+
if name not in raw_peripherals:
|
|
938
|
+
return ()
|
|
939
|
+
|
|
940
|
+
p_elem, _, _, _, p_derived = raw_peripherals[name]
|
|
941
|
+
regs_elem = p_elem.find("registers")
|
|
942
|
+
|
|
943
|
+
regs: list[SvdRegister] = []
|
|
944
|
+
if regs_elem is not None:
|
|
945
|
+
for r_elem in regs_elem.findall("register"):
|
|
946
|
+
regs.append(_parse_register(r_elem, dev_size, dev_reset_val, dev_reset_mask))
|
|
947
|
+
|
|
948
|
+
if p_derived and p_derived in raw_peripherals:
|
|
949
|
+
parent_regs = get_peripheral_registers(p_derived)
|
|
950
|
+
if not regs:
|
|
951
|
+
regs = list(parent_regs)
|
|
952
|
+
else:
|
|
953
|
+
existing_names = {r.name.upper() for r in regs}
|
|
954
|
+
for parent_reg in parent_regs:
|
|
955
|
+
if parent_reg.name.upper() not in existing_names:
|
|
956
|
+
regs.append(parent_reg)
|
|
957
|
+
|
|
958
|
+
result = tuple(regs)
|
|
959
|
+
registers_cache[name] = result
|
|
960
|
+
return result
|
|
961
|
+
|
|
962
|
+
for p_name, (_, p_base, p_desc, p_group, p_derived) in raw_peripherals.items():
|
|
963
|
+
regs = get_peripheral_registers(p_name)
|
|
964
|
+
peripherals_list.append(
|
|
965
|
+
SvdPeripheral(
|
|
966
|
+
name=p_name,
|
|
967
|
+
description=p_desc,
|
|
968
|
+
group_name=p_group,
|
|
969
|
+
base_address=p_base,
|
|
970
|
+
derived_from=p_derived,
|
|
971
|
+
registers=regs,
|
|
972
|
+
)
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
return SvdDevice(
|
|
976
|
+
name=dev_name,
|
|
977
|
+
version=version.strip() if version else None,
|
|
978
|
+
description=description,
|
|
979
|
+
vendor=vendor.strip() if vendor else None,
|
|
980
|
+
address_unit_bits=address_unit_bits,
|
|
981
|
+
width=width,
|
|
982
|
+
size=dev_size,
|
|
983
|
+
reset_value=dev_reset_val,
|
|
984
|
+
reset_mask=dev_reset_mask,
|
|
985
|
+
peripherals=tuple(peripherals_list),
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def parse_svd_file(file_path: Path | str) -> SvdDevice:
|
|
990
|
+
"""Parse an SVD XML file from the local file system.
|
|
991
|
+
|
|
992
|
+
Parameters
|
|
993
|
+
----------
|
|
994
|
+
file_path : Path | str
|
|
995
|
+
Path to the .svd file.
|
|
996
|
+
|
|
997
|
+
Returns
|
|
998
|
+
-------
|
|
999
|
+
SvdDevice
|
|
1000
|
+
Parsed device object.
|
|
1001
|
+
|
|
1002
|
+
Raises
|
|
1003
|
+
------
|
|
1004
|
+
SvdParseError
|
|
1005
|
+
If file cannot be read or XML parsing fails.
|
|
1006
|
+
"""
|
|
1007
|
+
path = Path(file_path)
|
|
1008
|
+
if not path.is_file():
|
|
1009
|
+
raise SvdParseError(f"SVD file not found: {path}")
|
|
1010
|
+
|
|
1011
|
+
try:
|
|
1012
|
+
content = path.read_bytes()
|
|
1013
|
+
except OSError as error:
|
|
1014
|
+
raise SvdParseError(f"Failed to read SVD file {path}: {error}") from error
|
|
1015
|
+
|
|
1016
|
+
return parse_svd_xml(content)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def load_svd_dict(file_path: Path | str) -> dict[str, Any]:
|
|
1020
|
+
"""Parse an SVD file and return it as a complete dictionary hierarchy.
|
|
1021
|
+
|
|
1022
|
+
Parameters
|
|
1023
|
+
----------
|
|
1024
|
+
file_path : Path | str
|
|
1025
|
+
Path to the .svd file.
|
|
1026
|
+
|
|
1027
|
+
Returns
|
|
1028
|
+
-------
|
|
1029
|
+
dict[str, Any]
|
|
1030
|
+
Dictionary representation of all peripherals, registers, and fields.
|
|
1031
|
+
"""
|
|
1032
|
+
device = parse_svd_file(file_path)
|
|
1033
|
+
return device.to_dict()
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def get_svd_by_name(
|
|
1037
|
+
vendor: str,
|
|
1038
|
+
svd_name: str,
|
|
1039
|
+
cache_dir: Path | None = None,
|
|
1040
|
+
force_download: bool = False,
|
|
1041
|
+
timeout: float = 15.0,
|
|
1042
|
+
) -> SvdDevice:
|
|
1043
|
+
"""Fetch an explicit SVD file by vendor and filename, and parse it.
|
|
1044
|
+
|
|
1045
|
+
This primitive allows callers (such as user-facing GDB commands) to explicitly
|
|
1046
|
+
override or specify an SVD when auto-detection is not available or desired.
|
|
1047
|
+
|
|
1048
|
+
Parameters
|
|
1049
|
+
----------
|
|
1050
|
+
vendor : str
|
|
1051
|
+
Vendor name or folder (e.g. STMicro, Nordic, NXP).
|
|
1052
|
+
svd_name : str
|
|
1053
|
+
SVD file name or base name (e.g. STM32F401.svd or nrf52840).
|
|
1054
|
+
cache_dir : Path | None
|
|
1055
|
+
Optional custom cache directory.
|
|
1056
|
+
force_download : bool
|
|
1057
|
+
Whether to bypass cache and re-download.
|
|
1058
|
+
timeout : float
|
|
1059
|
+
HTTP download timeout in seconds.
|
|
1060
|
+
|
|
1061
|
+
Returns
|
|
1062
|
+
-------
|
|
1063
|
+
SvdDevice
|
|
1064
|
+
The parsed SVD device definition.
|
|
1065
|
+
|
|
1066
|
+
Raises
|
|
1067
|
+
------
|
|
1068
|
+
SvdError
|
|
1069
|
+
If fetching or parsing fails.
|
|
1070
|
+
"""
|
|
1071
|
+
vendor_dir = resolve_svd_vendor(vendor)
|
|
1072
|
+
svd_path = fetch_svd_file(
|
|
1073
|
+
vendor_dir=vendor_dir,
|
|
1074
|
+
filename=svd_name,
|
|
1075
|
+
cache_dir=cache_dir,
|
|
1076
|
+
force_download=force_download,
|
|
1077
|
+
timeout=timeout,
|
|
1078
|
+
)
|
|
1079
|
+
return parse_svd_file(svd_path)
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def load_svd_dict_by_name(
|
|
1083
|
+
vendor: str,
|
|
1084
|
+
svd_name: str,
|
|
1085
|
+
cache_dir: Path | None = None,
|
|
1086
|
+
force_download: bool = False,
|
|
1087
|
+
timeout: float = 15.0,
|
|
1088
|
+
) -> dict[str, Any]:
|
|
1089
|
+
"""Fetch an explicit SVD by vendor and name, returning its dictionary mapping.
|
|
1090
|
+
|
|
1091
|
+
Parameters
|
|
1092
|
+
----------
|
|
1093
|
+
vendor : str
|
|
1094
|
+
Vendor name or folder.
|
|
1095
|
+
svd_name : str
|
|
1096
|
+
SVD file name or base name.
|
|
1097
|
+
cache_dir : Path | None
|
|
1098
|
+
Optional custom cache directory.
|
|
1099
|
+
force_download : bool
|
|
1100
|
+
Whether to bypass cache and re-download.
|
|
1101
|
+
timeout : float
|
|
1102
|
+
HTTP download timeout in seconds.
|
|
1103
|
+
|
|
1104
|
+
Returns
|
|
1105
|
+
-------
|
|
1106
|
+
dict[str, Any]
|
|
1107
|
+
Dictionary representation of the SVD device hierarchy.
|
|
1108
|
+
"""
|
|
1109
|
+
device = get_svd_by_name(
|
|
1110
|
+
vendor=vendor,
|
|
1111
|
+
svd_name=svd_name,
|
|
1112
|
+
cache_dir=cache_dir,
|
|
1113
|
+
force_download=force_download,
|
|
1114
|
+
timeout=timeout,
|
|
1115
|
+
)
|
|
1116
|
+
return device.to_dict()
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def get_svd_for_target(
|
|
1120
|
+
reader: TargetMemory | None = None,
|
|
1121
|
+
cpuid: CortexMTargetDescription | None = None,
|
|
1122
|
+
cache_dir: Path | None = None,
|
|
1123
|
+
force_download: bool = False,
|
|
1124
|
+
) -> SvdDevice:
|
|
1125
|
+
"""Detect the current target SoC, fetch its SVD file, and parse it.
|
|
1126
|
+
|
|
1127
|
+
Parameters
|
|
1128
|
+
----------
|
|
1129
|
+
reader : TargetMemory | None
|
|
1130
|
+
Typed reader for target memory. When None, TargetMemoryReader is used.
|
|
1131
|
+
cpuid : CortexMTargetDescription | None
|
|
1132
|
+
Optional pre-decoded CPUID. When None, it is read from target memory.
|
|
1133
|
+
cache_dir : Path | None
|
|
1134
|
+
Optional custom cache directory.
|
|
1135
|
+
force_download : bool
|
|
1136
|
+
Whether to bypass local cache and download fresh SVD.
|
|
1137
|
+
|
|
1138
|
+
Returns
|
|
1139
|
+
-------
|
|
1140
|
+
SvdDevice
|
|
1141
|
+
The parsed SVD device definition for the active target.
|
|
1142
|
+
|
|
1143
|
+
Raises
|
|
1144
|
+
------
|
|
1145
|
+
SvdError
|
|
1146
|
+
If detection, fetching, or parsing fails.
|
|
1147
|
+
"""
|
|
1148
|
+
if reader is None:
|
|
1149
|
+
reader = TargetMemoryReader()
|
|
1150
|
+
|
|
1151
|
+
if cpuid is None:
|
|
1152
|
+
try:
|
|
1153
|
+
raw_cpuid = reader.read_uint32(CPUID_ADDRESS)
|
|
1154
|
+
except TargetReadError as error:
|
|
1155
|
+
raise SvdError(f"Cannot read target CPUID register: {error}") from error
|
|
1156
|
+
cpuid = decode_cpuid(raw_cpuid)
|
|
1157
|
+
|
|
1158
|
+
discovery = discover_rom_tables(reader)
|
|
1159
|
+
report = DEFAULT_PROVIDER_REGISTRY.inspect(reader, cpuid, discovery)
|
|
1160
|
+
svd_path, _ = resolve_and_fetch_svd(report, cache_dir=cache_dir, force_download=force_download)
|
|
1161
|
+
return parse_svd_file(svd_path)
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def load_svd_dict_for_target(
|
|
1165
|
+
reader: TargetMemory | None = None,
|
|
1166
|
+
cpuid: CortexMTargetDescription | None = None,
|
|
1167
|
+
cache_dir: Path | None = None,
|
|
1168
|
+
force_download: bool = False,
|
|
1169
|
+
) -> dict[str, Any]:
|
|
1170
|
+
"""Detect current target SoC, fetch its SVD, and return the complete dictionary.
|
|
1171
|
+
|
|
1172
|
+
Parameters
|
|
1173
|
+
----------
|
|
1174
|
+
reader : TargetMemory | None
|
|
1175
|
+
Typed reader for target memory. When None, TargetMemoryReader is used.
|
|
1176
|
+
cpuid : CortexMTargetDescription | None
|
|
1177
|
+
Optional pre-decoded CPUID.
|
|
1178
|
+
cache_dir : Path | None
|
|
1179
|
+
Optional custom cache directory.
|
|
1180
|
+
force_download : bool
|
|
1181
|
+
Whether to bypass local cache and download fresh SVD.
|
|
1182
|
+
|
|
1183
|
+
Returns
|
|
1184
|
+
-------
|
|
1185
|
+
dict[str, Any]
|
|
1186
|
+
Dictionary representation of the target SVD.
|
|
1187
|
+
"""
|
|
1188
|
+
device = get_svd_for_target(
|
|
1189
|
+
reader=reader,
|
|
1190
|
+
cpuid=cpuid,
|
|
1191
|
+
cache_dir=cache_dir,
|
|
1192
|
+
force_download=force_download,
|
|
1193
|
+
)
|
|
1194
|
+
return device.to_dict()
|