Files
skills-win/rtl-ip-replace/scripts/initrom_to_ip.py
T
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

198 lines
9.7 KiB
Python

#!/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()