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,556 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Cortex-M PMSAv7 and PMSAv8 Memory Protection Unit inspection."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from enum import IntEnum, StrEnum, unique
10
+ from typing import TYPE_CHECKING
11
+
12
+ from ...target_memory import TargetReadError, TargetWriteError, WritableTargetMemory
13
+ from ..base import RegisterValue
14
+
15
+ if TYPE_CHECKING:
16
+ from .cortex_m import CortexMTargetDescription
17
+
18
+
19
+ MPU_BASE_ADDRESS = 0xE000ED90
20
+
21
+
22
+ @unique
23
+ class MpuArchitecture(StrEnum):
24
+ """Protected Memory System Architecture versions supported by this module."""
25
+
26
+ PMSA_V7 = "PMSAv7"
27
+ PMSA_V8 = "PMSAv8"
28
+
29
+
30
+ @unique
31
+ class MpuCommonRegister(IntEnum):
32
+ """MPU register offsets shared by PMSAv7 and PMSAv8."""
33
+
34
+ TYPE = 0x000
35
+ CTRL = 0x004
36
+ RNR = 0x008
37
+ RBAR = 0x00C
38
+
39
+
40
+ @unique
41
+ class MpuV7Register(IntEnum):
42
+ """PMSAv7-specific MPU register offsets."""
43
+
44
+ RASR = 0x010
45
+
46
+
47
+ @unique
48
+ class MpuV8Register(IntEnum):
49
+ """PMSAv8-specific MPU register offsets."""
50
+
51
+ RLAR = 0x010
52
+ RBAR_A1 = 0x014
53
+ RLAR_A1 = 0x018
54
+ RBAR_A2 = 0x01C
55
+ RLAR_A2 = 0x020
56
+ RBAR_A3 = 0x024
57
+ RLAR_A3 = 0x028
58
+ MAIR0 = 0x030
59
+ MAIR1 = 0x034
60
+
61
+
62
+ @unique
63
+ class MpuAccessPermission(StrEnum):
64
+ """Normalized MPU access permissions."""
65
+
66
+ NO_ACCESS = "no-access"
67
+ PRIVILEGED_READ_WRITE = "privileged-read-write"
68
+ PRIVILEGED_READ_WRITE_USER_READ = "privileged-read-write-user-read"
69
+ READ_WRITE = "read-write"
70
+ PRIVILEGED_READ_ONLY = "privileged-read-only"
71
+ READ_ONLY = "read-only"
72
+ RESERVED = "reserved"
73
+
74
+
75
+ @unique
76
+ class MpuMemoryType(StrEnum):
77
+ """Memory types represented by PMSA MPU attributes."""
78
+
79
+ STRONGLY_ORDERED = "strongly-ordered"
80
+ DEVICE = "device"
81
+ NORMAL = "normal"
82
+ UNKNOWN = "unknown"
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class MpuDescription:
87
+ """Static MPU interface description for one Cortex-M core."""
88
+
89
+ architecture: MpuArchitecture
90
+ registers: tuple[MpuCommonRegister | MpuV7Register | MpuV8Register, ...]
91
+ supports_pxn: bool
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class MpuMemoryAttributes:
96
+ """Decoded memory-type and cacheability attributes for one MPU region."""
97
+
98
+ memory_type: MpuMemoryType
99
+ shareable: bool | None
100
+ cacheable: bool | None
101
+ bufferable: bool | None
102
+ attribute_index: int | None
103
+ raw: int
104
+ description: str
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class MpuRegion:
109
+ """One decoded MPU region configuration."""
110
+
111
+ index: int
112
+ enabled: bool
113
+ start_address: int
114
+ end_address: int | None
115
+ size_bytes: int | None
116
+ access: MpuAccessPermission
117
+ privileged_executable: bool
118
+ unprivileged_executable: bool
119
+ attributes: MpuMemoryAttributes
120
+ subregion_disable_mask: int | None
121
+ raw_rbar: int
122
+ raw_attributes: int
123
+
124
+ @property
125
+ def executable(self) -> bool:
126
+ """Whether either privileged or unprivileged code can execute the region."""
127
+ return self.privileged_executable or self.unprivileged_executable
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class MpuRegionResult:
132
+ """A decoded MPU region or its explicit selection/read failure."""
133
+
134
+ index: int
135
+ region: MpuRegion | None = None
136
+ unavailable_reason: str | None = None
137
+
138
+ def __post_init__(self) -> None:
139
+ """Require exactly one region result state."""
140
+ if (self.region is None) == (self.unavailable_reason is None):
141
+ raise ValueError("an MPU region result must be available or unavailable")
142
+
143
+ @property
144
+ def is_available(self) -> bool:
145
+ """Whether the selected region was read and decoded."""
146
+ return self.region is not None
147
+
148
+ @classmethod
149
+ def unavailable(cls, index: int, reason: str) -> MpuRegionResult:
150
+ """Create an unavailable region result."""
151
+ return cls(index=index, unavailable_reason=reason)
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class MpuDump:
156
+ """MPU configuration and decoded region results for one Cortex-M target."""
157
+
158
+ description: MpuDescription | None
159
+ registers: tuple[RegisterValue, ...]
160
+ region_count: int | None
161
+ regions: tuple[MpuRegionResult, ...]
162
+ unavailable_reason: str | None = None
163
+ restore_error: str | None = None
164
+
165
+ @property
166
+ def is_available(self) -> bool:
167
+ """Whether the target's MPU configuration was inspected."""
168
+ return self.unavailable_reason is None
169
+
170
+ @classmethod
171
+ def unavailable(
172
+ cls,
173
+ description: MpuDescription | None,
174
+ reason: str,
175
+ registers: tuple[RegisterValue, ...] = (),
176
+ ) -> MpuDump:
177
+ """Create an explicit unavailable MPU inspection result."""
178
+ return cls(
179
+ description=description,
180
+ registers=registers,
181
+ region_count=None,
182
+ regions=(),
183
+ unavailable_reason=reason,
184
+ )
185
+
186
+ def get_register(self, name: str) -> RegisterValue | None:
187
+ """Find a captured MPU register by case-insensitive name."""
188
+ normalized_name = name.upper()
189
+ for register in self.registers:
190
+ if register.name.upper() == normalized_name:
191
+ return register
192
+ return None
193
+
194
+
195
+ PMSA_V7_MPU = MpuDescription(
196
+ MpuArchitecture.PMSA_V7,
197
+ (
198
+ MpuCommonRegister.TYPE,
199
+ MpuCommonRegister.CTRL,
200
+ MpuCommonRegister.RNR,
201
+ MpuCommonRegister.RBAR,
202
+ MpuV7Register.RASR,
203
+ ),
204
+ False,
205
+ )
206
+ PMSA_V8_MPU = MpuDescription(
207
+ MpuArchitecture.PMSA_V8,
208
+ (
209
+ MpuCommonRegister.TYPE,
210
+ MpuCommonRegister.CTRL,
211
+ MpuCommonRegister.RNR,
212
+ MpuCommonRegister.RBAR,
213
+ MpuV8Register.RLAR,
214
+ MpuV8Register.RBAR_A1,
215
+ MpuV8Register.RLAR_A1,
216
+ MpuV8Register.RBAR_A2,
217
+ MpuV8Register.RLAR_A2,
218
+ MpuV8Register.RBAR_A3,
219
+ MpuV8Register.RLAR_A3,
220
+ MpuV8Register.MAIR0,
221
+ MpuV8Register.MAIR1,
222
+ ),
223
+ False,
224
+ )
225
+ PMSA_V8_PXN_MPU = MpuDescription(
226
+ MpuArchitecture.PMSA_V8,
227
+ (
228
+ MpuCommonRegister.TYPE,
229
+ MpuCommonRegister.CTRL,
230
+ MpuCommonRegister.RNR,
231
+ MpuCommonRegister.RBAR,
232
+ MpuV8Register.RLAR,
233
+ MpuV8Register.RBAR_A1,
234
+ MpuV8Register.RLAR_A1,
235
+ MpuV8Register.RBAR_A2,
236
+ MpuV8Register.RLAR_A2,
237
+ MpuV8Register.RBAR_A3,
238
+ MpuV8Register.RLAR_A3,
239
+ MpuV8Register.MAIR0,
240
+ MpuV8Register.MAIR1,
241
+ ),
242
+ True,
243
+ )
244
+
245
+
246
+ def dump_mpu_regions(reader: WritableTargetMemory, target: CortexMTargetDescription) -> MpuDump:
247
+ """Read and decode MPU region configuration for a known Cortex-M core.
248
+
249
+ The function writes each region index to ``MPU_RNR`` and restores the
250
+ target's original selector value before returning. Region read failures are
251
+ retained per region; a selector restoration failure is retained in
252
+ :attr:`MpuDump.restore_error`.
253
+
254
+ Parameters
255
+ ----------
256
+ reader
257
+ Target-memory reader and writer used for MPU registers.
258
+ target
259
+ Known Cortex-M target description returned by :func:`decode_cpuid`.
260
+
261
+ Returns
262
+ -------
263
+ MpuDump
264
+ MPU register values, decoded regions, or an explicit unavailable reason.
265
+ """
266
+ description = target.core.mpu if target.core is not None else None
267
+ if description is None:
268
+ return MpuDump.unavailable(
269
+ None,
270
+ "this Cortex-M core has no PMSAv7 or PMSAv8 MPU description",
271
+ )
272
+
273
+ registers: list[RegisterValue] = []
274
+ mpu_type = _read_register(reader, MpuCommonRegister.TYPE)
275
+ registers.append(mpu_type)
276
+ if not mpu_type.is_available:
277
+ assert mpu_type.unavailable_reason is not None
278
+ return MpuDump.unavailable(description, mpu_type.unavailable_reason, tuple(registers))
279
+
280
+ assert mpu_type.value is not None
281
+ region_count = (mpu_type.value >> 8) & 0xFF
282
+ mpu_control = _read_register(reader, MpuCommonRegister.CTRL)
283
+ registers.append(mpu_control)
284
+ if region_count == 0:
285
+ return MpuDump(description, tuple(registers), 0, ())
286
+
287
+ mair_values: tuple[RegisterValue, RegisterValue] | None = None
288
+ if description.architecture is MpuArchitecture.PMSA_V8:
289
+ mair0 = _read_register(reader, MpuV8Register.MAIR0)
290
+ mair1 = _read_register(reader, MpuV8Register.MAIR1)
291
+ registers.extend((mair0, mair1))
292
+ mair_values = (mair0, mair1)
293
+
294
+ original_rnr = _read_register(reader, MpuCommonRegister.RNR)
295
+ registers.append(original_rnr)
296
+ if not original_rnr.is_available:
297
+ assert original_rnr.unavailable_reason is not None
298
+ return MpuDump.unavailable(description, original_rnr.unavailable_reason, tuple(registers))
299
+
300
+ assert original_rnr.value is not None
301
+ regions: list[MpuRegionResult] = []
302
+ restore_error: str | None = None
303
+ try:
304
+ for index in range(region_count):
305
+ try:
306
+ reader.write_uint32(MPU_BASE_ADDRESS + MpuCommonRegister.RNR, index)
307
+ except TargetWriteError as error:
308
+ regions.append(MpuRegionResult.unavailable(index, str(error)))
309
+ continue
310
+
311
+ regions.append(_read_region(reader, description, index, mair_values))
312
+ finally:
313
+ try:
314
+ reader.write_uint32(MPU_BASE_ADDRESS + MpuCommonRegister.RNR, original_rnr.value)
315
+ except TargetWriteError as error:
316
+ restore_error = str(error)
317
+
318
+ return MpuDump(
319
+ description,
320
+ tuple(registers),
321
+ region_count,
322
+ tuple(regions),
323
+ restore_error=restore_error,
324
+ )
325
+
326
+
327
+ def _read_register(
328
+ reader: WritableTargetMemory,
329
+ register: MpuCommonRegister | MpuV7Register | MpuV8Register,
330
+ ) -> RegisterValue:
331
+ """Read one MPU register while preserving a target access failure."""
332
+ address = MPU_BASE_ADDRESS + register.value
333
+ try:
334
+ value = reader.read_uint32(address)
335
+ except TargetReadError as error:
336
+ return RegisterValue.unavailable(register.name, address, 32, str(error))
337
+ return RegisterValue.known(register.name, address, 32, value)
338
+
339
+
340
+ def _read_region(
341
+ reader: WritableTargetMemory,
342
+ description: MpuDescription,
343
+ index: int,
344
+ mair_values: tuple[RegisterValue, RegisterValue] | None,
345
+ ) -> MpuRegionResult:
346
+ """Read and decode the currently selected MPU region."""
347
+ try:
348
+ rbar = reader.read_uint32(MPU_BASE_ADDRESS + MpuCommonRegister.RBAR)
349
+ attributes_offset = (
350
+ MpuV7Register.RASR
351
+ if description.architecture is MpuArchitecture.PMSA_V7
352
+ else MpuV8Register.RLAR
353
+ )
354
+ raw_attributes = reader.read_uint32(MPU_BASE_ADDRESS + attributes_offset)
355
+ except TargetReadError as error:
356
+ return MpuRegionResult.unavailable(index, str(error))
357
+
358
+ if description.architecture is MpuArchitecture.PMSA_V7:
359
+ return MpuRegionResult(
360
+ index=index,
361
+ region=_decode_pmsa_v7_region(index, rbar, raw_attributes),
362
+ )
363
+ assert mair_values is not None
364
+ return MpuRegionResult(
365
+ index=index,
366
+ region=_decode_pmsa_v8_region(description, index, rbar, raw_attributes, mair_values),
367
+ )
368
+
369
+
370
+ def _decode_pmsa_v7_region(index: int, rbar: int, rasr: int) -> MpuRegion:
371
+ """Decode a PMSAv7 ``RBAR``/``RASR`` region pair."""
372
+ size_encoding = (rasr >> 1) & 0x1F
373
+ size_bytes = 1 << (size_encoding + 1) if size_encoding >= 4 else None
374
+ start_address = rbar & 0xFFFFFFE0
375
+ end_address = start_address + size_bytes - 1 if size_bytes is not None else None
376
+ ap = (rasr >> 24) & 0x7
377
+ tex = (rasr >> 19) & 0x7
378
+ shareable = bool(rasr & (1 << 18))
379
+ cacheable = bool(rasr & (1 << 17))
380
+ bufferable = bool(rasr & (1 << 16))
381
+ execute_never = bool(rasr & (1 << 28))
382
+ return MpuRegion(
383
+ index=index,
384
+ enabled=bool(rasr & 0x1),
385
+ start_address=start_address,
386
+ end_address=end_address,
387
+ size_bytes=size_bytes,
388
+ access=_decode_pmsa_v7_access(ap),
389
+ privileged_executable=not execute_never,
390
+ unprivileged_executable=not execute_never,
391
+ attributes=_decode_pmsa_v7_attributes(tex, shareable, cacheable, bufferable, rasr),
392
+ subregion_disable_mask=(rasr >> 8) & 0xFF,
393
+ raw_rbar=rbar,
394
+ raw_attributes=rasr,
395
+ )
396
+
397
+
398
+ def _decode_pmsa_v8_region(
399
+ description: MpuDescription,
400
+ index: int,
401
+ rbar: int,
402
+ rlar: int,
403
+ mair_values: tuple[RegisterValue, RegisterValue],
404
+ ) -> MpuRegion:
405
+ """Decode a PMSAv8 ``RBAR``/``RLAR`` region pair."""
406
+ attribute_index = (rlar >> 1) & 0x7
407
+ start_address = rbar & 0xFFFFFFE0
408
+ end_address = (rlar & 0xFFFFFFE0) | 0x1F
409
+ size_bytes = end_address - start_address + 1 if end_address >= start_address else None
410
+ execute_never = bool(rbar & 0x1)
411
+ privileged_execute_never = description.supports_pxn and bool(rlar & (1 << 4))
412
+ return MpuRegion(
413
+ index=index,
414
+ enabled=bool(rlar & 0x1),
415
+ start_address=start_address,
416
+ end_address=end_address,
417
+ size_bytes=size_bytes,
418
+ access=_decode_pmsa_v8_access((rbar >> 1) & 0x3),
419
+ privileged_executable=not (execute_never or privileged_execute_never),
420
+ unprivileged_executable=not execute_never,
421
+ attributes=_decode_pmsa_v8_attributes(
422
+ attribute_index,
423
+ ((rbar >> 3) & 0x3) in (0b10, 0b11),
424
+ mair_values,
425
+ ),
426
+ subregion_disable_mask=None,
427
+ raw_rbar=rbar,
428
+ raw_attributes=rlar,
429
+ )
430
+
431
+
432
+ def _decode_pmsa_v7_access(ap: int) -> MpuAccessPermission:
433
+ """Decode PMSAv7 three-bit access permission fields."""
434
+ permissions = {
435
+ 0b000: MpuAccessPermission.NO_ACCESS,
436
+ 0b001: MpuAccessPermission.PRIVILEGED_READ_WRITE,
437
+ 0b010: MpuAccessPermission.PRIVILEGED_READ_WRITE_USER_READ,
438
+ 0b011: MpuAccessPermission.READ_WRITE,
439
+ 0b101: MpuAccessPermission.PRIVILEGED_READ_ONLY,
440
+ 0b110: MpuAccessPermission.READ_ONLY,
441
+ }
442
+ return permissions.get(ap, MpuAccessPermission.RESERVED)
443
+
444
+
445
+ def _decode_pmsa_v8_access(ap: int) -> MpuAccessPermission:
446
+ """Decode PMSAv8 two-bit access permission fields."""
447
+ permissions = {
448
+ 0b00: MpuAccessPermission.PRIVILEGED_READ_WRITE,
449
+ 0b01: MpuAccessPermission.READ_WRITE,
450
+ 0b10: MpuAccessPermission.PRIVILEGED_READ_ONLY,
451
+ 0b11: MpuAccessPermission.READ_ONLY,
452
+ }
453
+ return permissions[ap]
454
+
455
+
456
+ def _decode_pmsa_v7_attributes(
457
+ tex: int,
458
+ shareable: bool,
459
+ cacheable: bool,
460
+ bufferable: bool,
461
+ rasr: int,
462
+ ) -> MpuMemoryAttributes:
463
+ """Decode the PMSAv7 TEX/S/C/B memory attribute fields."""
464
+ if tex == 0 and not cacheable and not bufferable:
465
+ return MpuMemoryAttributes(
466
+ MpuMemoryType.STRONGLY_ORDERED,
467
+ True,
468
+ False,
469
+ False,
470
+ None,
471
+ rasr,
472
+ "strongly ordered",
473
+ )
474
+ if (tex == 0 and not cacheable and bufferable) or (tex == 2 and not cacheable):
475
+ return MpuMemoryAttributes(
476
+ MpuMemoryType.DEVICE,
477
+ shareable,
478
+ False,
479
+ bufferable,
480
+ None,
481
+ rasr,
482
+ "device memory",
483
+ )
484
+ if tex in (0, 1) or tex >= 4:
485
+ return MpuMemoryAttributes(
486
+ MpuMemoryType.NORMAL,
487
+ shareable,
488
+ cacheable,
489
+ bufferable,
490
+ None,
491
+ rasr,
492
+ "normal memory",
493
+ )
494
+ return MpuMemoryAttributes(
495
+ MpuMemoryType.UNKNOWN,
496
+ shareable,
497
+ cacheable,
498
+ bufferable,
499
+ None,
500
+ rasr,
501
+ "reserved PMSAv7 TEX/S/C/B encoding",
502
+ )
503
+
504
+
505
+ def _decode_pmsa_v8_attributes(
506
+ attribute_index: int,
507
+ shareable: bool,
508
+ mair_values: tuple[RegisterValue, RegisterValue],
509
+ ) -> MpuMemoryAttributes:
510
+ """Resolve a PMSAv8 region attribute index through MAIR."""
511
+ mair_register = mair_values[attribute_index // 4]
512
+ if not mair_register.is_available:
513
+ assert mair_register.unavailable_reason is not None
514
+ return MpuMemoryAttributes(
515
+ MpuMemoryType.UNKNOWN,
516
+ shareable,
517
+ None,
518
+ None,
519
+ attribute_index,
520
+ 0,
521
+ f"{mair_register.name} unavailable: {mair_register.unavailable_reason}",
522
+ )
523
+
524
+ assert mair_register.value is not None
525
+ attribute = (mair_register.value >> ((attribute_index % 4) * 8)) & 0xFF
526
+ outer = attribute >> 4
527
+ inner = attribute & 0xF
528
+ if outer == 0 and inner in (0x0, 0x4, 0x8, 0xC):
529
+ return MpuMemoryAttributes(
530
+ MpuMemoryType.DEVICE,
531
+ shareable,
532
+ False,
533
+ bool(inner & 0x4),
534
+ attribute_index,
535
+ attribute,
536
+ "device memory",
537
+ )
538
+ if outer != 0:
539
+ return MpuMemoryAttributes(
540
+ MpuMemoryType.NORMAL,
541
+ shareable,
542
+ outer != 0x4 or inner != 0x4,
543
+ None,
544
+ attribute_index,
545
+ attribute,
546
+ "normal memory",
547
+ )
548
+ return MpuMemoryAttributes(
549
+ MpuMemoryType.UNKNOWN,
550
+ shareable,
551
+ None,
552
+ None,
553
+ attribute_index,
554
+ attribute,
555
+ "reserved PMSAv8 MAIR attribute",
556
+ )
@@ -0,0 +1,10 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Arm architecture probe entry points."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from .cortex_m import CortexMProbe
9
+
10
+ DEFAULT_ARM_PROBES = (CortexMProbe(),)
@@ -0,0 +1,16 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Arm device-provider registry and manufacturer implementations."""
5
+
6
+ from .base import DeviceProvider, ProviderRegistry
7
+ from .manufacturers.stm32 import Stm32Provider
8
+
9
+ DEFAULT_PROVIDER_REGISTRY = ProviderRegistry((Stm32Provider(),))
10
+
11
+ __all__ = [
12
+ "DEFAULT_PROVIDER_REGISTRY",
13
+ "DeviceProvider",
14
+ "ProviderRegistry",
15
+ "Stm32Provider",
16
+ ]
@@ -0,0 +1,77 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Manufacturer-agnostic Arm device-provider contracts and dispatch."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Protocol
9
+
10
+ from ..coresight import CoreSightDiscovery
11
+ from ..cortex_m import CortexMTargetDescription
12
+ from ..models import DeviceReport, FieldValue
13
+ from ....target_memory import TargetMemory
14
+
15
+
16
+ class DeviceProvider(Protocol):
17
+ """A manufacturer-specific device recognizer."""
18
+
19
+ def inspect(
20
+ self,
21
+ reader: TargetMemory,
22
+ target: CortexMTargetDescription,
23
+ discovery: CoreSightDiscovery,
24
+ ) -> DeviceReport | None:
25
+ """Return a report if the provider recognizes the target."""
26
+ ...
27
+
28
+
29
+ class ProviderRegistry:
30
+ """Apply device providers in a defined order with a generic fallback."""
31
+
32
+ def __init__(self, providers: tuple[DeviceProvider, ...]) -> None:
33
+ """Create a registry from providers evaluated in tuple order."""
34
+ self._providers = providers
35
+
36
+ def inspect(
37
+ self,
38
+ reader: TargetMemory,
39
+ target: CortexMTargetDescription,
40
+ discovery: CoreSightDiscovery,
41
+ ) -> DeviceReport:
42
+ """Return the first matching provider report or a generic fallback."""
43
+ for provider in self._providers:
44
+ report = provider.inspect(reader, target, discovery)
45
+ if report is not None:
46
+ return report
47
+ return _generic_report(target, discovery)
48
+
49
+
50
+ def _generic_report(
51
+ target: CortexMTargetDescription,
52
+ discovery: CoreSightDiscovery,
53
+ ) -> DeviceReport:
54
+ """Return a useful report when no registered manufacturer matched."""
55
+ if discovery.mcu_rom.table is None:
56
+ assert discovery.mcu_rom.unavailable_reason is not None
57
+ reason = f"MCU-ROM identity unavailable: {discovery.mcu_rom.unavailable_reason}"
58
+ else:
59
+ peripheral_id = discovery.mcu_rom.table.identity.peripheral_id
60
+ if peripheral_id.jep106 is None:
61
+ reason = "MCU-ROM root does not advertise a JEDEC manufacturer identity"
62
+ else:
63
+ reason = (
64
+ "no registered vendor profile matches MCU-ROM "
65
+ f"{peripheral_id.jep106.display()}, part 0x{peripheral_id.part_number:03X}"
66
+ )
67
+ return DeviceReport(
68
+ target=target,
69
+ discovery=discovery,
70
+ vendor="Generic Cortex-M",
71
+ product_line=FieldValue.unavailable(reason),
72
+ part_number=FieldValue.unavailable(reason),
73
+ ram=FieldValue.unavailable(reason),
74
+ flash=FieldValue.unavailable(reason),
75
+ package=FieldValue.unavailable(reason),
76
+ serial_number=FieldValue.unavailable(reason),
77
+ )
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2026 H2Lab Development Team
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Arm device manufacturer provider implementations."""