#!/usr/bin/env python3 """Generate the dual-DUT cycle-true comparison testbench + xsim run script. python gen_cmp_tb.py [--outdir DIR] [--cycles N] [--ip-simdir DIR] [--compare-at PORT=SIGNAL ...] - DUT0 = compiled from /*.v (original HDL Coder RTL) - DUT1 = _IP compiled from /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 _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 ". - --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/)') 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()