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.
@@ -0,0 +1,1208 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The ``svd`` GDB command for CMSIS-SVD inspection, manipulation, and monitoring."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ import glob
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import gdb
16
+ from rich import box
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.table import Table
20
+ from rich.text import Text
21
+
22
+ from .arch.arm.session_state import device_report
23
+ from .session import SESSION as TOOLKIT_SESSION
24
+ from .session import SessionSlice
25
+ from .svd import (
26
+ SvdDevice,
27
+ SvdError,
28
+ SvdNotFoundError,
29
+ SvdPeripheral,
30
+ SvdRegister,
31
+ parse_svd_file,
32
+ resolve_and_fetch_svd,
33
+ )
34
+ from .target_memory import TargetMemory, TargetReadError
35
+
36
+ CONSOLE = Console(force_terminal=True)
37
+
38
+
39
+ @dataclass
40
+ class SvdSessionState(SessionSlice):
41
+ """Encapsulate the active SVD device model and dictionary in the GDB session."""
42
+
43
+ device: SvdDevice | None = None
44
+ svd_dict: dict[str, Any] | None = None
45
+ svd_path: Path | None = None
46
+ watchpoints: list[SvdWatchpoint] = field(default_factory=list)
47
+
48
+ def reset(self) -> None:
49
+ """Drop the loaded SVD model and delete the registered register watchpoints."""
50
+ for watchpoint in self.watchpoints:
51
+ watchpoint.delete()
52
+ self.watchpoints.clear()
53
+ self.device = None
54
+ self.svd_dict = None
55
+ self.svd_path = None
56
+
57
+
58
+ SESSION = TOOLKIT_SESSION.state(SvdSessionState)
59
+
60
+
61
+ def is_svd_loaded() -> bool:
62
+ """Return whether an SVD file is loaded in the current GDB session."""
63
+ return SESSION.device is not None and SESSION.svd_path is not None
64
+
65
+
66
+ def _get_c_type_for_size(size_bits: int) -> str:
67
+ """Return the canonical C integer type name matching a register bit width.
68
+
69
+ Parameters
70
+ ----------
71
+ size_bits : int
72
+ Register size in bits.
73
+
74
+ Returns
75
+ -------
76
+ str
77
+ C type identifier.
78
+ """
79
+ if size_bits <= 8:
80
+ return "unsigned char"
81
+ if size_bits <= 16:
82
+ return "unsigned short"
83
+ if size_bits <= 32:
84
+ return "unsigned int"
85
+ return "unsigned long long"
86
+
87
+
88
+ def _parse_numeric_value(val_str: str) -> int:
89
+ """Parse a numeric string supporting hex, binary, and decimal formats.
90
+
91
+ Parameters
92
+ ----------
93
+ val_str : str
94
+ Numeric representation.
95
+
96
+ Returns
97
+ -------
98
+ int
99
+ Decoded integer value.
100
+ """
101
+ val_str = val_str.strip()
102
+ if val_str.startswith("#"):
103
+ return int(val_str[1:], 16)
104
+ if val_str.startswith(("0x", "0X")):
105
+ return int(val_str, 16)
106
+ if val_str.startswith(("0b", "0B")):
107
+ return int(val_str, 2)
108
+ return int(val_str, 10)
109
+
110
+
111
+ def _read_register_value(reader: TargetMemory, base_address: int, reg: SvdRegister) -> int:
112
+ """Read a register value from target memory.
113
+
114
+ Parameters
115
+ ----------
116
+ reader : TargetMemory
117
+ Target memory reader.
118
+ base_address : int
119
+ Base physical address of the peripheral.
120
+ reg : SvdRegister
121
+ Register definition.
122
+
123
+ Returns
124
+ -------
125
+ int
126
+ The raw integer value read from memory.
127
+ """
128
+ reg_addr = base_address + reg.address_offset
129
+ size_bytes = max(1, (reg.size + 7) // 8)
130
+ raw_bytes = reader.read_bytes(reg_addr, size_bytes)
131
+ return int.from_bytes(raw_bytes, byteorder="little")
132
+
133
+
134
+ def _write_register_value(base_address: int, reg: SvdRegister, value: int) -> None:
135
+ """Write an integer value into a target register.
136
+
137
+ Parameters
138
+ ----------
139
+ base_address : int
140
+ Base physical address of the peripheral.
141
+ reg : SvdRegister
142
+ Register definition.
143
+ value : int
144
+ Value to write.
145
+ """
146
+ reg_addr = base_address + reg.address_offset
147
+ size_bytes = max(1, (reg.size + 7) // 8)
148
+ val_bytes = value.to_bytes(size_bytes, byteorder="little")
149
+ inferior = gdb.selected_inferior()
150
+ inferior.write_memory(reg_addr, val_bytes)
151
+
152
+
153
+ class SvdWatchpoint(gdb.Breakpoint):
154
+ """A hardware or software watchpoint tracking modifications to an SVD register."""
155
+
156
+ def __init__(
157
+ self,
158
+ spec: str,
159
+ peripheral_name: str,
160
+ register_name: str,
161
+ register: SvdRegister,
162
+ address: int,
163
+ initial_value: int,
164
+ ) -> None:
165
+ super().__init__(spec, type=gdb.BP_WATCHPOINT, wp_class=gdb.WP_WRITE)
166
+ self.peripheral_name = peripheral_name
167
+ self.register_name = register_name
168
+ self.register = register
169
+ self.address = address
170
+ self.last_value = initial_value
171
+
172
+ def stop(self) -> bool:
173
+ """Handle watchpoint triggers and notify the user with decoded bitfield changes.
174
+
175
+ Returns
176
+ -------
177
+ bool
178
+ Always True to halt execution and return control to the GDB prompt.
179
+ """
180
+ new_val = self.last_value
181
+ try:
182
+ reader = TOOLKIT_SESSION.memory
183
+ size_bytes = max(1, (self.register.size + 7) // 8)
184
+ raw_bytes = reader.read_bytes(self.address, size_bytes)
185
+ new_val = int.from_bytes(raw_bytes, byteorder="little")
186
+ except TargetReadError:
187
+ pass
188
+
189
+ render_watchpoint_hit(
190
+ self.peripheral_name,
191
+ self.register_name,
192
+ self.register,
193
+ self.address,
194
+ self.last_value,
195
+ new_val,
196
+ )
197
+ self.last_value = new_val
198
+ return True
199
+
200
+
201
+ def render_watchpoint_hit(
202
+ peripheral_name: str,
203
+ register_name: str,
204
+ register: SvdRegister,
205
+ address: int,
206
+ old_value: int,
207
+ new_value: int,
208
+ ) -> None:
209
+ """Render a Rich notification when an SVD register watchpoint triggers.
210
+
211
+ Parameters
212
+ ----------
213
+ peripheral_name : str
214
+ Name of the parent peripheral.
215
+ register_name : str
216
+ Name of the register.
217
+ register : SvdRegister
218
+ Register model definition.
219
+ address : int
220
+ Physical address of the register.
221
+ old_value : int
222
+ Value before write.
223
+ new_value : int
224
+ Value after write.
225
+ """
226
+ hex_len = max(2, (register.size + 3) // 4)
227
+ old_hex = f"0x{old_value:0{hex_len}X}"
228
+ new_hex = f"0x{new_value:0{hex_len}X}"
229
+
230
+ title_text = Text.assemble(
231
+ ("SVD Watchpoint Hit: ", "bold yellow"),
232
+ (f"{peripheral_name}->{register_name}", "bold cyan"),
233
+ (f" @ 0x{address:08X}", "bold magenta"),
234
+ )
235
+
236
+ table = Table(
237
+ title=title_text,
238
+ box=box.SIMPLE_HEAVY,
239
+ header_style="bold cyan",
240
+ show_header=True,
241
+ )
242
+ table.add_column("Property", style="bold", no_wrap=True)
243
+ table.add_column("Value")
244
+ table.add_row("Previous Value", old_hex)
245
+ table.add_row("New Value", Text(new_hex, style="bold green"))
246
+ table.add_row("Difference", f"0x{(old_value ^ new_value):0{hex_len}X}")
247
+
248
+ CONSOLE.print(table)
249
+
250
+ if register.fields:
251
+ field_table = Table(
252
+ title="Modified Bitfields",
253
+ box=box.SIMPLE_HEAVY,
254
+ header_style="bold cyan",
255
+ show_header=True,
256
+ )
257
+ field_table.add_column("Bits", style="cyan", no_wrap=True)
258
+ field_table.add_column("Field", style="bold", no_wrap=True)
259
+ field_table.add_column("Old Value", style="yellow")
260
+ field_table.add_column("New Value", style="bold green")
261
+ field_table.add_column("Description")
262
+
263
+ diff_count = 0
264
+ for field_obj in register.fields:
265
+ old_field_val = field_obj.extract_value(old_value)
266
+ new_field_val = field_obj.extract_value(new_value)
267
+ if old_field_val != new_field_val:
268
+ diff_count += 1
269
+ if field_obj.bit_width == 1:
270
+ bits_str = f"[{field_obj.bit_offset}]"
271
+ else:
272
+ msb = field_obj.bit_offset + field_obj.bit_width - 1
273
+ bits_str = f"[{msb}:{field_obj.bit_offset}]"
274
+
275
+ f_hex_len = max(1, (field_obj.bit_width + 3) // 4)
276
+ field_table.add_row(
277
+ bits_str,
278
+ field_obj.name,
279
+ f"0x{old_field_val:0{f_hex_len}X}",
280
+ f"0x{new_field_val:0{f_hex_len}X}",
281
+ field_obj.description or "--",
282
+ )
283
+
284
+ if diff_count > 0:
285
+ CONSOLE.print(field_table)
286
+
287
+
288
+ def render_load_success(
289
+ device: SvdDevice,
290
+ svd_filename: str,
291
+ cache_path: Path,
292
+ vendor: str | None,
293
+ product_line: str | None,
294
+ ) -> None:
295
+ """Render a summary table for a successfully detected and loaded SVD file.
296
+
297
+ Parameters
298
+ ----------
299
+ device : SvdDevice
300
+ Parsed device object.
301
+ svd_filename : str
302
+ Matched SVD filename.
303
+ cache_path : Path
304
+ Local file path to the SVD XML.
305
+ vendor : str | None
306
+ Target vendor identifier.
307
+ product_line : str | None
308
+ Target product line name.
309
+ """
310
+ table = Table(
311
+ title="SVD Target Auto-Detection & Load",
312
+ box=box.SIMPLE_HEAVY,
313
+ header_style="bold cyan",
314
+ show_header=True,
315
+ )
316
+ table.add_column("Property", style="bold", no_wrap=True)
317
+ table.add_column("Value")
318
+ if vendor:
319
+ table.add_row("Detected Vendor", vendor)
320
+ if product_line:
321
+ table.add_row("Product Line", product_line)
322
+ table.add_row("Matched SVD", Text(svd_filename, style="bold green"))
323
+ table.add_row("Device Name", device.name)
324
+ if device.version:
325
+ table.add_row("SVD Schema Version", device.version)
326
+ table.add_row("Peripherals Loaded", str(len(device.peripherals)))
327
+ table.add_row("Local Cache Path", str(cache_path))
328
+ CONSOLE.print(table)
329
+
330
+
331
+ def render_file_load_success(device: SvdDevice, file_path: Path) -> None:
332
+ """Render a summary table for an explicitly loaded SVD file.
333
+
334
+ Parameters
335
+ ----------
336
+ device : SvdDevice
337
+ Parsed SVD device.
338
+ file_path : Path
339
+ Path to the loaded SVD file.
340
+ """
341
+ table = Table(
342
+ title="SVD File Loaded Successfully",
343
+ box=box.SIMPLE_HEAVY,
344
+ header_style="bold cyan",
345
+ show_header=True,
346
+ )
347
+ table.add_column("Property", style="bold", no_wrap=True)
348
+ table.add_column("Value")
349
+ table.add_row("File Path", str(file_path))
350
+ table.add_row("Device Name", Text(device.name, style="bold green"))
351
+ if device.vendor:
352
+ table.add_row("Vendor", device.vendor)
353
+ if device.version:
354
+ table.add_row("Version", device.version)
355
+ table.add_row("Peripherals Loaded", str(len(device.peripherals)))
356
+ CONSOLE.print(table)
357
+
358
+
359
+ def render_peripheral_state(peripheral: SvdPeripheral, reader: TargetMemory) -> None:
360
+ """Render the canonical status of all registers in a peripheral.
361
+
362
+ Parameters
363
+ ----------
364
+ peripheral : SvdPeripheral
365
+ Target peripheral model.
366
+ reader : TargetMemory
367
+ Memory reader instance.
368
+ """
369
+ title_text = f"Peripheral: {peripheral.name} @ 0x{peripheral.base_address:08X}" + (
370
+ f" ({peripheral.description})" if peripheral.description else ""
371
+ )
372
+ table = Table(
373
+ title=title_text,
374
+ box=box.SIMPLE_HEAVY,
375
+ header_style="bold cyan",
376
+ show_header=True,
377
+ )
378
+ table.add_column("Offset", style="cyan", no_wrap=True)
379
+ table.add_column("Register", style="bold", no_wrap=True)
380
+ table.add_column("Address", style="magenta", no_wrap=True)
381
+ table.add_column("Size", no_wrap=True)
382
+ table.add_column("Access", no_wrap=True)
383
+ table.add_column("Reset Value", no_wrap=True)
384
+ table.add_column("Current Value", style="bold")
385
+ table.add_column("Description")
386
+
387
+ for reg in peripheral.registers:
388
+ reg_addr = peripheral.base_address + reg.address_offset
389
+ hex_len = max(2, (reg.size + 3) // 4)
390
+ reset_str = f"0x{reg.reset_value:0{hex_len}X}" if reg.reset_value is not None else "--"
391
+ access_str = reg.access or "--"
392
+ size_str = f"{reg.size}-bit"
393
+
394
+ try:
395
+ val = _read_register_value(reader, peripheral.base_address, reg)
396
+ val_str = Text(f"0x{val:0{hex_len}X}", style="bold green")
397
+ except TargetReadError:
398
+ val_str = Text("[Read Error]", style="bold yellow")
399
+
400
+ table.add_row(
401
+ f"+0x{reg.address_offset:04X}",
402
+ reg.name,
403
+ f"0x{reg_addr:08X}",
404
+ size_str,
405
+ access_str,
406
+ reset_str,
407
+ val_str,
408
+ reg.description or "--",
409
+ )
410
+
411
+ CONSOLE.print(table)
412
+
413
+
414
+ def render_register_detail(peripheral: SvdPeripheral, reg: SvdRegister, value: int) -> None:
415
+ """Render the detailed bitfield view for a specific register.
416
+
417
+ Parameters
418
+ ----------
419
+ peripheral : SvdPeripheral
420
+ Parent peripheral model.
421
+ reg : SvdRegister
422
+ Register model.
423
+ value : int
424
+ Current register value read from target memory.
425
+ """
426
+ reg_addr = peripheral.base_address + reg.address_offset
427
+ hex_len = max(2, (reg.size + 3) // 4)
428
+ raw_hex = f"0x{value:0{hex_len}X}"
429
+ bin_str = f"0b{value:0{reg.size}b}"
430
+
431
+ summary_table = Table(
432
+ title=f"Register: {peripheral.name}->{reg.name}",
433
+ box=box.SIMPLE_HEAVY,
434
+ header_style="bold cyan",
435
+ show_header=True,
436
+ )
437
+ summary_table.add_column("Property", style="bold", no_wrap=True)
438
+ summary_table.add_column("Value")
439
+ summary_table.add_row("Peripheral", f"{peripheral.name} (Base 0x{peripheral.base_address:08X})")
440
+ summary_table.add_row("Register Address", f"0x{reg_addr:08X} (+0x{reg.address_offset:04X})")
441
+ summary_table.add_row(
442
+ "Current Value", Text(f"{raw_hex} ({value}, {bin_str})", style="bold green")
443
+ )
444
+ if reg.reset_value is not None:
445
+ summary_table.add_row("Reset Value", f"0x{reg.reset_value:0{hex_len}X}")
446
+ summary_table.add_row("Access Rights", reg.access or "unspecified")
447
+ if reg.description:
448
+ summary_table.add_row("Description", reg.description)
449
+ CONSOLE.print(summary_table)
450
+
451
+ if not reg.fields:
452
+ CONSOLE.print(
453
+ Panel(
454
+ Text(
455
+ "No bitfield definitions available in SVD for this register.",
456
+ style="dim italic",
457
+ ),
458
+ box=box.ROUNDED,
459
+ )
460
+ )
461
+ return
462
+
463
+ fields_table = Table(
464
+ title=f"Bitfields for {peripheral.name}->{reg.name}",
465
+ box=box.SIMPLE_HEAVY,
466
+ header_style="bold cyan",
467
+ show_header=True,
468
+ )
469
+ fields_table.add_column("Bits", style="cyan", no_wrap=True)
470
+ fields_table.add_column("Field", style="bold", no_wrap=True)
471
+ fields_table.add_column("Access", no_wrap=True)
472
+ fields_table.add_column("Value (Hex)", style="bold green", no_wrap=True)
473
+ fields_table.add_column("Value (Dec/Bin)", no_wrap=True)
474
+ fields_table.add_column("Reset", no_wrap=True)
475
+ fields_table.add_column("Description")
476
+
477
+ for field_obj in sorted(reg.fields, key=lambda f: f.bit_offset, reverse=True):
478
+ extracted = field_obj.extract_value(value)
479
+ f_hex_len = max(1, (field_obj.bit_width + 3) // 4)
480
+ f_hex = f"0x{extracted:0{f_hex_len}X}"
481
+ f_bin = f"0b{extracted:0{field_obj.bit_width}b}"
482
+ f_dec_bin = f"{extracted} ({f_bin})" if field_obj.bit_width > 1 else str(extracted)
483
+
484
+ if field_obj.bit_width == 1:
485
+ bits_str = f"[{field_obj.bit_offset}]"
486
+ else:
487
+ msb = field_obj.bit_offset + field_obj.bit_width - 1
488
+ bits_str = f"[{msb}:{field_obj.bit_offset}]"
489
+
490
+ reset_str = (
491
+ f"0x{field_obj.reset_value:0{f_hex_len}X}"
492
+ if field_obj.reset_value is not None
493
+ else "--"
494
+ )
495
+
496
+ fields_table.add_row(
497
+ bits_str,
498
+ field_obj.name,
499
+ field_obj.access or "--",
500
+ f_hex,
501
+ f_dec_bin,
502
+ reset_str,
503
+ field_obj.description or "--",
504
+ )
505
+
506
+ CONSOLE.print(fields_table)
507
+
508
+
509
+ def render_help() -> None:
510
+ """Render the command overview and help table."""
511
+ table = Table(
512
+ title="CMSIS-SVD Target Inspection & Control Commands",
513
+ box=box.SIMPLE_HEAVY,
514
+ header_style="bold cyan",
515
+ show_header=True,
516
+ )
517
+ table.add_column("Command Syntax", style="bold yellow", no_wrap=True)
518
+ table.add_column("Description")
519
+ table.add_row(
520
+ "svd load", "Auto-detect target CPUID/SoC, fetch and load matching SVD definition"
521
+ )
522
+ table.add_row("svd read <file.svd>", "Load an explicit SVD XML file from local filesystem")
523
+ table.add_row(
524
+ "svd show <device>", "Display canonical memory state and registers of a peripheral"
525
+ )
526
+ table.add_row(
527
+ "svd show <device> <register>", "Display detailed register state and decoded bitfields"
528
+ )
529
+ table.add_row(
530
+ "svd write <device> <reg> <hex_val>", "Write numeric/hex value into peripheral register"
531
+ )
532
+ table.add_row(
533
+ "svd monitor <device> <reg>", "Set a watchpoint on a register and report bitfield diffs"
534
+ )
535
+ table.add_row(
536
+ "svd dump <device> <file.json>", "Dump snapshot of device/peripheral state into a JSON file"
537
+ )
538
+ table.add_row("svd list", "List the names of all peripherals in the loaded SVD device")
539
+ table.add_row("svd help", "Show this command reference")
540
+ CONSOLE.print(table)
541
+
542
+
543
+ def render_peripheral_list(device: SvdDevice) -> None:
544
+ """Render a table listing the names of all peripherals in a device.
545
+
546
+ Parameters
547
+ ----------
548
+ device : SvdDevice
549
+ Loaded SVD device.
550
+ """
551
+ table = Table(
552
+ title=f"Peripherals: {device.name}",
553
+ box=box.SIMPLE_HEAVY,
554
+ header_style="bold cyan",
555
+ show_header=True,
556
+ )
557
+ table.add_column("Name", style="bold", no_wrap=True)
558
+ table.add_column("Base Address", style="magenta", no_wrap=True)
559
+ table.add_column("Description")
560
+ for periph in sorted(device.peripherals, key=lambda p: p.base_address):
561
+ table.add_row(
562
+ periph.name,
563
+ f"0x{periph.base_address:08X}",
564
+ periph.description or "--",
565
+ )
566
+ CONSOLE.print(table)
567
+
568
+
569
+ class SvdCmd(gdb.Command):
570
+ """Inspect, manipulate, and monitor hardware registers using CMSIS-SVD definitions."""
571
+
572
+ def __init__(self) -> None:
573
+ super().__init__("svd", gdb.COMMAND_USER, gdb.COMPLETE_NONE, True)
574
+
575
+ def is_svd_loaded(self) -> bool:
576
+ """Return whether an SVD file is loaded in the current GDB session."""
577
+ return is_svd_loaded()
578
+
579
+ def invoke(self, arg: str, from_tty: bool) -> None:
580
+ """Show the svd command reference.
581
+
582
+ Parameters
583
+ ----------
584
+ arg : str
585
+ The argument string supplied by the user.
586
+ from_tty : bool
587
+ Whether GDB invoked the command interactively from its terminal.
588
+ """
589
+ del arg
590
+ del from_tty
591
+ render_help()
592
+
593
+ def complete_args(self, subcmd: str, text: str, word: str) -> list[str]:
594
+ """Provide intelligent GDB auto-completion for SVD command arguments.
595
+
596
+ Parameters
597
+ ----------
598
+ subcmd : str
599
+ Subcommand name being completed.
600
+ text : str
601
+ The full argument text typed so far.
602
+ word : str
603
+ The specific word currently being completed.
604
+
605
+ Returns
606
+ -------
607
+ list[str]
608
+ Matching completion candidates.
609
+ """
610
+ command_prefix = f"svd {subcmd}"
611
+ arg_text = text
612
+ if arg_text.startswith(command_prefix):
613
+ arg_text = arg_text[len(command_prefix) :].lstrip()
614
+ tokens = arg_text.split()
615
+
616
+ # Auto-completion for file path on 'svd read <file.svd>'
617
+ if subcmd == "read":
618
+ if not tokens or (len(tokens) == 1 and not arg_text.endswith(" ")):
619
+ pattern = f"{word}*"
620
+ expanded = os.path.expanduser(pattern)
621
+ matches = glob.glob(expanded)
622
+ results: list[str] = []
623
+ for m in matches:
624
+ if os.path.isdir(m):
625
+ results.append(f"{m}/")
626
+ elif m.lower().endswith((".svd", ".xml")):
627
+ results.append(m)
628
+ return results
629
+ return []
630
+
631
+ # Auto-completion for subcommands that target peripherals and registers
632
+ if subcmd in ("show", "write", "monitor", "dump"):
633
+ if SESSION.device is None:
634
+ return []
635
+
636
+ # Completing device / peripheral name
637
+ if not tokens or (len(tokens) == 1 and not arg_text.endswith(" ")):
638
+ periph_names = [p.name for p in SESSION.device.peripherals]
639
+ if subcmd == "dump":
640
+ periph_names.append("all")
641
+ periph_names.append(SESSION.device.name)
642
+ return [p for p in periph_names if p.upper().startswith(word.upper())]
643
+
644
+ # Completing register name
645
+ if subcmd in ("show", "write", "monitor"):
646
+ if (len(tokens) == 1 and arg_text.endswith(" ")) or (
647
+ len(tokens) == 2 and not arg_text.endswith(" ")
648
+ ):
649
+ periph_name = tokens[0]
650
+ periph = SESSION.device.get_peripheral(periph_name)
651
+ if periph is not None:
652
+ reg_names = [r.name for r in periph.registers]
653
+ return [r for r in reg_names if r.upper().startswith(word.upper())]
654
+
655
+ return []
656
+
657
+ def _invoke_load(self, args: list[str]) -> None:
658
+ """Handle 'svd load' subcommand.
659
+
660
+ Parameters
661
+ ----------
662
+ args : list[str]
663
+ Subcommand arguments (none expected).
664
+
665
+ Raises
666
+ ------
667
+ gdb.GdbError
668
+ If target memory cannot be read or detection fails.
669
+ """
670
+ if args:
671
+ raise gdb.GdbError("'svd load' takes no arguments.")
672
+
673
+ try:
674
+ report = device_report()
675
+ except TargetReadError as err:
676
+ raise gdb.GdbError(f"Cannot read target memory: {err}") from err
677
+
678
+ try:
679
+ svd_path, candidate_name = resolve_and_fetch_svd(report)
680
+ device = parse_svd_file(svd_path)
681
+ SESSION.device = device
682
+ SESSION.svd_dict = device.to_dict()
683
+ SESSION.svd_path = svd_path
684
+
685
+ prod_line = report.product_line.value if report.product_line.is_available else None
686
+ render_load_success(
687
+ device=device,
688
+ svd_filename=candidate_name,
689
+ cache_path=svd_path,
690
+ vendor=report.vendor,
691
+ product_line=prod_line,
692
+ )
693
+ except SvdNotFoundError as err:
694
+ prod_info = (
695
+ f"{report.vendor} {report.product_line.value}"
696
+ if report.product_line.is_available
697
+ else f"vendor {report.vendor}"
698
+ )
699
+ CONSOLE.print(
700
+ Panel(
701
+ Text.assemble(
702
+ ("SVD Not Found: ", "bold yellow"),
703
+ (f"No matching SVD file found for {prod_info}.\n", "default"),
704
+ ("Details: ", "bold"),
705
+ (f"{err}\n", "dim"),
706
+ ("Tip: ", "bold cyan"),
707
+ (
708
+ "You can supply an SVD file explicitly via 'svd read <file.svd>'.",
709
+ "default",
710
+ ),
711
+ ),
712
+ title="SVD Resolution Notice",
713
+ box=box.ROUNDED,
714
+ )
715
+ )
716
+ except SvdError as err:
717
+ raise gdb.GdbError(f"SVD operation failed: {err}") from err
718
+
719
+ def _invoke_read(self, args: list[str]) -> None:
720
+ """Handle 'svd read <file.svd>' subcommand.
721
+
722
+ Parameters
723
+ ----------
724
+ args : list[str]
725
+ Subcommand arguments (expecting 1 SVD file path).
726
+
727
+ Raises
728
+ ------
729
+ gdb.GdbError
730
+ If arguments are invalid or file parsing fails.
731
+ """
732
+ if len(args) != 1:
733
+ raise gdb.GdbError("Usage: svd read <file.svd>")
734
+
735
+ file_path = Path(args[0]).expanduser().resolve()
736
+ if not file_path.is_file():
737
+ raise gdb.GdbError(f"SVD file not found: {file_path}")
738
+
739
+ try:
740
+ device = parse_svd_file(file_path)
741
+ SESSION.device = device
742
+ SESSION.svd_dict = device.to_dict()
743
+ SESSION.svd_path = file_path
744
+ render_file_load_success(device, file_path)
745
+ except SvdError as err:
746
+ raise gdb.GdbError(f"Failed to parse SVD file: {err}") from err
747
+
748
+ def _invoke_show(self, args: list[str]) -> None:
749
+ """Handle 'svd show <device_name> [<register_name>]' subcommand.
750
+
751
+ Parameters
752
+ ----------
753
+ args : list[str]
754
+ Subcommand arguments: <device_name> [<register_name>].
755
+
756
+ Raises
757
+ ------
758
+ gdb.GdbError
759
+ If arguments are invalid or target reading fails.
760
+ """
761
+ if not args:
762
+ raise gdb.GdbError("Usage: svd show <device_name> [<register_name>]")
763
+
764
+ if SESSION.device is None:
765
+ raise gdb.GdbError(
766
+ "No SVD definition currently loaded. Run 'svd load' or 'svd read <file.svd>' first."
767
+ )
768
+
769
+ dev_name = args[0]
770
+ periph = SESSION.device.get_peripheral(dev_name)
771
+ if periph is None:
772
+ raise gdb.GdbError(
773
+ f"Peripheral '{dev_name}' not found in loaded SVD device '{SESSION.device.name}'."
774
+ )
775
+
776
+ try:
777
+ reader = TOOLKIT_SESSION.memory
778
+ except TargetReadError as err:
779
+ raise gdb.GdbError(f"Cannot access target memory: {err}") from err
780
+
781
+ if len(args) == 1:
782
+ render_peripheral_state(periph, reader)
783
+ elif len(args) == 2:
784
+ reg_name = args[1]
785
+ reg = periph.get_register(reg_name)
786
+ if reg is None:
787
+ raise gdb.GdbError(
788
+ f"Register '{reg_name}' not found in peripheral '{periph.name}'."
789
+ )
790
+ try:
791
+ val = _read_register_value(reader, periph.base_address, reg)
792
+ except TargetReadError as err:
793
+ raise gdb.GdbError(
794
+ f"Failed to read register {periph.name}->{reg.name}: {err}"
795
+ ) from err
796
+ render_register_detail(periph, reg, val)
797
+ else:
798
+ raise gdb.GdbError(
799
+ "Too many arguments. Usage: svd show <device_name> [<register_name>]"
800
+ )
801
+
802
+ def _invoke_list(self, args: list[str]) -> None:
803
+ """Handle 'svd list' subcommand.
804
+
805
+ Parameters
806
+ ----------
807
+ args : list[str]
808
+ Subcommand arguments (none expected).
809
+
810
+ Raises
811
+ ------
812
+ gdb.GdbError
813
+ If no SVD device is currently loaded.
814
+ """
815
+ if args:
816
+ raise gdb.GdbError("Usage: svd list")
817
+
818
+ if SESSION.device is None:
819
+ raise gdb.GdbError(
820
+ "No SVD definition currently loaded. Run 'svd load' or 'svd read <file.svd>' first."
821
+ )
822
+
823
+ render_peripheral_list(SESSION.device)
824
+
825
+ def _invoke_write(self, args: list[str]) -> None:
826
+ """Handle 'svd write <device_name> <register_name> <value_hex>' subcommand.
827
+
828
+ Parameters
829
+ ----------
830
+ args : list[str]
831
+ Subcommand arguments.
832
+
833
+ Raises
834
+ ------
835
+ gdb.GdbError
836
+ If arguments are invalid or writing fails.
837
+ """
838
+ if len(args) != 3:
839
+ raise gdb.GdbError("Usage: svd write <device_name> <register_name> <value_hex>")
840
+
841
+ if SESSION.device is None:
842
+ raise gdb.GdbError(
843
+ "No SVD definition currently loaded. Run 'svd load' or 'svd read <file.svd>' first."
844
+ )
845
+
846
+ dev_name, reg_name, val_str = args[0], args[1], args[2]
847
+ periph = SESSION.device.get_peripheral(dev_name)
848
+ if periph is None:
849
+ raise gdb.GdbError(
850
+ f"Peripheral '{dev_name}' not found in loaded SVD device '{SESSION.device.name}'."
851
+ )
852
+
853
+ reg = periph.get_register(reg_name)
854
+ if reg is None:
855
+ raise gdb.GdbError(f"Register '{reg_name}' not found in peripheral '{periph.name}'.")
856
+
857
+ try:
858
+ value = _parse_numeric_value(val_str)
859
+ except ValueError as err:
860
+ raise gdb.GdbError(
861
+ f"Invalid numeric value '{val_str}': must be hex (0x...), bin (0b...), or decimal."
862
+ ) from err
863
+
864
+ max_val = (1 << reg.size) - 1
865
+ if value < 0 or value > max_val:
866
+ raise gdb.GdbError(
867
+ f"Value 0x{value:X} exceeds register width ({reg.size}-bit, max: 0x{max_val:X})."
868
+ )
869
+
870
+ try:
871
+ reader = TOOLKIT_SESSION.memory
872
+ old_val = _read_register_value(reader, periph.base_address, reg)
873
+ except TargetReadError:
874
+ old_val = None
875
+
876
+ try:
877
+ _write_register_value(periph.base_address, reg, value)
878
+ except Exception as err:
879
+ raise gdb.GdbError(
880
+ f"Failed to write to register {periph.name}->{reg.name}: {err}"
881
+ ) from err
882
+
883
+ try:
884
+ readback = _read_register_value(reader, periph.base_address, reg)
885
+ except TargetReadError:
886
+ readback = None
887
+
888
+ hex_len = max(2, (reg.size + 3) // 4)
889
+ reg_addr = periph.base_address + reg.address_offset
890
+
891
+ table = Table(
892
+ title=f"Write Register: {periph.name}->{reg.name}",
893
+ box=box.SIMPLE_HEAVY,
894
+ header_style="bold cyan",
895
+ show_header=True,
896
+ )
897
+ table.add_column("Property", style="bold", no_wrap=True)
898
+ table.add_column("Value")
899
+ table.add_row("Address", f"0x{reg_addr:08X}")
900
+ if old_val is not None:
901
+ table.add_row("Previous Value", f"0x{old_val:0{hex_len}X}")
902
+ table.add_row("Written Value", Text(f"0x{value:0{hex_len}X}", style="bold green"))
903
+ if readback is not None:
904
+ table.add_row("Readback Value", f"0x{readback:0{hex_len}X}")
905
+ CONSOLE.print(table)
906
+
907
+ def _invoke_monitor(self, args: list[str]) -> None:
908
+ """Handle 'svd monitor <device_name> <regname>' subcommand.
909
+
910
+ Parameters
911
+ ----------
912
+ args : list[str]
913
+ Subcommand arguments.
914
+
915
+ Raises
916
+ ------
917
+ gdb.GdbError
918
+ If arguments are invalid or watchpoint creation fails.
919
+ """
920
+ if len(args) != 2:
921
+ raise gdb.GdbError("Usage: svd monitor <device_name> <regname>")
922
+
923
+ if SESSION.device is None:
924
+ raise gdb.GdbError(
925
+ "No SVD definition currently loaded. Run 'svd load' or 'svd read <file.svd>' first."
926
+ )
927
+
928
+ dev_name, reg_name = args[0], args[1]
929
+ periph = SESSION.device.get_peripheral(dev_name)
930
+ if periph is None:
931
+ raise gdb.GdbError(
932
+ f"Peripheral '{dev_name}' not found in loaded SVD device '{SESSION.device.name}'."
933
+ )
934
+
935
+ reg = periph.get_register(reg_name)
936
+ if reg is None:
937
+ raise gdb.GdbError(f"Register '{reg_name}' not found in peripheral '{periph.name}'.")
938
+
939
+ reg_addr = periph.base_address + reg.address_offset
940
+ try:
941
+ reader = TOOLKIT_SESSION.memory
942
+ initial_val = _read_register_value(reader, periph.base_address, reg)
943
+ except TargetReadError:
944
+ initial_val = 0
945
+
946
+ c_type = _get_c_type_for_size(reg.size)
947
+ spec = f"*({c_type} *){reg_addr:#x}"
948
+
949
+ try:
950
+ wp = SvdWatchpoint(
951
+ spec=spec,
952
+ peripheral_name=periph.name,
953
+ register_name=reg.name,
954
+ register=reg,
955
+ address=reg_addr,
956
+ initial_value=initial_val,
957
+ )
958
+ SESSION.watchpoints.append(wp)
959
+ except gdb.error as err:
960
+ raise gdb.GdbError(f"Failed to create watchpoint on {spec}: {err}") from err
961
+
962
+ hex_len = max(2, (reg.size + 3) // 4)
963
+ table = Table(
964
+ title="SVD Register Monitor Activated",
965
+ box=box.SIMPLE_HEAVY,
966
+ header_style="bold cyan",
967
+ show_header=True,
968
+ )
969
+ table.add_column("Property", style="bold", no_wrap=True)
970
+ table.add_column("Value")
971
+ table.add_row("Peripheral", periph.name)
972
+ table.add_row("Register", reg.name)
973
+ table.add_row("Address", f"0x{reg_addr:08X}")
974
+ table.add_row("Initial Value", f"0x{initial_val:0{hex_len}X}")
975
+ table.add_row("Watch Expression", spec)
976
+ table.add_row("Status", Text("Active (will notify on modification)", style="bold green"))
977
+ CONSOLE.print(table)
978
+
979
+ def _invoke_dump(self, args: list[str]) -> None:
980
+ """Handle 'svd dump <device_name> file.json' subcommand.
981
+
982
+ Parameters
983
+ ----------
984
+ args : list[str]
985
+ Subcommand arguments.
986
+
987
+ Raises
988
+ ------
989
+ gdb.GdbError
990
+ If arguments are invalid or dumping fails.
991
+ """
992
+ if len(args) != 2:
993
+ raise gdb.GdbError("Usage: svd dump <device_name> file.json")
994
+
995
+ if SESSION.device is None:
996
+ raise gdb.GdbError(
997
+ "No SVD definition currently loaded. Run 'svd load' or 'svd read <file.svd>' first."
998
+ )
999
+
1000
+ dev_name, out_file_str = args[0], args[1]
1001
+ out_path = Path(out_file_str).expanduser().resolve()
1002
+
1003
+ try:
1004
+ reader = TOOLKIT_SESSION.memory
1005
+ except TargetReadError as err:
1006
+ raise gdb.GdbError(f"Cannot access target memory: {err}") from err
1007
+
1008
+ dump_data: dict[str, Any] = {
1009
+ "device": SESSION.device.name,
1010
+ "vendor": SESSION.device.vendor,
1011
+ "version": SESSION.device.version,
1012
+ "timestamp_gdb": True,
1013
+ "peripherals": {},
1014
+ }
1015
+
1016
+ # Check if user requested all peripherals or a specific peripheral
1017
+ if dev_name.lower() in ("all", SESSION.device.name.lower()):
1018
+ target_peripherals = list(SESSION.device.peripherals)
1019
+ else:
1020
+ periph = SESSION.device.get_peripheral(dev_name)
1021
+ if periph is None:
1022
+ raise gdb.GdbError(
1023
+ f"Peripheral '{dev_name}' not found in loaded SVD device '{SESSION.device.name}'."
1024
+ )
1025
+ target_peripherals = [periph]
1026
+
1027
+ total_regs = 0
1028
+ for p in target_peripherals:
1029
+ p_dict: dict[str, Any] = {
1030
+ "name": p.name,
1031
+ "description": p.description,
1032
+ "base_address": f"0x{p.base_address:08X}",
1033
+ "base_address_int": p.base_address,
1034
+ "group_name": p.group_name,
1035
+ "registers": {},
1036
+ }
1037
+ for r in p.registers:
1038
+ total_regs += 1
1039
+ reg_addr = p.base_address + r.address_offset
1040
+ hex_len = max(2, (r.size + 3) // 4)
1041
+ try:
1042
+ val = _read_register_value(reader, p.base_address, r)
1043
+ val_hex = f"0x{val:0{hex_len}X}"
1044
+ decoded_fields = r.decode(val)
1045
+ read_error = None
1046
+ except TargetReadError as read_err:
1047
+ val = None
1048
+ val_hex = None
1049
+ decoded_fields = []
1050
+ read_error = str(read_err)
1051
+
1052
+ r_dict: dict[str, Any] = {
1053
+ "name": r.name,
1054
+ "display_name": r.display_name,
1055
+ "description": r.description,
1056
+ "address_offset": f"0x{r.address_offset:04X}",
1057
+ "address": f"0x{reg_addr:08X}",
1058
+ "size": r.size,
1059
+ "access": r.access,
1060
+ "reset_value": (
1061
+ f"0x{r.reset_value:0{hex_len}X}" if r.reset_value is not None else None
1062
+ ),
1063
+ "value_raw": val,
1064
+ "value_hex": val_hex,
1065
+ "fields": decoded_fields,
1066
+ }
1067
+ if read_error:
1068
+ r_dict["error"] = read_error
1069
+ p_dict["registers"][r.name] = r_dict
1070
+
1071
+ dump_data["peripherals"][p.name] = p_dict
1072
+
1073
+ try:
1074
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1075
+ with out_path.open("w", encoding="utf-8") as json_file:
1076
+ json.dump(dump_data, json_file, indent=2)
1077
+ except OSError as err:
1078
+ raise gdb.GdbError(f"Failed to write dump to '{out_path}': {err}") from err
1079
+
1080
+ file_size = out_path.stat().st_size
1081
+ table = Table(
1082
+ title="SVD Memory Dump Saved",
1083
+ box=box.SIMPLE_HEAVY,
1084
+ header_style="bold cyan",
1085
+ show_header=True,
1086
+ )
1087
+ table.add_column("Property", style="bold", no_wrap=True)
1088
+ table.add_column("Value")
1089
+ table.add_row("Target", dev_name)
1090
+ table.add_row("Output File", Text(str(out_path), style="bold green"))
1091
+ table.add_row("Peripherals Dumped", str(len(target_peripherals)))
1092
+ table.add_row("Registers Dumped", str(total_regs))
1093
+ table.add_row("File Size", f"{file_size:,} bytes")
1094
+ CONSOLE.print(table)
1095
+
1096
+
1097
+ class _SvdSubcommand(gdb.Command):
1098
+ """Base class for concrete ``svd`` subcommands registered under the prefix command."""
1099
+
1100
+ def __init__(self, parent: SvdCmd, name: str) -> None:
1101
+ self.parent = parent
1102
+ self.name = name
1103
+ super().__init__(f"svd {name}", gdb.COMMAND_USER)
1104
+
1105
+ def _argv(self, arg: str) -> list[str]:
1106
+ """Split command arguments using GDB's own CLI lexer."""
1107
+ return list(gdb.string_to_argv(arg)) if arg.strip() else []
1108
+
1109
+ def complete(self, text: str, word: str) -> list[str]:
1110
+ """Complete arguments for the concrete subcommand."""
1111
+ return self.parent.complete_args(self.name, text, word)
1112
+
1113
+
1114
+ class SvdLoadCmd(_SvdSubcommand):
1115
+ """Auto-detect target CPUID/SoC, fetch and load matching SVD definition."""
1116
+
1117
+ def __init__(self, parent: SvdCmd) -> None:
1118
+ super().__init__(parent, "load")
1119
+
1120
+ def invoke(self, arg: str, from_tty: bool) -> None:
1121
+ """Execute ``svd load``."""
1122
+ del from_tty
1123
+ self.parent._invoke_load(self._argv(arg))
1124
+
1125
+
1126
+ class SvdReadCmd(_SvdSubcommand):
1127
+ """Load an explicit SVD XML file from local filesystem."""
1128
+
1129
+ def __init__(self, parent: SvdCmd) -> None:
1130
+ super().__init__(parent, "read")
1131
+
1132
+ def invoke(self, arg: str, from_tty: bool) -> None:
1133
+ """Execute ``svd read``."""
1134
+ del from_tty
1135
+ self.parent._invoke_read(self._argv(arg))
1136
+
1137
+
1138
+ class SvdShowCmd(_SvdSubcommand):
1139
+ """Display peripheral or register state from the loaded SVD device."""
1140
+
1141
+ def __init__(self, parent: SvdCmd) -> None:
1142
+ super().__init__(parent, "show")
1143
+
1144
+ def invoke(self, arg: str, from_tty: bool) -> None:
1145
+ """Execute ``svd show``."""
1146
+ del from_tty
1147
+ self.parent._invoke_show(self._argv(arg))
1148
+
1149
+
1150
+ class SvdWriteCmd(_SvdSubcommand):
1151
+ """Write a numeric value into a peripheral register."""
1152
+
1153
+ def __init__(self, parent: SvdCmd) -> None:
1154
+ super().__init__(parent, "write")
1155
+
1156
+ def invoke(self, arg: str, from_tty: bool) -> None:
1157
+ """Execute ``svd write``."""
1158
+ del from_tty
1159
+ self.parent._invoke_write(self._argv(arg))
1160
+
1161
+
1162
+ class SvdMonitorCmd(_SvdSubcommand):
1163
+ """Set a watchpoint on a register and report bitfield differences."""
1164
+
1165
+ def __init__(self, parent: SvdCmd) -> None:
1166
+ super().__init__(parent, "monitor")
1167
+
1168
+ def invoke(self, arg: str, from_tty: bool) -> None:
1169
+ """Execute ``svd monitor``."""
1170
+ del from_tty
1171
+ self.parent._invoke_monitor(self._argv(arg))
1172
+
1173
+
1174
+ class SvdDumpCmd(_SvdSubcommand):
1175
+ """Dump a snapshot of device or peripheral state into a JSON file."""
1176
+
1177
+ def __init__(self, parent: SvdCmd) -> None:
1178
+ super().__init__(parent, "dump")
1179
+
1180
+ def invoke(self, arg: str, from_tty: bool) -> None:
1181
+ """Execute ``svd dump``."""
1182
+ del from_tty
1183
+ self.parent._invoke_dump(self._argv(arg))
1184
+
1185
+
1186
+ class SvdListCmd(_SvdSubcommand):
1187
+ """List the names of all peripherals in the loaded SVD device."""
1188
+
1189
+ def __init__(self, parent: SvdCmd) -> None:
1190
+ super().__init__(parent, "list")
1191
+
1192
+ def invoke(self, arg: str, from_tty: bool) -> None:
1193
+ """Execute ``svd list``."""
1194
+ del from_tty
1195
+ self.parent._invoke_list(self._argv(arg))
1196
+
1197
+
1198
+ class SvdHelpCmd(_SvdSubcommand):
1199
+ """Show the SVD command reference."""
1200
+
1201
+ def __init__(self, parent: SvdCmd) -> None:
1202
+ super().__init__(parent, "help")
1203
+
1204
+ def invoke(self, arg: str, from_tty: bool) -> None:
1205
+ """Execute ``svd help``."""
1206
+ del arg
1207
+ del from_tty
1208
+ render_help()