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
This commit is contained in:
bowei
2026-07-27 10:39:53 +08:00
parent 6dddab8923
commit ea4541ee90
23 changed files with 3171 additions and 12 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Step 0 of every rtl-ip-replace run (MANDATORY, user directive 2026-07-23):
move ALL existing content of the fixed output dir into backup_<timestamp>/,
so the run always generates everything fresh while keeping full history.
Usage: python backup_outdir.py <outdir>
Existing backup_* subdirectories are kept in place (not re-nested).
"""
import os, shutil, sys, time
def main():
if len(sys.argv) != 2:
sys.exit('usage: backup_outdir.py <outdir>')
d = os.path.abspath(sys.argv[1])
os.makedirs(d, exist_ok=True)
items = [x for x in os.listdir(d) if not x.startswith('backup_')]
if not items:
print('nothing to back up in', d)
return
bdir = os.path.join(d, 'backup_' + time.strftime('%Y%m%d_%H%M%S'))
os.makedirs(bdir)
for x in items:
shutil.move(os.path.join(d, x), os.path.join(bdir, x))
print(f'backed up {len(items)} item(s) -> {bdir}')
if __name__ == '__main__':
main()
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Filters -> Xilinx FIR Compiler (Task12 §六, requirement 4b).
GUIDED, by design: this script does the mechanical half (pull the integer taps
out of the HDL Coder coefficient module, write the .coe, register the IP in
ip_manifest, and write a surgery proposal with the exact latency-alignment
arithmetic). It NEVER rewrites the RTL by itself -- the structural surgery and
the delay alignment are reviewed with the user first (Task12 §6.3).
python fir_to_ip.py <HDLSRC> <OUT> [--manifest m.json]
[--only MODULE ...] [--filter-type Single_Rate|Interpolation|Decimation]
[--rate N] [--data-width W --data-frac F] [--channels N] [--paths N]
[--reload] # switching between SEVERAL FIXED coefficient
# sets; NOT an escape hatch for adaptive taps
[--no-aclken]
Outputs in <OUT>:
fir_<module>_<hash>.coe integer taps, radix 10 (no re-quantisation by the IP)
ip_manifest.json/.tcl += {type fir_compiler ...}
FIR_PROPOSAL_<module>.md what to cut, what to wire, how to align the latency
"""
import argparse, hashlib, json, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def coe_path(outdir, module, coeffs, width, frac):
h = hashlib.sha1((f'{width}/{frac}:' + ','.join(map(str, coeffs))).encode()).hexdigest()[:10]
name = f'fir_{module}_{h}'
p = os.path.join(outdir, name + '.coe')
with open(p, 'w') as f: # ASCII only: .coe is read by Vivado, not by people
f.write('; rtl-ip-replace: taps copied verbatim from the HDL Coder constant module\n')
f.write('; (Task12 6.2.1 -- integer coefficients, the IP must not re-quantise)\n')
f.write('radix = 10;\ncoefdata =\n')
f.write(',\n'.join(str(c) for c in coeffs) + ';\n')
return name, p
PROPOSAL = """# FIR IP 替换方案 — `{module}` (Task12 §六,引导式,待确认)
来源:`{file}` Source Path:`{src}`
IP:`{ipname}`(fir_compiler 7.2) — 系数 `.coe`:`{coe}`
## 1. 系数(契约 §6.2.1)
- 抽头数 **{taps}**,系数位宽 **{cw}**,小数位 **En{cf}**,对称:{sym}
- `.coe` 为**整数**系数、逐个抄自 `{coef_signal}_i` 常量,IP 侧 `Quantization=Integer_Coefficients`,
不做二次量化。改系数 = 改 .coe + 重生成 IP。
## 2. IP 配置(gen_ip.tcl 已按此写入)
| 项 | 值 | 理由 |
|---|---|---|
| Filter_Type | {ftype} | {rate_note} |
| Data_Width | {dw}(原 RTL 二进制点 En{df}) | 位宽照抄;**小数位一律填 0** |
| Coefficient_Width | {cw}(原 En{cf}) | 同上 |
| Output_Rounding_Mode | Full_Precision | 契约 §6.2.2:截位放到 IP 外面做 |
| RateSpecification / SamplePeriod | Input_Sample_Period / 1 | 禁止硬件折叠,延迟才是确定值 |
| Has_ACLKEN | {aclken} | 用 `enb` 门控,与原 RTL 的使能节奏逐拍一致 |
| Coefficient_Reload | {reload} | {reload_note} |
| M_DATA_Has_TREADY / S_DATA_Has_FIFO | false / false | 纯流水,不引入可变延迟 |
**二进制点填 0 的理由**:core 做的是整数 MAC,二进制点只影响 GUI 显示;填 0 既与生成 RTL 的
整数运算完全一致,又避开 `Data_Fractional_Bits` 的合法区间(0..16)限制。
**IP 硬限制(2026-07-24 在 Vivado 2018.1 实测)**:`Coefficient_Width∈[8,49]`、
`Data_Width∈[2,47]`、`Data_Fractional_Bits∈[0,16]`。本例:系数 {cw} 位、数据 {dw} 位 → **{limits}**。
超限时不得硬塞 IP,按 §一.12 双向调整原则回 Simulink 重新量化(HDL Coder 的 full-precision
51 位抽头本来就没有工程意义),并复验性能。
## 3. 手术步骤(需人工确认后执行)
1. 在 `<OUT>/{file}` 中圈出滤波器边界:输入 `{din}`、输出 `{dout}`、有效 `{valid}`;
复数({cplx})时每个实部/虚部各一路 IP(或 Number_Paths=2 走同一套系数)。
2. 例化 `{ipname}`:`aclk=clk`、`aclken=enb`、`s_axis_data_tvalid=<原输入有效拍>`、
`s_axis_data_tdata=<输入,符号扩展到 8 的倍数位>`。
3. **延迟对齐(契约 §6.2.3,最容易翻车的一步)**:
- 量 IP 延迟 L:综合后看 `m_axis_data_tvalid` 相对 `s_axis_data_tvalid` 的拍数
(或 xsim 打一个脉冲量);
- 量原 RTL 延迟 L0:数原来的 delay line + 乘加流水寄存器;
- **L ≤ L0**:在 IP 输出后补 `L0-L` 拍移位寄存器;
- **L > L0**:不许偷偷改节奏——按 §一.12 双向调整原则改 Simulink 源(整体延迟增加),
并用模型仿真复验 MER/收敛不劣化,结论写进 Result 文档。
4. 截位:把原 RTL 对乘加结果做的右移/饱和原样接到 `m_axis_data_tdata` 之后
(IP 输出是 Full Precision,位宽 = {out_width_hint})。
5. `m_axis_data_tvalid` 的翻转拍必须与原输出有效拍逐拍一致(契约 §6.2.4)。
## 4. 验收
`gen_cmp_tb.py` 双 DUT 逐拍对比,对齐后 **0 失配**方为 PASS(契约 §6.2.5);
不 PASS 不得接入综合(§三)。
"""
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc'); ap.add_argument('outdir')
ap.add_argument('--manifest')
ap.add_argument('--only', nargs='*', default=None, help='limit to these module names')
ap.add_argument('--filter-type', default=None,
choices=['Single_Rate', 'Interpolation', 'Decimation'])
ap.add_argument('--rate', type=int, default=1)
ap.add_argument('--data-width', type=int, default=None)
ap.add_argument('--data-frac', type=int, default=None)
ap.add_argument('--channels', type=int, default=1)
ap.add_argument('--paths', type=int, default=1)
ap.add_argument('--reload', action='store_true', help='adaptive taps via Coefficient_Reload')
ap.add_argument('--no-aclken', action='store_true')
a = ap.parse_args()
out = os.path.abspath(a.outdir)
man = json.load(open(a.manifest or os.path.join(out, 'manifest.json')))
filters = man.get('filter', [])
banks = [f for f in filters if f['kind'] == 'fir_coef_bank']
blocks = [f for f in filters if f['kind'].startswith('fir_block')]
adaptive = [f for f in filters if f['kind'] == 'adaptive_fir_candidate']
if a.only:
banks = [b for b in banks if b['module'] in a.only]
if not banks:
print('no fixed-coefficient FIR coefficient bank in the manifest '
'-- nothing in scope for fir_compiler.')
for f in adaptive:
print(f"INVENTORY ONLY (not replaced): {f['module']}.{f['tap_regs']} "
f"({f['taps']} runtime taps, delay line {f['delay_line']} deep {f['delay_depth']}). "
f"Per SKILL scope: Farrow/LMS/correlator filters keep their RTL.")
for f in [x for x in filters if x['kind'] == 'fir_block_candidate']:
print(f"INVENTORY ONLY (not replaced): {f['module']} [{f['file']}] -- "
f"coefficients are not constants ({len(f.get('coef_ports', []))} coef port(s)).")
ipman_p = os.path.join(out, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
done = 0
for b in banks:
# the FIR block that consumes this coefficient module (same Simulink parent path)
parent = next((x for x in blocks
if b['source_path'].startswith(x['source_path'])
and x['source_path'] and x['module'] != b['module']), None)
din = sorted(parent['data_in']) if parent else []
dout = sorted(parent['data_out']) if parent else []
dinfo = (parent['data_in'][din[0]] if din else {})
dw = a.data_width or dinfo.get('width') or 16
df = a.data_frac if a.data_frac is not None else (dinfo.get('frac') or 0)
ftype = a.filter_type or ({'interpolation': 'Interpolation',
'decimation': 'Decimation'}.get(
parent['rate_hint'] if parent else '', 'Single_Rate'))
if ftype != 'Single_Rate' and a.rate == 1:
print(f" !! {b['module']}: rate change inferred from the module name but no --rate "
f"given; falling back to Single_Rate. Confirm the factor in the Simulink block.")
ftype = 'Single_Rate'
ipname, coe = coe_path(out, b['module'], b['coeffs'], b['coef_width'], b['coef_frac'] or 0)
# FIR Compiler 7.2 hard limits (probed on 2026-07-24, Vivado 2018.1):
# Coefficient_Width 8..49, Data_Width 2..47, Data_Fractional_Bits 0..16.
# The binary point is documentation only -- the core does integer MACs, and
# feeding frac=0 keeps us inside the legal range AND matches the generated
# RTL, which is plain integer arithmetic on the raw bits.
limits = []
if not 8 <= b['coef_width'] <= 49:
limits.append(f"coefficient width {b['coef_width']} outside 8..49")
if not 2 <= dw <= 47:
limits.append(f"data width {dw} outside 2..47")
if limits:
print(' !! ' + '; '.join(limits) +
' -- fir_compiler cannot take this as-is: re-quantise the Simulink source '
'(Task12 rule 12, bidirectional adjustment) or keep the RTL.')
entry = {'type': 'fir_compiler', 'name': ipname, 'coe': os.path.abspath(coe),
'coef_width': b['coef_width'], 'coef_frac': 0,
'width': dw, 'frac': 0,
'coef_binpt_rtl': b['coef_frac'], 'data_binpt_rtl': df,
'limits_ok': 'no' if limits else 'yes', 'filter_type': ftype,
'interp': a.rate if ftype == 'Interpolation' else 1,
'decim': a.rate if ftype == 'Decimation' else 1,
'zero_pack': 1, 'channels': a.channels, 'paths': a.paths,
'aclken': 'false' if a.no_aclken else 'true',
'reload': 'true' if a.reload else 'false',
'taps': b['taps']}
ipman['ips'] = [i for i in ipman['ips'] if i.get('name') != ipname]
if limits:
print(' -> NOT registered in ip_manifest (would fail create_ip); '
'proposal written so the source can be fixed first.')
else:
ipman['ips'].append(entry)
done += 1
prop = os.path.join(out, f"FIR_PROPOSAL_{b['module']}_{b['coef_signal']}.md")
open(prop, 'w', encoding='utf-8').write(PROPOSAL.format(
module=b['module'], file=(parent or b)['file'], src=b['source_path'],
ipname=ipname, coe=os.path.basename(coe), taps=b['taps'],
cw=b['coef_width'], cf=b['coef_frac'], sym='是' if b['symmetric'] else '否',
coef_signal=b['coef_signal'], ftype=ftype,
rate_note=('抽取/内插比必须与 Simulink 块一致(本次取 %d)' % a.rate)
if ftype != 'Single_Rate' else '单速率',
dw=dw, df=df, aclken='false' if a.no_aclken else 'true',
reload='true' if a.reload else 'false',
reload_note='系数运行时更新(AXI-Stream config 通道)' if a.reload else '定系数,无需重载',
din=', '.join(din) or '(未定位,人工确认)', dout=', '.join(dout) or '(未定位,人工确认)',
valid='validIn/validOut' if (parent and parent.get('valid_in')) else '(人工确认)',
cplx='是' if (parent and parent.get('complex')) else '否',
limits='; '.join(limits) + ' —— 超限,先改源头' if limits else '均在范围内',
out_width_hint=f"{dw}+{b['coef_width']}+ceil(log2({b['taps']})) 位左右"))
print(f"FIR {b['module']}: {b['taps']} taps x {b['coef_width']}b (En{b['coef_frac']}) "
f"-> {ipname} [{os.path.basename(coe)}]\n proposal: {os.path.basename(prop)}")
done += 1
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(out, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()))
print(f'done: {done} FIR IP(s) registered; RTL untouched (guided surgery, Task12 6.3)')
if __name__ == '__main__':
main()
+168
View File
@@ -0,0 +1,168 @@
#!/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()
+169
View File
@@ -0,0 +1,169 @@
# =============================================================================
# gen_ip.tcl -- create the Vivado IPs listed in rtl_ip/ip_manifest.tcl
# usage: vivado -mode batch -source gen_ip.tcl -tclargs <ip_manifest.tcl> <ip_out_dir> [part]
# Managed-IP style: .xci + synthesis + simulation targets are generated so the
# cores can be (a) added to the synthesis project and (b) compiled for xsim.
# =============================================================================
set manifest [lindex $argv 0]
set ipdir [file normalize [lindex $argv 1]]
set part xc7z100ffg900-2
if {[llength $argv] > 2} { set part [lindex $argv 2] }
file mkdir $ipdir
create_project -in_memory -part $part
set_property target_language Verilog [current_project]
set ips {}
source $manifest ;# fills $ips: list of {type {..} name {..} width {..} depth {..} coe {..} mode {..}}
# manifest field with a default (older manifests do not carry the newer keys)
proc opt {arrname key def} {
upvar 1 $arrname c
if {[info exists c($key)] && $c($key) ne {}} { return $c($key) }
return $def
}
foreach ip $ips {
array unset c; array set c $ip
set name $c(name)
if {[file exists $ipdir/$name/$name.xci]} {
puts "###### SKIP $name (exists)"
continue
}
switch -- $c(type) {
dist_mem_gen {
create_ip -name dist_mem_gen -vendor xilinx.com -library ip -version 8.0 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.memory_type {rom} \
CONFIG.data_width $c(width) \
CONFIG.depth $c(depth) \
CONFIG.coefficient_file $c(coe) \
] [get_ips $name]
}
blk_mem_gen {
# SDP, common clock, READ_FIRST on the write port, no output register,
# ENA/ENB pins -- matches SimpleDualPortRAM_generic semantics exactly.
create_ip -name blk_mem_gen -vendor xilinx.com -library ip -version 8.4 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.Memory_Type {Simple_Dual_Port_RAM} \
CONFIG.Assume_Synchronous_Clk {true} \
CONFIG.Use_Byte_Write_Enable {false} \
CONFIG.Write_Width_A $c(width) \
CONFIG.Write_Depth_A $c(depth) \
CONFIG.Read_Width_A $c(width) \
CONFIG.Write_Width_B $c(width) \
CONFIG.Read_Width_B $c(width) \
CONFIG.Operating_Mode_A {READ_FIRST} \
CONFIG.Enable_A {Use_ENA_Pin} \
CONFIG.Enable_B {Use_ENB_Pin} \
CONFIG.Register_PortB_Output_of_Memory_Primitives {false} \
CONFIG.Register_PortB_Output_of_Memory_Core {false} \
] [get_ips $name]
}
fifo_generator {
# guided: mode field carries standard|fwft; depth/width from manifest
create_ip -name fifo_generator -vendor xilinx.com -library ip -version 13.2 \
-module_name $name -dir $ipdir
set rm {Standard_FIFO}
if {[info exists c(mode)] && $c(mode) eq "fwft"} { set rm {First_Word_Fall_Through} }
set_property -dict [list \
CONFIG.Fifo_Implementation {Common_Clock_Block_RAM} \
CONFIG.Performance_Options $rm \
CONFIG.Input_Data_Width $c(width) \
CONFIG.Input_Depth $c(depth) \
CONFIG.Output_Data_Width $c(width) \
CONFIG.Output_Depth $c(depth) \
] [get_ips $name]
}
cordic {
# guided: Arc_Tan phase-seed kernel. Coarse rotation = full-quadrant atan2
# (bidirectional-adjustment principle: better than the FSM's RHP quirk);
# Radians 3QN phase; Truncate rounding; ACLKEN so enb gating stays exact;
# parallel/maximum pipelining -> latency ~input_width+4, well under the
# 64-cycle commit budget.
create_ip -name cordic -vendor xilinx.com -library ip -version 6.0 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.Functional_Selection {Arc_Tan} \
CONFIG.Input_Width $c(width) \
CONFIG.Output_Width $c(out_width) \
CONFIG.Data_Format {SignedFraction} \
CONFIG.Phase_Format {Radians} \
CONFIG.Coarse_Rotation {true} \
CONFIG.Round_Mode {Truncate} \
CONFIG.ACLKEN {true} \
CONFIG.Pipelining_Mode {Maximum} \
] [get_ips $name]
}
fir_compiler {
# guided (Task12 §六). Cycle-true-critical settings:
# Integer_Coefficients : taps go in as integers, the IP must NOT re-quantise
# Full_Precision output : truncation stays OUTSIDE, done the way the RTL did it
# Input_Sample_Period/1 : one sample per clock, no hardware folding
# -> the latency is a fixed number we can align to
# ACLKEN : gated by enb exactly like the generated RTL
# no TREADY / no input FIFO: pure pipeline, no variable-latency handshake
create_ip -name fir_compiler -vendor xilinx.com -library ip -version 7.2 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.Filter_Type [opt c filter_type {Single_Rate}] \
CONFIG.CoefficientSource {COE_File} \
CONFIG.Coefficient_File $c(coe) \
CONFIG.Quantization {Integer_Coefficients} \
CONFIG.Coefficient_Width $c(coef_width) \
CONFIG.Coefficient_Fractional_Bits [opt c coef_frac 0] \
CONFIG.Coefficient_Sign {Signed} \
CONFIG.Coefficient_Structure [opt c coef_structure {Inferred}] \
CONFIG.Data_Width $c(width) \
CONFIG.Data_Fractional_Bits [opt c frac 0] \
CONFIG.Data_Sign {Signed} \
CONFIG.Output_Rounding_Mode {Full_Precision} \
CONFIG.RateSpecification {Input_Sample_Period} \
CONFIG.SamplePeriod 1 \
CONFIG.HardwareOversamplingRate 1 \
CONFIG.Interpolation_Rate [opt c interp 1] \
CONFIG.Decimation_Rate [opt c decim 1] \
CONFIG.Zero_Pack_Factor [opt c zero_pack 1] \
CONFIG.Number_Channels [opt c channels 1] \
CONFIG.Number_Paths [opt c paths 1] \
CONFIG.Coefficient_Reload [opt c reload {false}] \
CONFIG.Num_Reload_Slots [opt c reload_slots 1] \
CONFIG.Has_ACLKEN [opt c aclken {true}] \
CONFIG.Has_ARESETn [opt c aresetn {false}] \
CONFIG.M_DATA_Has_TREADY {false} \
CONFIG.S_DATA_Has_FIFO {false} \
] [get_ips $name]
}
cic_compiler {
# guided: only for large integer rate changes (CIC), fixed rate so the
# decimation/interpolation cadence is static and comparable cycle by cycle.
create_ip -name cic_compiler -vendor xilinx.com -library ip -version 4.0 \
-module_name $name -dir $ipdir
set rate [opt c rate 1]
set_property -dict [list \
CONFIG.Filter_Type [opt c filter_type {Decimation}] \
CONFIG.Number_Of_Stages [opt c stages 3] \
CONFIG.Differential_Delay [opt c diff_delay 1] \
CONFIG.Sample_Rate_Changes {Fixed} \
CONFIG.Fixed_Or_Initial_Rate $rate \
CONFIG.Minimum_Rate [opt c min_rate $rate] \
CONFIG.Maximum_Rate [opt c max_rate $rate] \
CONFIG.Input_Data_Width $c(width) \
CONFIG.Quantization {Full_Precision} \
CONFIG.RateSpecification {Sample_Period} \
CONFIG.SamplePeriod 1 \
CONFIG.Number_Of_Channels [opt c channels 1] \
CONFIG.Use_Xtreme_DSP_Slice {false} \
CONFIG.HAS_ACLKEN [opt c aclken {true}] \
CONFIG.HAS_ARESETN [opt c aresetn {false}] \
CONFIG.HAS_DOUT_TREADY {false} \
] [get_ips $name]
}
default { puts "###### UNKNOWN IP TYPE: $c(type) ($name)"; continue }
}
generate_target {instantiation_template synthesis simulation} [get_ips $name]
puts "###### GENERATED $name ($c(type))"
}
puts "###### DONE [llength $ips] ip(s) in $ipdir"
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""Incremental mode for rtl-ip-replace (Task12 rules 13-16 / Task12 §5).
Baseline = the newest backup_<ts>/ under the fixed dir, i.e. the previous run's
ORIGINAL MATLAB RTL (src_orig/) + rewritten RTL (*.v) + ip/ + cmp/.
plan <HDLSRC> <OUT> [--apply] [--full] [--baseline DIR]
Run right after backup_outdir.py. Classifies every source file against the
baseline snapshot; --apply copies back the rewritten .v of files whose
MATLAB source is byte-identical (nothing to redo for them).
finalize <HDLSRC> <OUT> [--baseline DIR]
Run after the transform scripts (step 2), BEFORE create_ip / cmp sim.
Writes TRANSFORM.diff, compares it + ip_manifest + .coe + scripts_sha1
against the baseline, and decides whether step 3 (IP gen) and step 4
(cycle-true sim) may be skipped. Decisions land in incr_plan.json and are
appended to PROVENANCE.log.
Skipping the sim is only allowed when the rewrite this run performed is
byte-for-byte the same rewrite the baseline PASS was proven on (Task12 §5.4).
"""
import argparse, json, os, shutil, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ipflow_common import (SRC_ORIG, TRANSFORM_DIFF, append_provenance, # noqa: E402
scripts_sha1, sha1_file, snapshot_sources, src_files)
PLAN = 'incr_plan.json'
def find_baseline(out, explicit=None):
if explicit:
return os.path.abspath(explicit)
cands = sorted(d for d in os.listdir(out)
if d.startswith('backup_') and os.path.isdir(os.path.join(out, d)))
for d in reversed(cands): # newest usable one
if os.path.isdir(os.path.join(out, d, SRC_ORIG)):
return os.path.join(out, d)
return None
def read_sha(p):
return sha1_file(p) if os.path.exists(p) else None
def baseline_cmp_pass(base):
p = os.path.join(base, 'cmp', 'cmp_result.txt')
if not os.path.exists(p):
return False, 'no cmp_result.txt in baseline'
first = open(p, errors='replace').readline().strip()
return first.startswith('PASS'), first
def baseline_scripts_sha1(base):
p = os.path.join(base, 'PROVENANCE.log')
if os.path.exists(p):
for ln in open(p, errors='replace'):
if ln.startswith('scripts_sha1:'):
return ln.split(':', 1)[1].strip()
return None
def save(out, plan):
json.dump(plan, open(os.path.join(out, PLAN), 'w'), indent=2)
def load(out):
p = os.path.join(out, PLAN)
return json.load(open(p)) if os.path.exists(p) else {}
def cmd_plan(a):
hdlsrc, out = os.path.abspath(a.hdlsrc), os.path.abspath(a.outdir)
os.makedirs(out, exist_ok=True)
snapshot_sources(hdlsrc, out) # this run's baseline-to-be (rule 14)
base = None if a.full else find_baseline(out, a.baseline)
plan = {'baseline': base, 'incremental': bool(base), 'files': {}, 'reasons': []}
if not base:
plan['reasons'].append('forced --full' if a.full else 'no usable baseline (backup_*/src_orig)')
save(out, plan)
print('FULL run: ' + plan['reasons'][0])
return
bsrc = os.path.join(base, SRC_ORIG)
new, old = src_files(hdlsrc), src_files(bsrc)
for fn in new:
if fn not in old:
plan['files'][fn] = 'new'
elif read_sha(os.path.join(hdlsrc, fn)) == read_sha(os.path.join(bsrc, fn)):
plan['files'][fn] = 'same'
else:
plan['files'][fn] = 'changed'
for fn in old:
if fn not in new:
plan['files'][fn] = 'removed'
same = [f for f, c in plan['files'].items() if c == 'same']
all_same = bool(same) and all(c == 'same' for c in plan['files'].values())
plan['all_same'] = all_same
applied = []
if a.apply:
for fn in same:
src = os.path.join(base, fn) # the REWRITTEN file
if os.path.exists(src):
shutil.copy(src, os.path.join(out, fn))
applied.append(fn)
# .coe / manifests are only safe to take wholesale when NOTHING changed;
# otherwise the transform scripts must re-derive them (a stale .coe of a
# table that moved would silently survive).
extra = (['ip_manifest.tcl', 'ip_manifest.json', 'manifest.json']
+ [f for f in sorted(os.listdir(base)) if f.endswith('.coe')]) if all_same else []
for fn in extra:
s, d = os.path.join(base, fn), os.path.join(out, fn)
if os.path.exists(s) and not os.path.exists(d):
shutil.copy(s, d)
applied.append(fn)
plan['reused_from_baseline'] = applied
save(out, plan)
ok, first = baseline_cmp_pass(base)
print(f'baseline: {base}')
print(' cmp_result: ' + (first if first else 'MISSING'))
for c in ('same', 'changed', 'new', 'removed'):
fs = [f for f, k in plan['files'].items() if k == c]
if fs:
print(f' {c:8s}: {" ".join(sorted(fs))}')
if applied:
print(f' applied {len(applied)} file(s) from baseline (skip transform for them)')
if plan['all_same']:
print(' ALL MATLAB sources unchanged -> step 2 (transform scripts) can be skipped entirely')
if not ok:
print(' NOTE: baseline has no PASS -- cycle-true sim CANNOT be inherited')
def cmd_finalize(a):
from ipflow_common import write_transform_diff
hdlsrc, out = os.path.abspath(a.hdlsrc), os.path.abspath(a.outdir)
write_transform_diff(out)
plan = load(out)
if plan and not plan.get('incremental'):
base = None # a full run was decided in `plan` -- honour it
else:
base = plan.get('baseline') or find_baseline(out, a.baseline)
blockers, ip_reuse, sim_inherit = [], False, False
if not base:
blockers.append('full run (no baseline / --full): everything regenerated')
else:
if any(c in ('new', 'removed') for c in plan.get('files', {}).values()):
blockers.append('source file set changed (new/removed)')
d_new = read_sha(os.path.join(out, TRANSFORM_DIFF))
d_old = read_sha(os.path.join(base, TRANSFORM_DIFF))
if d_old is None:
blockers.append('baseline has no TRANSFORM.diff')
elif d_new != d_old:
blockers.append('TRANSFORM.diff differs from baseline (rewrite is not the same one)')
m_new = read_sha(os.path.join(out, 'ip_manifest.tcl'))
m_old = read_sha(os.path.join(base, 'ip_manifest.tcl'))
coe_new = {f: read_sha(os.path.join(out, f)) for f in os.listdir(out) if f.endswith('.coe')}
coe_old = {f: read_sha(os.path.join(base, f)) for f in os.listdir(base) if f.endswith('.coe')}
ip_same = (m_new is not None and m_new == m_old and coe_new == coe_old)
if not ip_same:
blockers.append('ip_manifest/.coe differ from baseline')
have_ip = os.path.isdir(os.path.join(out, 'ip')) or os.path.isdir(os.path.join(base, 'ip'))
if ip_same and not have_ip: # Task12 §5.5: an incomplete baseline may not be inherited
ip_same = False
blockers.append('baseline has no ip/ directory')
s_now, s_old = scripts_sha1(), baseline_scripts_sha1(base)
if s_old != s_now:
blockers.append(f'skill scripts changed ({s_old} -> {s_now})')
ok, first = baseline_cmp_pass(base)
if not ok:
blockers.append('baseline cmp_result is not PASS')
ip_reuse = ip_same and not any(b.startswith(('no baseline', 'source file set')) for b in blockers)
sim_inherit = not blockers
if ip_reuse and not os.path.isdir(os.path.join(out, 'ip')) \
and os.path.isdir(os.path.join(base, 'ip')):
shutil.copytree(os.path.join(base, 'ip'), os.path.join(out, 'ip'))
for f in ('ip_manifest.json',):
if os.path.exists(os.path.join(base, f)) and not os.path.exists(os.path.join(out, f)):
shutil.copy(os.path.join(base, f), os.path.join(out, f))
plan.update({'ip_reuse': ip_reuse, 'sim_inherit': sim_inherit, 'blockers': blockers})
save(out, plan)
files = plan.get('files', {})
reused = sorted(f for f, c in files.items() if c == 'same')
regen = sorted(f for f, c in files.items() if c != 'same')
append_provenance(out, [
'',
'# --- incremental run (Task12 rules 13-16) ---',
'incremental: ' + ('on' if base else 'off'),
'baseline: ' + (os.path.basename(base) + '/' if base else '-'),
'reused_files: ' + (' '.join(reused) or '-'),
'regenerated_files: ' + (' '.join(regen) or '-'),
'ip_reuse: ' + ('full' if ip_reuse else 'none'),
'cmp_sim: ' + (f'INHERITED PASS from {os.path.basename(base)}/cmp/cmp_result.txt'
if sim_inherit else 'MUST RUN'),
'blockers: ' + ('; '.join(blockers) or '-'),
])
print('ip_reuse :', 'YES (skip step 3 create_ip)' if ip_reuse else 'no (run step 3)')
print('sim_inherit :', 'YES (skip step 4 cycle-true sim, PASS inherited from %s)'
% os.path.basename(base) if sim_inherit else 'no (run step 4)')
for b in blockers:
print(' blocker:', b)
def main():
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = p.add_subparsers(dest='cmd', required=True)
for name in ('plan', 'finalize'):
s = sub.add_parser(name)
s.add_argument('hdlsrc')
s.add_argument('outdir')
s.add_argument('--baseline', help='explicit baseline dir (default: newest backup_*)')
if name == 'plan':
s.add_argument('--apply', action='store_true',
help='copy back rewritten files whose source is unchanged')
s.add_argument('--full', action='store_true', help='force a full run')
a = p.parse_args()
(cmd_plan if a.cmd == 'plan' else cmd_finalize)(a)
if __name__ == '__main__':
main()
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Replace the HDL Coder `persistent isempty-init` table idiom with dist ROM IP.
Idiom (MATLAB `persistent T; if isempty(T), T = <const>; end`):
wire [..] P [0:D-1]; + D const assigns (the constant image)
wire [..] T_1 [0:D-1]; generate: T_1[i] = P[i];
reg [..] T_reg_reg [0:D-1]; always@(posedge clk/reset):
reset -> 0; enb -> T_reg_reg <= T_reg_reg_next
wire [..] T_reg_reg_next [0:D-1]; generate: T[i] = T_reg_reg[i];
T_reg_reg_next[i] = TMP[i];
reg [..] TMP [0:D-1]; always@*: TMP = (S) ? T_1 : T; // S = first-call flag
Net semantics: T reads 0 until the first (enb && S) tick, then the constant
image forever. TMP reads (S || loaded) ? const : 0.
Replacement (bit- and cycle-true):
dist_mem_gen ROM (content = P) per read site
reg T__ipinited: reset->0, (enb && S)->1
read T[e] -> (T__ipinited ? ROM[e] : 0)
read TMP[e] -> ((S || T__ipinited) ? ROM[e] : 0)
all five arrays + three blocks deleted.
Usage: python initrom_to_ip.py <hdlsrc> (operates on <hdlsrc>/rtl_ip copies;
run AFTER rtl_ip_scan + source copy [rom_to_ip.py --copy-only or manual],
BEFORE rom_to_ip's rewriting of any remaining plain ROMs, BEFORE namespace_ip.)
"""
import argparse, hashlib, json, os, re, shutil, sys
CONST_ASSIGN = re.compile(r"^\s*assign\s+(\w+)\[(\d+)\]\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_]+)\s*;\s*$", re.M)
def to_bits(radix, lit, width):
lit = lit.replace('_', '')
v = int(lit, {'b': 2, 'd': 10, 'h': 16}[radix]) & ((1 << width) - 1)
return format(v, '0%db' % width)
def find_block(lines, i0, opener):
"""Return (start,end) line span of the statement block whose header line index
is i0 (an `always`/`generate` line); tracks begin/end + generate/endgenerate."""
depth, i = 0, i0
started = False
while i < len(lines):
t = lines[i]
depth += len(re.findall(r'\bbegin\b|\bgenerate\b|\bcase\b', t))
depth -= len(re.findall(r'\bend\b(?!module|generate|case)|\bendgenerate\b|\bendcase\b', t))
if depth > 0:
started = True
if started and depth <= 0:
return i0, i
i += 1
sys.exit(f'unbalanced block at line {i0}: {lines[i0]}')
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', help='FIXED output dir (e.g. <repo>/vivado/rtl_ip/<model>)')
a = ap.parse_args()
from ipflow_common import copy_sources
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip = os.path.abspath(a.outdir)
copy_sources(hdlsrc, rtl_ip)
ipman_p = os.path.join(rtl_ip, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
coe_of_hash = {ip.get('content_hash'): ip['name'] for ip in ipman['ips'] if 'content_hash' in ip}
for fn in [f for f in os.listdir(rtl_ip) if f.endswith('.v')]:
path = os.path.join(rtl_ip, fn)
txt = open(path, encoding='utf-8', errors='replace').read()
# locate idiom instances: assign T_1[gv] = P[gv]; inside a generate loop
pairs = []
for m in re.finditer(r'assign\s+(\w+)_1\[(\w+)\]\s*=\s*(\w+)\[\2\]\s*;', txt):
T, P = m.group(1), m.group(3)
if f'{T}_reg_reg' in txt:
pairs.append((T, P))
if not pairs:
continue
print(f'[{fn}] isempty-init tables: {pairs}')
for T, P in pairs:
# ---- table content ----
vals, W = {}, None
for m in CONST_ASSIGN.finditer(txt):
if m.group(1) == P:
W = W or len(to_bits(m.group(4), m.group(5), int(m.group(3))))
vals[int(m.group(2))] = to_bits(m.group(4), m.group(5), int(m.group(3)))
D = len(vals)
if D == 0 or set(vals) != set(range(D)):
print(f' SKIP {T}: const image {P} incomplete'); continue
AW = max(1, (D - 1).bit_length())
content = '\n'.join(vals[i] for i in range(D))
h = hashlib.sha1((f'{W}x{D}\n' + content).encode()).hexdigest()[:10]
if h not in coe_of_hash:
ipname = f'rom_{W}x{D}_{h}'
with open(os.path.join(rtl_ip, ipname + '.coe'), 'w') as f:
f.write('memory_initialization_radix=2;\nmemory_initialization_vector=\n')
f.write(',\n'.join(vals[i] for i in range(D)) + ';\n')
ipman['ips'].append({'type': 'dist_mem_gen', 'name': ipname, 'width': W, 'depth': D,
'coe': os.path.abspath(os.path.join(rtl_ip, ipname + '.coe')),
'content_hash': h})
coe_of_hash[h] = ipname
ipname = coe_of_hash[h]
lines = txt.split('\n')
# ---- discover TMP name + guard signal from the mux always block ----
mm = re.search(r'(\w+)\[\w+\]\s*=\s*%s_1\[\w+\]\s*;' % re.escape(T), txt)
if not mm:
print(f' SKIP {T}: mux TMP not found'); continue
TMP = mm.group(1)
gm = None
for i, ln in enumerate(lines):
if re.search(r'\bif\s*\(\s*(\w+)\s*==\s*1\'b0\s*\)', ln) is not None:
# candidate: block must contain TMP writes
s2, e2 = find_block(lines, max(0, i - 1), 'always')
body = '\n'.join(lines[s2:e2 + 1])
if f'{TMP}[' in body and f'{T}_1[' in body:
gm = re.search(r'\bif\s*\(\s*(\w+)\s*==\s*1\'b0\s*\)', ln).group(1)
break
if not gm:
print(f' SKIP {T}: guard signal not found'); continue
S = gm
# ---- delete the three blocks (reg process, copy generate, next generate, mux always) ----
kill = set()
for i, ln in enumerate(lines):
if (re.search(r'begin\s*:\s*%s_reg_process' % re.escape(T), ln)
or re.search(r'begin\s*:\s*%s_1_gen' % re.escape(T), ln)
or re.search(r'begin\s*:\s*%s_reg_reg_next_gen' % re.escape(T), ln)):
j = i
while j > 0 and not re.search(r'^\s*(always|generate|genvar|\s*for)', lines[j]):
j -= 1
# generate blocks start at 'generate'/'genvar'; always at 'always'
while j > 0 and re.match(r'^\s*for\b', lines[j]):
j -= 1
if re.match(r'^\s*genvar', lines[j - 1] if j else ''):
j -= 1
s2, e2 = find_block(lines, j, None)
# extend to endgenerate if present
while e2 + 1 < len(lines) and re.match(r'^\s*endgenerate', lines[e2 + 1]):
e2 += 1
kill.update(range(s2, e2 + 1))
if re.match(r'^\s*always\s*@\*', ln):
s2, e2 = find_block(lines, i, None)
body = '\n'.join(lines[s2:e2 + 1])
if f'{TMP}[' in body and f'{T}_1[' in body and S in body:
kill.update(range(s2, e2 + 1))
# delete array declarations
for i, ln in enumerate(lines):
if re.search(r'\b(%s|%s_1|%s_reg_reg|%s_reg_reg_next|%s)\s*\[0:%d\]\s*;'
% tuple(map(re.escape, (T, T, T, T, TMP))) % () if False else
r'\b(' + '|'.join(map(re.escape, (T, T + '_1', T + '_reg_reg',
T + '_reg_reg_next', TMP))) + r')\s+\[0:' + str(D - 1) + r'\]\s*;', ln):
kill.add(i)
if re.match(r'^\s*assign\s+%s\[\d+\]' % re.escape(P), ln):
kill.add(i)
if re.search(r'\b%s\s+\[0:%d\]\s*;' % (re.escape(P), D - 1), ln):
kill.add(i)
survivors = [ln for i, ln in enumerate(lines) if i not in kill]
txt = '\n'.join(survivors)
# ---- rewrite remaining real read sites ----
inited = f'{T}__ipinited'
inst, k = [], 0
zeros = f"{W}'sb" + '0' * W
for arr, gate in ((T, inited), (TMP, f'({S} || {inited})')):
for m in list(re.finditer(re.escape(arr) + r'\[([^\]\n]+)\]', txt)):
expr = m.group(1)
if expr.isdigit():
continue
aq, qq = f'{T}_ipa{k}', f'{T}_ipq{k}'
inst.append(f' wire [{AW-1}:0] {aq};\n wire [{W-1}:0] {qq};\n'
f' assign {aq} = {expr};\n'
f' {ipname} u_{T}_ip{k} (.a({aq}), .spo({qq}));\n')
txt = txt.replace(f'{arr}[{expr}]', f'({gate} ? $signed({qq}) : {zeros})')
k += 1
flag = (f' reg {inited};\n'
f' always @(posedge clk or posedge reset)\n'
f' if (reset) {inited} <= 1\'b0;\n'
f' else if (enb && ({S})) {inited} <= 1\'b1;\n')
txt = txt.replace('endmodule',
f' // ---- rtl-ip-replace(initrom): {T} -> {ipname} ({k} read site(s)) ----\n'
+ flag + ''.join(inst) + '\nendmodule', 1)
print(f' {T} ({W}x{D}) -> {ipname}, {k} read site(s), guard={S}')
open(path, 'w', encoding='utf-8').write(txt)
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(rtl_ip, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()
if k != 'content_hash'))
print('ip_manifest updated')
if __name__ == '__main__':
main()
+110
View File
@@ -0,0 +1,110 @@
"""Shared helpers: source copying + PROVENANCE.log (mandatory traceability).
RULE (user directive 2026-07-23): script-processed RTL lives ONLY in the fixed
directory <repo>/vivado/rtl_ip/<model>/ ; the Vivado project may reference RTL
ONLY from there, NEVER from the MATLAB hdlsrc directly. PROVENANCE.log in the
fixed directory records the absolute path of the original MATLAB RTL.
Generated Xilinx IPs live in <fixed_dir>/ip/.
"""
import difflib, hashlib, os, re, shutil, time
SRC_ORIG = 'src_orig' # verbatim snapshot of the MATLAB RTL this run is based on
TRANSFORM_DIFF = 'TRANSFORM.diff'
def src_files(d):
return [fn for fn in sorted(os.listdir(d))
if fn.endswith('.v') and not fn.endswith('_tb.v')]
def sha1_file(p):
return hashlib.sha1(open(p, 'rb').read()).hexdigest()
def copy_sources(hdlsrc, outdir, force=False):
os.makedirs(outdir, exist_ok=True)
copied = []
for fn in src_files(hdlsrc):
dst = os.path.join(outdir, fn)
if force or not os.path.exists(dst):
shutil.copy(os.path.join(hdlsrc, fn), dst)
copied.append(fn)
snapshot_sources(hdlsrc, outdir)
write_provenance(outdir, hdlsrc)
return copied
def snapshot_sources(hdlsrc, outdir):
"""Task12 rule 14: keep the ORIGINAL MATLAB RTL this run is based on, so the
next run can diff against it and reuse unchanged work (incremental mode)."""
d = os.path.join(outdir, SRC_ORIG)
os.makedirs(d, exist_ok=True)
for fn in src_files(hdlsrc):
dst = os.path.join(d, fn)
if not os.path.exists(dst) or sha1_file(dst) != sha1_file(os.path.join(hdlsrc, fn)):
shutil.copy(os.path.join(hdlsrc, fn), dst)
return d
_HUNK = re.compile(r'^@@ -\d+(,\d+)? \+\d+(,\d+)? @@')
def write_transform_diff(outdir):
"""src_orig/*.v -> rewritten *.v, as removed/added lines only (n=0) with hunk
line numbers normalised away: two runs whose scripts performed the SAME
rewrite produce byte-identical files, even if the MATLAB source changed
elsewhere (that is exactly the class-B case of Task12 §5.2)."""
orig = os.path.join(outdir, SRC_ORIG)
out = []
for fn in src_files(orig):
mod = os.path.join(outdir, fn)
if not os.path.exists(mod):
out.append(f'=== {fn}: MISSING in outdir')
continue
a = open(os.path.join(orig, fn), errors='replace').read().splitlines()
b = open(mod, errors='replace').read().splitlines()
d = [_HUNK.sub('@@', ln) for ln in
difflib.unified_diff(a, b, fromfile=f'orig/{fn}', tofile=f'ip/{fn}',
n=0, lineterm='')]
if d:
out.extend(d)
p = os.path.join(outdir, TRANSFORM_DIFF)
open(p, 'w').write('\n'.join(out) + '\n')
return p
def scripts_sha1(script_dir=None):
"""Fingerprint of the skill itself -- changed scripts forbid inheriting a
previous cycle-true PASS (Task12 §5.4/5.5)."""
d = script_dir or os.path.dirname(os.path.abspath(__file__))
h = hashlib.sha1()
for fn in sorted(os.listdir(d)):
if fn.endswith(('.py', '.tcl')):
h.update(fn.encode())
h.update(open(os.path.join(d, fn), 'rb').read())
return h.hexdigest()[:12]
def write_provenance(outdir, hdlsrc):
hdlsrc = os.path.abspath(hdlsrc)
lines = ['# PROVENANCE.log -- generated by rtl-ip-replace, do not edit',
'# RULE: the Vivado project must use RTL from THIS directory only;',
'# direct use of the MATLAB-generated hdlsrc below is FORBIDDEN.',
'source_hdlsrc: ' + hdlsrc,
'updated_at: ' + time.strftime('%Y-%m-%d %H:%M:%S'),
'ip_subdir: ip/',
'files:']
for fn in src_files(hdlsrc):
p = os.path.join(hdlsrc, fn)
h = sha1_file(p)[:12]
mt = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(p)))
lines.append(f' {fn} src_sha1={h} src_mtime={mt}')
lines.append('src_snapshot: ' + SRC_ORIG + '/ # original MATLAB RTL kept for the next incremental run')
lines.append('scripts_sha1: ' + scripts_sha1())
open(os.path.join(outdir, 'PROVENANCE.log'), 'w').write('\n'.join(lines) + '\n')
def append_provenance(outdir, lines):
"""Append the incremental-run block (Task12 §5.6). Called last, after the
transform scripts have rewritten PROVENANCE.log."""
with open(os.path.join(outdir, 'PROVENANCE.log'), 'a') as f:
f.write('\n'.join(lines) + '\n')
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Suffix every module in rtl_ip/ with _IP so original and IP-based RTL can
coexist in one xsim `work` library for the dual-DUT cycle-true comparison.
- renames `module X` -> `module X_IP` for every module DECLARED in rtl_ip/*.v
- rewrites instantiations of those modules accordingly (word-boundary match)
- IP core instances (dist_mem_gen/blk_mem_gen/... names from ip_manifest) are
left untouched -- they are not declared here.
Run LAST, after rom_to_ip.py / ram_to_ip.py / any hand edits (CORDIC, FIFO).
Idempotent: modules already ending in _IP are skipped.
"""
import argparse, os, re
def main():
ap = argparse.ArgumentParser()
ap.add_argument('rtl_ip_dir')
a = ap.parse_args()
d = os.path.abspath(a.rtl_ip_dir)
files = [os.path.join(d, f) for f in os.listdir(d) if f.endswith('.v')]
mods = []
for p in files:
for m in re.finditer(r'^\s*module\s+(\w+)', open(p, encoding='utf-8', errors='replace').read(), re.M):
if not m.group(1).endswith('_IP'):
mods.append(m.group(1))
mods = sorted(set(mods), key=len, reverse=True)
print('modules:', mods)
for p in files:
txt = open(p, encoding='utf-8', errors='replace').read()
for mod in mods:
txt = re.sub(r'\b%s\b(?!_IP)' % re.escape(mod), mod + '_IP', txt)
open(p, 'w', encoding='utf-8').write(txt)
print(f'suffixed {len(mods)} modules across {len(files)} files')
if __name__ == '__main__':
main()
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Replace SimpleDualPortRAM_generic (HDL Coder) with blk_mem_gen SDP instances.
Semantics of the original (must be preserved exactly):
- single clock, `enb` gates BOTH write and read registration
- read latency 1 cycle; write/read same address -> read returns OLD data (read-first)
=> blk_mem_gen: Simple Dual Port, common clock, Port-A write mode READ_FIRST,
ENA/ENB pins used, NO output register.
The wrapper keeps the module name/ports/params; a generate-if selects the
per-(AddrWidth,DataWidth) IP; unlisted combos fall back to the original
behavioral body (so simulation still works, and synthesis of an unlisted combo
is loudly visible in the utilization report instead of silently wrong).
Run AFTER rom_to_ip.py (needs rtl_ip/ copies), BEFORE namespace_ip.py.
"""
import argparse, json, os, re
WRAP = '''// regenerated by ram_to_ip.py -- blk_mem_gen SDP wrappers (read-first, 1-cycle, common clk)
`timescale 1 ns / 1 ns
module {mod}
(clk, enb, wr_din, wr_addr, wr_en, rd_addr, dout);
parameter integer AddrWidth = 1;
parameter integer DataWidth = 1;
input clk;
input enb;
input signed [DataWidth - 1:0] wr_din;
input [AddrWidth - 1:0] wr_addr;
input wr_en;
input [AddrWidth - 1:0] rd_addr;
output signed [DataWidth - 1:0] dout;
generate
{cases} begin: g_behav // fallback: original behavioral RAM (unlisted param combo)
reg [DataWidth - 1:0] ram [2**AddrWidth - 1:0];
reg [DataWidth - 1:0] data_int;
integer i;
initial begin
for (i=0; i<=2**AddrWidth - 1; i=i+1) ram[i] = 0;
data_int = 0;
end
always @(posedge clk)
if (enb == 1'b1) begin
if (wr_en == 1'b1) ram[wr_addr] <= wr_din;
data_int <= ram[rd_addr];
end
assign dout = data_int;
end
endgenerate
endmodule
'''
CASE = ''' if (AddrWidth == {aw} && DataWidth == {dw}) begin: g_a{aw}d{dw}
wire [{dwm}:0] doutb_i;
{ip} u_bram (
.clka(clk), .ena(enb), .wea(wr_en), .addra(wr_addr), .dina(wr_din),
.clkb(clk), .enb(enb), .addrb(rd_addr), .doutb(doutb_i));
assign dout = doutb_i;
end else
'''
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', help='FIXED output dir (e.g. <repo>/vivado/rtl_ip/<model>)')
ap.add_argument('--manifest', default=None)
a = ap.parse_args()
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip = os.path.abspath(a.outdir)
man = json.load(open(a.manifest or os.path.join(rtl_ip, 'manifest.json')))
combos = sorted({(r['AddrWidth'], r['DataWidth']) for r in man['ram']
if r['AddrWidth'] and r['DataWidth']})
if not combos:
print('no SimpleDualPortRAM instantiations found'); return
ipman_p = os.path.join(rtl_ip, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
have = {ip['name'] for ip in ipman['ips']}
cases = ''
for aw, dw in combos:
ip = f'bram_sdp_a{aw}d{dw}'
cases += CASE.format(aw=aw, dw=dw, dwm=dw - 1, ip=ip)
if ip not in have:
ipman['ips'].append({'type': 'blk_mem_gen', 'name': ip, 'width': dw,
'depth': 2 ** aw, 'mode': 'sdp_readfirst_commonclk'})
# rewrite every SimpleDualPortRAM_*.v module file present in rtl_ip
n = 0
for fn in os.listdir(rtl_ip):
if re.match(r'SimpleDualPortRAM_\w+\.v$', fn):
mod = fn[:-2]
open(os.path.join(rtl_ip, fn), 'w').write(WRAP.format(mod=mod, cases=cases))
print('wrapped', mod, 'combos:', combos)
n += 1
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(rtl_ip, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()))
print(f'done: {n} RAM module(s) wrapped, {len(combos)} blk_mem_gen config(s)')
if __name__ == '__main__':
main()
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Replace fully-constant wire-array ROMs with Vivado dist_mem_gen instances.
- Copies every non-tb .v from <hdlsrc> into <hdlsrc>/rtl_ip/ (first run only, or --force).
- For each fully-constant ROM in the manifest:
* extracts the table into a .coe (dedup by content hash: identical tables share one IP config)
* deletes the wire-array declaration and the constant assigns from the copy
* per READ SITE inserts: wire [AW-1:0] <name>_a<k>; wire [W-1:0] <name>_q<k>;
assign <name>_a<k> = <read_expr>;
<ip> u_<name>_r<k> (.a(<name>_a<k>), .spo(<name>_q<k>));
and rewrites `<name>[<read_expr>]` -> `$signed(<name>_q<k>)` (or unsigned).
- Appends dist_mem_gen entries to ip_manifest.json / ip_manifest.tcl.
dist_mem_gen is ASYNC read => bit- and cycle-true against the original combinational read.
Run AFTER rtl_ip_scan.py, BEFORE namespace_ip.py.
"""
import argparse, hashlib, json, os, re, shutil, sys
def to_bits(radix, lit, width):
lit = lit.replace('_', '')
base = {'b': 2, 'd': 10, 'h': 16}[radix]
v = int(lit, base) & ((1 << width) - 1)
return format(v, '0%db' % width)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', help='FIXED output dir (e.g. <repo>/vivado/rtl_ip/<model>)')
ap.add_argument('--manifest', default=None)
ap.add_argument('--force', action='store_true')
a = ap.parse_args()
from ipflow_common import copy_sources
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip = os.path.abspath(a.outdir)
os.makedirs(rtl_ip, exist_ok=True)
man = json.load(open(a.manifest or os.path.join(rtl_ip, 'manifest.json')))
# 1. copy sources + PROVENANCE.log
copy_sources(hdlsrc, rtl_ip, force=a.force)
ipman_p = os.path.join(rtl_ip, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
coe_of_hash, roms_done = {}, 0
CONST_ASSIGN = re.compile(r"^\s*assign\s+(\w+)\[(\d+)\]\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_]+)\s*;\s*$", re.M)
by_file = {}
for rom in man['const_rom']:
if not rom.get('fully_const'):
print('SKIP (partial const):', rom['name']); continue
by_file.setdefault(rom['file'], []).append(rom)
for fn, roms in by_file.items():
path = os.path.join(rtl_ip, fn)
txt = open(path, encoding='utf-8', errors='replace').read()
for rom in roms:
name, W, D, signed = rom['name'], rom['width'], rom['depth'], rom['signed']
AW = max(1, (D - 1).bit_length())
# collect table
bits = {}
for m in CONST_ASSIGN.finditer(txt):
if m.group(1) == name:
bits[int(m.group(2))] = to_bits(m.group(4), m.group(5), W)
if len(bits) != D:
print(f'ERROR {name}: found {len(bits)}/{D} const assigns -- skipped'); continue
content = '\n'.join(bits[i] for i in range(D))
h = hashlib.sha1((f'{W}x{D}\n' + content).encode()).hexdigest()[:10]
if h not in coe_of_hash:
ipname = f'rom_{W}x{D}_{h}'
coe = os.path.join(rtl_ip, ipname + '.coe')
with open(coe, 'w') as f:
f.write('memory_initialization_radix=2;\nmemory_initialization_vector=\n')
f.write(',\n'.join(bits[i] for i in range(D)) + ';\n')
coe_of_hash[h] = ipname
ipman['ips'].append({'type': 'dist_mem_gen', 'name': ipname, 'width': W,
'depth': D, 'coe': os.path.abspath(coe)})
ipname = coe_of_hash[h]
# remove decl + assigns
txt = re.sub(r'^\s*wire\s+(signed\s+)?\[%d:0\]\s+%s\s+\[0:%d\]\s*;.*\n'
% (W - 1, re.escape(name), D - 1), '', txt, flags=re.M)
txt = re.sub(r"^\s*assign\s+%s\[\d+\]\s*=\s*[^;]+;\s*\n" % re.escape(name), '', txt, flags=re.M)
# rewrite read sites (unique exprs; every occurrence of same expr shares one instance)
inst_lines, k = [], 0
for expr in dict.fromkeys(rom['read_exprs']): # preserve order, dedup
aq, qq = f'{name}_a{k}', f'{name}_q{k}'
inst_lines.append(f' wire [{AW-1}:0] {aq};\n wire [{W-1}:0] {qq};\n'
f' assign {aq} = {expr};\n'
f' {ipname} u_{name}_r{k} (.a({aq}), .spo({qq}));\n')
rd = f'$signed({qq})' if signed else qq
txt = txt.replace(f'{name}[{expr}]', rd)
k += 1
# place instances right before `endmodule`
txt = txt.replace('endmodule', ' // ---- rtl-ip-replace: %s -> %s (%d read site(s)) ----\n%s\nendmodule'
% (name, ipname, k, ''.join(inst_lines)), 1)
roms_done += 1
print(f'ROM {name} ({W}x{D}, {k} reads) -> {ipname} [{fn}]')
open(path, 'w', encoding='utf-8').write(txt)
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(rtl_ip, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()))
print(f'done: {roms_done} ROMs, {len(coe_of_hash)} unique tables; ip_manifest updated')
if __name__ == '__main__':
main()
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""Scan HDL Coder generated Verilog for IP-replaceable structures.
Emits a JSON manifest of:
const_rom : `wire [W:0] name [0:D];` fully populated by constant assigns -> dist/blk ROM IP
reg_table : `reg` arrays (runtime-written tables) -> review by hand (RAM or leave)
ram : SimpleDualPortRAM_generic instantiations (AddrWidth/DataWidth combos) -> blk_mem_gen
fifo : modules/instances whose name contains 'fifo' -> fifo_generator (guided)
cordic : files with the cx/cy/cz + atanT iterative kernel -> cordic IP (guided)
filter : FIR filters (Task12 §六) -> fir_compiler / cic_compiler (guided)
fir_coef_bank : HDL Coder <Name>Coef module, constant CoefOut_i taps
fir_block : module whose header says "Discrete FIR Filter"
adaptive_fir_candidate: hand-rolled tap regs + delay line + MAC (LMS etc.)
Usage: python rtl_ip_scan.py <hdlsrc_dir> [<outdir>] [-o manifest.json]
"""
import argparse, json, os, re, sys
ROM_DECL = re.compile(r'^\s*wire\s+(signed\s+)?\[(\d+):0\]\s+(\w+)\s+\[0:(\d+)\]\s*;', re.M)
REG_DECL = re.compile(r'^\s*reg\s+(signed\s+)?\[(\d+):0\]\s+(\w+)\s+\[0:(\d+)\]\s*;', re.M)
CONST_ASSIGN = re.compile(r"^\s*assign\s+(\w+)\[(\d+)\]\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_xzXZ]+)\s*;", re.M)
RAM_INST = re.compile(r'SimpleDualPortRAM_\w+\s*#\s*\(((?:[^()]|\([^()]*\))*)\)\s*(\w+)\s*\(', re.S)
PARAM = re.compile(r'\.(\w+)\s*\(\s*(\d+)\s*\)')
# ---- filter (Task12 §六) ----------------------------------------------------
MODULE = re.compile(r'^[ \t]*module\s+(\w+)\b(.*?)^\s*endmodule', re.M | re.S)
DECL = re.compile(r'^\s*(input|output|inout|wire|reg)\s+(signed\s+)?(?:\[(\d+):0\]\s+)?(\w+)\s*;'
r'(?:[ \t]*//[ \t]*([usq]fix\d+(?:_En-?\d+)?|\w+))?', re.M)
CONST_SIG = re.compile(r"^\s*assign\s+(\w+)\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_]+)\s*;", re.M)
BASE_IDX = re.compile(r'^(.*?)_?(\d+)$')
DLY_CHAIN = re.compile(r'^\s*(\w*?)(\d+)_next(_re|_im)?\s*=\s*\1(\d+)(?:_re|_im)?\s*;', re.M)
FIXPT = re.compile(r'([us])fix(\d+)(?:_En(-?\d+))?')
def to_signed(width, radix, digits):
v = int(digits.replace('_', ''), {'b': 2, 'd': 10, 'h': 16}[radix])
return v - (1 << width) if v >> (width - 1) else v
def split_modules(txt):
"""[(name, body, header_comment_block)] -- HDL Coder writes the Simulink block
kind ('Discrete FIR Filter') and Source Path in the comment above the module."""
out = []
for m in MODULE.finditer(txt):
out.append((m.group(1), m.group(2), txt[max(0, m.start() - 1200):m.start()]))
return out
def decls_of(body):
d = {}
for m in DECL.finditer(body):
d[m.group(4)] = {'dir': m.group(1), 'signed': m.group(2) is not None,
'width': int(m.group(3)) + 1 if m.group(3) else 1,
'type': m.group(5) or ''}
return d
def fixpt_of(type_comment, width):
m = FIXPT.search(type_comment or '')
if not m:
return {'width': width, 'frac': None, 'signed': None}
return {'width': int(m.group(2)), 'frac': int(m.group(3) or 0), 'signed': m.group(1) == 's'}
def const_groups(body, decls):
"""constant `assign base_<i> = W'sh...;` families, index-complete from 0."""
vals = {}
for m in CONST_SIG.finditer(body):
name, w, radix, digits = m.group(1), int(m.group(2)), m.group(3), m.group(4)
vals[name] = (w, to_signed(w, radix, digits))
groups = {}
for name, (w, v) in vals.items():
bm = BASE_IDX.match(name)
if bm:
groups.setdefault((bm.group(1), w), {})[int(bm.group(2))] = v
# HDL Coder names the first element without an index (CoefData, CoefData_1, ...)
for (base, w), members in groups.items():
if 0 not in members and base in vals and vals[base][0] == w:
members[0] = vals[base][1]
out = []
for (base, w), members in groups.items():
n = len(members)
if n < 4 or sorted(members) != list(range(n)):
continue
coeffs = [members[i] for i in range(n)]
ex = decls.get(base + '_0') or decls.get(base + '0') or decls.get(base) or {}
out.append({'base': base, 'n': n, 'width': w, 'coeffs': coeffs,
'frac': fixpt_of(ex.get('type', ''), w)['frac'],
'symmetric': coeffs == coeffs[::-1]})
return out
def scan_filters(path, txt):
"""fir_coef_bank / fir_block / adaptive_fir_candidate (guided replacement)."""
found = []
fn = os.path.basename(path)
for name, body, hdr in split_modules(txt):
decls = decls_of(body)
is_fir_hdr = 'Discrete FIR Filter' in hdr
srcpath = (re.search(r'//\s*Source Path:\s*(\S+)', hdr) or [None, ''])[1] \
if re.search(r'//\s*Source Path:\s*(\S+)', hdr) else ''
ports = {k: v for k, v in decls.items() if v['dir'] in ('input', 'output')}
coef_in = [p for p in ports if re.match(r'coef', p, re.I) and ports[p]['dir'] == 'input']
for g in const_groups(body, decls):
# A constant family is only a TAP BANK if it is named/exported as one, or
# if it is fed to multipliers inside a FIR module. Without this guard every
# HDL Coder state/parameter constant family (state_1, mu_2, ...) looks like
# a filter -- 5 false positives on RxAlignCore alone.
exports = [p for p in ports if ports[p]['dir'] == 'output'
and re.search(r'coef|tap', p, re.I)]
named = re.search(r'coef|tap', name, re.I) or re.search(r'coef|tap', g['base'], re.I)
multiplied = any(re.search(r'\b%s(?:_\d+)?\s*\*|\*\s*%s(?:_\d+)?\b'
% (re.escape(g['base']), re.escape(g['base'])), body)
for _ in (0,)) and (is_fir_hdr or re.search(r'fir|filt', name, re.I))
if not (exports or named or multiplied):
continue
found.append({'file': fn, 'module': name, 'kind': 'fir_coef_bank',
'source_path': srcpath, 'coef_signal': g['base'],
'taps': g['n'], 'coef_width': g['width'], 'coef_frac': g['frac'],
'symmetric': g['symmetric'], 'coeffs': g['coeffs'],
'exports_taps': bool(exports),
'note': 'fixed coefficients -> fir_compiler (.coe), guided (Task12 §6.2)'})
din = [p for p in ports if re.match(r'(data|d)?in', p, re.I) and ports[p]['dir'] == 'input'
and ports[p]['width'] > 1]
dout = [p for p in ports if re.search(r'out', p, re.I) and ports[p]['dir'] == 'output'
and ports[p]['width'] > 1]
if is_fir_hdr or (re.search(r'fir|filt', name, re.I) and din and dout):
e = {'file': fn, 'module': name,
'kind': 'fir_block' if is_fir_hdr else 'fir_block_candidate',
'source_path': srcpath,
'complex': any(p.endswith('_re') for p in din),
'data_in': {p: fixpt_of(ports[p]['type'], ports[p]['width']) for p in sorted(din)},
'data_out': {p: fixpt_of(ports[p]['type'], ports[p]['width']) for p in sorted(dout)},
'valid_in': any(re.match(r'valid|.*vld', p, re.I) and ports[p]['dir'] == 'input'
for p in ports),
'valid_out': any(re.match(r'valid|.*vld', p, re.I) and ports[p]['dir'] == 'output'
for p in ports),
'coef_ports': sorted(coef_in),
'adaptive': bool(coef_in),
'rate_hint': ('interpolation' if re.search(r'interp|up', name, re.I) else
'decimation' if re.search(r'decim|down', name, re.I) else 'single_rate?'),
'note': 'rate/phase must be confirmed against the Simulink block before IP config'}
found.append(e)
# hand-rolled adaptive FIR: tap regs + delay-line chain + multiplies
chains = {}
for m in DLY_CHAIN.finditer(body):
if int(m.group(2)) == int(m.group(4)) + 1:
chains.setdefault(m.group(1), set()).add(int(m.group(2)))
chain = max(((b, len(s)) for b, s in chains.items()), key=lambda x: x[1], default=None)
# tap registers: indexed reg group named like weights/taps. Do NOT require the
# reg itself to sit next to a `*` -- HDL Coder feeds multipliers through operand
# temporaries (da0_re * db0_re), so that test misses every real case (EqCore).
tapregs = {}
for rn, d in decls.items():
bm = BASE_IDX.match(re.sub(r'_(re|im)$', '', rn))
if d['dir'] == 'reg' and d['width'] > 1 and bm \
and re.fullmatch(r'w\w*|tap\w*|coef\w*|h', bm.group(1), re.I):
tapregs.setdefault(bm.group(1), set()).add(int(bm.group(2)))
taps = max(((b, len(s)) for b, s in tapregs.items()), key=lambda x: x[1], default=None)
if chain and chain[1] >= 3 and taps and taps[1] >= 3 and '*' in body:
found.append({'file': fn, 'module': name, 'kind': 'adaptive_fir_candidate',
'source_path': srcpath, 'tap_regs': taps[0], 'taps': taps[1],
'delay_line': chain[0], 'delay_depth': chain[1] + 1,
'note': 'runtime-updated taps: fir_compiler Reloadable, or KEEP the RTL '
'with a written reason (Task12 §6.1)'})
return found
KINDS = ('const_rom', 'reg_table', 'ram', 'fifo', 'cordic', 'filter')
def scan_file(path):
txt = open(path, encoding='utf-8', errors='replace').read()
out = {k: [] for k in KINDS}
# constant assigns grouped by array name
assigns = {}
for m in CONST_ASSIGN.finditer(txt):
assigns.setdefault(m.group(1), {})[int(m.group(2))] = (m.group(3), m.group(4), m.group(5))
for m in ROM_DECL.finditer(txt):
signed, msb, name, dmax = m.group(1) is not None, int(m.group(2)), m.group(3), int(m.group(4))
depth = dmax + 1
vals = assigns.get(name, {})
# read sites: name[expr] not on its own assign-constant lines
reads = []
for rm in re.finditer(re.escape(name) + r'\[([^\]\n]+)\]', txt):
idx = rm.group(1)
if idx.isdigit(): # constant index = the table-fill assigns
continue
reads.append(idx.strip())
entry = {'file': os.path.basename(path), 'name': name, 'width': msb + 1,
'depth': depth, 'signed': signed, 'n_const': len(vals),
'fully_const': len(vals) == depth, 'read_exprs': reads}
if len(vals) == depth and reads:
out['const_rom'].append(entry)
elif len(vals) > 0:
entry['note'] = 'PARTIAL const fill -- inspect by hand'
out['const_rom'].append(entry)
for m in REG_DECL.finditer(txt):
out['reg_table'].append({'file': os.path.basename(path), 'name': m.group(3),
'width': int(m.group(2)) + 1, 'depth': int(m.group(4)) + 1,
'note': 'runtime-written reg array: treat as RAM or leave as-is'})
for m in RAM_INST.finditer(txt):
params = dict((k, int(v)) for k, v in PARAM.findall(m.group(1)))
out['ram'].append({'file': os.path.basename(path), 'inst': m.group(2),
'AddrWidth': params.get('AddrWidth'), 'DataWidth': params.get('DataWidth')})
for m in re.finditer(r'^\s*module\s+(\w*[fF][iI][fF][oO]\w*)', txt, re.M):
out['fifo'].append({'file': os.path.basename(path), 'module': m.group(1)})
if re.search(r'\bcx\b', txt) and re.search(r'\bcz\b', txt) and 'atanT' in txt:
out['cordic'].append({'file': os.path.basename(path),
'note': 'iterative CORDIC kernel (cx/cy/cz + atanT) -- guided replacement'})
out['filter'] = scan_filters(path, txt)
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', nargs='?', default=None,
help='FIXED output dir; manifest goes to <outdir>/manifest.json')
ap.add_argument('-o', '--out', default=None)
a = ap.parse_args()
if a.outdir and not a.out:
a.out = os.path.join(a.outdir, 'manifest.json')
manifest = dict({'hdlsrc': os.path.abspath(a.hdlsrc), 'files': {}}, **{k: [] for k in KINDS})
for fn in sorted(os.listdir(a.hdlsrc)):
if not fn.endswith('.v') or fn.endswith('_tb.v'):
continue
r = scan_file(os.path.join(a.hdlsrc, fn))
for k in KINDS:
manifest[k].extend(r[k])
# a Discrete FIR Filter block expands into a tree of helper modules (Filter,
# subFilter, FilterTapSystolic*): keep the block, drop its own guts.
tops = [f['source_path'] for f in manifest['filter']
if f['kind'] == 'fir_block' and f.get('source_path')]
kept = []
for f in manifest['filter']:
inner = any(f.get('source_path', '').startswith(t + '/') for t in tops)
if inner and f['kind'] == 'fir_block_candidate':
continue # internal plumbing of a detected FIR block
if inner and f['kind'] == 'fir_coef_bank':
f['belongs_to'] = next(t for t in tops if f['source_path'].startswith(t + '/'))
kept.append(f)
manifest['filter'] = kept
outp = a.out or os.path.join(a.hdlsrc, 'rtl_ip', 'manifest.json')
os.makedirs(os.path.dirname(outp), exist_ok=True)
json.dump(manifest, open(outp, 'w'), indent=1)
print('manifest ->', outp)
for k in KINDS:
print(f' {k}: {len(manifest[k])}')
for e in manifest[k]:
brief = {kk: (f'[{len(vv)} values]' if kk == 'coeffs' else vv)
for kk, vv in e.items() if kk != 'read_exprs'}
print(' ', brief,
f"reads={len(e.get('read_exprs', []))}" if 'read_exprs' in e else '')
if __name__ == '__main__':
main()