Initial commit

This commit is contained in:
Myfro9
2026-05-09 01:12:34 -05:00
commit 61b4731d06
44 changed files with 104560 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+170
View File
@@ -0,0 +1,170 @@
---
name: fft-analyse
description: >
Performs FFT spectrum analysis on IQ data files (CSV, TXT, TSV). Use this skill
whenever the user asks to analyse IQ data, plot a spectrum, check OBW (occupied
bandwidth), look at FFT output, run spectrum analysis, or verify a filter's
frequency response from simulation output. Also triggers for phrases like
"分析频谱", "频谱分析", "查看IQ数据", "FFT分析", "OBW". Runs headless — no GUI
required. Auto-installs missing Python dependencies.
---
# FFT Spectrum Analyser Skill
Analyse IQ data from CSV/TXT/TSV files, produce a 3-panel spectrum plot, and
report 90 %/99 % occupied bandwidth plus top spectral peaks.
## Bundled Script
`scripts/fft_analyse.py` — general-purpose CLI analyser.
**Python requirement:** Python ≥ 3.8
**Dependencies:** `numpy >= 1.20`, `matplotlib >= 3.4`
The script calls `_ensure_deps()` at startup and auto-installs any missing
package via `pip` — no manual setup needed.
---
## Workflow
### 1. Find a working Python interpreter
On Windows, the default `python` / `python3` commands may resolve to the
Microsoft Store stub (exit code 49, not a real interpreter).
Search in this order until one works (`python --version` returns something):
```
D:\ProgramData\anaconda3\python.exe
C:\ProgramData\anaconda3\python.exe
D:\Anaconda3\python.exe
C:\Anaconda3\python.exe
D:\miniconda3\python.exe
C:\miniconda3\python.exe
%LOCALAPPDATA%\Programs\Python\Python3*\python.exe
python3 (system PATH)
python (system PATH)
```
If none found, tell the user to install Python 3.8+ (Anaconda recommended) and
provide a download link. Do not proceed without a working interpreter.
On Linux/macOS `python3` normally works directly.
### 2. Understand the input file
Ask the user (or infer from context):
| Question | Why |
|----------|-----|
| File path? | Required |
| Sample rate (fs)? | Affects frequency axis. Script auto-detects from filename keywords (e.g. `120m` → 122.88 MHz, `240m` / `245` → 245.76 MHz, `491` → 491.52 MHz). Ask if ambiguous. |
| Bit width? | Sets full-scale reference (dBFS). Common: 8 / 12 / 16 / 24 / 32. Default: 16-bit. |
| I and Q column names? | Script auto-detects common patterns (`data_i`, `m_i`, `i_dec`, `i[`, etc.). If auto-detection might be wrong, confirm or pass `--i-col` / `--q-col`. |
| Rows to skip? | Use `--skip N` to drop FIR startup zeros or header-less data. |
| Frequency display range? | Optional `--xlim -10,10` (MHz). Defaults to ±fs/2. |
### 3. Run the analyser
```bash
<python> <skill_dir>/scripts/fft_analyse.py \
"<input_file>" \
--fs <sample_rate_hz> \
--bits <bit_width> \
[--i-col <col>] [--q-col <col>] \
[--skip <N>] \
[--xlim <lo_mhz>,<hi_mhz>] \
[--out <output_dir>] \
[--prefix <name>]
```
**Examples**
```bash
# 16-bit IQ at 122.88 MHz, auto-detect columns, output next to input
python scripts/fft_analyse.py data.csv --fs 122.88e6 --bits 16
# 24-bit signed hex output from xsim, 245.76 MHz, specific columns
python scripts/fft_analyse.py output_iq.txt \
--fs 245.76e6 --bits 24 \
--i-col m_i_dec --q-col m_q_dec \
--skip 50 --xlim -70,70
# Full-scale override instead of bit-width
python scripts/fft_analyse.py ila_data.csv --fs 120e6 --fs-val 32768
```
### 4. Report results to the user
After the script finishes, tell the user:
- Path(s) of saved files: `*_analysis.png`, `*_analysis.svg`, `*_spectrum.csv`, `*_summary.txt`
- Key numbers from the summary: **90 % OBW**, **99 % OBW**, **top peak** (frequency + dBFS)
- Any warnings printed by the script (column auto-detection, skipped rows, etc.)
If the script errors, show the full traceback and diagnose the issue before
asking the user to retry.
---
## Column Auto-Detection Reference
The script matches column names case-insensitively using these patterns:
| Channel | Matched names (examples) |
|---------|--------------------------|
| I | `data_i`, `iq_i`, `ch_i`, `m_i`, `i_dec`, `i_data`, `i_val`, `i[…]` |
| Q | `data_q`, `iq_q`, `ch_q`, `m_q`, `q_dec`, `q_data`, `q_val`, `q[…]` |
If auto-detection picks the wrong columns (e.g. file has unusual names), pass
`--i-col` / `--q-col` explicitly.
---
## Output File Naming
All outputs share a timestamp suffix:
`<prefix>_<YYYYMMDD_HHMMSS>_analysis.png`
`<prefix>_<YYYYMMDD_HHMMSS>_spectrum.csv`
`<prefix>_<YYYYMMDD_HHMMSS>_summary.txt`
Default `<prefix>` = input filename stem (special chars → `_`).
Default output directory = same folder as input file.
Override with `--out <dir>` and `--prefix <name>`.
---
## Interpreting the Three-Panel Plot
| Panel | Content |
|-------|---------|
| Top: I/Q waveform | First 512 samples of I and Q after skip — checks data looks sane |
| Middle: Magnitude | Full-length `\|I+jQ\|` with RMS line — checks for amplitude variation / dropouts |
| Bottom: Spectrum | Hann-windowed FFT in dBFS, OBW markers, top-5 peak labels |
---
## Handling Hex IQ Data (xsim / ILA output)
Simulation output often stores 24-bit samples as unsigned hex strings, e.g.:
```
time_ns,m_i_24b_hex,m_q_24b_hex,m_i_dec,m_q_dec
163,000000,000000,0,0
...
3277,03f200,03f200,258560,258560
```
The script handles both hex (`m_i_24b_hex`) and decimal (`m_i_dec`) columns.
Prefer the `_dec` (signed decimal) columns when available — they are already
sign-extended and require no bit-width guess. When using hex columns, pass
`--bits 24` (or appropriate width) so two's-complement conversion is correct.
---
## Skill Script Location
```
C:\Users\bowei\.claude\skills\fft-analyse\scripts\fft_analyse.py
```
Always use the **absolute path** when invoking from any working directory.
+23
View File
@@ -0,0 +1,23 @@
{
"skill_name": "fft-analyse",
"evals": [
{
"id": 1,
"prompt": "帮我分析这个仿真输出文件的IQ频谱:e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/output_iq.txt。采样率是245.76MHz,数据是24位有符号整数,I列是m_i_dec,Q列是m_q_dec。跳过前50个样本(FIR启动)。频率显示范围 -70到70MHz。输出到 e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/eval_out/",
"expected_output": "Runs fft_analyse.py with correct arguments, produces analysis PNG and summary TXT, reports 99% OBW around 8 MHz",
"files": ["e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/output_iq.txt"]
},
{
"id": 2,
"prompt": "请对这个ILA捕获文件做FFT频谱分析:e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/ila_dbg/iladata-120m.csv。采样率120MHz,16位数据。",
"expected_output": "Auto-detects I/Q columns from the file, runs FFT, produces spectrum plot, reports OBW",
"files": ["e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/ila_dbg/iladata-120m.csv"]
},
{
"id": 3,
"prompt": "我有一个CSV文件 e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/output_iq.txt,帮我画个频谱图,采样率 245.76e6,24位",
"expected_output": "Handles slightly vague request, infers correct arguments, produces spectrum plot",
"files": ["e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/output_iq.txt"]
}
]
}
+481
View File
@@ -0,0 +1,481 @@
#!/usr/bin/env python3
"""
fft_analyse.py — General-purpose IQ spectrum analyser
Python >= 3.8 Dependencies: numpy>=1.20, matplotlib>=3.4
Usage (standalone):
python fft_analyse.py <input_file> [options]
Options:
--i-col NAME Column name for I channel (auto-detected if omitted)
--q-col NAME Column name for Q channel (auto-detected if omitted)
--fs FLOAT Sample rate in Hz, e.g. 120e6 (default: auto from filename or 1.0)
--bits INT Signed bit-width for full-scale ref: 8/12/16/24/32 (default: auto)
--fs-val FLOAT Full-scale amplitude override (overrides --bits)
--xlim F,F Frequency display range in MHz, e.g. -10,10 (default: +-fs/2)
--out DIR Output directory (default: same directory as input file)
--prefix STR Filename prefix for outputs (default: derived from input name)
--skip INT Skip first N rows of data (e.g. FIR startup zeros)
--title STR Plot title override
Supported input formats
-----------------------
CSV / TSV with header row. Column order doesn't matter.
I-channel values can be:
• Signed decimal integer: -1234 or 1234
• Unsigned hex string: 0x0bad or 0BAD (2's complement by bit-width)
Auto-detection priority for column names
-----------------------------------------
1. User-supplied --i-col / --q-col
2. Columns matching patterns below (case-insensitive, first match wins):
I : data_i, iq_i, ch_i, m_i, i_dec, i_data, i[ … followed by Q equivalent
Q : data_q, iq_q, ch_q, m_q, q_dec, q_data, q[
3. If only two non-time numeric columns exist, treat first as I and second as Q.
Outputs (saved to --out dir, filename = <prefix>_<YYYYMMDD_HHMMSS>_*)
--------
*_analysis.png Three-panel figure (waveform / magnitude / spectrum)
*_analysis.svg Vector copy
*_spectrum.csv freq_hz, freq_mhz, amp, dbfs columns
*_summary.txt Numeric statistics (OBW, peaks, RMS …)
"""
from __future__ import annotations
import argparse
import csv
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
# ── dependency check / auto-install ─────────────────────────────────────────
def _ensure_deps() -> None:
missing = []
for pkg in ("numpy", "matplotlib"):
try:
__import__(pkg)
except ImportError:
missing.append(pkg)
if missing:
print(f"[fft_analyse] Installing missing packages: {missing}")
subprocess.check_call([sys.executable, "-m", "pip", "install", "--quiet"] + missing)
_ensure_deps()
import matplotlib
matplotlib.use("Agg") # headless — no GUI needed
import matplotlib.pyplot as plt
import numpy as np
# ── helpers ──────────────────────────────────────────────────────────────────
RUN_STAMP = datetime.now().strftime("%Y%m%d_%H%M%S")
_I_PATTERNS = re.compile(
r"(?:^|_)(?:data_i|iq_i|ch_i|m_i|i_dec|i_data|i_val|[_\b]i[_\b]|i\[)", re.I
)
_Q_PATTERNS = re.compile(
r"(?:^|_)(?:data_q|iq_q|ch_q|m_q|q_dec|q_data|q_val|[_\b]q[_\b]|q\[)", re.I
)
def _guess_iq_cols(headers: list[str]) -> tuple[str | None, str | None]:
"""Two-pass: prefer non-hex columns; fall back to hex columns if nothing else matches."""
def _scan(hdrs):
i_col = q_col = None
for h in hdrs:
if i_col is None and _I_PATTERNS.search(h):
i_col = h
if q_col is None and _Q_PATTERNS.search(h):
q_col = h
return i_col, q_col
non_hex = [h for h in headers if "hex" not in h.lower()]
i_col, q_col = _scan(non_hex)
if i_col is None or q_col is None:
i_col2, q_col2 = _scan(headers)
i_col = i_col or i_col2
q_col = q_col or q_col2
return i_col, q_col
def _parse_value(s: str, bits: int, force_hex: bool = False) -> float:
s = s.strip()
if not s or s.lower() in ("x", "z", "nan", ""):
return 0.0
# hex?
if force_hex or (re.match(r"^[0-9a-fA-F]+$", s) and any(c in s.lower() for c in "abcdef")):
try:
v = int(s, 16) & ((1 << bits) - 1)
return float(v - (1 << bits) if v & (1 << (bits - 1)) else v)
except ValueError:
pass
# 0x prefix
if s.lower().startswith("0x"):
v = int(s, 16) & ((1 << bits) - 1)
return float(v - (1 << bits) if v & (1 << (bits - 1)) else v)
# decimal (may already be signed)
return float(s)
def _auto_bits(col_name: str, rows: list[dict], col: str) -> int:
"""Guess bit-width from column name or data range."""
m = re.search(r"\[(\d+):0\]", col_name)
if m:
return int(m.group(1)) + 1
# from data range
try:
vals = [float(r[col]) for r in rows[:200] if r.get(col, "").strip()]
mx = max(abs(min(vals)), abs(max(vals)))
for b in (8, 12, 15, 16, 23, 24, 31, 32):
if mx < (1 << b):
return b + 1
except Exception:
pass
return 16
def rms(x: np.ndarray) -> float:
return float(np.sqrt(np.mean(np.square(x))))
def symmetric_obw(freq_hz: np.ndarray, amp: np.ndarray, target: float) -> tuple[float, float]:
power = np.square(amp)
total = float(np.sum(power))
if total <= 0.0:
return 0.0, 0.0
abs_freq = np.abs(freq_hz)
for edge in np.unique(abs_freq):
frac = float(np.sum(power[abs_freq <= edge]) / total)
if frac >= target:
return float(2.0 * edge / 1e6), frac
return float(2.0 * abs_freq[-1] / 1e6), 1.0
def local_peaks(freq_hz: np.ndarray, amp: np.ndarray, dbfs: np.ndarray,
count: int = 10) -> list[dict]:
if len(freq_hz) < 3:
return []
bin_hz = abs(freq_hz[1] - freq_hz[0])
mask = np.abs(freq_hz) > 2.0 * bin_hz
idx = np.nonzero(mask)[0]
cands = [int(k) for k in idx
if 0 < k < len(dbfs) - 1
and dbfs[k] >= dbfs[k - 1] and dbfs[k] >= dbfs[k + 1]]
cands.sort(key=lambda k: dbfs[k], reverse=True)
return [{"freq_hz": float(freq_hz[k]), "freq_mhz": float(freq_hz[k] / 1e6),
"dbfs": float(dbfs[k]), "amp": float(amp[k]), "bin": int(k)}
for k in cands[:count]]
def find_steady_start(i: np.ndarray, q: np.ndarray, skip: int) -> int:
if skip > 0:
return skip
mag = np.abs(i) + np.abs(q)
idx = np.nonzero(mag > 0)[0]
return int(idx[0]) if len(idx) else 0
# ── core analysis ────────────────────────────────────────────────────────────
def analyse(
input_path: Path,
i_col: str | None,
q_col: str | None,
fs_hz: float,
full_scale: float,
skip: int,
xlim_mhz: tuple[float, float] | None,
out_dir: Path,
prefix: str,
title: str | None,
bits: int,
) -> dict:
# ── read file ──────────────────────────────────────────────────────────
rows: list[dict] = []
with input_path.open(newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
headers = reader.fieldnames or []
for row in reader:
rows.append(row)
if not rows:
print("[fft_analyse] ERROR: no data rows found.")
return {}
# ── resolve column names ───────────────────────────────────────────────
if i_col is None or q_col is None:
gi, gq = _guess_iq_cols(headers)
i_col = i_col or gi
q_col = q_col or gq
if i_col is None or q_col is None:
print(f"[fft_analyse] Could not identify I/Q columns.")
print(f" Headers: {headers}")
print(" Use --i-col and --q-col to specify them.")
return {}
print(f"[fft_analyse] Input : {input_path.name} ({len(rows)} rows)")
print(f"[fft_analyse] I col : {i_col}")
print(f"[fft_analyse] Q col : {q_col}")
print(f"[fft_analyse] fs : {fs_hz/1e6:.4f} MHz FS ref: {full_scale:.0f}")
# ── parse IQ values ────────────────────────────────────────────────────
# Vivado ILA CSV: column names contain "[N:0]" and values are bare hex (no 0x prefix)
# e.g. column "AxC0_491_data_i[15:0]", value "0b6f" → must force hex parsing
def _is_hex_col(col_name: str, sample_rows: list, col: str) -> bool:
if "hex" in col_name.lower():
return True
if re.search(r"\[\d+:\d+\]", col_name):
# Vivado ILA bit-range notation — peek at first non-empty value
for r in sample_rows[:20]:
v = r.get(col, "").strip()
if v and v.lower() not in ("x", "z", "nan"):
# If it looks like hex (has a-f OR is exactly 4 hex digits)
if re.match(r"^[0-9a-fA-F]+$", v):
return True
break
return False
i_hex = _is_hex_col(i_col, rows, i_col)
q_hex = _is_hex_col(q_col, rows, q_col)
i_data = np.array([_parse_value(r.get(i_col, "0"), bits, force_hex=i_hex) for r in rows], dtype=np.float64)
q_data = np.array([_parse_value(r.get(q_col, "0"), bits, force_hex=q_hex) for r in rows], dtype=np.float64)
# skip startup / leading zeros
s = find_steady_start(i_data, q_data, skip)
if s > 0:
print(f"[fft_analyse] Skipping {s} leading samples (zeros / startup)")
i_data, q_data = i_data[s:], q_data[s:]
if len(i_data) < 4:
print("[fft_analyse] Too few samples after skip.")
return {}
# ── FFT ───────────────────────────────────────────────────────────────
fft_n = 2 ** int(np.floor(np.log2(len(i_data))))
i_mean, q_mean = float(np.mean(i_data)), float(np.mean(q_data))
iq = (i_data[:fft_n] - i_mean) + 1j * (q_data[:fft_n] - q_mean)
win = np.hanning(fft_n)
cg = float(np.sum(win))
fft_data = np.fft.fftshift(np.fft.fft(iq * win, n=fft_n))
amp = np.abs(fft_data) / cg
dbfs = 20.0 * np.log10((amp + np.finfo(np.float64).eps) / full_scale)
freq_hz = (np.arange(fft_n) - fft_n / 2.0) * fs_hz / fft_n
freq_mhz = freq_hz / 1e6
obw90, frac90 = symmetric_obw(freq_hz, amp, 0.90)
obw99, frac99 = symmetric_obw(freq_hz, amp, 0.99)
peaks = local_peaks(freq_hz, amp, dbfs)
mag_time = np.abs(i_data + 1j * q_data)
print(f"[fft_analyse] FFT size: {fft_n} 90% OBW: {obw90:.4f} MHz 99% OBW: {obw99:.4f} MHz")
if peaks:
p = peaks[0]
print(f"[fft_analyse] Top peak: {p['freq_mhz']:+.4f} MHz {p['dbfs']:.2f} dBFS")
result = dict(
i_data=i_data, q_data=q_data, mag_time=mag_time, i_mean=i_mean, q_mean=q_mean,
fft_n=fft_n, freq_hz=freq_hz, freq_mhz=freq_mhz, amp=amp, dbfs=dbfs,
obw90=obw90, obw99=obw99, frac90=frac90, frac99=frac99, peaks=peaks,
fs_hz=fs_hz, full_scale=full_scale, i_col=i_col, q_col=q_col,
input_name=input_path.name, skip=s,
)
# ── write outputs ──────────────────────────────────────────────────────
out_dir.mkdir(parents=True, exist_ok=True)
csv_path = out_dir / f"{prefix}_{RUN_STAMP}_spectrum.csv"
with csv_path.open("w", newline="") as f:
w = csv.writer(f)
w.writerow(["freq_hz", "freq_mhz", "amp", "dbfs"])
for fh, a, d in zip(freq_hz, amp, dbfs):
w.writerow([f"{fh:.6f}", f"{fh/1e6:.9f}", f"{a:.9f}", f"{d:.6f}"])
lines = [
f"IQ FFT analysis — {input_path.name}",
f"Run: {RUN_STAMP}",
f"I column : {i_col}",
f"Q column : {q_col}",
f"Fs : {fs_hz/1e6:.4f} MHz",
f"FS ref : {full_scale:.0f}",
f"Samples : {len(i_data)} (skipped {s})",
f"FFT size : {fft_n}",
f"Bin spacing: {fs_hz/fft_n:.4f} Hz",
f"I min/max/mean/rms: {np.min(i_data):.1f} / {np.max(i_data):.1f} / "
f"{i_mean:.4f} / {rms(i_data):.4f}",
f"Q min/max/mean/rms: {np.min(q_data):.1f} / {np.max(q_data):.1f} / "
f"{q_mean:.4f} / {rms(q_data):.4f}",
f"Mag RMS : {rms(mag_time):.4f}",
f"90% OBW : {obw90:.6f} MHz (fraction {frac90:.6f})",
f"99% OBW : {obw99:.6f} MHz (fraction {frac99:.6f})",
"",
"Top spectral peaks:",
] + [
f" {p['freq_hz']:14.3f} Hz ({p['freq_mhz']:+.4f} MHz) "
f"{p['dbfs']:.2f} dBFS amp {p['amp']:.2f}"
for p in peaks
]
(out_dir / f"{prefix}_{RUN_STAMP}_summary.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
_plot(result, xlim_mhz, out_dir, prefix, title)
print(f"[fft_analyse] Saved: {prefix}_{RUN_STAMP}_analysis.png (and .svg, _spectrum.csv, _summary.txt)")
return result
def _plot(result: dict, xlim_mhz, out_dir: Path, prefix: str, title: str | None) -> None:
i_data = result["i_data"]
q_data = result["q_data"]
mag_time = result["mag_time"]
freq_mhz = result["freq_mhz"]
dbfs = result["dbfs"]
fs_hz = result["fs_hz"]
fft_n = result["fft_n"]
if xlim_mhz is None:
half = fs_hz / 2e6
xlim_mhz = (-half, half)
plt.rcParams.update({"font.size": 14, "axes.titlesize": 16, "axes.labelsize": 14,
"legend.fontsize": 12})
fig, axes = plt.subplots(3, 1, figsize=(20, 14), constrained_layout=True)
fig.patch.set_facecolor("white")
main_title = title or f"IQ FFT Analysis — {result['input_name']}"
fig.suptitle(main_title, fontsize=16)
# panel 1 — IQ waveform
count = min(512, len(i_data))
axes[0].plot(i_data[:count], label=f"I ({result['i_col']})", linewidth=1.4)
axes[0].plot(q_data[:count], label=f"Q ({result['q_col']})", linewidth=1.4)
axes[0].set_title(f"I/Q waveform — first {count} samples (after skip={result['skip']})")
axes[0].set_xlabel("Sample")
axes[0].set_ylabel("Counts")
axes[0].grid(True, alpha=0.3)
axes[0].legend(loc="upper right")
# panel 2 — magnitude
axes[1].plot(mag_time, color="#28734f", linewidth=1.0, label="|I+jQ|")
axes[1].axhline(rms(mag_time), color="#b22222", linestyle="--", linewidth=1.2,
label=f"RMS = {rms(mag_time):.1f}")
axes[1].set_title("Complex magnitude |I + jQ|, all samples")
axes[1].set_xlabel("Sample")
axes[1].set_ylabel("Counts")
axes[1].grid(True, alpha=0.3)
axes[1].legend(loc="upper right")
# panel 3 — spectrum
axes[2].plot(freq_mhz, dbfs, color="#1f5a95", linewidth=1.0)
obw90, obw99 = result["obw90"], result["obw99"]
if obw90 > 0:
axes[2].axvline(-obw90 / 2, color="#ff7f0e", ls="--", lw=1.3,
label=f"90% OBW: {obw90:.2f} MHz")
axes[2].axvline( obw90 / 2, color="#ff7f0e", ls="--", lw=1.3)
if obw99 > 0:
axes[2].axvline(-obw99 / 2, color="#9467bd", ls="--", lw=1.3,
label=f"99% OBW: {obw99:.2f} MHz")
axes[2].axvline( obw99 / 2, color="#9467bd", ls="--", lw=1.3)
for p in result["peaks"][:5]:
if xlim_mhz[0] <= p["freq_mhz"] <= xlim_mhz[1]:
axes[2].plot(p["freq_mhz"], p["dbfs"], "o", color="#c23b22", ms=6)
axes[2].annotate(f"{p['freq_mhz']:.2f} MHz", (p["freq_mhz"], p["dbfs"]),
xytext=(0, 8), textcoords="offset points",
ha="center", fontsize=11)
axes[2].set_title(f"{fft_n}-pt complex FFT, Hann window, DC removed, "
f"fs = {fs_hz/1e6:.2f} MHz, FS ref = {result['full_scale']:.0f}")
axes[2].set_xlabel("Frequency (MHz)")
axes[2].set_ylabel("Magnitude (dBFS)")
axes[2].set_xlim(xlim_mhz)
in_view = (freq_mhz >= xlim_mhz[0]) & (freq_mhz <= xlim_mhz[1])
if np.any(in_view):
y_top = max(-20.0, float(np.ceil(np.max(dbfs[in_view]) / 10) * 10 + 10))
else:
y_top = 10.0
axes[2].set_ylim(-140, y_top)
axes[2].grid(True, alpha=0.3)
axes[2].legend(loc="lower right")
fig.savefig(out_dir / f"{prefix}_{RUN_STAMP}_analysis.png", dpi=150)
fig.savefig(out_dir / f"{prefix}_{RUN_STAMP}_analysis.svg")
plt.close(fig)
# ── CLI ───────────────────────────────────────────────────────────────────────
def _auto_fs(path: Path) -> float:
name = path.stem.lower()
for pat, hz in [("491", 491.52e6), ("245", 245.76e6), ("240m", 245.76e6),
("122", 122.88e6), ("120m", 122.88e6), ("61", 61.44e6),
("250m", 250e6), ("125m", 125e6)]:
if pat in name:
return hz
return 1.0 # unknown — caller should supply --fs
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="IQ FFT spectrum analyser")
parser.add_argument("input", help="Input CSV / TSV / TXT file with IQ columns")
parser.add_argument("--i-col", default=None)
parser.add_argument("--q-col", default=None)
parser.add_argument("--fs", type=float, default=None,
help="Sample rate Hz, e.g. 120e6")
parser.add_argument("--bits", type=int, default=None,
help="IQ bit width (determines full-scale ref)")
parser.add_argument("--fs-val", type=float, default=None,
help="Full-scale amplitude override")
parser.add_argument("--xlim", default=None,
help="Frequency display range MHz, e.g. -10,10 or --xlim=-10,10")
parser.add_argument("--out", default=None,
help="Output directory (default: same as input)")
parser.add_argument("--prefix", default=None,
help="Output filename prefix")
parser.add_argument("--skip", type=int, default=0,
help="Skip first N data rows (e.g. FIR startup)")
parser.add_argument("--title", default=None)
# argparse treats "-70,70" as a flag; collapse "--xlim -N,M" → "--xlim=-N,M"
raw = list(argv) if argv is not None else sys.argv[1:]
for i in range(len(raw) - 1):
if raw[i] == "--xlim" and re.match(r"^-\d", raw[i + 1]):
raw[i:i + 2] = [f"--xlim={raw[i + 1]}"]
break
args = parser.parse_args(raw)
inp = Path(args.input).resolve()
if not inp.exists():
sys.exit(f"[fft_analyse] File not found: {inp}")
fs_hz = args.fs if args.fs is not None else _auto_fs(inp)
out_dir = Path(args.out).resolve() if args.out else inp.parent
prefix = args.prefix or re.sub(r"[^\w]", "_", inp.stem)
xlim_mhz = tuple(float(x) for x in args.xlim.split(",")) if args.xlim else None
# determine full-scale
if args.fs_val is not None:
full_scale = args.fs_val
bits = args.bits or 16
elif args.bits is not None:
bits = args.bits
full_scale = float(1 << (bits - 1))
else:
bits = 16
full_scale = 32768.0 # default: 16-bit
analyse(
input_path=inp,
i_col=args.i_col,
q_col=args.q_col,
fs_hz=fs_hz,
full_scale=full_scale,
skip=args.skip,
xlim_mhz=xlim_mhz,
out_dir=out_dir,
prefix=prefix,
title=args.title,
bits=bits,
)
if __name__ == "__main__":
main()
@@ -0,0 +1,6 @@
{
"eval_id": 1,
"eval_name": "axis-fifo-handshake-sim",
"prompt": "我有一个 AXI-Stream FIFO 模块,接口如下:input clk, input rst_n, input [7:0] s_tdata, input s_tvalid, output s_tready, output [7:0] m_tdata, output m_tvalid, input m_tready, output [3:0] fill_count。请帮我生成 testbench,模拟正常的 tvalid/tready 握手时序,用批处理模式在 Vivado 中运行仿真。",
"assertions": []
}
@@ -0,0 +1,45 @@
// AXI-Stream FIFO 模块(测试用,深度4)
module axis_fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 4
)(
input wire clk,
input wire rst_n,
// 从机(写入)端口
input wire [DATA_WIDTH-1:0] s_tdata,
input wire s_tvalid,
output wire s_tready,
// 主机(读出)端口
output wire [DATA_WIDTH-1:0] m_tdata,
output wire m_tvalid,
input wire m_tready
);
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [$clog2(DEPTH):0] wr_ptr, rd_ptr, count;
assign s_tready = (count < DEPTH);
assign m_tvalid = (count > 0);
assign m_tdata = mem[rd_ptr[$clog2(DEPTH)-1:0]];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 0; rd_ptr <= 0; count <= 0;
end else begin
if (s_tvalid && s_tready) begin
mem[wr_ptr[$clog2(DEPTH)-1:0]] <= s_tdata;
wr_ptr <= wr_ptr + 1;
count <= count + 1;
end
if (m_tvalid && m_tready) begin
rd_ptr <= rd_ptr + 1;
count <= count - 1;
end
// 同时读写(count 保持不变已在上面分开处理,这里修正)
if (s_tvalid && s_tready && m_tvalid && m_tready) begin
count <= count; // 读写同时,count 不变
end
end
end
endmodule
@@ -0,0 +1,26 @@
# Vivado 批处理仿真脚本 - AXI-Stream FIFO 测试
# 测试用例2:用户口头描述接口,生成握手时序激励,批处理模式运行
# 创建仿真工程
create_project -force sim_proj ./sim_proj -part xc7a200tfbg676-2
set_property simulator_language Verilog [current_project]
# 添加源文件(被测模块)
add_files -norecurse ./axis_fifo.v
# 添加仿真文件(testbench)
add_files -fileset sim_1 -norecurse ./tb_axis_fifo.v
# 设置顶层模块为 testbench
set_property top tb_axis_fifo [get_filesets sim_1]
set_property top_lib xil_defaultlib [get_filesets sim_1]
# 设置仿真运行时间(握手时序需要足够时间)
set_property -name {xsim.simulate.runtime} -value {2000ns} -objects [get_filesets sim_1]
# 运行仿真(批处理模式,不启动 GUI)
launch_simulation
run all
close_sim
exit
@@ -0,0 +1,118 @@
`timescale 1ns/1ps
module tb_axis_fifo;
// ---- 参数 ----
parameter CLK_PERIOD = 10; // 100 MHz 时钟,周期 10ns
// ---- 信号声明 ----
reg clk;
reg rst_n;
// AXI-Stream 写端口(Master -> FIFO)
reg [7:0] s_tdata;
reg s_tvalid;
wire s_tready;
// AXI-Stream 读端口(FIFO -> Slave)
wire [7:0] m_tdata;
wire m_tvalid;
reg m_tready;
// ---- 被测模块例化 ----
axis_fifo uut (
.clk (clk),
.rst_n (rst_n),
.s_tdata (s_tdata),
.s_tvalid (s_tvalid),
.s_tready (s_tready),
.m_tdata (m_tdata),
.m_tvalid (m_tvalid),
.m_tready (m_tready)
);
// ---- 时钟生成 ----
initial clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
// ---- 任务:发送一个 AXI-Stream 数据包 ----
task axis_write;
input [7:0] data;
begin
s_tdata = data;
s_tvalid = 1;
// 等待 tready 握手完成
@(posedge clk);
while (!s_tready) @(posedge clk);
s_tvalid = 0;
end
endtask
// ---- 任务:读一个 AXI-Stream 数据包 ----
task axis_read;
begin
m_tready = 1;
// 等待 tvalid 握手完成
@(posedge clk);
while (!m_tvalid) @(posedge clk);
m_tready = 0;
end
endtask
// ---- VCD 波形记录 ----
initial begin
$dumpfile("tb_axis_fifo.vcd");
$dumpvars(0, tb_axis_fifo);
end
// ---- 复位与激励 ----
initial begin
// 初始化所有信号
s_tdata = 8'h00;
s_tvalid = 0;
m_tready = 0;
// 复位序列
rst_n = 0;
repeat(5) @(posedge clk);
rst_n = 1;
repeat(2) @(posedge clk);
// 测试1:连续写入 4 个数据
$display("[%0t] 开始测试:连续写入4个数据", $time);
axis_write(8'hAA);
axis_write(8'hBB);
axis_write(8'hCC);
axis_write(8'hDD);
repeat(2) @(posedge clk);
// 测试2:连续读出 4 个数据
$display("[%0t] 开始读出数据", $time);
axis_read();
axis_read();
axis_read();
axis_read();
repeat(2) @(posedge clk);
// 测试3:写入同时读出(背压测试)
$display("[%0t] 背压测试:写入同时读出", $time);
fork
begin
axis_write(8'h11);
axis_write(8'h22);
axis_write(8'h33);
end
begin
repeat(3) @(posedge clk);
m_tready = 1;
repeat(10) @(posedge clk);
m_tready = 0;
end
join
repeat(10) @(posedge clk);
$display("[%0t] 仿真完成", $time);
$finish;
end
endmodule
@@ -0,0 +1,43 @@
{
"eval_id": 0,
"eval_name": "counter8-auto-stim",
"prompt": "我有一个8位计数器模块,文件路径是 counter8.v,请帮我自动生成激励,运行仿真,并将波形保存到 D:/sim_results/counter_waves.csv",
"assertions": [
{
"id": "tb_file_exists",
"text": "生成了 tb_counter8.v testbench 文件"
},
{
"id": "tb_has_dumpfile",
"text": "testbench 包含 $dumpfile 和 $dumpvars,用于 VCD 波形记录"
},
{
"id": "tb_has_clkgen",
"text": "testbench 包含时钟生成逻辑(always #N clk = ~clk)"
},
{
"id": "tb_has_reset",
"text": "testbench 包含复位序列"
},
{
"id": "tb_has_finish",
"text": "testbench 包含 $finish 以正常结束仿真"
},
{
"id": "tcl_file_exists",
"text": "生成了 run_sim.tcl Vivado 批处理脚本"
},
{
"id": "tcl_batch_mode",
"text": "TCL 脚本使用批处理模式(-mode batch)运行"
},
{
"id": "vcd_to_csv_exists",
"text": "提供了 vcd_to_csv.py 波形转换脚本"
},
{
"id": "output_path_correct",
"text": "CSV 输出路径指向用户指定的 D:/sim_results/counter_waves.csv"
}
]
}
@@ -0,0 +1,19 @@
// 8位计数器模块
module counter8 (
input wire clk, // 系统时钟
input wire rst_n, // 低有效复位
input wire en, // 使能信号
output reg [7:0] count, // 计数输出
output wire overflow // 溢出标志(count == 8'hFF)
);
assign overflow = (count == 8'hFF) & en;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'h00;
else if (en)
count <= count + 1'b1;
end
endmodule
@@ -0,0 +1,31 @@
# Vivado 批处理仿真脚本 - 8位计数器
# 由 vivado-sim 技能自动生成
# 创建仿真工程
create_project -force sim_proj ./sim_proj -part xc7a200tfbg676-2
set_property simulator_language Verilog [current_project]
# 添加设计源文件
add_files -norecurse ./counter8.v
# 添加仿真文件
add_files -fileset sim_1 -norecurse ./tb_counter8.v
# 设置顶层仿真模块
set_property top tb_counter8 [get_filesets sim_1]
set_property top_lib xil_defaultlib [get_filesets sim_1]
# 设置仿真时长(100个时钟周期 × 10ns = 1000ns,留一些余量用2000ns)
set_property -name {xsim.simulate.runtime} -value {2000ns} -objects [get_filesets sim_1]
# 启动仿真
launch_simulation
# 运行仿真
run all
# 关闭仿真
close_sim
# 退出 Vivado
exit
@@ -0,0 +1,72 @@
`timescale 1ns/1ps
module tb_counter8;
// ---- 参数 ----
parameter CLK_PERIOD = 10; // 100MHz 时钟,周期 10ns
// ---- 信号声明 ----
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
wire overflow;
// ---- 被测模块例化 ----
counter8 uut (
.clk (clk),
.rst_n (rst_n),
.en (en),
.count (count),
.overflow (overflow)
);
// ---- 时钟生成 ----
initial clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
// ---- VCD 波形记录 ----
initial begin
$dumpfile("tb_counter8.vcd");
$dumpvars(0, tb_counter8);
end
// ---- 复位与激励 ----
initial begin
// 初始化
rst_n = 0;
en = 0;
// 复位保持 5 个时钟周期
repeat(5) @(posedge clk);
rst_n = 1;
// 使能计数,连续计数 200 个周期
@(posedge clk);
en = 1;
repeat(200) @(posedge clk);
// 暂停使能,观察保持行为
en = 0;
repeat(10) @(posedge clk);
// 重新使能,再计数 60 个周期(观察溢出行为)
en = 1;
repeat(60) @(posedge clk);
// 再次复位
rst_n = 0;
repeat(3) @(posedge clk);
rst_n = 1;
repeat(5) @(posedge clk);
$finish;
end
// ---- 监控关键事件 ----
always @(posedge clk) begin
if (overflow)
$display("[%0t ns] 溢出! count = %0d", $time, count);
end
endmodule
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""将 VCD 波形文件转换为 CSV 格式"""
import re
import csv
import sys
from pathlib import Path
def parse_vcd(vcd_path):
signals = {} # id_char -> (name, width)
changes = [] # (time, id_char, value)
current_time = 0
with open(vcd_path, 'r') as f:
for line in f:
line = line.strip()
if not line:
continue
# 解析信号定义
m = re.match(r'\$var\s+\w+\s+(\d+)\s+(\S+)\s+(\S+)', line)
if m:
width, id_char, name = int(m.group(1)), m.group(2), m.group(3)
signals[id_char] = (name, width)
continue
# 时间戳
m = re.match(r'#(\d+)', line)
if m:
current_time = int(m.group(1))
continue
# 标量变化: 0a 1b xc
m = re.match(r'([01xzXZ])(\S+)', line)
if m:
val, id_char = m.group(1), m.group(2)
if id_char in signals:
changes.append((current_time, id_char, val))
continue
# 向量变化: b1010 a
m = re.match(r'[bB]([01xzXZ_]+)\s+(\S+)', line)
if m:
val_str, id_char = m.group(1).replace('_',''), m.group(2)
if id_char in signals:
changes.append((current_time, id_char, val_str))
continue
return signals, changes
def vcd_to_csv(vcd_path, csv_path):
signals, changes = parse_vcd(vcd_path)
if not signals:
print("未找到信号定义,请检查 VCD 文件")
return
time_events = {}
for t, id_char, val in changes:
if t not in time_events:
time_events[t] = {}
time_events[t][id_char] = val
sig_ids = list(signals.keys())
sig_names = [signals[i][0] for i in sig_ids]
current_vals = {i: 'x' for i in sig_ids}
rows = []
for t in sorted(time_events.keys()):
current_vals.update(time_events[t])
row = {'time_ns': t}
for i, name in zip(sig_ids, sig_names):
row[name] = current_vals[i]
rows.append(row)
if not rows:
print("没有波形数据")
return
fieldnames = ['time_ns'] + sig_names
with open(csv_path, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"已导出 {len(rows)} 个时间点,{len(sig_names)} 个信号到 {csv_path}")
if __name__ == '__main__':
if len(sys.argv) < 3:
print(f"用法: python vcd_to_csv.py <input.vcd> <output.csv>")
sys.exit(1)
vcd_to_csv(sys.argv[1], sys.argv[2])
@@ -0,0 +1,27 @@
{
"eval_id": 2,
"eval_name": "dff-sync-rst-gui-mode",
"prompt": "我有一个带同步复位的 D触发器模块 dff_sync_rst.v,路径是 D:/fpga/dff_sync_rst.v。请帮我在 Vivado GUI 模式下运行仿真,同时也生成 CSV 文件保存到 D:/sim_results/dff_waves.csv",
"assertions": [
{
"id": "tb_generated",
"text": "生成了 tb_dff_sync_rst.v 文件,包含时钟生成、复位序列和 D 输入激励",
"check": "file_exists:tb_dff_sync_rst.v"
},
{
"id": "gui_tcl_correct",
"text": "run_sim_gui.tcl 中使用 GUI 模式(不含 -mode batch,不含 close_sim 和 exit)",
"check": "file_not_contains:run_sim_gui.tcl:-mode batch"
},
{
"id": "vcd_dump_in_tb",
"text": "testbench 包含 $dumpfile 和 $dumpvars 语句以记录波形",
"check": "file_contains:tb_dff_sync_rst.v:$dumpfile"
},
{
"id": "vcd_to_csv_mentioned",
"text": "提供了 vcd_to_csv.py 脚本或明确说明如何导出 CSV",
"check": "file_exists:vcd_to_csv.py"
}
]
}
@@ -0,0 +1,16 @@
// 带同步复位的 D 触发器
module dff_sync_rst (
input wire clk, // 时钟
input wire rst, // 同步复位(高有效)
input wire d, // 数据输入
output reg q // 数据输出
);
always @(posedge clk) begin
if (rst)
q <= 1'b0;
else
q <= d;
end
endmodule
@@ -0,0 +1,42 @@
# Vivado 仿真 TCL 脚本 - GUI 交互模式(用户可在 Vivado 中直接查看波形)
# 模块: dff_sync_rst(同步复位 D 触发器)
# 注意: 此脚本不包含 close_sim 和 exit,Vivado 将保持打开状态
# 创建工程
create_project -force sim_proj ./sim_proj -part xc7a200tfbg676-2
set_property simulator_language Verilog [current_project]
# 添加源文件(请将路径替换为实际路径)
add_files -norecurse dff_sync_rst.v
add_files -fileset sim_1 -norecurse tb_dff_sync_rst.v
# 更新 IP 编译顺序
update_compile_order -fileset sources_1
update_compile_order -fileset sim_1
# 设置顶层为 testbench
set_property top tb_dff_sync_rst [get_filesets sim_1]
set_property top_lib xil_defaultlib [get_filesets sim_1]
# 仿真设置 - GUI 模式运行足够长时间以便观察
set_property -name {xsim.simulate.runtime} -value {500ns} -objects [get_filesets sim_1]
# 启动仿真(GUI 模式)
launch_simulation
# 运行全部激励
run all
# ---- GUI 模式特有操作 ----
# 以下命令在 GUI 打开后添加所有信号到波形窗口
# add_wave /tb_dff_sync_rst/*
# 用户可在 Vivado 波形窗口中手动缩放/查看
# 注意: 不加 close_sim 和 exit,让 Vivado GUI 保持打开
# 用户可以在 Vivado 界面中:
# 1. 在 Waveform 窗口查看所有信号
# 2. 使用 File > Export > Export Simulation 导出数据
# 3. 手动运行更长时间: run 1000ns
# 同时也生成 VCD 文件,以便后续转换为 CSV
# (testbench 中已包含 $dumpfile/$dumpvars 语句)
@@ -0,0 +1,69 @@
`timescale 1ns/1ps
module tb_dff_sync_rst;
// ---- 参数 ----
parameter CLK_PERIOD = 10; // 100 MHz 时钟,周期 10ns
// ---- 信号声明 ----
reg clk;
reg rst; // 同步复位,高有效
reg d;
wire q;
// ---- 被测模块例化 ----
dff_sync_rst uut (
.clk (clk),
.rst (rst),
.d (d),
.q (q)
);
// ---- 时钟生成 ----
initial clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
// ---- 复位与激励 ----
initial begin
// 初始化信号
rst = 1;
d = 0;
// 复位保持 5 个时钟周期
repeat(5) @(posedge clk);
rst = 0;
// 施加激励:交替改变 d,每 2 个周期变一次
@(posedge clk); d = 1;
@(posedge clk); d = 0;
@(posedge clk); d = 1;
@(posedge clk); d = 1;
@(posedge clk); d = 0;
// 测试同步复位:在中途再次复位
@(posedge clk); rst = 1; d = 1; // 即使 d=1,复位期间 q 应该为 0
@(posedge clk);
@(posedge clk); rst = 0;
// 复位后继续激励
@(posedge clk); d = 1;
@(posedge clk); d = 0;
@(posedge clk); d = 1;
// 等待几个周期后结束
repeat(5) @(posedge clk);
$finish;
end
// ---- VCD 波形记录 ----
initial begin
$dumpfile("tb_dff_sync_rst.vcd");
$dumpvars(0, tb_dff_sync_rst);
end
// ---- 监控输出(可选,方便在 log 中看到信号变化)----
initial begin
$monitor("time=%0t clk=%b rst=%b d=%b q=%b", $time, clk, rst, d, q);
end
endmodule
@@ -0,0 +1,32 @@
{
"eval_id": 2,
"eval_name": "eval-fft-iladata",
"prompt": "请对这个ILA捕获文件做FFT频谱分析:e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/ila_dbg/iladata-120m.csv。采样率120MHz,16位数据。",
"assertions": [
{
"text": "Script fft_analyse.py is invoked",
"passed": false,
"evidence": ""
},
{
"text": "I/Q columns are auto-detected or explicitly identified",
"passed": false,
"evidence": ""
},
{
"text": "Output PNG or SVG is produced",
"passed": false,
"evidence": ""
},
{
"text": "--fs 120e6 or equivalent is passed",
"passed": false,
"evidence": ""
},
{
"text": "Agent reports OBW or peak frequency to user",
"passed": false,
"evidence": ""
}
]
}
@@ -0,0 +1,32 @@
{
"eval_id": 2,
"eval_name": "eval-fft-iladata",
"prompt": "请对这个ILA捕获文件做FFT频谱分析:iladata-120m.csv。采样率120MHz,16位数据。",
"assertions": [
{
"text": "Script fft_analyse.py is invoked",
"passed": true,
"evidence": "Script ran successfully"
},
{
"text": "I/Q columns are auto-detected correctly",
"passed": true,
"evidence": "I col: data_120m_i[15:0], Q col: data_120m_q[15:0] — correct"
},
{
"text": "Output PNG or SVG is produced",
"passed": true,
"evidence": "iladata_120m_v2_*_analysis.png and .svg produced"
},
{
"text": "--fs 120e6 or equivalent is passed",
"passed": true,
"evidence": "fs: 120.0000 MHz confirmed in output"
},
{
"text": "Agent reports OBW or peak frequency to user",
"passed": true,
"evidence": "90% OBW: 7.1484 MHz, top peak: -0.9375 MHz at -32.95 dBFS"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 265 KiB

@@ -0,0 +1,26 @@
IQ FFT analysis — iladata-120m.csv
Run: 20260508_170724
I column : data_120m_i[15:0]
Q column : data_120m_q[15:0]
Fs : 120.0000 MHz
FS ref : 32768
Samples : 4096 (skipped 0)
FFT size : 4096
Bin spacing: 29296.8750 Hz
I min/max/mean/rms: -8944.0 / 9534.0 / -348.3206 / 2691.2248
Q min/max/mean/rms: -10407.0 / 8948.0 / -349.4451 / 2687.5327
Mag RMS : 3803.3568
90% OBW : 7.148438 MHz (fraction 0.900623)
99% OBW : 80.859375 MHz (fraction 0.990004)
Top spectral peaks:
-937500.000 Hz (-0.9375 MHz) -32.95 dBFS amp 737.96
-3486328.125 Hz (-3.4863 MHz) -34.08 dBFS amp 647.54
1494140.625 Hz (+1.4941 MHz) -35.66 dBFS amp 540.33
-820312.500 Hz (-0.8203 MHz) -35.66 dBFS amp 539.87
-410156.250 Hz (-0.4102 MHz) -35.79 dBFS amp 531.85
996093.750 Hz (+0.9961 MHz) -35.82 dBFS amp 530.23
3076171.875 Hz (+3.0762 MHz) -35.83 dBFS amp 529.36
-1699218.750 Hz (-1.6992 MHz) -35.99 dBFS amp 519.89
1142578.125 Hz (+1.1426 MHz) -36.14 dBFS amp 511.23
-732421.875 Hz (-0.7324 MHz) -36.25 dBFS amp 504.31
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 265 KiB

@@ -0,0 +1,26 @@
IQ FFT analysis — iladata-120m.csv
Run: 20260508_171512
I column : data_120m_i[15:0]
Q column : data_120m_q[15:0]
Fs : 120.0000 MHz
FS ref : 32768
Samples : 4096 (skipped 0)
FFT size : 4096
Bin spacing: 29296.8750 Hz
I min/max/mean/rms: -8944.0 / 9534.0 / -348.3206 / 2691.2248
Q min/max/mean/rms: -10407.0 / 8948.0 / -349.4451 / 2687.5327
Mag RMS : 3803.3568
90% OBW : 7.148438 MHz (fraction 0.900623)
99% OBW : 80.859375 MHz (fraction 0.990004)
Top spectral peaks:
-937500.000 Hz (-0.9375 MHz) -32.95 dBFS amp 737.96
-3486328.125 Hz (-3.4863 MHz) -34.08 dBFS amp 647.54
1494140.625 Hz (+1.4941 MHz) -35.66 dBFS amp 540.33
-820312.500 Hz (-0.8203 MHz) -35.66 dBFS amp 539.87
-410156.250 Hz (-0.4102 MHz) -35.79 dBFS amp 531.85
996093.750 Hz (+0.9961 MHz) -35.82 dBFS amp 530.23
3076171.875 Hz (+3.0762 MHz) -35.83 dBFS amp 529.36
-1699218.750 Hz (-1.6992 MHz) -35.99 dBFS amp 519.89
1142578.125 Hz (+1.1426 MHz) -36.14 dBFS amp 511.23
-732421.875 Hz (-0.7324 MHz) -36.25 dBFS amp 504.31
@@ -0,0 +1,32 @@
{
"eval_id": 1,
"eval_name": "eval-fft-sim-output",
"prompt": "帮我分析这个仿真输出文件的IQ频谱:e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/output_iq.txt。采样率是245.76MHz,数据是24位有符号整数,I列是m_i_dec,Q列是m_q_dec。跳过前50个样本(FIR启动)。频率显示范围 -70到70MHz。输出到 e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/eval_out/",
"assertions": [
{
"text": "Script fft_analyse.py is invoked via the full Python path",
"passed": false,
"evidence": ""
},
{
"text": "Output PNG file is produced in the specified output directory",
"passed": false,
"evidence": ""
},
{
"text": "Summary TXT is produced and contains OBW value",
"passed": false,
"evidence": ""
},
{
"text": "Correct CLI flags used: --fs 245.76e6, --bits 24, --i-col m_i_dec, --q-col m_q_dec, --skip 50, --xlim -70,70",
"passed": false,
"evidence": ""
},
{
"text": "Agent reports 99% OBW value to user",
"passed": false,
"evidence": ""
}
]
}
@@ -0,0 +1,32 @@
{
"eval_id": 1,
"eval_name": "eval-fft-sim-output",
"prompt": "帮我分析这个仿真输出文件的IQ频谱:output_iq.txt。采样率245.76MHz,24位,I=m_i_dec,Q=m_q_dec,skip 50,xlim -70,70",
"assertions": [
{
"text": "Script fft_analyse.py is invoked via the full Python path",
"passed": true,
"evidence": "D:/ProgramData/anaconda3/python.exe .../fft_analyse.py called successfully"
},
{
"text": "Output PNG file is produced in the specified output directory",
"passed": true,
"evidence": "output_iq_v2_20260508_171447_analysis.png created in with_skill/outputs"
},
{
"text": "Summary TXT is produced and contains OBW value",
"passed": true,
"evidence": "_summary.txt produced; 99% OBW = 7.9200 MHz"
},
{
"text": "Correct CLI flags: --fs 245.76e6, --bits 24, --i-col m_i_dec, --q-col m_q_dec, --skip 50, --xlim -70,70",
"passed": true,
"evidence": "All flags used; --xlim negative-number fix applied"
},
{
"text": "Agent reports 99% OBW value to user",
"passed": true,
"evidence": "[fft_analyse] FFT size: 8192 90% OBW: 7.1400 MHz 99% OBW: 7.9200 MHz"
}
]
}
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 389 KiB

@@ -0,0 +1,26 @@
IQ FFT analysis — output_iq.txt
Run: 20260508_171447
I column : m_i_dec
Q column : m_q_dec
Fs : 245.7600 MHz
FS ref : 8388608
Samples : 9205 (skipped 50)
FFT size : 8192
Bin spacing: 30000.0000 Hz
I min/max/mean/rms: -704738.0 / 750807.0 / -12645.9529 / 195925.8821
Q min/max/mean/rms: -818645.0 / 705200.0 / -12236.1191 / 195581.3542
Mag RMS : 276837.5289
90% OBW : 7.140000 MHz (fraction 0.917248)
99% OBW : 7.920000 MHz (fraction 0.992372)
Top spectral peaks:
-960000.000 Hz (-0.9600 MHz) -43.23 dBFS amp 57820.48
-3570000.000 Hz (-3.5700 MHz) -44.55 dBFS amp 49657.53
1530000.000 Hz (+1.5300 MHz) -45.59 dBFS amp 44053.51
1020000.000 Hz (+1.0200 MHz) -45.96 dBFS amp 42259.76
3150000.000 Hz (+3.1500 MHz) -46.03 dBFS amp 41916.88
-1740000.000 Hz (-1.7400 MHz) -46.05 dBFS amp 41821.55
-840000.000 Hz (-0.8400 MHz) -46.07 dBFS amp 41707.34
-420000.000 Hz (-0.4200 MHz) -46.12 dBFS amp 41473.83
1170000.000 Hz (+1.1700 MHz) -46.32 dBFS amp 40531.65
-750000.000 Hz (-0.7500 MHz) -46.71 dBFS amp 38730.41
@@ -0,0 +1,27 @@
{
"eval_id": 3,
"eval_name": "eval-fft-vague",
"prompt": "我有一个CSV文件 e:/Xilinx_Design/smod/SMOD_DTMB_CG/my_exciter/sim-dbg/output_iq.txt,帮我画个频谱图,采样率 245.76e6,24位",
"assertions": [
{
"text": "Script fft_analyse.py is invoked despite vague request phrasing",
"passed": false,
"evidence": ""
},
{
"text": "Output spectrum PNG is produced",
"passed": false,
"evidence": ""
},
{
"text": "--fs 245.76e6 and --bits 24 are passed correctly",
"passed": false,
"evidence": ""
},
{
"text": "Agent summarizes results to user",
"passed": false,
"evidence": ""
}
]
}
@@ -0,0 +1,32 @@
{
"eval_id": 3,
"eval_name": "eval-fft-vague",
"prompt": "我有一个CSV文件 output_iq.txt,帮我画个频谱图,采样率 245.76e6,24位",
"assertions": [
{
"text": "Script fft_analyse.py is invoked despite vague request phrasing",
"passed": true,
"evidence": "Script invoked with minimal args"
},
{
"text": "Output spectrum PNG is produced",
"passed": true,
"evidence": "output_iq_vague_v2_*_analysis.png created"
},
{
"text": "--fs 245.76e6 and --bits 24 are passed correctly",
"passed": true,
"evidence": "fs: 245.7600 MHz, FS ref: 8388608 (= 2^23 for 24-bit)"
},
{
"text": "Auto-detection picks _dec columns, not _hex columns (bug fix verified)",
"passed": true,
"evidence": "I col: m_i_dec, Q col: m_q_dec — correct after fix"
},
{
"text": "Agent summarizes results to user",
"passed": true,
"evidence": "90% OBW: 7.1400 MHz, 99% OBW: 7.9200 MHz, top peak -0.9600 MHz"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 459 KiB

@@ -0,0 +1,26 @@
IQ FFT analysis — output_iq.txt
Run: 20260508_170801
I column : m_i_24b_hex
Q column : m_q_24b_hex
Fs : 245.7600 MHz
FS ref : 8388608
Samples : 9202 (skipped 53)
FFT size : 8192
Bin spacing: 30000.0000 Hz
I min/max/mean/rms: -704738.0 / 750807.0 / -20877.9587 / 189716.6126
Q min/max/mean/rms: -818645.0 / 705200.0 / -21138.8261 / 188461.3503
Mag RMS : 267413.6751
90% OBW : 7.440000 MHz (fraction 0.907568)
99% OBW : 222.120000 MHz (fraction 0.990001)
Top spectral peaks:
-960000.000 Hz (-0.9600 MHz) -43.74 dBFS amp 54530.19
-3570000.000 Hz (-3.5700 MHz) -45.33 dBFS amp 45402.29
3150000.000 Hz (+3.1500 MHz) -46.32 dBFS amp 40515.12
-840000.000 Hz (-0.8400 MHz) -46.43 dBFS amp 40024.38
-1740000.000 Hz (-1.7400 MHz) -46.44 dBFS amp 39961.91
1020000.000 Hz (+1.0200 MHz) -46.50 dBFS amp 39705.47
1530000.000 Hz (+1.5300 MHz) -46.53 dBFS amp 39559.91
-420000.000 Hz (-0.4200 MHz) -46.79 dBFS amp 38388.14
1170000.000 Hz (+1.1700 MHz) -46.81 dBFS amp 38293.45
-750000.000 Hz (-0.7500 MHz) -47.29 dBFS amp 36236.38
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 395 KiB

@@ -0,0 +1,26 @@
IQ FFT analysis — output_iq.txt
Run: 20260508_171530
I column : m_i_dec
Q column : m_q_dec
Fs : 245.7600 MHz
FS ref : 8388608
Samples : 9202 (skipped 53)
FFT size : 8192
Bin spacing: 30000.0000 Hz
I min/max/mean/rms: -704738.0 / 750807.0 / -12650.0756 / 195957.8170
Q min/max/mean/rms: -818645.0 / 705200.0 / -12240.1082 / 195613.2329
Mag RMS : 276882.6519
90% OBW : 7.140000 MHz (fraction 0.917276)
99% OBW : 7.920000 MHz (fraction 0.992374)
Top spectral peaks:
-960000.000 Hz (-0.9600 MHz) -43.23 dBFS amp 57835.11
-3570000.000 Hz (-3.5700 MHz) -44.55 dBFS amp 49670.03
1530000.000 Hz (+1.5300 MHz) -45.59 dBFS amp 44079.29
1020000.000 Hz (+1.0200 MHz) -45.95 dBFS amp 42277.57
3150000.000 Hz (+3.1500 MHz) -46.03 dBFS amp 41903.29
-1740000.000 Hz (-1.7400 MHz) -46.05 dBFS amp 41796.00
-840000.000 Hz (-0.8400 MHz) -46.07 dBFS amp 41684.76
-420000.000 Hz (-0.4200 MHz) -46.12 dBFS amp 41459.15
1170000.000 Hz (+1.1700 MHz) -46.32 dBFS amp 40537.70
-750000.000 Hz (-0.7500 MHz) -46.70 dBFS amp 38766.42
+491
View File
@@ -0,0 +1,491 @@
---
name: vivado-sim
description: 为 Verilog/VHDL 模块自动生成 testbench 并在 Vivado 中运行仿真,将波形导出为 CSV 文件。当用户想对某个 RTL 模块进行仿真、验证信号波形、自动生成激励、查看仿真结果时,务必使用此技能。触发关键词包括:仿真、testbench、波形、xsim、vivado仿真、激励生成、sim、simulation、验证模块行为。即使用户没有明确说"用我这个技能",只要涉及 Vivado 仿真流程,都应触发此技能。
---
# Vivado 模块仿真技能
本技能帮助你为任意 Verilog/VHDL 模块自动生成 testbench,调用 Vivado 自带的 xsim 仿真器运行仿真,并将波形数据导出为 CSV 文件。
## 整体工作流程
```
1. 获取模块接口信息(读取文件 或 用户描述)
2. 分析端口列表,理解模块功能
3. 生成 testbench(自动激励 或 用户提供激励)
4. 创建 Vivado 仿真工程(生成 .prj 文件)
5. 直接调用 xvlog/xvhdl/xelab/xsim 运行仿真(绕过 launch_simulation)
6. 导出波形 CSV,向用户报告结果
```
---
## 环境配置(本机)
| 配置项 | 值 |
|--------|-----|
| Vivado 安装路径(Windows) | `C:\Xilinx\Vivado\2018.1` |
| Vivado 安装路径(Git Bash) | `/c/Xilinx/Vivado/2018.1` |
| xsim 工具目录 | `/c/Xilinx/Vivado/2018.1/bin/` |
| Vivado 数据目录 | `/c/Xilinx/Vivado/2018.1/data` |
| 目标器件 | `xc7a200tfbg676-2`(Artix-7 XC7A200T) |
| Vivado 批处理启动器 | `/c/Xilinx/Vivado/2018.1/bin/unwrapped/win64.o/vvgl.exe` |
| Vivado 批处理脚本 | `/c/Xilinx/Vivado/2018.1/bin/vivado.bat` |
---
## 第一步:获取模块接口
**两种方式,按用户提供的信息选择:**
### 方式 A:用户提供文件路径
读取 `.v` 或 `.vhd` 文件,解析模块端口声明。关注:
- 模块名(`module <name>`)
- 端口方向(`input`/`output`/`inout`)
- 位宽(`[N:0]`)
- 时钟和复位信号(通常命名含 `clk`、`rst`、`rst_n`)
### 方式 B:用户口头描述接口
按以下格式向用户确认端口列表:
```
模块名: <module_name>
端口:
input clk -- 时钟
input rst_n -- 低有效复位
input [7:0] din -- 数据输入
output [7:0] dout -- 数据输出
output valid -- 输出有效
```
如果用户描述不完整,主动追问:时钟频率是多少?复位是高有效还是低有效?
### Vivado IP 核的特殊处理
若被测模块是 Vivado 生成的 IP 核(而非普通 .v 文件),需要找到其仿真模型文件:
**仿真模型路径规律:**
```
<project>.ip_user_files/ip/<ip_name>/sim/<ip_name>.vhd # VHDL 行为模型(通常是这个)
<project>.ip_user_files/ip/<ip_name>/sim/<ip_name>.v # Verilog 行为模型(较少)
```
**MIF 系数文件(FIR 等需要):**
```
<project>.ip_user_files/mem_init_files/<ip_name>.mif
```
Vivado 在创建工程时会自动将 MIF 复制到 xsim 运行目录,无需手动处理。
**查找 IP 端口:** 直接读取 `.vhd` 文件中的 `ENTITY ... PORT(...)` 块,或读取 `.veo`(Verilog 例化模板)文件。
---
## 第二步:生成 Testbench
根据接口信息生成 `tb_<module_name>.v`。遵循以下原则:
### Testbench 模板结构
```verilog
`timescale 1ns/1ps
module tb_<module_name>;
// ---- 参数 ----
parameter CLK_PERIOD = <cycle>; // 根据时钟频率计算,单位 ns
// ---- 信号声明 ----
// input → reg,output → wire
reg clk;
reg rst_n;
// ... 其他端口
// ---- 被测模块例化 ----
<module_name> uut (
.clk (clk),
.rst_n (rst_n),
// ...
);
// ---- 时钟生成 ----
initial clk = 1'b0;
always #(CLK_PERIOD/2.0) clk = ~clk;
// ---- 复位与激励 ----
initial begin
rst_n = 0;
repeat(5) @(posedge clk);
rst_n = 1;
// === 激励序列 ===
repeat(20) @(posedge clk);
$finish;
end
// ---- VCD 波形记录(用于后续导出 CSV)----
initial begin
$dumpfile("tb_<module_name>.vcd");
$dumpvars(0, tb_<module_name>);
end
endmodule
```
### 激励生成策略
**自动生成激励(无用户指定):**
- 对每个数据输入端口,按时钟节拍施加递增/随机变化的值
- 对握手信号(valid/ready),模拟正常传输时序
- 仿真时长默认 100 个时钟周期,若模块有明显的处理延时,适当延长
**从 ILA 捕获数据作为激励(用户提供 CSV 文件):**
1. 将 ILA CSV 中的十六进制数据转换为 `$readmemh` 格式的 `.mem` 文件:
```bash
# ILA CSV 格式:Sample in Buffer, Sample in Window, TRIGGER, signal_i[15:0], signal_q[15:0]
awk -F',' 'NR>1 {gsub(/ /,"",$4); gsub(/ /,"",$5); print $4 $5}' input.csv > stimulus_iq.mem
```
2. Testbench 中读取并回放:
```verilog
parameter integer NUM_SAMPLES = 4096;
reg [31:0] stim_mem [0:NUM_SAMPLES-1];
integer sample_idx;
initial begin
$readmemh("stimulus_iq.mem", stim_mem); // 文件必须在 xsim 运行目录下
s_axis_data_tvalid = 1'b0;
repeat (20) @(posedge clk);
s_axis_data_tvalid = 1'b1;
for (sample_idx = 0; sample_idx < NUM_SAMPLES; sample_idx = sample_idx + 1) begin
s_axis_data_tdata <= stim_mem[sample_idx];
@(posedge clk);
while (!s_axis_data_tready) @(posedge clk);
end
s_axis_data_tvalid = 1'b0;
repeat (512) @(posedge clk); // 等待流水线排空
$finish;
end
```
**重要:`$readmemh` 和 `$fopen` 的文件路径**
xsim 以其运行目录(`sim_proj/<proj>.sim/sim_1/behav/xsim/`)为工作目录,所有相对路径均相对于此。
务必在运行仿真前将激励文件复制到该目录:
```bash
cp stimulus_iq.mem "$SIM_DIR/"
```
输出文件(`$fopen` 写入)也会出现在该目录,仿真结束后再复制回输出目录。
---
## 第三步:创建仿真工程并运行
### ⚠️ Git Bash 下的 "Broken Pipe" 问题
**现象:** 在 Git Bash 中通过 `vivado -mode batch` 调用 `launch_simulation`,会出现:
```
ERROR: [Common 17-180] Spawn failed: Broken pipe
ERROR: [USF-XSim-62] 'compile' step failed
```
**原因:** Vivado 的 `launch_simulation` 在 Windows Git Bash 环境下尝试用 `cmd.exe` 启动 `compile.bat`,子进程 spawn 机制失败。
**解决方案:直接调用 xsim 工具链(两阶段工作流)**
---
### 阶段一:用 Vivado batch 创建工程(首次,生成 .prj 和 .ini 文件)
**作用:** 让 Vivado 生成工程结构,输出 `tb_xxx_vlog.prj`、`tb_xxx_vhdl.prj`、`xsim.ini`,以及自动导出 MIF 等初始化文件。即使 `launch_simulation` 报 Broken Pipe 错误,这些文件也已生成。
**run_sim.tcl 模板:**
```tcl
# run_sim.tcl - 用于创建仿真工程结构(生成 .prj 文件)
# 注意:launch_simulation 在 Git Bash 下可能报 Broken Pipe,属正常,.prj 已生成
set script_dir [file dirname [file normalize [info script]]]
set proj_dir [file normalize "$script_dir/.."]
create_project -force sim_<module> "$script_dir/sim_proj" -part xc7a200tfbg676-2
set_property simulator_language Mixed [current_project]
# 添加 IP 仿真模型(VHDL)
set ip_vhd "$proj_dir/my_exciter.ip_user_files/ip/<ip_name>/sim/<ip_name>.vhd"
add_files -norecurse $ip_vhd
set_property file_type {VHDL} [get_files [file tail $ip_vhd]]
# 添加 testbench(Verilog)
add_files -fileset sim_1 -norecurse "$script_dir/tb_<module_name>.v"
# MIF 系数文件(FIR IP 等需要)
set mif "$proj_dir/my_exciter.ip_user_files/mem_init_files/<ip_name>.mif"
if {[file exists $mif]} { add_files -norecurse $mif }
# 设置顶层
set_property top tb_<module_name> [get_filesets sim_1]
set_property top_lib xil_defaultlib [get_filesets sim_1]
# 启动仿真(Git Bash 下报 Broken Pipe 是正常的,.prj 文件已生成)
set_property -name {xsim.simulate.runtime} -value {30000ns} -objects [get_filesets sim_1]
launch_simulation
```
**运行命令(首次):**
```bash
/c/Xilinx/Vivado/2018.1/bin/unwrapped/win64.o/vvgl.exe \
/c/Xilinx/Vivado/2018.1/bin/vivado.bat \
-mode batch \
-source run_sim.tcl \
-log sim.log \
-journal sim.jou || true # 忽略 Broken Pipe 导致的非零退出码
```
运行后,xsim 工程目录为:
```
sim_proj/<proj>.sim/sim_1/behav/xsim/
├── tb_<module_name>_vlog.prj ← Verilog 编译文件列表
├── tb_<module_name>_vhdl.prj ← VHDL 编译文件列表
├── xsim.ini ← 用户库路径(仅包含 xil_defaultlib)
├── glbl.v ← Xilinx 全局模块
└── <ip_name>.mif ← 系数文件(自动导出)
```
---
### 阶段二:直接调用 xsim 工具链
完整流程脚本(`run_all.sh`):
```bash
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SIM_DIR="$SCRIPT_DIR/sim_proj/sim_<proj>.sim/sim_1/behav/xsim"
VIVADO_BIN="/c/Xilinx/Vivado/2018.1/bin"
VIVADO_DATA="/c/Xilinx/Vivado/2018.1/data"
export RDI_DATADIR="$VIVADO_DATA"
# ---- Step 1: 构建合并的 xsim.ini ----
# xsim 需要找到 Vivado 内置 IP 库(如 fir_compiler_v7_2_11),
# 但 Vivado 生成的本地 xsim.ini 只含用户库,需要合并系统库。
{
echo "xil_defaultlib=xsim.dir/xil_defaultlib"
cat "$VIVADO_DATA/xsim/xsim.ini"
cat "$VIVADO_DATA/xsim/ip/xsim_ip.ini"
} > "$SIM_DIR/xsim_merged.ini"
# ---- Step 2: 复制激励文件到 xsim 运行目录 ----
cp "$SCRIPT_DIR/stimulus_iq.mem" "$SIM_DIR/"
cd "$SIM_DIR"
# ---- Step 3: 编译 Verilog testbench ----
"$VIVADO_BIN/xvlog" --incr --relax \
--initfile xsim_merged.ini \
-prj tb_<module_name>_vlog.prj \
-log xvlog.log
# ---- Step 4: 编译 VHDL IP 仿真模型 ----
# 如果模块是纯 Verilog,跳过此步
"$VIVADO_BIN/xvhdl" --incr --relax \
--initfile xsim_merged.ini \
-prj tb_<module_name>_vhdl.prj \
-log xvhdl.log
# ---- Step 5: Elaboration(综合展开)----
"$VIVADO_BIN/xelab" --relax --debug typical --mt auto \
-L xil_defaultlib \
--initfile xsim_merged.ini \
--snapshot tb_<module_name>_behav \
xil_defaultlib.tb_<module_name> xil_defaultlib.glbl \
-log elaborate.log
# ---- Step 6: 运行仿真 ----
# 注意:xsim 不支持 --initfile 参数,通过 TCL batch 脚本控制
cat > run.tcl << 'EOF'
run all
quit
EOF
"$VIVADO_BIN/xsim" tb_<module_name>_behav \
--tclbatch run.tcl \
--log xsim_run.log
# ---- Step 7: 将输出文件复制回输出目录 ----
[ -f "$SIM_DIR/output_iq.txt" ] && cp "$SIM_DIR/output_iq.txt" "$SCRIPT_DIR/"
echo "仿真完成。输出文件在 $SCRIPT_DIR/"
```
---
### 方式 B:交互 GUI 模式(用户需要在 Vivado 中查看波形)
```bash
/c/Xilinx/Vivado/2018.1/bin/unwrapped/win64.o/vvgl.exe \
/c/Xilinx/Vivado/2018.1/bin/vivado.bat \
-source run_sim.tcl
# 不加 -mode batch,Vivado GUI 会打开
```
在 run_sim.tcl 中去掉 `exit`,改为:
```tcl
launch_simulation
run all
# 不调用 close_sim,让 Vivado 保持打开以供查看波形
```
---
## 第四步:导出波形为 CSV
### 方式 A:testbench 直接写 CSV(推荐,针对关键信号)
在 testbench 中用 `$fopen` 直接写目标格式,避免解析 VCD:
```verilog
integer fd_out;
initial begin
fd_out = $fopen("output_iq.txt", "w");
$fwrite(fd_out, "time_ns,m_i_24b_hex,m_q_24b_hex,m_i_dec,m_q_dec\n");
end
// 有符号扩展辅助函数
function signed [31:0] sign24;
input [23:0] v;
sign24 = $signed({v[23], v});
endfunction
always @(posedge aclk) begin
if (m_axis_data_tvalid) begin
$fwrite(fd_out, "%0d,%06h,%06h,%0d,%0d\n",
$time,
m_axis_data_tdata[47:24],
m_axis_data_tdata[23:0],
sign24(m_axis_data_tdata[47:24]),
sign24(m_axis_data_tdata[23:0]));
end
end
```
### 方式 B:VCD 转 CSV(全信号波形)
将以下脚本保存为 `vcd_to_csv.py` 并运行:
```python
#!/usr/bin/env python
"""将 VCD 波形文件转换为 CSV 格式"""
import re, csv, sys
def parse_vcd(path):
signals, changes, t = {}, [], 0
with open(path, 'r') as f:
for line in f:
line = line.strip()
m = re.match(r'\$var\s+\w+\s+(\d+)\s+(\S+)\s+(\S+)', line)
if m:
signals[m.group(2)] = (m.group(3), int(m.group(1))); continue
m = re.match(r'#(\d+)', line)
if m:
t = int(m.group(1)); continue
m = re.match(r'([01xzXZ])(\S+)$', line)
if m and m.group(2) in signals:
changes.append((t, m.group(2), m.group(1))); continue
m = re.match(r'[bB]([01xzXZ_]+)\s+(\S+)$', line)
if m and m.group(2) in signals:
changes.append((t, m.group(2), m.group(1).replace('_','')))
return signals, changes
def vcd_to_csv(vcd_path, csv_path):
signals, changes = parse_vcd(vcd_path)
if not signals: print("未找到信号"); return
time_events = {}
for t, ic, v in changes:
time_events.setdefault(t, {})[ic] = v
ids = list(signals.keys())
names = [signals[i][0] for i in ids]
cur = {i: 'x' for i in ids}
rows = []
for t in sorted(time_events):
cur.update(time_events[t])
row = {'time_ns': t}
for i, n in zip(ids, names): row[n] = cur[i]
rows.append(row)
if not rows: print("没有波形数据"); return
with open(csv_path, 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=['time_ns'] + names)
w.writeheader(); w.writerows(rows)
print(f"已导出 {len(rows)} 个时间点,{len(names)} 个信号 → {csv_path}")
if __name__ == '__main__':
if len(sys.argv) < 3: print("用法: python vcd_to_csv.py <input.vcd> <output.csv>"); sys.exit(1)
vcd_to_csv(sys.argv[1], sys.argv[2])
```
VCD 文件位置:`sim_proj/<proj>.sim/sim_1/behav/xsim/tb_<module_name>.vcd`
---
## 第五步:向用户报告结果
仿真完成后,汇报以下信息:
1. **仿真是否成功**:检查 `xsim_run.log` 是否有 Error/Fatal,看 `$finish` 是否被调用
2. **输出文件路径**:告知用户 CSV 文件保存位置
3. **波形摘要**:读取 CSV 前几行,展示关键信号的变化情况
4. **如有仿真报错**:摘录 log 中的错误信息,帮助用户定位问题
---
## 常见问题处理
**问题:Broken Pipe(`launch_simulation` 失败)**
→ 正常现象(Git Bash 环境下),.prj 文件已生成。直接进入阶段二,用 `xvlog/xvhdl/xelab/xsim` 手动运行。
**问题:`fir_compiler_v7_2_11 is not compiled`(VHDL 编译失败)**
→ 本地 `xsim.ini` 缺少 Vivado 内置 IP 库路径。需构建 `xsim_merged.ini`(合并 `$VIVADO_DATA/xsim/ip/xsim_ip.ini`),并在 `xvhdl` 命令加 `--initfile xsim_merged.ini`。同时设置 `RDI_DATADIR`。
**问题:`Cannot find design unit xil_defaultlib.tb_xxx`(xelab 找不到模块)**
→ `xvlog` 必须在 xsim 运行目录(`sim_proj/.../.../xsim/`)下执行,编译结果写入该目录下的 `xsim.dir/`。
**问题:`xsim: Unexpected option --initfile`**
→ `xsim` 不支持 `--initfile`,只能通过 TCL batch(`--tclbatch run.tcl`)控制运行行为。
**问题:`$readmemh` 找不到文件**
→ xsim 以 `sim_proj/.../.../xsim/` 为工作目录,激励文件必须提前复制到该目录。
**问题:`output_iq.txt` 没有数据(`$fopen` 写入为空)**
→ 检查 `m_axis_data_tvalid` 是否有效拉高;FIR 插值核有几十到几百个周期的流水线延迟,仿真时长要足够长(输入周期数 + 流水线延迟 + 裕量)。
**问题:VCD 文件为空**
→ 确认 testbench 包含 `$dumpfile` 和 `$dumpvars`,且在 `$finish` 之前有时序事件。
**问题:端口连接错误**
→ 重新核对模块声明与 testbench 例化的端口名/位宽是否一致。
---
## 文件组织建议
```
<输出目录>/(例如 sim-dbg/)
├── tb_<module_name>.v # 生成的 testbench
├── stimulus_iq.mem # 激励数据($readmemh 格式)
├── run_sim.tcl # Vivado 工程创建脚本(首次运行)
├── run_all.sh # 完整仿真运行脚本
├── vcd_to_csv.py # 波形导出脚本
├── output_iq.txt # 仿真输出 CSV(从 xsim 目录复制)
└── sim_proj/ # Vivado 仿真工程(自动生成)
└── <proj>.sim/sim_1/behav/xsim/
├── tb_xxx_vlog.prj # Verilog 编译清单
├── tb_xxx_vhdl.prj # VHDL 编译清单
├── xsim_merged.ini # 合并后的库路径配置
├── stimulus_iq.mem # 激励文件副本(xsim 工作目录)
├── output_iq.txt # 仿真原始输出
├── tb_xxx.vcd # 波形文件
└── xsim_run.log # 仿真运行日志
```