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,629 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Arm Cortex-M fault-analysis diagnostic service."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import struct
9
+ from dataclasses import dataclass
10
+ from typing import Callable
11
+
12
+ from ...target_memory import TargetMemory, TargetReadError
13
+ from ..base import Architecture, SystemRegisterSet, TargetDescription
14
+ from ..diagnostics import (
15
+ DiagnosticPanel,
16
+ DiagnosticReport,
17
+ DiagnosticRuntimeAccess,
18
+ DiagnosticServiceName,
19
+ DiagnosticTable,
20
+ DiagnosticTableRow,
21
+ )
22
+ from .cortex_m import CortexMTargetDescription, read_scb
23
+
24
+ _EXC_RETURN_MASK = 0xFFFFFF00
25
+ _EXCEPTION_NAMES: dict[int, str] = {
26
+ 1: "Reset",
27
+ 2: "NMI",
28
+ 3: "HardFault",
29
+ 4: "MemManage",
30
+ 5: "BusFault",
31
+ 6: "UsageFault",
32
+ 7: "SecureFault",
33
+ 11: "SVCall",
34
+ 12: "DebugMonitor",
35
+ 14: "PendSV",
36
+ 15: "SysTick",
37
+ }
38
+ _UFSR_BITS: dict[int, tuple[str, str]] = {
39
+ 0: ("UNDEFINSTR", "Undefined instruction executed"),
40
+ 1: ("INVSTATE", "Invalid Thumb state (bit T=0 or branch to even address)"),
41
+ 2: ("INVPC", "Invalid EXC_RETURN value on exception return"),
42
+ 3: ("NOCP", "Access to disabled or non-existent coprocessor/FPU"),
43
+ 4: ("STKOF", "Stack overflow detected"),
44
+ 8: ("UNALIGNED", "Unaligned access trapped (CCR.UNALIGN_TRP=1)"),
45
+ 9: ("DIVBYZERO", "Integer division by zero (CCR.DIV_0_TRP=1)"),
46
+ }
47
+ _BFSR_BITS: dict[int, tuple[str, str]] = {
48
+ 0: ("IBUSERR", "Bus error on instruction fetch"),
49
+ 1: ("PRECISERR", "Precise bus error on data access (BFAR valid)"),
50
+ 2: ("IMPRECISERR", "Imprecise bus error (asynchronous write buffer)"),
51
+ 3: ("UNSTKERR", "Bus error on exception return unstacking"),
52
+ 4: ("STKERR", "Bus error on exception entry stacking"),
53
+ 5: ("LSPERR", "Bus error during FPU lazy preservation"),
54
+ 7: ("BFARVALID", "BFAR contains valid fault address"),
55
+ }
56
+ _MMFSR_BITS: dict[int, tuple[str, str]] = {
57
+ 0: ("IACCVIOL", "MPU/security violation on instruction fetch (XN)"),
58
+ 1: ("DACCVIOL", "MPU/security violation on data access"),
59
+ 3: ("MUNSTKERR", "MPU violation on exception return unstacking"),
60
+ 4: ("MSTKERR", "MPU violation on exception entry stacking"),
61
+ 5: ("MLSPERR", "MPU violation during FPU lazy preservation"),
62
+ 7: ("MMARVALID", "MMFAR contains valid fault address"),
63
+ }
64
+ _HFSR_BITS: dict[int, tuple[str, str]] = {
65
+ 1: ("VECTTBL", "Vector table read error"),
66
+ 30: ("FORCED", "Fault escalated to HardFault (source handler disabled/masked)"),
67
+ 31: ("DEBUGEVT", "Debug event / breakpoint"),
68
+ }
69
+ _DFSR_BITS: dict[int, tuple[str, str]] = {
70
+ 0: ("HALTED", "Core halted by debug request / DAP"),
71
+ 1: ("BKPT", "BKPT instruction executed"),
72
+ 2: ("DWTTRAP", "DWT trap / watchpoint"),
73
+ 3: ("VCATCH", "Vector catch triggered"),
74
+ 4: ("EXTERNAL", "External debug request"),
75
+ }
76
+ _SFSR_BITS: dict[int, tuple[str, str]] = {
77
+ 0: ("INVEP", "Invalid entry point (missing SG instruction)"),
78
+ 1: ("INVIS", "Invalid integrity signature"),
79
+ 2: ("INVER", "Invalid Secure exception return"),
80
+ 3: ("AUVIOL", "Attribution unit violation (SAU/IDAU)"),
81
+ 4: ("INVTRAN", "Illegal security domain transition"),
82
+ 5: ("LSPERR", "SAU/IDAU error during lazy preservation"),
83
+ 6: ("SFARVALID", "SFAR contains valid fault address"),
84
+ 7: ("LSERR", "Lazy state enable/disable error"),
85
+ }
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class StackedFrame:
90
+ """Decoded hardware exception frame restored from the active stack."""
91
+
92
+ lr_exc_return: int
93
+ sp_name: str
94
+ sp_address: int
95
+ return_mode: str
96
+ target_domain: str
97
+ has_fpu: bool
98
+ secure_stacking: bool
99
+ r0: int
100
+ r1: int
101
+ r2: int
102
+ r3: int
103
+ r12: int
104
+ lr: int
105
+ pc: int
106
+ xpsr: int
107
+ fp_registers: tuple[int, ...] | None = None
108
+ fpscr: int | None = None
109
+ s0_float: float | None = None
110
+
111
+
112
+ def _is_exc_return(value: int) -> bool:
113
+ """Return whether a value has the Cortex-M EXC_RETURN high-byte pattern."""
114
+ return value & _EXC_RETURN_MASK == _EXC_RETURN_MASK
115
+
116
+
117
+ def _stack_register_names(exc_return: int, sp_name: str) -> tuple[str, str]:
118
+ """Return the banked stack register selected by EXC_RETURN before its alias."""
119
+ stack_register = sp_name.lower()
120
+ security_suffix = "s" if exc_return & (1 << 6) else "ns"
121
+ return (f"{stack_register}_{security_suffix}", stack_register)
122
+
123
+
124
+ def _decode_flags(value: int | None, table: dict[int, tuple[str, str]]) -> list[tuple[str, str]]:
125
+ """Decode active named bit flags from one system-register value."""
126
+ if value is None:
127
+ return []
128
+ return [
129
+ (name, description)
130
+ for bit, (name, description) in sorted(table.items())
131
+ if value & (1 << bit)
132
+ ]
133
+
134
+
135
+ def _memory_region(address: int | None) -> str:
136
+ """Classify an address using the standard Cortex-M memory map."""
137
+ if address is None:
138
+ return "?"
139
+ if address < 0x1000:
140
+ return "NULL-pointer / Vector Table"
141
+ if address < 0x20000000:
142
+ return "CODE (Flash / ROM)"
143
+ if address < 0x40000000:
144
+ return "SRAM"
145
+ if address < 0x60000000:
146
+ return "Peripherals (APB/AHB)"
147
+ if address < 0x80000000:
148
+ return "External RAM"
149
+ if address < 0xA0000000:
150
+ return "External Device"
151
+ if address < 0xE0000000:
152
+ return "System / Reserved"
153
+ if address < 0xE0100000:
154
+ return "PPB (SCB / NVIC / Core)"
155
+ return "Vendor / Reserved"
156
+
157
+
158
+ def read_stacked_frame(
159
+ reader: TargetMemory,
160
+ lr_value: int,
161
+ sp_value: int,
162
+ sp_name: str,
163
+ ) -> StackedFrame | str:
164
+ """Decode an EXC_RETURN-selected basic or extended Cortex-M stack frame."""
165
+ has_fpu = not bool(lr_value & (1 << 4))
166
+ word_count = 26 if has_fpu else 8
167
+ words: list[int] = []
168
+ for index in range(word_count):
169
+ address = sp_value + 4 * index
170
+ try:
171
+ words.append(reader.read_uint32(address))
172
+ except (TargetReadError, ValueError) as error:
173
+ return f"Cannot read stacked frame at 0x{address:08X}: {error}"
174
+
175
+ core_offset = 18 if has_fpu else 0
176
+ fp_registers = tuple(words[:16]) if has_fpu else None
177
+ fpscr = words[16] if has_fpu else None
178
+ s0_float = None
179
+ if fp_registers is not None:
180
+ s0_float = struct.unpack("<f", struct.pack("<I", fp_registers[0]))[0]
181
+ return StackedFrame(
182
+ lr_exc_return=lr_value,
183
+ sp_name=sp_name,
184
+ sp_address=sp_value,
185
+ return_mode="Handler" if not lr_value & (1 << 3) else "Thread",
186
+ target_domain="Secure" if not lr_value & 1 else "Non-Secure",
187
+ has_fpu=has_fpu,
188
+ secure_stacking=bool(lr_value & (1 << 6)),
189
+ r0=words[core_offset],
190
+ r1=words[core_offset + 1],
191
+ r2=words[core_offset + 2],
192
+ r3=words[core_offset + 3],
193
+ r12=words[core_offset + 4],
194
+ lr=words[core_offset + 5],
195
+ pc=words[core_offset + 6],
196
+ xpsr=words[core_offset + 7],
197
+ fp_registers=fp_registers,
198
+ fpscr=fpscr,
199
+ s0_float=s0_float,
200
+ )
201
+
202
+
203
+ class CortexMFaultCollector:
204
+ """Collect the Cortex-M implementation of portable fault analysis."""
205
+
206
+ architecture = Architecture.ARM
207
+ service = DiagnosticServiceName.FAULT_ANALYSIS
208
+
209
+ def supports(self, target: TargetDescription) -> bool:
210
+ """Return whether this service recognizes a typed Cortex-M target."""
211
+ return isinstance(target, CortexMTargetDescription) and target.core is not None
212
+
213
+ def collect(
214
+ self,
215
+ reader: TargetMemory,
216
+ target: TargetDescription,
217
+ access: DiagnosticRuntimeAccess | None = None,
218
+ ) -> DiagnosticReport:
219
+ """Collect typed SCB fault state and the selected exception frame."""
220
+ if not isinstance(target, CortexMTargetDescription) or target.core is None:
221
+ raise ValueError("Cortex-M fault analysis requires a known core description")
222
+
223
+ registers = access.registers if access is not None else None
224
+ symbols = access.symbols if access is not None else None
225
+ read_register = registers.read_first if registers is not None else lambda names: None
226
+ pc = read_register(("pc", "r15"))
227
+ lr = read_register(("lr", "r14"))
228
+ xpsr = read_register(("xpsr",))
229
+ ipsr_value = read_register(("ipsr",))
230
+ ipsr = ipsr_value & 0x1FF if ipsr_value is not None else (xpsr or 0) & 0x1FF
231
+ scb = read_scb(reader, target, cpuid_value=target.raw_cpuid)
232
+ stacked_frame: StackedFrame | str | None = None
233
+ if lr is not None and _is_exc_return(lr):
234
+ uses_psp = bool(lr & (1 << 2))
235
+ sp_name = "PSP" if uses_psp else "MSP"
236
+ sp = read_register(_stack_register_names(lr, sp_name))
237
+ if sp is None:
238
+ sp = read_register(("sp", "r13"))
239
+ if sp is None:
240
+ stacked_frame = f"Cannot read stacked frame: {sp_name} is unavailable."
241
+ else:
242
+ stacked_frame = read_stacked_frame(reader, lr, sp, sp_name)
243
+
244
+ resolve = symbols.resolve if symbols is not None else lambda address: "?"
245
+ overview = self._overview_table(target, ipsr, pc, lr, xpsr, resolve)
246
+ stack_table = self._stacked_frame_table(stacked_frame, resolve)
247
+ scb_table = self._scb_table(scb)
248
+ panels = self._diagnostic_panels(scb, stacked_frame, resolve)
249
+ blocks: list[DiagnosticTable | DiagnosticPanel] = [overview]
250
+ if stack_table is not None:
251
+ blocks.append(stack_table)
252
+ elif isinstance(stacked_frame, str):
253
+ blocks.append(DiagnosticPanel("Stack Frame", (stacked_frame,)))
254
+ blocks.extend((scb_table, *panels))
255
+ return DiagnosticReport(
256
+ self.service,
257
+ target,
258
+ tables=tuple(table for table in blocks if isinstance(table, DiagnosticTable)),
259
+ panels=tuple(panel for panel in blocks if isinstance(panel, DiagnosticPanel)),
260
+ blocks=tuple(blocks),
261
+ )
262
+
263
+ @staticmethod
264
+ def _overview_table(
265
+ target: CortexMTargetDescription,
266
+ ipsr: int,
267
+ pc: int | None,
268
+ lr: int | None,
269
+ xpsr: int | None,
270
+ resolve: Callable[[int], str],
271
+ ) -> DiagnosticTable:
272
+ """Build the established overview as generic table data."""
273
+ exception = _EXCEPTION_NAMES.get(ipsr, f"Interrupt #{ipsr}")
274
+ exception_display = (
275
+ f"{exception} (Exception #{ipsr})"
276
+ if ipsr in (2, 3, 4, 5, 6, 7)
277
+ else "None (Thread Mode)" if ipsr == 0 else f"{exception} (#{ipsr})"
278
+ )
279
+ rows: list[DiagnosticTableRow] = [
280
+ DiagnosticTableRow(
281
+ (
282
+ "Target Core",
283
+ f"{target.core_name} ({target.rnp_revision}) - {target.implementer_name}",
284
+ )
285
+ ),
286
+ DiagnosticTableRow(("Active Exception", exception_display)),
287
+ DiagnosticTableRow(
288
+ (
289
+ "Execution Mode",
290
+ "HANDLER (in exception)" if ipsr else "THREAD (normal execution)",
291
+ )
292
+ ),
293
+ DiagnosticTableRow(
294
+ ("Current PC", f"0x{pc:08X} [{resolve(pc)}]" if pc is not None else "Unknown")
295
+ ),
296
+ ]
297
+ if lr is None:
298
+ rows.append(DiagnosticTableRow(("Current LR", "Unknown")))
299
+ elif _is_exc_return(lr):
300
+ rows.append(DiagnosticTableRow(("Current LR", f"0x{lr:08X} (valid EXC_RETURN)")))
301
+ else:
302
+ rows.append(DiagnosticTableRow(("Current LR", f"0x{lr:08X} [{resolve(lr)}]")))
303
+ if xpsr is not None:
304
+ rows.append(
305
+ DiagnosticTableRow(
306
+ ("Current xPSR", f"0x{xpsr:08X} (IPSR={xpsr & 0x1FF}, T={(xpsr >> 24) & 1})")
307
+ )
308
+ )
309
+ return DiagnosticTable("ARM Cortex-M Fault Overview", ("Property", "Value"), tuple(rows))
310
+
311
+ @staticmethod
312
+ def _stacked_frame_table(
313
+ frame: StackedFrame | str | None,
314
+ resolve: Callable[[int], str],
315
+ ) -> DiagnosticTable | None:
316
+ """Build stacked-frame table data only when hardware frame recovery succeeded."""
317
+ if not isinstance(frame, StackedFrame):
318
+ return None
319
+ summary = (
320
+ f"Stack: {frame.sp_name} @ 0x{frame.sp_address:08X} | "
321
+ f"Return to: {frame.return_mode} ({frame.target_domain}) | "
322
+ f"Frame: {'Extended (standard FPU)' if frame.has_fpu else 'Basic (8 registers)'}"
323
+ )
324
+ rows = [
325
+ DiagnosticTableRow(("r0", f"0x{frame.r0:08X}", "")),
326
+ DiagnosticTableRow(("r1", f"0x{frame.r1:08X}", "")),
327
+ DiagnosticTableRow(("r2", f"0x{frame.r2:08X}", "")),
328
+ DiagnosticTableRow(("r3", f"0x{frame.r3:08X}", "")),
329
+ DiagnosticTableRow(("r12", f"0x{frame.r12:08X}", "")),
330
+ DiagnosticTableRow(("lr", f"0x{frame.lr:08X}", f"Caller: {resolve(frame.lr)}")),
331
+ DiagnosticTableRow(
332
+ ("pc", f"0x{frame.pc:08X}", f"<-- Faulting instruction: {resolve(frame.pc)}")
333
+ ),
334
+ DiagnosticTableRow(
335
+ (
336
+ "xpsr",
337
+ f"0x{frame.xpsr:08X}",
338
+ f"IPSR={frame.xpsr & 0x1FF}, T={(frame.xpsr >> 24) & 1} "
339
+ f"({'Thumb' if frame.xpsr & (1 << 24) else 'ARM (invalid on Cortex-M)'})",
340
+ )
341
+ ),
342
+ ]
343
+ if frame.fp_registers is not None:
344
+ rows.extend(
345
+ DiagnosticTableRow(
346
+ (
347
+ f"s{index}",
348
+ f"0x{value:08X}",
349
+ (
350
+ f"s0 (float32) = {frame.s0_float:.7g}"
351
+ if index == 0 and frame.s0_float is not None
352
+ else ""
353
+ ),
354
+ )
355
+ )
356
+ for index, value in enumerate(frame.fp_registers)
357
+ )
358
+ if frame.fpscr is not None:
359
+ rows.append(
360
+ DiagnosticTableRow(("fpscr", f"0x{frame.fpscr:08X}", "FPU Status & Control"))
361
+ )
362
+ return DiagnosticTable(
363
+ f"Stacked frame at crash time ({summary})",
364
+ ("Register", "Stacked Value", "Details / Symbol"),
365
+ tuple(rows),
366
+ )
367
+
368
+ @staticmethod
369
+ def _scb_table(scb: SystemRegisterSet) -> DiagnosticTable:
370
+ """Build the SCB status table from typed architecture register results."""
371
+ get: Callable[[str], int | None] = lambda name: CortexMFaultCollector._scb_value(scb, name)
372
+ cfsr = get("CFSR")
373
+ rows: list[DiagnosticTableRow] = []
374
+ if cfsr is None:
375
+ rows.append(
376
+ DiagnosticTableRow(
377
+ ("CFSR", "Unavailable", "Not present on this target or memory inaccessible")
378
+ )
379
+ )
380
+ else:
381
+ mmfsr, bfsr, ufsr = cfsr & 0xFF, (cfsr >> 8) & 0xFF, (cfsr >> 16) & 0xFFFF
382
+ flags = [
383
+ f"{name}: {detail} (MemManage)"
384
+ for name, detail in _decode_flags(mmfsr, _MMFSR_BITS)
385
+ ]
386
+ flags.extend(
387
+ f"{name}: {detail} (BusFault)" for name, detail in _decode_flags(bfsr, _BFSR_BITS)
388
+ )
389
+ flags.extend(
390
+ f"{name}: {detail} (UsageFault)" for name, detail in _decode_flags(ufsr, _UFSR_BITS)
391
+ )
392
+ rows.extend(
393
+ (
394
+ DiagnosticTableRow(
395
+ (
396
+ "CFSR",
397
+ f"0x{cfsr:08X}",
398
+ "\n".join(flags) if flags else "No active error flags",
399
+ )
400
+ ),
401
+ DiagnosticTableRow(
402
+ (
403
+ " ├─ MMFSR",
404
+ f"0x{mmfsr:02X}",
405
+ f"{len(_decode_flags(mmfsr, _MMFSR_BITS))} active flag(s)",
406
+ )
407
+ ),
408
+ DiagnosticTableRow(
409
+ (
410
+ " ├─ BFSR",
411
+ f"0x{bfsr:02X}",
412
+ f"{len(_decode_flags(bfsr, _BFSR_BITS))} active flag(s)",
413
+ )
414
+ ),
415
+ DiagnosticTableRow(
416
+ (
417
+ " └─ UFSR",
418
+ f"0x{ufsr:04X}",
419
+ f"{len(_decode_flags(ufsr, _UFSR_BITS))} active flag(s)",
420
+ )
421
+ ),
422
+ )
423
+ )
424
+ status_registers: tuple[tuple[str, dict[int, tuple[str, str]], str], ...] = (
425
+ ("HFSR", _HFSR_BITS, "No active error flags"),
426
+ ("DFSR", _DFSR_BITS, "No debug event"),
427
+ )
428
+ for name, table, empty in status_registers:
429
+ value = get(name)
430
+ if value is not None:
431
+ decoded_flags = _decode_flags(value, table)
432
+ rows.append(
433
+ DiagnosticTableRow(
434
+ (
435
+ name,
436
+ f"0x{value:08X}",
437
+ "\n".join(f"{key}: {text}" for key, text in decoded_flags) or empty,
438
+ )
439
+ )
440
+ )
441
+ mmfar, bfar = get("MMFAR"), get("BFAR")
442
+ if mmfar is not None:
443
+ rows.append(
444
+ DiagnosticTableRow(
445
+ (
446
+ "MMFAR",
447
+ f"0x{mmfar:08X}",
448
+ f"{'[VALID]' if cfsr is not None and cfsr & 0x80 else '[INVALID]'} "
449
+ f"Region: {_memory_region(mmfar)}",
450
+ )
451
+ )
452
+ )
453
+ if bfar is not None:
454
+ rows.append(
455
+ DiagnosticTableRow(
456
+ (
457
+ "BFAR",
458
+ f"0x{bfar:08X}",
459
+ f"{'[VALID]' if cfsr is not None and cfsr & (1 << 15) else '[INVALID]'} "
460
+ f"Region: {_memory_region(bfar)}",
461
+ )
462
+ )
463
+ )
464
+ shcsr = get("SHCSR")
465
+ if shcsr is not None:
466
+ enabled = [
467
+ name
468
+ for bit, name in (
469
+ (16, "MemManage"),
470
+ (17, "BusFault"),
471
+ (18, "UsageFault"),
472
+ (19, "SecureFault"),
473
+ )
474
+ if shcsr & (1 << bit)
475
+ ]
476
+ rows.append(
477
+ DiagnosticTableRow(
478
+ (
479
+ "SHCSR",
480
+ f"0x{shcsr:08X}",
481
+ "Enabled configurable handlers: "
482
+ + (
483
+ ", ".join(enabled)
484
+ if enabled
485
+ else "None (direct escalation to HardFault)"
486
+ ),
487
+ )
488
+ )
489
+ )
490
+ vtor = get("VTOR")
491
+ if vtor is not None:
492
+ rows.append(
493
+ DiagnosticTableRow(
494
+ ("VTOR", f"0x{vtor:08X}", f"Vector table @ {_memory_region(vtor)}")
495
+ )
496
+ )
497
+ sfsr = get("SFSR")
498
+ if sfsr is not None and sfsr not in (0, 0xFFFFFFFF):
499
+ decoded_flags = _decode_flags(sfsr, _SFSR_BITS)
500
+ rows.append(
501
+ DiagnosticTableRow(
502
+ (
503
+ "SFSR",
504
+ f"0x{sfsr:08X}",
505
+ "\n".join(f"{key}: {text}" for key, text in decoded_flags),
506
+ )
507
+ )
508
+ )
509
+ sfar = get("SFAR")
510
+ if sfar is not None and sfsr & (1 << 6):
511
+ rows.append(
512
+ DiagnosticTableRow(
513
+ ("SFAR", f"0x{sfar:08X}", f"[VALID] Region: {_memory_region(sfar)}")
514
+ )
515
+ )
516
+ return DiagnosticTable(
517
+ "SCB (System Control Block) Status Registers",
518
+ ("Register", "Value", "Active Flags & Meaning"),
519
+ tuple(rows),
520
+ )
521
+
522
+ @staticmethod
523
+ def _diagnostic_panels(
524
+ scb: SystemRegisterSet,
525
+ frame: StackedFrame | str | None,
526
+ resolve: Callable[[int], str],
527
+ ) -> tuple[DiagnosticPanel, ...]:
528
+ """Derive ordered probable-cause lines from SCB fault information."""
529
+ get: Callable[[str], int | None] = lambda name: CortexMFaultCollector._scb_value(scb, name)
530
+ cfsr, hfsr = get("CFSR") or 0, get("HFSR") or 0
531
+ bfar, mmfar, cpacr = get("BFAR"), get("MMFAR"), get("CPACR")
532
+ mmfsr, bfsr, ufsr = cfsr & 0xFF, (cfsr >> 8) & 0xFF, (cfsr >> 16) & 0xFFFF
533
+ lines: list[str] = []
534
+ if hfsr & (1 << 30):
535
+ source = (
536
+ "BusFault"
537
+ if bfsr
538
+ else "MemManage Fault" if mmfsr else "An UsageFault" if ufsr else None
539
+ )
540
+ lines.append(
541
+ "• HardFault escalation: "
542
+ + (
543
+ f"A {source} was forced to HardFault (source handler disabled in SHCSR)."
544
+ if source is not None
545
+ else "Forced HardFault without configurable cause bits (handler masked by PRIMASK/FAULTMASK)."
546
+ )
547
+ )
548
+ if bfsr & (1 << 1):
549
+ if bfsr & (1 << 7) and bfar is not None:
550
+ if bfar < 0x1000:
551
+ lines.append(
552
+ f"• NULL pointer dereference: Invalid memory access at 0x{bfar:08X} ({_memory_region(bfar)})."
553
+ )
554
+ elif 0x40000000 <= bfar < 0x60000000:
555
+ lines.append(
556
+ f"• Peripheral bus error (0x{bfar:08X}): Verify that the peripheral clock (RCC/PCLK) is enabled before any access."
557
+ )
558
+ else:
559
+ lines.append(
560
+ f"• Precise BusFault at 0x{bfar:08X}: Invalid memory access in {_memory_region(bfar)} region."
561
+ )
562
+ elif bfsr & (1 << 2):
563
+ lines.append(
564
+ "• Imprecise (asynchronous) BusFault: Caused by a write buffer. The stacked PC is downstream of the actual faulting access. To locate the exact access, temporarily disable write buffering (e.g. ACTLR.DISDEFWBUF)."
565
+ )
566
+ if bfsr & 1:
567
+ lines.append(
568
+ "• Instruction Fetch BusFault: Attempted execution from an invalid or inaccessible memory region (corrupted function pointer, overwritten vtable)."
569
+ )
570
+ if bfsr & (1 << 4):
571
+ lines.append(
572
+ "• Bus error during Stacking: The stack (MSP/PSP) overflowed or points to invalid memory."
573
+ )
574
+ if bfsr & (1 << 3):
575
+ lines.append("• Bus error during Unstacking: Corrupted stack upon exception return.")
576
+ if mmfsr & (1 << 1):
577
+ address = f" at 0x{mmfar:08X}" if mmfsr & (1 << 7) and mmfar is not None else ""
578
+ lines.append(
579
+ f"• MPU violation on data{address}: Access prohibited by MPU region permissions."
580
+ )
581
+ if mmfsr & 1:
582
+ lines.append(
583
+ "• MPU violation on instruction: Attempted execution in an MPU region marked eXecute-Never (XN)."
584
+ )
585
+ for bit, line in (
586
+ (
587
+ 0,
588
+ "• Undefined instruction (UNDEFINSTR): Unknown or corrupted opcode (branching into data/NULL or incorrect instruction alignment).",
589
+ ),
590
+ (
591
+ 1,
592
+ "• Invalid Thumb state (INVSTATE): Branch to an even address (bit T=0). On Cortex-M, function pointers must have the least significant bit (LSB) set to 1.",
593
+ ),
594
+ (
595
+ 2,
596
+ "• Invalid EXC_RETURN (INVPC): Illegal exception return value loaded into PC (LR or stack corruption).",
597
+ ),
598
+ (
599
+ 4,
600
+ "• Hardware stack overflow (STKOF): Stack pointer exceeded configured limit (MSPLIM/PSPLIM).",
601
+ ),
602
+ (8, "• Unaligned access (UNALIGNED): Unaligned access trapped by CCR.UNALIGN_TRP."),
603
+ (9, "• Divide by zero (DIVBYZERO): Integer division by zero trapped by CCR.DIV_0_TRP."),
604
+ ):
605
+ if ufsr & (1 << bit):
606
+ lines.append(line)
607
+ if ufsr & (1 << 3):
608
+ lines.append(
609
+ "• Disabled FPU coprocessor (NOCP): FPU instruction executed while FPU is disabled. Add SCB->CPACR |= (0xF << 20); during initialization."
610
+ if cpacr is not None and cpacr & 0x00F00000 != 0x00F00000
611
+ else "• Unimplemented coprocessor access (NOCP): Access to a non-existent or unconfigured coprocessor."
612
+ )
613
+ if hfsr & (1 << 1):
614
+ lines.append(
615
+ "• Vector Table read error (VECTTBL): Vector table is unreadable. Verify VTOR register configuration and Flash memory."
616
+ )
617
+ if isinstance(frame, StackedFrame):
618
+ lines.append(
619
+ f"• Crash location: Instruction at 0x{frame.pc:08X} ({resolve(frame.pc)}), called from 0x{frame.lr:08X} ({resolve(frame.lr)})."
620
+ )
621
+ if not lines:
622
+ lines.append("• No obvious error conditions detected in SCB registers.")
623
+ return (DiagnosticPanel("Diagnostics & Probable Causes", tuple(lines)),)
624
+
625
+ @staticmethod
626
+ def _scb_value(scb: SystemRegisterSet, name: str) -> int | None:
627
+ """Return the available value of a typed SCB register."""
628
+ register = scb.get(name)
629
+ return register.value if register is not None else None
@@ -0,0 +1,60 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Arm target-report models shared by device providers and renderers."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from .coresight import CoreSightDiscovery
11
+ from .cortex_m import CortexMTargetDescription
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class FieldValue:
16
+ """A report field that is either known or explicitly unavailable."""
17
+
18
+ value: str | None = None
19
+ unavailable_reason: str | None = None
20
+
21
+ def __post_init__(self) -> None:
22
+ """Ensure that a field has exactly one state."""
23
+ if (self.value is None) == (self.unavailable_reason is None):
24
+ raise ValueError("a field must have either a value or an unavailable reason")
25
+
26
+ @classmethod
27
+ def known(cls, value: str) -> FieldValue:
28
+ """Create a known report field."""
29
+ return cls(value=value)
30
+
31
+ @classmethod
32
+ def unavailable(cls, reason: str) -> FieldValue:
33
+ """Create a field with an explicit unavailable-state reason."""
34
+ return cls(unavailable_reason=reason)
35
+
36
+ @property
37
+ def is_available(self) -> bool:
38
+ """Whether this field holds a known value."""
39
+ return self.value is not None
40
+
41
+ def display(self) -> str:
42
+ """Return the report-ready field text."""
43
+ if self.value is not None:
44
+ return self.value
45
+ return f"Unavailable: {self.unavailable_reason}"
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class DeviceReport:
50
+ """Arm CPU and optional vendor device information for ``lscpu``."""
51
+
52
+ target: CortexMTargetDescription
53
+ discovery: CoreSightDiscovery
54
+ vendor: str
55
+ product_line: FieldValue
56
+ part_number: FieldValue
57
+ ram: FieldValue
58
+ flash: FieldValue
59
+ package: FieldValue
60
+ serial_number: FieldValue