A halted CPU shows the state; trace shows the story: the last thousand branches before the hang, the DMA completion that never came, the interrupt that arrived one cycle too late. But trace is a firehose aimed at a straw, several sources producing words every clock into a funnel with one narrow exit. Without a bandwidth budget the trace overflows exactly on the interesting burst and records everything except the bug. The questions are how wide the exit must be, what it costs in pins, and what happens when it still overflows.
Each trace source, the CPU instruction stream, the fabric transaction monitor, the DMA event flags, emits packets only on interesting events rather than every cycle: branches taken, error responses, job completions. A funnel merges the enabled sources with source tags and timestamps into one stream, so every packet carries overhead beyond its payload, and the stream leaves through a trace port or into a small on-chip buffer. The budget below is the standing calculation: filtered source rates plus packet overhead against exit bandwidth, with the overflow policy stated up front, stall the sources or drop with a marker, because silent wrap is how a trace lies. A filter that makes the stream exactly equal to the exit has no headroom, so the budget must show margin.
The pin count belongs in the budget too. NanoSoC's user pins are 16 GPIO pads (the UART and SPI share six of them) and it has no debug or test pins, so a parallel trace port is a real cost: a 4-byte port is 32 data pins and a clock, and even a 2-byte port needs 16 data pins and a clock. Cross-triggers connect the domains: the DMA error flag halts the CPU trace, the CPU breakpoint starts fabric capture, each through a two-flop synchroniser with the level held, because a one-cycle pulse across the 96 to 24 MHz boundary is the lost-interrupt failure wearing a debug badge.
# Trace bandwidth budget for the debug extension (all widths and filters illustrative).
CPU_MHZ, FAB_MHZ, PKT_BYTES = 192.0, 96.0, 4 # NanoSoC clocks, 4-byte packets
cpu_peak = CPU_MHZ * PKT_BYTES # every CPU cycle traced
fab_peak = FAB_MHZ * PKT_BYTES # every fabric beat traced
assert abs(cpu_peak - 768.0) < 0.1 and abs(fab_peak - 384.0) < 0.1
cpu_real = cpu_peak / 16 # 1-in-16 branch filter
fab_real = 0.05 * fab_peak # fabric flags: 5 percent of beats
OVERHEAD = 0.25 # source tag and timestamp
payload = cpu_real + fab_real
total = payload * (1 + OVERHEAD)
assert abs(cpu_real - 48.0) < 0.01 and abs(fab_real - 19.2) < 0.01
assert abs(total - 84.0) < 0.01, total
def exit_bw(width_bytes, mhz): # single data rate port
return width_bytes * mhz
wide, narrow = exit_bw(4, 24.0), exit_bw(2, 24.0) # 32 data pins vs 16 data pins
assert wide == 96.0 and narrow == 48.0
assert total < wide and (wide - total) / wide > 0.1 # fits with 12.5 percent headroom
assert total > narrow, "the 2-byte port overflows: a policy is required"
# size the on-chip buffer that rides out a 100 microsecond burst on the narrow port
burst_s = 100e-6
buffer_bytes = (total - narrow) * 1e6 * burst_s
assert abs(buffer_bytes - 3600.0) < 1.0, buffer_bytes
print("PASS: 84 MB/s total; 96 MB/s port fits with 12.5 percent headroom; 48 MB/s port overflows, 3600 B rides out 100 us")The budget above recomputed for the shipped source set, with the filter assumptions and the packet overhead written beside each rate, and headroom stated.
The pin count of the trace port written next to its bandwidth, reconciled with the pins the chip can spare.
The overflow policy stated per source: stall or drop-with-marker, never silent wrap.
Every cross-trigger named with its synchroniser and its hold requirement, reviewed like any other crossing.
Overflow the trace deliberately in simulation with all sources enabled and unfiltered: confirm the policy engages, stall back-pressures or markers appear, and no packet is silently lost. Test each cross-trigger across its ratio with alignment sweeps; a trigger that arrives but one cycle too late to stop the trace before the event is a trigger that decorates the failure without capturing it.
Either the stop trigger arrived late, so the error packet was never captured, or the funnel dropped packets during the burst without a marker. Check trigger latency first, then the overflow policy. A wrapping buffer would have kept the newest packets, so a wrap alone does not explain a missing error response.