#!/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 [--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 : fir__.coe integer taps, radix 10 (no re-quantisation by the IP) ip_manifest.json/.tcl += {type fir_compiler ...} FIR_PROPOSAL_.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. 在 `/{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()