Files
skills-win/rtl-ip-replace/scripts/fir_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

216 lines
12 KiB
Python

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