Tang Nano 20K - on-chip trace capture and analysis guide¶
Full path: Verilog/fpga/tang-nano-20k/TRACE-CAPTURE-GUIDE.md
How to capture 512-sample signal traces from the ND-120 running on the Tang Nano 20K silicon and decode them. Written to be followed step by step. This is the tool that solved the memory-write mystery in one evening after weeks of blind builds - prefer it over guessing.
1. What the analyzer is¶
A 512 x 16-bit ring buffer inside src/ND120_TANG20K_TOP.v ("On-chip
write-path analyzer"), sampling a 16-bit debug bus on every clk2x edge
(~74 ns per sample at the G1 slow-bring-up clock; two consecutive samples
per CPU clock). After a trigger it keeps a configurable number of
post-trigger samples, then dumps all 512 samples as hex lines over the
console UART at UART_BAUD_RATE - 115200 on every bitstream since
27-AUG-2026, 9600 before (taking the TX pin over from the CPU; the divider is
src/ND120_TANG20K_TOP.v:1747).
- Debug bus source:
assign DBG_MEMW = {...}inCPU-BOARD-3202/circuit/ND3202D.v(insideifdef MAIN_RAM_SDRAM). THE BIT MAP LIVES THERE - always read the current assign before decoding a dump; the map changes per investigation (v1 = WRITE-generation chain, v2 = DD write-data path, v3 = AA/BANK address path...). - Trigger: rising edge of bus bit [7] after a ~2.5 s arm delay (the
delay skips boot noise). Bit [7] is by convention the trigger signal -
currently
wdec, the recomputed microcode write-command decode, which fires exactly once per OPCOM deposit. Keep the trigger at [7] when retargeting and the top level never needs to change. - Capture shape: 64 pre-trigger + 448 post-trigger samples
(
cap_post <= 9'd448in the top). ~33 us of post-trigger visibility. - Dump hold-off: after the trigger the console stays on the CPU for
~10 s (
hold_cnt) so the deposit echo and a follow-up examine remain visible, THEN the dump takes over TX and prints 512 lines of 4 hex digits (oldest sample first). After the dump the console stays dead until S1 (reset) or reprogramming - that is expected. - LEDs (active low): LED1 = trigger signal live, LED2 = sticky "WRITE seen since arm", LED3 = sticky "trigger decode seen since arm", LED4 = dump ran, LED6 = heartbeat. A dark LED3 after a deposit is itself a measurement (the decode never fired).
2. Hardware setup (WSL2)¶
One-time per Windows boot (PowerShell as admin on the host):
Then in WSL:
ls /dev/ttyUSB* # expect ttyUSB0 (JTAG) + ttyUSB1 (UART)
sudo chmod 666 /dev/ttyUSB1 # if permissions block you
3. Build and program¶
cd Verilog/fpga/tang-nano-20k
# Optional fast gate: compile the EXACT Gowin file list with iverilog
cd sim && make nd120_tang20k_tb.vvp && cd .. # seconds; catches syntax/port errors
# Bitstream (Gowin gw_sh on the Windows host via gowin_build.ps1, callable
# from WSL, ~3 min)
make gowin VARIANT=slow
# Verify the bitstream is FRESH before programming (stale .fs = wasted run)
ls -la build/nd120_tang20k_build/impl/pnr/nd120_tang20k_build.fs
# Program the Gowin bitstream into SRAM (volatile); 'make load' is the OSS one
PATH=$PATH:~/oss-cad-suite/bin make load-gowin
4. Run a capture session¶
One self-contained shell block (runnable as a script). Keep ONE listener attached for the entire session; never program while listening.
SCRATCH=/tmp # or your scratch dir
stty -F /dev/ttyUSB1 115200 raw -echo -echoe -echok # 9600 for pre-27-AUG bitstreams
timeout 75 cat /dev/ttyUSB1 > $SCRATCH/capture.log &
sleep 6 # boot (~1 s) + 2.5 s arm + margin
# examine cell 22 (OPCOM chars must be paced ~0.3 s/char or MOPC drops them)
for c in 2 2 /; do printf "%s" "$c" > /dev/ttyUSB1; sleep 0.35; done
sleep 1
# deposit 054321 - the CR executes the write and fires the wdec trigger
for c in 0 5 4 3 2 1; do printf "%s" "$c" > /dev/ttyUSB1; sleep 0.35; done
printf "\r" > /dev/ttyUSB1
sleep 2
# readback examine while the 10 s hold keeps the console alive
for c in 2 2 /; do printf "%s" "$c" > /dev/ttyUSB1; sleep 0.35; done
sleep 16 # rest of hold + ~3.5 s dump
wc -c $SCRATCH/capture.log # expect ~3.1 KB (echo + 512 lines)
The log contains the console echo first (22/000000 054321 etc.), then
512 lines of 4 uppercase hex digits.
5. Decode and analyze¶
Adapt the field lambda to the CURRENT bit map in ND3202D.v:
import re
raw = open('capture.log').read()
vals = [int(x, 16) for x in re.findall(r'\b([0-9A-F]{4})\b', raw)]
assert len(vals) == 512, len(vals)
# EDIT THIS to match the DBG_MEMW assign in ND3202D.v (v3 shown):
def fld(v):
return dict(aa=(v >> 8) & 0xFF, wdec=(v >> 7) & 1, write=(v >> 6) & 1,
bank0=(v >> 5) & 1, bank1=(v >> 4) & 1, bank2=(v >> 3) & 1,
mw50_n=(v >> 2) & 1, cas=(v >> 1) & 1, ras=v & 1)
trig = next(i for i, v in enumerate(vals) if (v >> 7) & 1) # ~64 by design
for i in range(max(0, trig - 8), min(512, trig + 60)):
print(i, f"{vals[i]:04X}", fld(vals[i]))
Reading the trace:
- Every value appears twice in a row (2 samples per CPU clock at G1 speeds). A signal that changes every sample is suspect (metastable or clock-domain junk).
- Index the access against the known-good sequence: write decode ->
WRITErises at next CLK ->ECREQ->CGNT_nlow (grant) ->RASrises (row on AA) ->CASrises (column on AA, write data on DD) -> window ends. See../../docs/nd120-dram-memory.mdfor the protocol ground truth (the v1/v2 annotated traces of the 8-JUL write bug were inVerilog/docs/HANDOFF-basys3-memory-write.md, deleted 28-SEP-2026 - git history keeps them). - Compare against the Verilator/iverilog sim of the same access when in doubt - the sim is the working reference.
6. Retargeting at a new signal set¶
- Pick signals visible in
ND3202D.v(or extendMEM_43.v'sDBG_MEMWassign and overlay inND3202D.v, as v3 does). No new ports are needed for anything already at board level. - Keep the trigger signal on bit [7] and
WRITEon bit [6]; thenND120_TANG20K_TOP.v(trigger, LEDs, dump) needs no edits. - Update the bit-map comment in the assign - it is the decode key.
- Rebuild + program (section 3), rerun (section 4).
- If you need a different trigger event, change the edge detect on
s_dbg_memw[7]in the top's capture block (wdec_d2logic).
7. Gotchas (all learned the hard way)¶
- Verify the
.fstimestamp before programming - gw_sh leaves the old bitstream in place when it fails early. - One listener per session. A second
catsteals bytes. And apkill catinside a composite command kills its own shell. - Never program while a listener is attached - you get NUL junk.
- OPCOM input pacing: >= 0.3 s between characters or MOPC drops them.
- After the dump the console is dead (dump_fin latches TX over) until S1 or reprogram. LED4 on = the dump ran.
- The arm delay is 2.5 s - anything you type earlier can trigger on boot activity instead of your deposit.
\ris not a Verilog escape - in testbenches use8'h0D.- Sim first:
sim/make nd120_tang20k_tb.vvpcompiles the exact Gowin file list in seconds and catches port/syntax errors before the 3-minute synthesis.
8. Case study (why this works)¶
The 8-JUL-2026 memory-write investigation, three capture generations in one evening, each build ~10 min end-to-end:
- v1 (WRITE-generation chain): disproved the standing theory - the
microcode write decode fires and
WRITEasserts on silicon, correctly captured by the DGA F924 at the CLK edge. - v2 (DD write-data path): write data (054321) present and stable on
DDthrough the whole RAS/CAS window,MWRITE_nasserted - the MEM_43-level write is perfect, yet readback still returned 000000. - v3 (AA/BANK): moved the probe to the address/bank-select path, the last layer between MEM_43 and the SDRAM bridge.
Each generation eliminated a whole subsystem by measurement instead of inference. That is the method: probe, decode, move one layer, repeat.
9. The nd100x oracle traces (the reference side)¶
Moved here 28-SEP-2026 from the finished page-fault campaign plans (git
history keeps them). Recorded 23-AUG-2026; the files live outside the repo
under $ND120_ORACLE_DIR and were not re-checked since.
- Oracle side = trace files, not debugger stepping.
oracle_full.trc= 25,000,001 lines with full register columns, past the banner threshold (~17-18M instructions) - the reference boot trace.oracle_simimg.trc= 25,000,001 lines, compact mode.oracle_trace_full.log= 1,955,151 lines only (~2M instructions, readable, NOT a full boot). The source disc image of each existing trace is not recorded in the files - verify or regenerate before any image-dependent conclusion. - Regenerate with
$ND120_ORACLE_DIR/oracle_make.sh <max-instr> <abs-outfile> [compact] [img](nd100x--trace). FULL is ~104 B/line, COMPACT ~63 B/line. Always trace a COPY of the disc image - booting modifies it. - Compare tools in the same place:
lockstep.py(timing-tolerant lockstep, ND-120 trace vs oracle),boot_compare.py,ctxdiff.py/ctxdiff_tight.py(first differing opcode/next-PC for the same context). Both sides emit the same columns:PIL PC OPCODE A D T X B L STS PIE PID IIE IID PGS MMU INT SEX. The ND-120 side of that rig has always been Verilator, never the Tang. - nd100x facts:
--cputype=ND120CXworks; untraced speed ~2.9 M instructions/s; boot toSINTRAN III RUNNINGtakes 10M-20M instructions;-tcosts ~190 B/instruction raw.--watch=[phys:]ADDR[:r|w|rw](32 max) and-B ADDRexist; octal needs a LEADING ZERO (--watch=0175740:w, not175740).