1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
|
# sysview v2 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make SEGGER SystemView a first-class TinyUSB analysis feature — leveled in-tree instrumentation (`CFG_TUD_SYSVIEW`/`CFG_TUH_SYSVIEW` = 0–4, like `CFG_TUSB_DEBUG`) for USB task/ISR/per-function timing plus heap/stack metrics, a pinned `get_deps` dependency, machine-readable reports, post-mortem crash capture, and an OpenOCD capture route.
**Architecture:** Target-side instrumentation moves from skill scripts into `src/common/tusb_sysview.[ch]` behind `CFG_TUD_SYSVIEW`/`CFG_TUH_SYSVIEW` levels (default 0 = off, zero cost; 1 = USB ISR, 2 = +usbd/usbh, 3 = +dcd/hcd API at the usbd/usbh call sites, 4 = +class drivers), hooked at `tusb_rhport_init` (init) and `tusb_int_handler` (ISR) so **no application edits are needed** — `cmake -DSYSVIEW=<level>` is the entire instrumentation step. Host side keeps the validated v1 recorder (GUI live recorder under Xvfb) and gains a post-mortem dump script and an OpenOCD raw-capture recipe, both feeding the existing `--from-raw` decode. The reporter grows function/stack/heap/marker/blocked-time tables and `--json`.
**Tech Stack:** C99 (TinyUSB style), CMake (`hw/bsp/family_support.cmake`), SEGGER SystemView target sources V4.12.0 (github.com/SEGGERMicro/SystemView) + vendored `lib/SEGGER_RTT`, Python 3 host scripts, same54_xplained + J-Trace `jtrace` on htpc for validation, stm32h743nucleo (OpenOCD/ST-Link, ci.lan rig) for the OpenOCD route.
## Global Constraints
- Branch: work on `claude/add-systemview-debug` in worktree `.worktrees/add-systemview-debug` (LOCAL — never push). Symlink deps per CLAUDE.md worktree note.
- Commits: imperative mood, **no Co-Authored-By/Claude-Session trailers** (authorship rule). `pre-commit run --files <touched>` before every commit.
- C99, 2-space indent, snake_case, `TU_`/`tusb_` prefixes; headers self-contained.
- `CFG_TUD_SYSVIEW=0`/`CFG_TUH_SYSVIEW=0` (default) must add **zero** code/data — gated by a code-size compare (Task 8).
- Never modify `lib/FreeRTOS-Kernel` or `hw/mcu/*` (vendor trees). `lib/SystemView` is a fetched dep — never edit its contents.
- Hardware: one probe, one client (kill stray `systemview`/`Xvfb`/`JLink*` first); captures to scratchpad or `/tmp/sysview-*` (never `/tmp/sv-*` — SystemView deletes those); pristine reflash + revert any temporary validation edits before finishing a task that flashed hardware.
- Validation board `same54_xplained`: J-Link device `ATSAME54P20`, probe nickname `jtrace`, TTY `/dev/serial/by-id/usb-TinyUSB_TinyUSB_Device_*-if00`. Traffic one-liner (referenced by tasks as “CDC load”):
`stty -F $TTY raw -echo; (timeout 7 cat $TTY >/dev/null &); for i in $(seq 65); do head -c 512 /dev/zero > $TTY; sleep 0.1; done`
- Worktree paths below are relative to the worktree root.
---
### Task 1: `lib/SystemView` as a pinned optional dependency
**Files:**
- Modify: `tools/get_deps.py` (deps_optional dict, alphabetical position after `lib/lwip`)
**Interfaces:**
- Produces: `lib/SystemView/` populated at V4.12.0 (`SYSVIEW/SEGGER_SYSVIEW.c`, `Sample/FreeRTOSV11/…`, `Config/…`). Task 3's CMake requires exactly this path.
- [ ] **Step 1: Add the dependency entry**
In `tools/get_deps.py`, `deps_optional`, after the `'lib/lwip'` entry:
```python
'lib/SystemView': ['https://github.com/SEGGERMicro/SystemView.git',
'92ca7a810c5765ba64911919acd511c61b6b083f',
'all'],
```
(That commit is the `V4.12.0` tag.)
- [ ] **Step 2: Verify the fetch**
Run: `python3 tools/get_deps.py samd5x_e5x` (from the worktree root)
Expected: clones into `lib/SystemView`; then
`ls lib/SystemView/SYSVIEW/SEGGER_SYSVIEW.c lib/SystemView/Sample/FreeRTOSV11/SEGGER_SYSVIEW_FreeRTOS.c` → both exist.
- [ ] **Step 3: Commit**
```bash
git add tools/get_deps.py
git commit -m "add SEGGER SystemView target sources as optional dependency (V4.12.0)"
```
---
### Task 2: In-tree core instrumentation (`tusb_sysview.[ch]` + hooks)
**Files:**
- Create: `src/common/tusb_sysview.h`, `src/common/tusb_sysview.c`
- Modify: `src/tusb_option.h` (add `CFG_TUD_SYSVIEW`/`CFG_TUH_SYSVIEW` defaults), `src/tusb.c:65` (`tusb_rhport_init` — init hook), `src/tusb.c:130` (`tusb_int_handler` — ISR wrap)
**Interfaces:**
- Produces: `tusb_sysview_init(void)`; `TU_SYSVIEW_ISR_ENTER()/TU_SYSVIEW_ISR_EXIT()`; `TUD_SYSVIEW_CALL/RET(level, id)` + `TUH_SYSVIEW_CALL/RET(level, id)` with `tu_sysview_id_t` enum and `CFG_TUSB_SYSVIEW_LEVEL_{ISR,USB,PORT,CLASS}` category levels; `tusb_sysview_stack_report(void)`; `tusb_sysview_heap_alloc(void*, unsigned)` / `tusb_sysview_heap_free(void*)`. All compile to nothing when both config levels are 0; a site compiles only when its level ≤ the configured level (TU_LOG-style token paste). Tasks 3–5 consume these exact names.
- [ ] **Step 1: `tusb_option.h` defaults**
Next to the other `CFG_TUSB_*` defaults. Leveled like `CFG_TUSB_DEBUG`
(0 = off; a site is compiled only when its level ≤ the configured level):
```c
// SEGGER SystemView instrumentation level (like CFG_TUSB_DEBUG):
// 0=off 1=USB ISR 2=+usbd/usbh functions 3=+dcd/hcd API 4=+class driver API
// Requires lib/SystemView (python3 tools/get_deps.py) and a SYSVIEW=<level> build.
#ifndef CFG_TUD_SYSVIEW
#define CFG_TUD_SYSVIEW 0
#endif
#ifndef CFG_TUH_SYSVIEW
#define CFG_TUH_SYSVIEW 0
#endif
```
- [ ] **Step 2: Write `src/common/tusb_sysview.h`**
```c
#ifndef TUSB_SYSVIEW_H_
#define TUSB_SYSVIEW_H_
#include "tusb_option.h"
#include "common/tusb_compiler.h" // TU_XSTRCAT
#define TU_SYSVIEW_ENABLED (CFG_TUD_SYSVIEW || CFG_TUH_SYSVIEW)
#if TU_SYSVIEW_ENABLED
#include "SEGGER_SYSVIEW.h"
// Category levels — overridable, must expand to a literal 1..4
#ifndef CFG_TUSB_SYSVIEW_LEVEL_ISR
#define CFG_TUSB_SYSVIEW_LEVEL_ISR 1 // USB interrupt enter/exit
#endif
#ifndef CFG_TUSB_SYSVIEW_LEVEL_USB
#define CFG_TUSB_SYSVIEW_LEVEL_USB 2 // usbd/usbh core functions
#endif
#ifndef CFG_TUSB_SYSVIEW_LEVEL_PORT
#define CFG_TUSB_SYSVIEW_LEVEL_PORT 3 // dcd/hcd API (wrapped at usbd/usbh call sites)
#endif
#ifndef CFG_TUSB_SYSVIEW_LEVEL_CLASS
#define CFG_TUSB_SYSVIEW_LEVEL_CLASS 4 // class driver API
#endif
// Function-timing event ids: index into the module description string in
// tusb_sysview.c — the two MUST stay in the same order.
typedef enum {
TU_SV_ID_TUD_TASK = 0, // one usbd event processed (level USB)
TU_SV_ID_USBD_XFER, // usbd_edpt_xfer (level USB)
TU_SV_ID_DCD_XFER, // dcd_edpt_xfer call (level PORT)
TU_SV_ID_CDC_FLUSH, // tud_cdc_n_write_flush (level CLASS)
TU_SV_ID_CDC_READ, // tud_cdc_n_read (level CLASS)
TU_SV_ID_MSC_XFER, // mscd_xfer_cb (level CLASS)
TU_SV_ID_TUH_TASK, // one usbh event processed (level USB)
TU_SV_ID_HCD_XFER, // hcd_edpt_xfer call (level PORT)
TU_SV_ID_COUNT
} tu_sysview_id_t;
extern SEGGER_SYSVIEW_MODULE tusb_sysview_module;
void tusb_sysview_init(void);
void tusb_sysview_stack_report(void);
void tusb_sysview_heap_alloc(void* ptr, unsigned size);
void tusb_sysview_heap_free(void* ptr);
// Per-level backends: _TUD_SV_CALL_<n> is live iff CFG_TUD_SYSVIEW >= n.
// TU_LOG-style: TUD_SYSVIEW_CALL(level, id) token-pastes to the backend, so a
// site whose level exceeds the config expands to nothing. The level argument
// is one of the CFG_TUSB_SYSVIEW_LEVEL_* macros (expands to 1..4 first).
#define _TU_SV_RECORD(_id) SEGGER_SYSVIEW_RecordVoid(tusb_sysview_module.EventOffset + (_id))
#define _TU_SV_END(_id) SEGGER_SYSVIEW_RecordEndCall(tusb_sysview_module.EventOffset + (_id))
#if CFG_TUD_SYSVIEW >= 1
#define _TUD_SV_CALL_1(_id) _TU_SV_RECORD(_id)
#define _TUD_SV_RET_1(_id) _TU_SV_END(_id)
#else
#define _TUD_SV_CALL_1(_id)
#define _TUD_SV_RET_1(_id)
#endif
#if CFG_TUD_SYSVIEW >= 2
#define _TUD_SV_CALL_2(_id) _TU_SV_RECORD(_id)
#define _TUD_SV_RET_2(_id) _TU_SV_END(_id)
#else
#define _TUD_SV_CALL_2(_id)
#define _TUD_SV_RET_2(_id)
#endif
#if CFG_TUD_SYSVIEW >= 3
#define _TUD_SV_CALL_3(_id) _TU_SV_RECORD(_id)
#define _TUD_SV_RET_3(_id) _TU_SV_END(_id)
#else
#define _TUD_SV_CALL_3(_id)
#define _TUD_SV_RET_3(_id)
#endif
#if CFG_TUD_SYSVIEW >= 4
#define _TUD_SV_CALL_4(_id) _TU_SV_RECORD(_id)
#define _TUD_SV_RET_4(_id) _TU_SV_END(_id)
#else
#define _TUD_SV_CALL_4(_id)
#define _TUD_SV_RET_4(_id)
#endif
// same 4-block ladder for _TUH_SV_CALL_/RET_ gated on CFG_TUH_SYSVIEW
// (write it out — 16 more lines, identical shape)
#define TUD_SYSVIEW_CALL(_level, _id) TU_XSTRCAT(_TUD_SV_CALL_, _level)(_id)
#define TUD_SYSVIEW_RET(_level, _id) TU_XSTRCAT(_TUD_SV_RET_, _level)(_id)
#define TUH_SYSVIEW_CALL(_level, _id) TU_XSTRCAT(_TUH_SV_CALL_, _level)(_id)
#define TUH_SYSVIEW_RET(_level, _id) TU_XSTRCAT(_TUH_SV_RET_, _level)(_id)
// ISR wrap serves the shared tusb_int_handler entry (device and/or host)
#if (CFG_TUD_SYSVIEW >= CFG_TUSB_SYSVIEW_LEVEL_ISR) || (CFG_TUH_SYSVIEW >= CFG_TUSB_SYSVIEW_LEVEL_ISR)
#define TU_SYSVIEW_ISR_ENTER() SEGGER_SYSVIEW_RecordEnterISR()
#define TU_SYSVIEW_ISR_EXIT() SEGGER_SYSVIEW_RecordExitISR()
#else
#define TU_SYSVIEW_ISR_ENTER()
#define TU_SYSVIEW_ISR_EXIT()
#endif
#else // !TU_SYSVIEW_ENABLED
#define TU_SYSVIEW_ISR_ENTER()
#define TU_SYSVIEW_ISR_EXIT()
#define TUD_SYSVIEW_CALL(_level, _id)
#define TUD_SYSVIEW_RET(_level, _id)
#define TUH_SYSVIEW_CALL(_level, _id)
#define TUH_SYSVIEW_RET(_level, _id)
#define tusb_sysview_init()
#define tusb_sysview_stack_report()
#endif // TU_SYSVIEW_ENABLED
#endif // TUSB_SYSVIEW_H_
```
- [ ] **Step 3: Write `src/common/tusb_sysview.c`**
Port of the validated v1 `SEGGER_SYSVIEW_Config_TinyUSB.c` plus module +
heap/stack helpers. The whole file is inside `#if CFG_TUD_SYSVIEW || CFG_TUH_SYSVIEW`:
```c
#include "tusb_option.h"
#if CFG_TUD_SYSVIEW || CFG_TUH_SYSVIEW
#include "common/tusb_sysview.h"
#if CFG_TUSB_OS == OPT_OS_FREERTOS
#include "FreeRTOS.h"
#include "task.h"
extern const SEGGER_SYSVIEW_OS_API SYSVIEW_X_OS_TraceAPI;
#define SYSVIEW_OS_API (&SYSVIEW_X_OS_TraceAPI)
#define SYSVIEW_OS_DESC ",O=FreeRTOS"
#else
#define SYSVIEW_OS_API 0
#define SYSVIEW_OS_DESC ""
#endif
#ifndef SYSVIEW_APP_NAME
#define SYSVIEW_APP_NAME "TinyUSB"
#endif
#ifndef SYSVIEW_DEVICE_NAME
#define SYSVIEW_DEVICE_NAME "Cortex-M"
#endif
#ifndef SYSVIEW_RAM_BASE
#define SYSVIEW_RAM_BASE (0x20000000)
#endif
extern unsigned int SystemCoreClock;
// Event names, same order as tu_sysview_id_t
SEGGER_SYSVIEW_MODULE tusb_sysview_module = {
"M=TinyUSB,"
"0 tud_task,"
"1 usbd_edpt_xfer,"
"2 dcd_edpt_xfer,"
"3 tud_cdc_write_flush,"
"4 tud_cdc_read,"
"5 mscd_xfer_cb,"
"6 tuh_task,"
"7 hcd_edpt_xfer",
TU_SV_ID_COUNT, 0, NULL, NULL
};
static void send_sys_desc(void) {
SEGGER_SYSVIEW_SendSysDesc("N=" SYSVIEW_APP_NAME ",D=" SYSVIEW_DEVICE_NAME SYSVIEW_OS_DESC);
SEGGER_SYSVIEW_SendSysDesc("I#15=SysTick");
}
void tusb_sysview_init(void) {
static bool inited = false;
if (inited) { return; }
inited = true;
/* DWT cycle counter drives timestamps; enable on-target so recording
* never depends on a debugger-side enable (also needed post-mortem). */
(*(volatile unsigned int*) 0xE000EDFC) |= (1u << 24); /* DEMCR.TRCENA */
(*(volatile unsigned int*) 0xE0001000) |= 1u; /* DWT_CTRL.CYCCNTENA */
SEGGER_SYSVIEW_Init(SystemCoreClock, SystemCoreClock, SYSVIEW_OS_API, send_sys_desc);
SEGGER_SYSVIEW_SetRAMBase(SYSVIEW_RAM_BASE);
SEGGER_SYSVIEW_RegisterModule(&tusb_sysview_module);
SEGGER_SYSVIEW_Start(); /* self-start: host recorders only drain */
}
void tusb_sysview_heap_alloc(void* ptr, unsigned size) {
static bool heap_defined = false;
if (!heap_defined) {
heap_defined = true;
SEGGER_SYSVIEW_HeapDefine(ptr, ptr, size, 0); /* base unknowable here; events still carry ptr+size */
}
SEGGER_SYSVIEW_HeapAlloc(ptr, ptr, size);
}
void tusb_sysview_heap_free(void* ptr) {
SEGGER_SYSVIEW_HeapFree(ptr, ptr);
}
void tusb_sysview_stack_report(void) {
#if CFG_TUSB_OS == OPT_OS_FREERTOS && (configUSE_TRACE_FACILITY == 1)
TaskStatus_t status[8];
UBaseType_t n = uxTaskGetSystemState(status, 8, NULL);
for (UBaseType_t i = 0; i < n; i++) {
SEGGER_SYSVIEW_TASKINFO info = {0};
info.TaskID = (U32)(uintptr_t) status[i].xHandle;
info.sName = status[i].pcTaskName;
info.Prio = status[i].uxCurrentPriority;
info.StackBase = (U32)(uintptr_t) status[i].pxStackBase;
info.StackUsage = status[i].usStackHighWaterMark * sizeof(StackType_t);
SEGGER_SYSVIEW_SendTaskInfo(&info);
}
#endif
}
#endif /* CFG_TUD_SYSVIEW || CFG_TUH_SYSVIEW */
```
Note: `SEGGER_SYSVIEW_TASKINFO` field names must be checked against
`lib/SystemView/SYSVIEW/SEGGER_SYSVIEW.h` when compiling (V4.12 has
`TaskID/sName/Prio/StackBase/StackSize/StackUsage` — use whichever of
StackSize/StackUsage exist; `SendTaskInfo` also emits the STACK_INFO event).
- [ ] **Step 4: Hook init and ISR in `src/tusb.c`**
Top of file, with the other includes: `#include "common/tusb_sysview.h"`.
In `tusb_rhport_init()` (line ~65), first statement of the function body:
```c
tusb_sysview_init(); // no-op unless a SYSVIEW level is set
```
In `tusb_int_handler()` (line ~130), wrap the existing body:
```c
void tusb_int_handler(uint8_t rhport, bool in_isr) {
TU_SYSVIEW_ISR_ENTER();
... existing body unchanged ...
TU_SYSVIEW_ISR_EXIT();
}
```
(If the body has early `return`s, restructure to a single exit so
`TU_SYSVIEW_ISR_EXIT()` always runs — check the actual body first.)
- [ ] **Step 5: Verify disabled build is untouched**
Run: `cd examples/device/cdc_msc_freertos && rm -rf build-off && cmake -B build-off -DBOARD=same54_xplained -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel -DJLINK_OPTION="-USB jtrace" . && cmake --build build-off`
Expected: builds green with NO sysview sources compiled (`grep -c SYSVIEW build-off/.ninja_log` → 0).
- [ ] **Step 6: Commit**
```bash
git add src/common/tusb_sysview.h src/common/tusb_sysview.c src/tusb_option.h src/tusb.c
git commit -m "add leveled CFG_TUD/TUH_SYSVIEW SystemView instrumentation core"
```
---
### Task 3: Build wiring (`SYSVIEW=<level>`) in family_support.cmake + hardware parity run
**Files:**
- Modify: `hw/bsp/family_support.cmake` (inside `family_configure_common`, adjacent to the `LOGGER` rtt block at ~line 480)
- Delete: `.claude/skills/sysview/scripts/sysview.cmake`, `.claude/skills/sysview/scripts/SEGGER_SYSVIEW_Config_TinyUSB.c` (superseded — delete in the SAME commit that lands the replacement)
**Interfaces:**
- Consumes: Task 1 dep path, Task 2 sources/defines.
- Produces: `cmake -DSYSVIEW=<1..4|ON>` as the complete instrumentation build switch (Tasks 4–8 build with it). CMake cache vars honored: `SYSVIEW_BUFFER_SIZE` (default 16384), `SYSVIEW_RAM_BASE` (default 0x20000000).
- [ ] **Step 1: Add the SYSVIEW block**
In `family_configure_common`, after the LOGGER handling:
```cmake
if (SYSVIEW)
if (SYSVIEW STREQUAL "ON")
set(SYSVIEW 4) # bare -DSYSVIEW=ON = full instrumentation
endif ()
if (NOT SYSVIEW MATCHES "^[1-4]$")
message(FATAL_ERROR "SYSVIEW must be 1..4 (1=ISR 2=+usbd/usbh 3=+dcd/hcd 4=+class)")
endif ()
set(SYSVIEW_SRC ${TOP}/lib/SystemView)
if (NOT EXISTS ${SYSVIEW_SRC}/SYSVIEW/SEGGER_SYSVIEW.c)
message(FATAL_ERROR "SYSVIEW needs lib/SystemView: run python3 tools/get_deps.py <family>")
endif ()
if (NOT DEFINED SYSVIEW_BUFFER_SIZE)
set(SYSVIEW_BUFFER_SIZE 16384) # 4 KB overflows under bulk USB load
endif ()
if (NOT DEFINED SYSVIEW_RAM_BASE)
set(SYSVIEW_RAM_BASE 0x20000000)
endif ()
# vendored lib/SEGGER_RTT predates SEGGER_RTT_ConfDefaults.h; bridge it
file(WRITE ${CMAKE_BINARY_DIR}/sysview_shim/SEGGER_RTT_ConfDefaults.h "#include \"SEGGER_RTT_Conf.h\"\n")
target_sources(${TARGET} PUBLIC
${SYSVIEW_SRC}/SYSVIEW/SEGGER_SYSVIEW.c
${TOP}/src/common/tusb_sysview.c
${TOP}/lib/SEGGER_RTT/RTT/SEGGER_RTT_ASM_ARMv7M.S)
if (NOT LOGGER STREQUAL "rtt")
target_sources(${TARGET} PUBLIC ${TOP}/lib/SEGGER_RTT/RTT/SEGGER_RTT.c)
endif ()
target_include_directories(${TARGET} PUBLIC
${SYSVIEW_SRC}/SEGGER ${SYSVIEW_SRC}/SYSVIEW ${SYSVIEW_SRC}/Config
${CMAKE_BINARY_DIR}/sysview_shim
${TOP}/lib/SEGGER_RTT/RTT ${TOP}/lib/SEGGER_RTT/Config)
# vendor sources are not expected to pass TinyUSB's -Werror set
set_source_files_properties(
${SYSVIEW_SRC}/SYSVIEW/SEGGER_SYSVIEW.c
${SYSVIEW_SRC}/Sample/FreeRTOSV11/SEGGER_SYSVIEW_FreeRTOS.c
${TOP}/lib/SEGGER_RTT/RTT/SEGGER_RTT.c
TARGET_DIRECTORY ${TARGET} PROPERTIES COMPILE_OPTIONS "-w")
target_compile_definitions(${TARGET} PUBLIC
CFG_TUD_SYSVIEW=${SYSVIEW}
CFG_TUH_SYSVIEW=${SYSVIEW}
SYSVIEW_APP_NAME="${TARGET}"
SYSVIEW_DEVICE_NAME="${BOARD}"
SYSVIEW_RAM_BASE=${SYSVIEW_RAM_BASE}
SEGGER_SYSVIEW_RTT_BUFFER_SIZE=${SYSVIEW_BUFFER_SIZE}
SYSVIEW_FREERTOS_MAX_NOF_TASKS=16)
if (TARGET freertos_kernel)
target_sources(${TARGET} PUBLIC ${SYSVIEW_SRC}/Sample/FreeRTOSV11/SEGGER_SYSVIEW_FreeRTOS.c)
# freertos_config is the INTERFACE target the kernel and app both see:
# defines + include dirs there make the FreeRTOSConfig.h guarded block work
target_compile_definitions(freertos_config INTERFACE CFG_TUD_SYSVIEW=${SYSVIEW} CFG_TUH_SYSVIEW=${SYSVIEW} SYSVIEW_FREERTOS_MAX_NOF_TASKS=16)
target_include_directories(freertos_config INTERFACE
${SYSVIEW_SRC}/SEGGER ${SYSVIEW_SRC}/SYSVIEW ${SYSVIEW_SRC}/Config
${SYSVIEW_SRC}/Sample/FreeRTOSV11
${CMAKE_BINARY_DIR}/sysview_shim
${TOP}/lib/SEGGER_RTT/RTT ${TOP}/lib/SEGGER_RTT/Config)
endif ()
message(STATUS "SYSVIEW instrumentation enabled (buffer=${SYSVIEW_BUFFER_SIZE})")
endif ()
```
NOTE: until Task 4 adds the FreeRTOSConfig.h guarded include, task events
come only after Task 4 — this task's parity run therefore ALSO adds Task 4's
samd5x_e5x FreeRTOSConfig block if built before Task 4… to avoid that
ordering trap, **run Task 4's Step 1 edit before this task's hardware step**
(tasks 3 and 4 land as separate commits but validate together).
- [ ] **Step 2: Build instrumented**
Run: `python3 tools/get_deps.py samd5x_e5x` (if not yet), then
`cd examples/device/cdc_msc_freertos && rm -rf build-sv && cmake -B build-sv -DBOARD=same54_xplained -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel -DSYSVIEW=4 -DJLINK_OPTION="-USB jtrace" . && cmake --build build-sv`
Expected: green; `arm-none-eabi-nm build-sv/cdc_msc_freertos.elf | grep -c SEGGER_SYSVIEW` ≥ 20.
- [ ] **Step 3: Hardware parity run (the v1 regression test)**
Flash + record + report exactly as the sysview SKILL documents (`ninja -C build-sv cdc_msc_freertos-jlink`, then `sysview_record.py --device ATSAME54P20 --probe jtrace --elf build-sv/cdc_msc_freertos.elf --duration-ms 8000 --out /tmp/sysview-v2 --traffic-cmd "<CDC load>"`, then `sysview_report.py /tmp/sysview-v2`).
Expected: report parity with v1 numbers — cdc ~3–4 % CPU, usbd task present, USB `ISR 96/98/99` ~34 µs p50, overflow 0 — with **zero source edits** (the win this task exists for).
- [ ] **Step 4: Commit (including script deletions)**
```bash
git rm .claude/skills/sysview/scripts/sysview.cmake .claude/skills/sysview/scripts/SEGGER_SYSVIEW_Config_TinyUSB.c
git add hw/bsp/family_support.cmake
git commit -m "wire leveled SYSVIEW build option; retire skill-local cmake injection"
```
---
### Task 4: FreeRTOS stack + heap metrics
**Files:**
- Modify: `hw/bsp/samd5x_e5x/FreeRTOSConfig/FreeRTOSConfig.h` (guarded block at end of file, before the final `#endif`)
- Modify: `src/device/usbd.c` (periodic `tusb_sysview_stack_report()` from the task loop)
- Modify: `.claude/skills/sysview/scripts/sysview_report.py` (stack + heap + blocked-time output)
**Interfaces:**
- Consumes: Task 2's `tusb_sysview_stack_report/heap_alloc/heap_free`, Task 3's `freertos_config` defines.
- Produces: `Stack Information` populated in contexts.csv; `Heap Alloc/Free` rows in events.txt (dynamic builds); reporter prints `stack` table (task, stack high-water bytes) + `heap` line (allocs/frees/net bytes) + `blocked_ms` column.
- [ ] **Step 1: FreeRTOSConfig guarded block (samd5x_e5x)**
At the END of the file, immediately before the closing `#endif`:
```c
/* SEGGER SystemView instrumentation (SYSVIEW=<level> build) — must be last */
#if (defined(CFG_TUD_SYSVIEW) && CFG_TUD_SYSVIEW) || (defined(CFG_TUH_SYSVIEW) && CFG_TUH_SYSVIEW)
#undef INCLUDE_uxTaskPriorityGet
#define INCLUDE_uxTaskPriorityGet 1
#undef INCLUDE_xTaskGetIdleTaskHandle
#define INCLUDE_xTaskGetIdleTaskHandle 1
extern void tusb_sysview_heap_alloc(void* ptr, unsigned size);
extern void tusb_sysview_heap_free(void* ptr);
#define traceMALLOC(pvAddress, uiSize) tusb_sysview_heap_alloc(pvAddress, uiSize)
#define traceFREE(pvAddress, uiSize) tusb_sysview_heap_free(pvAddress)
#include "SEGGER_SYSVIEW_FreeRTOS.h"
#endif
```
- [ ] **Step 2: Periodic stack report from the usbd task**
In `src/device/usbd.c`, inside the `tud_task_ext` event loop (after an event
is dequeued and processed):
```c
#if CFG_TUD_SYSVIEW
{ static uint16_t sv_cnt = 0;
if (0 == (++sv_cnt & 0x3FFu)) { tusb_sysview_stack_report(); } }
#endif
```
(`#include "common/tusb_sysview.h"` with usbd.c's other includes.)
- [ ] **Step 3: Reporter — stack, heap, blocked time**
In `sysview_report.py`: (a) contexts table gains `blk_ms` column from the
existing `Total Blocked Time` CSV field (`parse_time_s`, already imported);
(b) after the ISR table, if events.txt present, parse stack/heap rows.
Format discovery is part of this step: record once (Step 4), inspect the
literal `event` names in events.txt (`Stack Info`, `Heap Alloc`/`Heap
Free` are expected but MUST be read from the actual export), then lock the
parser to what is observed. Output shape:
```
stack high-water bytes_used
usbd 372
cdc 288
...
heap: allocs=N frees=M net_bytes=K (or "heap: no events (static allocation build)")
```
- [ ] **Step 4: Hardware validation (stack; heap absent)**
Rebuild `build-sv`, flash, record 8 s under CDC load, report.
Expected: stack table lists blinky/usbd/cdc/IDLE with plausible high-water
bytes; heap line prints the static-allocation notice; `blk_ms` populated
(cdc mostly blocked); overflow 0; task/ISR numbers unchanged from Task 3.
- [ ] **Step 5: Heap-mapping validation (temporary dynamic build)**
Temporarily set `configSUPPORT_DYNAMIC_ALLOCATION 1` in the samd5x_e5x
FreeRTOSConfig.h, rebuild, flash, record boot (queues/semaphores allocate
at init), report. Expected: `heap: allocs>0`. **Revert the temporary edit**
(`git checkout hw/bsp/samd5x_e5x/FreeRTOSConfig/FreeRTOSConfig.h` would
also revert Step 1 — instead flip the single line back by editing), rebuild,
reflash pristine-instrumented.
- [ ] **Step 6: Commit**
```bash
git add hw/bsp/samd5x_e5x/FreeRTOSConfig/FreeRTOSConfig.h src/device/usbd.c .claude/skills/sysview/scripts/sysview_report.py
git commit -m "sysview: FreeRTOS stack high-water + heap trace mapping + reporter columns"
```
---
### Task 5: Per-function timing (hot functions) + markers/Printf/DisableEvents recipes
**Files:**
- Modify: `src/device/usbd.c` (`TUD_SYSVIEW_CALL/RET` around event processing + `usbd_edpt_xfer`), `src/class/cdc/cdc_device.c` (`tud_cdc_n_write_flush`, `tud_cdc_n_read`), `src/class/msc/msc_device.c` (`mscd_xfer_cb`), `src/host/usbh.c` (`tuh_task_ext` event processing, guarded `#if CFG_TUH_ENABLED`)
- Modify: `.claude/skills/sysview/scripts/sysview_report.py` (function-timing table + marker table)
**Interfaces:**
- Consumes: Task 2 macros/ids.
- Produces: reporter `function` table: name, n, p50/p99/max µs; `marker` table (same shape, from `Mark Start/Stop` pairs).
- [ ] **Step 1: Instrument call sites (leveled)**
Pattern (every site pairs CALL with RET on ALL exit paths — restructure to
single-exit where needed; example for `usbd_edpt_xfer` in usbd.c, level USB,
with the dcd boundary inside it at level PORT):
```c
bool usbd_edpt_xfer(...) {
TUD_SYSVIEW_CALL(CFG_TUSB_SYSVIEW_LEVEL_USB, TU_SV_ID_USBD_XFER);
...
TUD_SYSVIEW_CALL(CFG_TUSB_SYSVIEW_LEVEL_PORT, TU_SV_ID_DCD_XFER);
bool ok = dcd_edpt_xfer(rhport, ep_addr, buffer, total_bytes);
TUD_SYSVIEW_RET(CFG_TUSB_SYSVIEW_LEVEL_PORT, TU_SV_ID_DCD_XFER);
...
TUD_SYSVIEW_RET(CFG_TUSB_SYSVIEW_LEVEL_USB, TU_SV_ID_USBD_XFER);
return ret;
}
```
Sites and their levels (dcd/hcd instrumentation lives at the usbd/usbh call
sites — portable drivers are never edited):
| site | macro/level | id |
|---|---|---|
| `tud_task_ext` per-event processing (around the event switch, NOT the blocking queue wait) | `TUD` / `LEVEL_USB` | `TU_SV_ID_TUD_TASK` |
| `usbd_edpt_xfer` body | `TUD` / `LEVEL_USB` | `TU_SV_ID_USBD_XFER` |
| the `dcd_edpt_xfer(...)` call inside it | `TUD` / `LEVEL_PORT` | `TU_SV_ID_DCD_XFER` |
| `tud_cdc_n_write_flush` | `TUD` / `LEVEL_CLASS` | `TU_SV_ID_CDC_FLUSH` |
| `tud_cdc_n_read` | `TUD` / `LEVEL_CLASS` | `TU_SV_ID_CDC_READ` |
| `mscd_xfer_cb` | `TUD` / `LEVEL_CLASS` | `TU_SV_ID_MSC_XFER` |
| `tuh_task_ext` per-event processing | `TUH` / `LEVEL_USB` | `TU_SV_ID_TUH_TASK` |
| the `hcd_edpt_xfer(...)` call in usbh.c | `TUH` / `LEVEL_PORT` | `TU_SV_ID_HCD_XFER` |
Each file adds `#include "common/tusb_sysview.h"`.
- [ ] **Step 1b: Level-gating compile check**
Build the example twice: `-DSYSVIEW=1` and `-DSYSVIEW=4`.
Expected: both green; the level-1 ELF contains NO `_TU_SV_RECORD`-emitting
sites (`arm-none-eabi-nm build-sv1/*.elf | grep -c RecordVoid` → 0 uses via
module events; simplest observable: level-1 events.txt from a short record
shows ISR events but no `tud_task` module events, level-4 shows both).
- [ ] **Step 2: Reporter — function + marker tables**
Discovery step (same protocol as Task 4 Step 3): record once, read how
module events and `RecordEndCall` render in events.txt (named per the module
description, e.g. `tud_task` + a return/end event), then implement pairing:
per id, duration = end_ts − start_ts within the same context; emit
n/p50/p99/max µs. Markers: pair `Mark Start`/`Mark Stop` per marker id the
same way. If module events export as raw ids (decode risk noted in spec),
fall back to mapping `eventint` (= module EventOffset + id, offset visible
in the `Module Description` row) → name table copied from
`tu_sysview_id_t`.
- [ ] **Step 3: Hardware validation**
Rebuild, flash, record 8 s under CDC load + msc mount, report.
Expected: function table shows `tud_task`, `usbd_edpt_xfer`,
`tud_cdc_write_flush`, `tud_cdc_read` with n>100 and p50 in single-digit to
tens of µs; overflow 0 at 16 KB (if nonzero, that is Task 5's data for the
DisableEvents recipe — record the working mask).
- [ ] **Step 4: Overflow A/B + DisableEvents mask (deterministic, 4 KB)**
Build with `-DSYSVIEW_BUFFER_SIZE=4096`; record under CDC load →
expected: overflow > 0 (reliable at 4 KB, per v1). Then add a temporary
`SEGGER_SYSVIEW_DisableEvents(<mask>)` after init in `tusb_sysview_init`,
determining `<mask>` empirically (start: disable API/module+PLOT classes,
keep task+ISR — masks in `SEGGER_SYSVIEW.h` `SYSVIEW_EVTMASK_*`) until the
same 4 KB run reports overflow 0. Record the final mask as a documented
`SYSVIEW_EVENT_MASK`-style define in tusb_sysview.c (compile-time optional:
`#ifdef SYSVIEW_DISABLE_EVENTS ... #endif`), remove the temporary line.
- [ ] **Step 5: Markers + PrintfHost proof**
Temporarily add to `examples/device/cdc_msc_freertos/src/main.c` cdc_task:
`SEGGER_SYSVIEW_MarkStart(0)/MarkStop(0)` around the echo loop body and one
`SEGGER_SYSVIEW_PrintfHost("cdc rx %u", count);` — rebuild, record with
`--export-terminal`, report. Expected: marker table row with n>0;
terminal.csv contains the printf lines. **Revert the example edit.**
- [ ] **Step 6: Commit**
```bash
git add src/device/usbd.c src/class/cdc/cdc_device.c src/class/msc/msc_device.c src/host/usbh.c src/common/tusb_sysview.c .claude/skills/sysview/scripts/sysview_report.py
git commit -m "sysview: per-function timing instrumentation + marker/function report tables"
```
---
### Task 6: Post-mortem capture (`SYSVIEW_POST_MORTEM=1` + `sysview_dump.py`)
**Files:**
- Modify: `hw/bsp/family_support.cmake` (extend Task 3 block)
- Create: `.claude/skills/sysview/scripts/sysview_dump.py`
**Interfaces:**
- Consumes: Task 3 build block; v1 `sysview_record.py --from-raw` (unchanged).
- Produces: `sysview_dump.py --device X --probe Y --elf Z --out DIR [--resume]` → `DIR/capture.SVDat` (raw, decodable via `--from-raw`).
- [ ] **Step 1: CMake option**
Inside the Task 3 `if (SYSVIEW)` block:
```cmake
if (SYSVIEW_POST_MORTEM)
target_compile_definitions(${TARGET} PUBLIC SEGGER_SYSVIEW_POST_MORTEM_MODE=1)
# SEGGER_SYSVIEW_SYNC_PERIOD_SHIFT stays at its source default (8)
endif ()
```
- [ ] **Step 2: Write `sysview_dump.py`**
Flow (reuse `resolve_probe`/`rtt_cb_from_elf` patterns from
`sysview_record.py` — copy the two helpers, do not import):
1. Resolve probe serial; RTT control block address from `--elf`.
2. One JLinkExe session (`-CommandFile`, NO reset commands — attach, `h`):
`mem32 <cb+0x18+aUp[1] offsets>` to read the channel-1 descriptor
(pBuffer, SizeOfBuffer, WrOff, RdOff — offsets: aUp[] starts at cb+0x18,
each SEGGER_RTT_BUFFER_UP is 6 words: sName,pBuffer,SizeOfBuffer,WrOff,
RdOff,Flags → channel 1 descriptor at cb+0x18+24), verify magic
"SEGGER RTT" at cb first (abort if absent), then
`savebin <tmp>/ring.bin, <pBuffer>, <SizeOfBuffer>`.
3. Linearize BOTH candidates (spec's open question):
A = ring[WrOff:] + ring[:WrOff]; B = ring[WrOff:] + ring[:RdOff].
Write A as `capture.SVDat`, B as `capture_rdoff.SVDat` when RdOff differs.
4. Default leaves the core halted (mid-autopsy); `--resume` sends `g`.
Print WrOff/RdOff/size and which files were written.
- [ ] **Step 3: Hardware validation (split-point discovery)**
Build `-DSYSVIEW=4 -DSYSVIEW_POST_MORTEM=1`, flash, run CDC load ~10 s,
run `sysview_dump.py` mid-load (its halt = the simulated crash), decode
candidate A via `sysview_record.py --from-raw … --no-events`, report.
Expected: contexts table with cdc/usbd/ISRs and sane numbers (leading
pre-sync garbage tolerated). If A fails/garbles wholesale, decode B.
**Record the winner in the dump script (drop the loser) and in the SKILL
(Task 8).** Reflash pristine (non-post-mortem) build after.
- [ ] **Step 4: Commit**
```bash
git add hw/bsp/family_support.cmake .claude/skills/sysview/scripts/sysview_dump.py
git commit -m "sysview: post-mortem mode build option + halted-target dump script"
```
---
### Task 7: OpenOCD capture route (non-SEGGER probes)
**Files:**
- No code — validation + recipe evidence for Task 8's SKILL text.
**Interfaces:**
- Consumes: existing OpenOCD rtt recipes (target-debug SKILL), `--from-raw`.
- Produces: verified command sequence for the SKILL's OpenOCD section.
- [ ] **Step 1: Build for stm32h743nucleo**
`python3 tools/get_deps.py stm32h7`; build `cdc_msc_freertos`
`-DBOARD=stm32h743nucleo -DSYSVIEW=4` (add the Task 4 guarded block to
`hw/bsp/stm32h7/FreeRTOSConfig/FreeRTOSConfig.h` — same text, own commit).
- [ ] **Step 2: Rig validation (ci.lan, lock held)**
Hold `board_lock.py hold stm32h743nucleo --reason "sysview openocd validation"`.
Flash via the board's openocd target. Capture raw channel 1:
```bash
openocd -f interface/stlink-dap.cfg -c 'adapter serial <uid from tinyusb.json>' -f target/stm32h7x.cfg \
-c init -c 'rtt setup 0x24000000 0x80000 "SEGGER RTT"' -c 'rtt start' \
-c 'rtt server start 19333 1' &
timeout 10 nc localhost 19333 > /tmp/sysview-h7/capture.SVDat # drive CDC load meanwhile
```
(RAM base/size from the h743 linker script; rtt re-`start` after reflash.)
Decode: `sysview_record.py --from-raw /tmp/sysview-h7/capture.SVDat --out /tmp/sysview-h7 --no-events` (decode runs on htpc — copy the file over) → report.
Expected: contexts/ISR tables populate; note overflow count (OpenOCD polls —
nonzero overflow under burst is the documented caveat, not a failure).
Release the lock; reflash pristine.
- [ ] **Step 3: Commit (the h7 config block from Step 1)**
```bash
git add hw/bsp/stm32h7/FreeRTOSConfig/FreeRTOSConfig.h
git commit -m "sysview: enable FreeRTOS trace hooks for stm32h7 family"
```
---
### Task 8: `--json` report, SKILL.md rewrite, zero-cost gate, GREEN verification
**Files:**
- Modify: `.claude/skills/sysview/scripts/sysview_report.py` (`--json`)
- Modify: `.claude/skills/sysview/SKILL.md` (build = `-DSYSVIEW=<level>` with the level table, no source edits, function/stack/heap tables, post-mortem, OpenOCD route, DisableEvents mask, updated warnings)
- Modify: `.claude/skills/target-debug/SKILL.md` + `.claude/agents/target-debugger.md` (one-line row updates: sysview needs no source edits now; post-mortem answers "what ran before the crash")
**Interfaces:**
- Consumes: everything above.
- Produces: `sysview_report.py <dir> --json` → single JSON object `{contexts:[{name,activations,cpu_pct,total_ms,min_us,avg_us,max_us,blocked_ms}], isr:[…], ready_run:[…], functions:[…], markers:[…], stack:[…], heap:{allocs,frees,net_bytes}|null, overflow:N}` printed to stdout (tables suppressed) — the future HIL/PR-comment payload.
- [ ] **Step 1: Implement `--json`** (serialize the already-computed tables; `json.dumps(..., indent=1)`; every numeric field a number, not a formatted string).
- [ ] **Step 2: Zero-cost gate** — code-size skill single-example compare:
`python3 tools/metrics_compare_base.py -e device/cdc_msc_freertos -b same54_xplained` (per code-size SKILL) comparing branch (SYSVIEW **off**) vs master.
Expected: 0 flash / 0 RAM delta. Nonzero = a hook leaked outside `#if` — fix before proceeding.
- [ ] **Step 3: Build sweep for touched families** — full example set for
`same54_xplained` and `stm32h743nucleo`, both WITHOUT `SYSVIEW` (default path unchanged) and WITH `-DSYSVIEW=4`. Expected: all green. Plus `pre-commit run --all-files`.
- [ ] **Step 4: Rewrite SKILL.md** — Iron-Law order: the doc is rewritten only now, AFTER every recipe above was hardware-proven; carry over v1's still-true gotchas (recorder internals, `/tmp/sv-*`, license dialog), replace the instrument section ("build with `-DSYSVIEW=<level>` — no source edits; level table 1=ISR 2=+usbd/usbh 3=+dcd/hcd 4=+class"), add function/stack/heap reading guide, post-mortem + OpenOCD sections with the exact validated commands, `--json`.
- [ ] **Step 5: GREEN verification (writing-skills)** — fresh subagent, dry-run scenario: "find the hot TinyUSB functions and check task stacks on same54_xplained during CDC jitter, headless; also: what would you do after a hardfault, and on an ST-Link-only board?" Expected: correct `-DSYSVIEW=<level>` flow with zero source edits (picking level 3+ to see dcd timing), report/`--json` for numbers, post-mortem dump for the crash, OpenOCD raw route for ST-Link. REFACTOR SKILL.md on any gap and re-verify.
- [ ] **Step 6: Final commit + bookkeeping**
```bash
git add .claude/skills/sysview .claude/skills/target-debug/SKILL.md .claude/agents/target-debugger.md
git commit -m "sysview v2: json report, first-class build docs, post-mortem + openocd sections"
```
Update the session memory (`sysview_skill.md`): v2 landed on branch, still unpushed.
---
## Self-review notes
- Spec coverage: dependency (T1), in-tree first-class + zero-edit (T2/T3), hot functions (T5), stack+heap (T4), reportable/json (T4/T5/T8), post-mortem + split-point discovery (T6), OpenOCD answer validated (T7), quick wins markers/Printf/DisableEvents deterministic A/B (T5), out-of-scope list untouched.
- Known discovery points are explicit steps with acceptance criteria (events.txt naming for module/stack/heap rows; post-mortem split point; DisableEvents mask) — not placeholders.
- Type/name consistency: `tu_sysview_id_t`, `tusb_sysview_module`, `TUD/TUH_SYSVIEW_CALL/RET(level, id)`, `tusb_sysview_stack_report`, `tusb_sysview_heap_alloc/free` used identically across T2/T4/T5.
|