Files
bowei ea4541ee90 sync: SHANGHAI-U9 2026-07-27 三个 Simulink→RTL skill 转系统级 + 新增 simulink-rtl-redev
- simulink_rtl_dev / rtl-ip-replace: 从工程级 .claude/skills 迁到用户级并去工程耦合
  (模型名/Notes 文档依赖/工程实测基线移出; addpath 与脚本路径改用户级)
- rtl-ip-replace/scripts/gen_cmp_tb.py: Vivado 路径不再写死, 改读 VIVADO_PATH /
  XILINX_VIVADO (兼容 install root / bin / bin\vivado.bat 三种写法), 回落时提示
  去设 skills_env.md 的 VIVADO_PATH
- simulink-rtl-redev: 新增。原生模型改动后的仿真模型同步规范, 含两个实测过的工具
  check_sync.py (三条时序链新鲜度闸门) 与 measure_k.m (时域/频域/包络三估计器互证)
- sim-rtl-flow/SKILL.md: 加入 sync_sim:* 步骤, 引用上述三个 skill
2026-07-27 10:39:53 +08:00

169 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Generate the dual-DUT cycle-true comparison testbench + xsim run script.
python gen_cmp_tb.py <top_module> <hdlsrc_dir> [--outdir DIR] [--cycles N]
[--ip-simdir DIR] [--compare-at PORT=SIGNAL ...]
- DUT0 = <top> compiled from <hdlsrc>/*.v (original HDL Coder RTL)
- DUT1 = <top>_IP compiled from <hdlsrc>/rtl_ip/*.v (+ IP simulation netlists)
- identical stimulus into both: clk; reset (asserted RST_CYCLES then deasserted);
enable-like inputs (enb/clk_enable/*_en names) held 1; every other input driven
from a per-port LFSR (deterministic, seeded by port name hash).
!! For control-heavy tops, replace the LFSR block with replayed vectors from
the HDL Coder <top>_tb.v -- see SKILL.md.
- compare: every output port, every posedge after both-ready; mismatches counted
and logged; result file first line = PASS or "FAIL <n>".
- --compare-at OUT=QUALIFIER limits comparison of OUT to cycles where the named
qualifier signal (an output of the DUTs, e.g. a done/valid strobe) is 1 --
the CORDIC consumption-point contract.
"""
import argparse, hashlib, os, re, sys
# Vivado install root, for the generated run_cmp.bat (xvlog/xelab/xsim + glbl.v).
# Machine-specific, so it comes from the environment, never from a literal path.
# VIVADO_PATH may point at the install root, its bin/, or bin/vivado.bat --
# all three are accepted. XILINX_VIVADO (set by Vivado's settings64) also works.
VIVADO_FALLBACK = r'D:\Xilinx\Vivado\2018.1'
def _vivado_root():
v = (os.environ.get('VIVADO_PATH') or os.environ.get('XILINX_VIVADO') or '').strip().strip('"')
if not v:
print(f'NOTE: neither VIVADO_PATH nor XILINX_VIVADO is set -- falling back to\n'
f' {VIVADO_FALLBACK}\n'
f' That literal is one machine\'s install. Set VIVADO_PATH to this\n'
f' machine\'s Vivado (see the VIVADO_PATH entry in ~/skills_env.md)\n'
f' or run_cmp.bat may silently use the wrong version.')
return VIVADO_FALLBACK
p = v.rstrip('\\/')
if p.lower().endswith(('.bat', '.exe')) or os.path.isfile(p):
p = os.path.dirname(p) # ...\bin\vivado.bat -> ...\bin
if os.path.basename(p).lower() == 'bin':
p = os.path.dirname(p) # ...\bin -> install root
return p or VIVADO_FALLBACK
PORT_RE = re.compile(r'^\s*(input|output)\s+(signed\s+)?(\[[^\]]+\]\s*)?(\w+)\s*[,;)]', re.M)
def parse_ports(vfile, top):
txt = open(vfile, encoding='utf-8', errors='replace').read()
m = re.search(r'module\s+%s\b(.*?)endmodule' % re.escape(top), txt, re.S)
if not m:
sys.exit(f'module {top} not found in {vfile}')
body = m.group(1)
ports = []
for pm in PORT_RE.finditer(body):
d, signed, rng, name = pm.group(1), pm.group(2), (pm.group(3) or '').strip(), pm.group(4)
ports.append({'dir': d, 'signed': bool(signed), 'range': rng, 'name': name})
return ports
def lfsr_seed(name):
return int(hashlib.sha1(name.encode()).hexdigest()[:8], 16) | 1
def main():
ap = argparse.ArgumentParser()
ap.add_argument('top'); ap.add_argument('hdlsrc')
ap.add_argument('--rtl-ip', required=True, help='FIXED rtl_ip dir (vivado/rtl_ip/<model>)')
ap.add_argument('--outdir', default=None)
ap.add_argument('--cycles', type=int, default=200000)
ap.add_argument('--rst-cycles', type=int, default=20)
ap.add_argument('--ip-simdir', default='', help='dir with generated IP sim .v files (globbed recursively)')
ap.add_argument('--compare-at', nargs='*', default=[], metavar='OUT=QUAL')
a = ap.parse_args()
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip_dir = os.path.abspath(a.rtl_ip)
outdir = a.outdir or os.path.join(rtl_ip_dir, 'cmp')
os.makedirs(outdir, exist_ok=True)
qual = dict(kv.split('=') for kv in a.compare_at)
src = None
for fn in os.listdir(hdlsrc):
if fn.endswith('.v') and re.search(r'^\s*module\s+%s\b' % re.escape(a.top),
open(os.path.join(hdlsrc, fn), encoding='utf-8', errors='replace').read(), re.M):
src = os.path.join(hdlsrc, fn); break
if not src:
sys.exit(f'{a.top} not declared in {hdlsrc}/*.v')
ports = parse_ports(src, a.top)
ins = [p for p in ports if p['dir'] == 'input']
outs = [p for p in ports if p['dir'] == 'output']
def is_clk(n): return n in ('clk', 'clock')
def is_rst(n): return 'reset' in n or n == 'rst'
def is_en(n): return n in ('enb', 'clk_enable') or n.endswith('_en') or n.endswith('_enable')
tb = ['`timescale 1 ns / 1 ns', 'module tb_cmp;',
' reg clk = 0; always #2 clk = ~clk;',
f' reg rst = 1; initial begin repeat ({a.rst_cycles}) @(posedge clk); rst <= 0; end',
' integer mismatches = 0; integer fd;']
conn0, conn1 = [], []
for p in ins:
n, rng = p['name'], p['range']
if is_clk(n):
conn0.append(f'.{n}(clk)'); conn1.append(f'.{n}(clk)'); continue
if is_rst(n):
conn0.append(f'.{n}(rst)'); conn1.append(f'.{n}(rst)'); continue
if is_en(n):
tb.append(f' wire {rng} {n} = 1\'b1;')
else:
w = 32
m = re.match(r'\[(\d+):0\]', rng or '')
if m: w = int(m.group(1)) + 1
tb.append(f' reg [31:0] lfsr_{n} = 32\'h{lfsr_seed(n):08x};')
tb.append(f' always @(posedge clk) lfsr_{n} <= {{lfsr_{n}[30:0], lfsr_{n}[31]^lfsr_{n}[21]^lfsr_{n}[1]^lfsr_{n}[0]}};')
tb.append(f' wire {rng} {n} = lfsr_{n}[{min(w,32)-1}:0];')
conn0.append(f'.{n}({n})'); conn1.append(f'.{n}({n})')
for p in outs:
n, rng = p['name'], p['range']
tb.append(f' wire {rng} o0_{n}, o1_{n};')
conn0.append(f'.{n}(o0_{n})'); conn1.append(f'.{n}(o1_{n})')
tb += [f' {a.top} dut0 ({", ".join(conn0)});',
f' {a.top}_IP dut1 ({", ".join(conn1)});',
f' reg armed = 0; initial begin repeat ({a.rst_cycles + 5}) @(posedge clk); armed <= 1; end',
' always @(posedge clk) if (armed) begin']
for p in outs:
n = p['name']
g = f' && (o0_{qual[n]} === 1\'b1)' if n in qual else ''
tb.append(f' if ((o0_{n} !== o1_{n}){g}) begin')
tb.append(f' mismatches = mismatches + 1;')
tb.append(f' if (mismatches <= 50) $display("MISMATCH t=%0t port={n} orig=%h ip=%h", $time, o0_{n}, o1_{n});')
tb.append(' end')
tb += [ ' end',
f' initial begin repeat ({a.cycles}) @(posedge clk);',
f' fd = $fopen("cmp_result.txt", "w");',
' if (mismatches == 0) begin $fdisplay(fd, "PASS"); $display("CMP PASS"); end',
' else begin $fdisplay(fd, "FAIL %0d", mismatches); $display("CMP FAIL %0d", mismatches); end',
' $fclose(fd); $finish;',
' end', 'endmodule']
open(os.path.join(outdir, 'tb_cmp.v'), 'w').write('\n'.join(tb) + '\n')
vroot = _vivado_root()
xbin = os.path.join(vroot, 'bin')
glbl = os.path.join(vroot, 'data', 'verilog', 'src', 'glbl.v')
if not os.path.isfile(glbl):
print(f'WARNING: glbl.v not found under {vroot}\n'
f' set VIVADO_PATH (or XILINX_VIVADO) to your Vivado install, '
f'its bin/, or bin/vivado.bat; run_cmp.bat will fail as written.')
bat = [f'@echo off', f'cd /d %~dp0',
f'call "{xbin}\\xvlog.bat" "{glbl}"',
f'for %%f in ("{hdlsrc}\\*.v") do if /I not "%%~nxf"=="{a.top}_tb.v" call "{xbin}\\xvlog.bat" "%%f"',
f'for %%f in ("{rtl_ip_dir}\\*.v") do call "{xbin}\\xvlog.bat" "%%f"']
if a.ip_simdir:
bat.append(f'for /R "{a.ip_simdir}" %%f in (*.v) do call "{xbin}\\xvlog.bat" "%%f"')
# dist_mem_gen/blk_mem_gen sim models load .mif by BARE filename relative to
# the xsim run directory -- without this copy every ROM reads 0.
bat.append(f'for /R "{a.ip_simdir}" %%f in (*.mif) do copy /Y "%%f" . >nul')
bat += [f'call "{xbin}\\xvlog.bat" tb_cmp.v',
f'call "{xbin}\\xelab.bat" tb_cmp glbl -s cmp_sim -timescale 1ns/1ns '
f'-L dist_mem_gen_v8_0 -L blk_mem_gen_v8_4 -L fifo_generator_v13_2 '
f'-L cordic_v6_0_14 -L fir_compiler_v7_2_11 -L cic_compiler_v4_0_13 '
f'-L xbip_utils_v3_0_8 -L xbip_utils_v3_0_9 -L xbip_pipe_v3_0_5 '
f'-L unisims_ver -L unimacro_ver -L secureip',
f'call "{xbin}\\xsim.bat" cmp_sim -R',
f'type cmp_result.txt']
open(os.path.join(outdir, 'run_cmp.bat'), 'w').write('\r\n'.join(bat) + '\r\n')
print(f'generated {outdir}\\tb_cmp.v ({len(ins)} in / {len(outs)} out ports) and run_cmp.bat')
print('NOTE: exclude the original *_tb.v from compile; for control-heavy tops replace the')
print(' LFSR stimulus with replayed HDL Coder testbench vectors (see SKILL.md).')
if __name__ == '__main__':
main()