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/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()