From ea4541ee90357be48226e1422a5a0b85278538bd Mon Sep 17 00:00:00 2001 From: bowei Date: Mon, 27 Jul 2026 10:39:53 +0800 Subject: [PATCH] =?UTF-8?q?sync:=20SHANGHAI-U9=202026-07-27=20=E4=B8=89?= =?UTF-8?q?=E4=B8=AA=20Simulink=E2=86=92RTL=20skill=20=E8=BD=AC=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=BA=A7=20+=20=E6=96=B0=E5=A2=9E=20simulink-rtl-rede?= =?UTF-8?q?v?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- rtl-ip-replace/SKILL.md | 217 ++++++++++++++ rtl-ip-replace/scripts/backup_outdir.py | 27 ++ rtl-ip-replace/scripts/fir_to_ip.py | 215 ++++++++++++++ rtl-ip-replace/scripts/gen_cmp_tb.py | 168 +++++++++++ rtl-ip-replace/scripts/gen_ip.tcl | 169 +++++++++++ rtl-ip-replace/scripts/incr_plan.py | 227 +++++++++++++++ rtl-ip-replace/scripts/initrom_to_ip.py | 197 +++++++++++++ rtl-ip-replace/scripts/ipflow_common.py | 110 ++++++++ rtl-ip-replace/scripts/namespace_ip.py | 38 +++ rtl-ip-replace/scripts/ram_to_ip.py | 105 +++++++ rtl-ip-replace/scripts/rom_to_ip.py | 108 +++++++ rtl-ip-replace/scripts/rtl_ip_scan.py | 266 ++++++++++++++++++ sim-rtl-flow/SKILL.md | 41 ++- simulink-rtl-redev/SKILL.md | 237 ++++++++++++++++ simulink-rtl-redev/check_sync.py | 185 ++++++++++++ simulink-rtl-redev/measure_k.m | 199 +++++++++++++ simulink_rtl_dev/SKILL.md | 154 ++++++++++ .../reference/block_substitutions.md | 65 +++++ simulink_rtl_dev/scripts/audit_hdl_blocks.m | 208 ++++++++++++++ simulink_rtl_dev/scripts/check_exemptions.py | 116 ++++++++ simulink_rtl_dev/scripts/check_hdl_support.m | 64 +++++ .../scripts/exemptions.example.json | 17 ++ simulink_rtl_dev/scripts/hdl_block_search.m | 50 ++++ 23 files changed, 3171 insertions(+), 12 deletions(-) create mode 100644 rtl-ip-replace/SKILL.md create mode 100644 rtl-ip-replace/scripts/backup_outdir.py create mode 100644 rtl-ip-replace/scripts/fir_to_ip.py create mode 100644 rtl-ip-replace/scripts/gen_cmp_tb.py create mode 100644 rtl-ip-replace/scripts/gen_ip.tcl create mode 100644 rtl-ip-replace/scripts/incr_plan.py create mode 100644 rtl-ip-replace/scripts/initrom_to_ip.py create mode 100644 rtl-ip-replace/scripts/ipflow_common.py create mode 100644 rtl-ip-replace/scripts/namespace_ip.py create mode 100644 rtl-ip-replace/scripts/ram_to_ip.py create mode 100644 rtl-ip-replace/scripts/rom_to_ip.py create mode 100644 rtl-ip-replace/scripts/rtl_ip_scan.py create mode 100644 simulink-rtl-redev/SKILL.md create mode 100644 simulink-rtl-redev/check_sync.py create mode 100644 simulink-rtl-redev/measure_k.m create mode 100644 simulink_rtl_dev/SKILL.md create mode 100644 simulink_rtl_dev/reference/block_substitutions.md create mode 100644 simulink_rtl_dev/scripts/audit_hdl_blocks.m create mode 100644 simulink_rtl_dev/scripts/check_exemptions.py create mode 100644 simulink_rtl_dev/scripts/check_hdl_support.m create mode 100644 simulink_rtl_dev/scripts/exemptions.example.json create mode 100644 simulink_rtl_dev/scripts/hdl_block_search.m diff --git a/rtl-ip-replace/SKILL.md b/rtl-ip-replace/SKILL.md new file mode 100644 index 0000000..70b347d --- /dev/null +++ b/rtl-ip-replace/SKILL.md @@ -0,0 +1,217 @@ +--- +name: rtl-ip-replace +description: > + 把 MATLAB/Simulink HDL Coder 生成的 RTL 二次加工:查找表(ROM)→ Vivado dist_mem_gen/blk_mem_gen IP、 + RAM(SimpleDualPortRAM_generic 等)→ blk_mem_gen IP、CORDIC → Xilinx CORDIC IP、FIFO(reFIFO 等)→ + fifo_generator IP、滤波器(**仅固定抽头**的 FIR/抽取/内插;LMS 自适应、Farrow、运行时参考抽头 + 的相关器不在替换范畴)→ fir_compiler / cic_compiler IP;改写后的 RTL 只存放在 + 固定目录 vivado/rtl_ip//(含 PROVENANCE.log 溯源与 ip/ 子目录),每次重跑先整体备份到 + backup_<时间戳>/(含上次原始 RTL src_orig/ 与上次改写后 RTL,作为增量基线);与原始生成 RTL 做 + cycle-true 双 DUT 对比仿真,PASS 后才作为 vivado 综合输入,vivado 工程绝不允许直接引用 matlab hdlsrc。 + 触发词:IP替换、ROM IP、RAM IP、CORDIC IP、FIFO IP、滤波器 IP、FIR IP、查找表替换、rtl_ip、 + cycle-true 对比、二次修改rtl、makehdl 后处理、增量替换。 + 每次 regen_all/makehdl 重新生成 RTL 后都要重跑本流程(默认走增量)。适用于任何 HDL Coder→Vivado 工程。 +--- + +# rtl-ip-replace — HDL Coder RTL 的 Vivado IP 化 + cycle-true 验证 + +> 系统级 skill,适用于任何 HDL Coder→Vivado 工程。 +> 文中的模型名、模块名一律是**占位示例**;各工程的需求规格、实测清单与"暂不实施"名单, +> 记在该工程自己的任务书与 Notes 下,不写进本 skill。 + +## 〇、安装位置与环境依赖 + +脚本位于 `~/.claude/skills/rtl-ip-replace/scripts/` +(Windows:`%USERPROFILE%\.claude\skills\rtl-ip-replace\scripts\`)。下文的 `scripts/` 均指此处。 + +**Vivado 路径来自环境变量,不写死**:`gen_cmp_tb.py` 生成 `run_cmp.bat` 时按 +`VIVADO_PATH` → `XILINX_VIVADO` 的顺序取安装根,三种写法都接受—— +安装根、其 `bin\`、或 `bin\vivado.bat`。都没设时回落到内置默认值并打印 WARNING。 +换机器/换版本只需改环境变量。 + +**实施对象(``)不绑定任何默认值,必须由用户在每次调用时明确指定** +(如 "在 `` 上实施");未指定时先问用户,不得自行猜测。 +"查找表"指**固定内容**的表(sin、cos、log、atan 等常量表); +运行时写入的数据表不属于替换范围。 + +**滤波器替换范围(用户指令 2026-07-24)**:**只有固定抽头(常量系数)的滤波器才适用 +fir_compiler/cic_compiler 替换**。以下一律**不替换**: +- Farrow/多项式内插器(系数是 mu 等运行时变量的多项式,如 TxPreOffset); +- LMS 等自适应滤波器(逐样本更新权值,如 EqCore); +- 参考抽头为运行时捕获序列的相关器(如 RxAlignCore 的 refdl 相关)。 +扫描器只把**能识别到的**这类结构报为 `adaptive_fir_candidate`/`fir_block_candidate`(仅供盘点, +不进替换清单):抽头寄存器组 + 延迟线 + 乘法(EqCore 的 LMS 就是这样被认出来的); +Farrow(TxPreOffset)与相关器(RxAlignCore)没有抽头寄存器组,扫描器不一定报得出来—— +**它们由本规则排除,不依赖扫描器识别**。`fir_to_ip.py` 只处理 `fir_coef_bank`(常量系数), +自适应类一律只打印 INVENTORY ONLY,不生成 IP。 +**"暂不实施"名单**:工程可以把某些模型/子系统登记为暂不替换(典型:激励源、上游 OFDM 源), +名单写在该工程的任务书里,**待用户明示后才可对其运行本 skill**。本 skill 不内置任何名单。 + +**双向调整原则(用户指令 2026-07-23)**:替换不必单向迁就原 RTL——**允许修改 MATLAB/Simulink +源设计来适配 Vivado IP 核**(位宽、量化、接口节奏、算法结构),只要**仿真性能不受影响** +(收敛行为、MER 等系统指标不劣化,须用模型仿真复验)。位级逐拍等价做不到时 +(典型:CORDIC 的内部舍入),按此原则改源头或按消费点契约放宽,并在 Task12/Result +文档显式记录差异及性能复验结论。 + +## ★ 强制目录规定(用户指令 2026-07-23,严格执行) + +1. **固定目录**:脚本处理后的 RTL 一律存放于 `vivado/rtl_ip//`。 + **Vivado 工程只允许使用该目录下的 RTL,绝对不允许直接引用 `matlab/**/hdlsrc/**` 的自动生成代码** + (对比仿真编译原始代码除外——那只进 xsim,不进综合)。 +2. **溯源 log**:固定目录下必须有 `PROVENANCE.log`,写明本次修改所依据的原始 MATLAB RTL 的**绝对路径**、 + 每个源文件的 sha1/mtime(由 `ipflow_common.py` 自动生成维护)。 +3. **IP 子目录**:所有生成的 Xilinx IP(.xci 及产物)放在固定目录的 `ip/` 子目录。 +4. **备份先行**:每次重跑本 skill,第一步必须把固定目录现有全部内容移入 `backup_/` + 子目录,然后全新生成(`backup_outdir.py` 完成,backup_* 目录本身不重复嵌套)。 +5. 接入综合前 lint:`.xpr` 中不得再有指向 hdlsrc 的**综合**源(检查:grep hdlsrc *.xpr)。 + +## 适用范围(HDL Coder 25.2 生成代码的习惯用法,实测归纳) + +| 结构 | 生成代码特征 | 目标 IP | cycle-true 约束 | +|---|---|---|---| +| 常量查找表 (ROM) | `wire signed [W-1:0] [0:D-1];` + D 条 `assign [i] = W'sb...;` + 组合读 `[expr]` | `dist_mem_gen`(默认,异步读,天然 cycle-true) | 零改动等价 | +| **isempty-init 表** ★ | 常量表 `P` + `T_1[i]=P[i]` 拷贝 generate + `T_reg_reg` 寄存器阵列(复位清 0,首个 `enb&&s` 拍整表载入常量,此后保持)+ `TMP = s ? T_1 : T` 组合 mux。**这是 13 万 FF 毒源的本体**(sinT/cosT 4096×16) | `dist_mem_gen` + 1 个 `__ipinited` 标志 FF | 读点改写:`T[e] → inited ? ROM[e] : 0`;`TMP[e] → (s\|\|inited) ? ROM[e] : 0`,逐拍等价(复现"载入前读 0") | +| RAM | `SimpleDualPortRAM_generic`(单时钟 SDP,enb 使能,**读旧值 read-first**,读延迟 1 拍) | `blk_mem_gen` SDP,写口 **Read-First**,无输出寄存器 | 碰撞语义必须 Read-First **且 `CONFIG.Assume_Synchronous_Clk true`(C_COMMON_CLK=1)——异步双钟假设下模型不保证碰撞读旧值,实测 dlock 提交后 tx_al 发散 17640 拍** | +| CORDIC | 内联 FSM:`cx/cy/cz` 迭代寄存器 + `atanT` 表,多拍出一次结果 | `cordic`(arctan/rotate) | **无法逐拍等价**;契约=消费点等价:IP 延迟 ≤ 原 FSM 出结果拍数,IP 早出的结果寄存保持到原提交拍再输出 | +| FIFO | `reFIFO` 等:reg 数组 + wr/rd 指针 | `fifo_generator` | 读延迟(standard=1拍/FWFT=0拍)与 empty/full 翻转拍必须逐拍一致;TB 同时比数据和 flag | +| 滤波器(**仅固定抽头**) | **常量**抽头数组 + delay line + 乘加链(成型/内插 FIR 等) | 定系数→`fir_compiler`;大抽取/内插比→`cic_compiler`。**抽头非常量(LMS/Farrow/运行时参考)不在替换范畴** | 系数整数值逐个相同、Full Precision 输出 + 外部按原 RTL 方式截位、延迟差用固定移位寄存器补平后**逐拍 0 失配**;`tvalid` 与原输出有效拍一致 | + +注意:`reg` 数组小表(w_out/refdl/t0 等 5~18 深)是真正的运行时数据,**保留原状**,不替换。 + +## 流程(0~5 步,顺序执行) + +设 `HDLSRC` = 该工程 HDL Coder 的输出目录(形如 `matlab/<工程>/hdlsrc/`), +`OUT = vivado/rtl_ip/`(固定目录,见上节规定),脚本在 `~/.claude/skills/rtl-ip-replace/scripts/`。 + +### 0. 备份(强制第一步) +``` +python scripts/backup_outdir.py +``` +备份后的 `backup_/` 同时含**上次的原始 MATLAB RTL**(`src_orig/`)与**上次改写后的 RTL**, +即下一步增量判定的基线。 + +### 0.5 增量判定(Task12 规定 13~16;要全量复核时加 `--full`) +``` +python scripts/incr_plan.py plan --apply +``` +- 把新 hdlsrc 与基线 `src_orig/` 逐文件比 sha1:`same` 的文件**直接取回上次改写后的 .v**; +- 打印 `ALL MATLAB sources unchanged` 时,**第 2 步(transform)整步可跳过**; +- 同时把本次源码快照写进 `/src_orig/`(下次的基线),计划落 `/incr_plan.json`。 + +### 1. 扫描盘点 +``` +python scripts/rtl_ip_scan.py +``` +manifest 落在 `/manifest.json`。人工 review:const_rom 里"唯一读点"若其实是 +`T_1[gv]=P[gv]` 的 generate 拷贝,该表属于 isempty-init 惯用法,**必须走 initrom_to_ip.py, +不得用 rom_to_ip.py 直接改**(会把 ROM 塞进 genvar 循环,语义错误)。 + +### 2. 生成替换 RTL(另存到 ,不动原始文件) +``` +python scripts/initrom_to_ip.py # isempty-init 表(sinT/cosT/atanT) +python scripts/rom_to_ip.py # 剩余纯常量 ROM +python scripts/ram_to_ip.py # SimpleDualPortRAM → blk_mem_gen +python scripts/fir_to_ip.py [--rate N ...] # 定系数 FIR → .coe + fir_compiler 登记 + # + FIR_PROPOSAL_*.md(不改 RTL) +#(CORDIC/FIFO/滤波器手术引导式手改 里的副本,见下方契约;所需 IP 手工追加进 ip_manifest) +python scripts/namespace_ip.py # 最后统一加 _IP 后缀(幂等) +``` +产物:改写后 `*.v`、去重 `*.coe`、`ip_manifest.json/.tcl`、`PROVENANCE.log`、`src_orig/`(自动)。 + +### 2.5 增量结算(决定第 3、4 步能不能省) +``` +python scripts/incr_plan.py finalize +``` +生成 `TRANSFORM.diff`(src_orig → 改写后的 removed/added 行,行号已归一化)并与基线比对,输出两个决策: +- `ip_reuse: YES` — `ip_manifest.tcl` + 全部 `.coe` 与基线逐字节相同,基线 `ip/` 已复制回来,**跳过第 3 步**; +- `sim_inherit: YES` — 还满足 `TRANSFORM.diff` 与基线相同、scripts_sha1 未变、文件集未增删、 + 基线 `cmp_result.txt` 为 PASS ⇒ **跳过第 4 步**,PASS 继承并写入 `PROVENANCE.log`。 + 任一条不满足即打印 blocker 并要求实跑。**接入综合出板前的最后一次必须 `--full` 全量复核。** + +### 3. 生成 Vivado IP(固定目录的 ip/ 子目录) — `ip_reuse: YES` 时跳过 +``` +vivado -mode batch -source scripts/gen_ip.tcl -tclargs /ip_manifest.tcl /ip +``` + +### 4. cycle-true 对比仿真(闸门,不 PASS 不准上综合) — `sim_inherit: YES` 时跳过 +``` +python scripts/gen_cmp_tb.py --rtl-ip +/cmp/run_cmp.bat +``` +- 双 DUT:原始 ``(从 HDLSRC 编译,仅用于仿真)vs `_IP`(从 编译); + 复位释放后逐拍比较全部输出;CORDIC 输出用 `--compare-at OUT=done信号` 限定提交拍。 +- 激励:默认 LFSR;控制密集顶层优先复用 HDL Coder `_tb.v` 向量。 +- xelab 库:`-L dist_mem_gen_v8_0 -L blk_mem_gen_v8_4 -L fifo_generator_v13_2 -L cordic_v6_0_14 + -L fir_compiler_v7_2_11 -L cic_compiler_v4_0_13 -L xbip_utils_v3_0_9 -L unisims_ver` + + IP 仿真网表(`--ip-simdir /ip`)。 +- 判据:`cmp_result.txt` 首行 **PASS(0 失配)**;FAIL 时禁止接入综合。 + +### 5. 接入综合 +- 工程源:`/*.v` + `/ip/**/*.xci`,顶层实例化 `_IP`; + **移除工程中所有 hdlsrc 综合源引用**(规定 §1/§5)。 +- **共享模块名检查(实测踩坑)**:移除被转换模型的 hdlsrc 源前,grep 其余未转换模型是否 + 实例化**同名**模块(HDL Coder 对多个模型会生成同名的通用模块,如 `SimpleDualPortRAM_generic`, + 工程里可能一直借用另一个模型目录下的那一份)——是则把**该模型自己目录下的同名 .v** + 加进工程,否则综合报 `module not found`。 +- isempty-init 表 IP 化后,`vivado/constrs/mc_paths.tcl` 的 ROM 多周期豁免段(`MC rom=`)删除。 +- 重新综合前全清 `.runs/.cache`(残留 `__synthesis_is_running__` 标记会死锁调度)。 + +## CORDIC 替换契约(引导式) + +1. 圈出迭代锥:`cx/cy/cz` 寄存器、`atanT`(isempty-init 表)、迭代计数、结果提交点 + (本项目:相关峰种子 candR/candI → phi 种子,1 iter/4 clk)。 +2. 包成边界模块 `cordic_kernel_IP(clk, en, start, x_in, y_in, done, phase_out)`,内部用 + `cordic` IP(Translate/arctan,流水延迟 L ≤ 原 FSM 提交拍数)。 +3. `done`/`phase_out` 按原 FSM 提交拍对齐(早出结果寄存保持)→ 外部逐拍等价。 +4. TB 在提交拍断言相等;量化差须靠 IP 位宽/舍入配置调平至 0 或显式记录 ≤1 LSB。 + +## FIFO 替换契约(引导式) + +1. 原模块 `rd_en` 后 1 拍出数 = **Standard**;当拍出数 = **FWFT**。 +2. `empty/full/count` 翻转拍逐拍对比;IP 的 `valid/underflow` 不接原逻辑。 +3. 深度向上取 2 的幂;复位保持 ≥ 双方要求最大值,双方 ready 后才开始比对。 + +## 滤波器替换契约(引导式,需求 4b) + +1. **先分类(判定门槛就是"抽头是不是常量")**: + - **抽头固定**(成型/抽取/内插/信道化,系数是模型里的常量)→ `fir_compiler`(系数进 .coe); + 抽取/内插比很大的级联积分梳状结构 → `cic_compiler`。**只有这一类进替换清单。** + - **抽头非常量** —— LMS/自适应均衡器(EqCore)、Farrow/多项式内插器(TxPreOffset)、 + 参考抽头为运行时捕获序列的相关器(RxAlignCore)—— **一律不替换,保留原 RTL**。 + 不要用 Reloadable Coefficients 去凑:那是低速重载通道(一整帧系数经 AXI-Stream 灌入), + 表达不了逐样本更新的权值,也表达不了每拍随 mu 变化的 Farrow 系数;硬套只会既破坏 + cycle-true 又拿不到面积收益。 + (`fir_to_ip.py` 的 `--reload` 只用于**多组固定系数之间切换**的场合,不是自适应的出口。) +2. 系数取 HDL Coder 常量表的**定点整数值**原样写 .coe,不让 IP 再量化。 +3. 输出用 **Full Precision**,截位放到 IP 外面按原 RTL 的移位/饱和方式做,**不开 IP 内建 rounding**。 +4. 延迟对齐:IP 延迟 L 与原 L0 之差用固定移位寄存器补平;L > L0 只能按双向调整原则改 Simulink 源, + 并复验 MER/收敛不劣化。 +5. 节奏:输入非满拍时用 `s_axis_data_tvalid`;`m_axis_data_tvalid` 翻转拍与原输出有效拍逐拍一致。 +6. 验收:对齐后 TB 逐拍 0 失配。 + +**IP 侧硬事实(2026-07-24 在本机 Vivado 2018.1 实测,已写进 gen_ip.tcl / fir_to_ip.py)**: + +- `fir_compiler` 7.2 合法区间:`Coefficient_Width∈[8,49]`、`Data_Width∈[2,47]`、 + `Data_Fractional_Bits∈[0,16]`。HDL Coder 的 full-precision 抽头(如 OFDM 源的 51 位) + **超限**,`fir_to_ip.py` 会拒绝登记该 IP 并写方案,要求先按双向调整原则回 Simulink 重新量化。 +- **二进制点一律填 0**(`Coefficient_Fractional_Bits=0`、`Data_Fractional_Bits=0`):core 做整数 + MAC,小数位只影响 GUI;填 0 与生成 RTL 的整数语义一致,又避开上面的 0..16 限制。 +- 定拍关键项:`RateSpecification=Input_Sample_Period` + `SamplePeriod=1`(禁折叠)、 + `Has_ACLKEN=true`(接 `enb`)、`M_DATA_Has_TREADY=false`、`S_DATA_Has_FIFO=false`。 +- `cic_compiler` 4.0 的同名参数**取值不同**:`RateSpecification` 只认 + `Frequency_Specification | Sample_Period`(**没有** `Input_Sample_Period`,填错直接报 + IP_Flow 19-3461)。 +- 对比仿真需要 `-L fir_compiler_v7_2_11 -L cic_compiler_v4_0_13`(gen_cmp_tb.py 已加)。 + +## 新工程接入本 skill + +1. 在工程任务书里写明**实施对象清单**与**"暂不实施"名单**(§适用范围); +2. 跑一次全量扫描(第 1 步),把结果作为**基线清单**记进工程 Notes—— + 各类结构各几处(isempty-init 表 / RAM / CORDIC / FIFO / 定系数滤波器), + 以及被报为 `adaptive_fir_candidate` 但按范围规定**保留 RTL** 的那些; +3. 确认 `VIVADO_PATH` 已指向本机 Vivado(§〇),否则 `run_cmp.bat` 跑不起来; +4. 首次运行没有 `src_orig/`/`TRANSFORM.diff` 时会自动退回全量,并顺带把增量基线补齐; + 此后每次 `makehdl` 之后仍从第 0 步(备份)重跑,但走增量。 + +**典型收益点**:isempty-init 常量表(sin/cos 一类)在 HDL Coder 里会展开成 FF 阵列 + 巨型读 mux, +数万到十几万 FF,是时序与布局的头号毒源;IP 化从根上解决。接入后优先扫这一类。 diff --git a/rtl-ip-replace/scripts/backup_outdir.py b/rtl-ip-replace/scripts/backup_outdir.py new file mode 100644 index 0000000..c57e631 --- /dev/null +++ b/rtl-ip-replace/scripts/backup_outdir.py @@ -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_/, +so the run always generates everything fresh while keeping full history. + +Usage: python backup_outdir.py +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 ') + 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() diff --git a/rtl-ip-replace/scripts/fir_to_ip.py b/rtl-ip-replace/scripts/fir_to_ip.py new file mode 100644 index 0000000..780d691 --- /dev/null +++ b/rtl-ip-replace/scripts/fir_to_ip.py @@ -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 [--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() diff --git a/rtl-ip-replace/scripts/gen_cmp_tb.py b/rtl-ip-replace/scripts/gen_cmp_tb.py new file mode 100644 index 0000000..8af9ec4 --- /dev/null +++ b/rtl-ip-replace/scripts/gen_cmp_tb.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Generate the dual-DUT cycle-true comparison testbench + xsim run script. + + python gen_cmp_tb.py [--outdir DIR] [--cycles N] + [--ip-simdir DIR] [--compare-at PORT=SIGNAL ...] + +- DUT0 = compiled from /*.v (original HDL Coder RTL) +- DUT1 = _IP compiled from /rtl_ip/*.v (+ IP simulation netlists) +- identical stimulus into both: clk; reset (asserted RST_CYCLES then deasserted); + enable-like inputs (enb/clk_enable/*_en names) held 1; every other input driven + from a per-port LFSR (deterministic, seeded by port name hash). + !! For control-heavy tops, replace the LFSR block with replayed vectors from + the HDL Coder _tb.v -- see SKILL.md. +- compare: every output port, every posedge after both-ready; mismatches counted + and logged; result file first line = PASS or "FAIL ". +- --compare-at OUT=QUALIFIER limits comparison of OUT to cycles where the named + qualifier signal (an output of the DUTs, e.g. a done/valid strobe) is 1 -- + the CORDIC consumption-point contract. +""" +import argparse, hashlib, os, re, sys + +# Vivado install root, for the generated run_cmp.bat (xvlog/xelab/xsim + glbl.v). +# Machine-specific, so it comes from the environment, never from a literal path. +# VIVADO_PATH may point at the install root, its bin/, or bin/vivado.bat -- +# all three are accepted. XILINX_VIVADO (set by Vivado's settings64) also works. +VIVADO_FALLBACK = r'D:\Xilinx\Vivado\2018.1' + + +def _vivado_root(): + v = (os.environ.get('VIVADO_PATH') or os.environ.get('XILINX_VIVADO') or '').strip().strip('"') + if not v: + print(f'NOTE: neither VIVADO_PATH nor XILINX_VIVADO is set -- falling back to\n' + f' {VIVADO_FALLBACK}\n' + f' That literal is one machine\'s install. Set VIVADO_PATH to this\n' + f' machine\'s Vivado (see the VIVADO_PATH entry in ~/skills_env.md)\n' + f' or run_cmp.bat may silently use the wrong version.') + return VIVADO_FALLBACK + p = v.rstrip('\\/') + if p.lower().endswith(('.bat', '.exe')) or os.path.isfile(p): + p = os.path.dirname(p) # ...\bin\vivado.bat -> ...\bin + if os.path.basename(p).lower() == 'bin': + p = os.path.dirname(p) # ...\bin -> install root + return p or VIVADO_FALLBACK + +PORT_RE = re.compile(r'^\s*(input|output)\s+(signed\s+)?(\[[^\]]+\]\s*)?(\w+)\s*[,;)]', re.M) + +def parse_ports(vfile, top): + txt = open(vfile, encoding='utf-8', errors='replace').read() + m = re.search(r'module\s+%s\b(.*?)endmodule' % re.escape(top), txt, re.S) + if not m: + sys.exit(f'module {top} not found in {vfile}') + body = m.group(1) + ports = [] + for pm in PORT_RE.finditer(body): + d, signed, rng, name = pm.group(1), pm.group(2), (pm.group(3) or '').strip(), pm.group(4) + ports.append({'dir': d, 'signed': bool(signed), 'range': rng, 'name': name}) + return ports + +def lfsr_seed(name): + return int(hashlib.sha1(name.encode()).hexdigest()[:8], 16) | 1 + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('top'); ap.add_argument('hdlsrc') + ap.add_argument('--rtl-ip', required=True, help='FIXED rtl_ip dir (vivado/rtl_ip/)') + ap.add_argument('--outdir', default=None) + ap.add_argument('--cycles', type=int, default=200000) + ap.add_argument('--rst-cycles', type=int, default=20) + ap.add_argument('--ip-simdir', default='', help='dir with generated IP sim .v files (globbed recursively)') + ap.add_argument('--compare-at', nargs='*', default=[], metavar='OUT=QUAL') + a = ap.parse_args() + hdlsrc = os.path.abspath(a.hdlsrc) + rtl_ip_dir = os.path.abspath(a.rtl_ip) + outdir = a.outdir or os.path.join(rtl_ip_dir, 'cmp') + os.makedirs(outdir, exist_ok=True) + qual = dict(kv.split('=') for kv in a.compare_at) + + src = None + for fn in os.listdir(hdlsrc): + if fn.endswith('.v') and re.search(r'^\s*module\s+%s\b' % re.escape(a.top), + open(os.path.join(hdlsrc, fn), encoding='utf-8', errors='replace').read(), re.M): + src = os.path.join(hdlsrc, fn); break + if not src: + sys.exit(f'{a.top} not declared in {hdlsrc}/*.v') + ports = parse_ports(src, a.top) + ins = [p for p in ports if p['dir'] == 'input'] + outs = [p for p in ports if p['dir'] == 'output'] + + def is_clk(n): return n in ('clk', 'clock') + def is_rst(n): return 'reset' in n or n == 'rst' + def is_en(n): return n in ('enb', 'clk_enable') or n.endswith('_en') or n.endswith('_enable') + + tb = ['`timescale 1 ns / 1 ns', 'module tb_cmp;', + ' reg clk = 0; always #2 clk = ~clk;', + f' reg rst = 1; initial begin repeat ({a.rst_cycles}) @(posedge clk); rst <= 0; end', + ' integer mismatches = 0; integer fd;'] + conn0, conn1 = [], [] + for p in ins: + n, rng = p['name'], p['range'] + if is_clk(n): + conn0.append(f'.{n}(clk)'); conn1.append(f'.{n}(clk)'); continue + if is_rst(n): + conn0.append(f'.{n}(rst)'); conn1.append(f'.{n}(rst)'); continue + if is_en(n): + tb.append(f' wire {rng} {n} = 1\'b1;') + else: + w = 32 + m = re.match(r'\[(\d+):0\]', rng or '') + if m: w = int(m.group(1)) + 1 + tb.append(f' reg [31:0] lfsr_{n} = 32\'h{lfsr_seed(n):08x};') + tb.append(f' always @(posedge clk) lfsr_{n} <= {{lfsr_{n}[30:0], lfsr_{n}[31]^lfsr_{n}[21]^lfsr_{n}[1]^lfsr_{n}[0]}};') + tb.append(f' wire {rng} {n} = lfsr_{n}[{min(w,32)-1}:0];') + conn0.append(f'.{n}({n})'); conn1.append(f'.{n}({n})') + for p in outs: + n, rng = p['name'], p['range'] + tb.append(f' wire {rng} o0_{n}, o1_{n};') + conn0.append(f'.{n}(o0_{n})'); conn1.append(f'.{n}(o1_{n})') + tb += [f' {a.top} dut0 ({", ".join(conn0)});', + f' {a.top}_IP dut1 ({", ".join(conn1)});', + f' reg armed = 0; initial begin repeat ({a.rst_cycles + 5}) @(posedge clk); armed <= 1; end', + ' always @(posedge clk) if (armed) begin'] + for p in outs: + n = p['name'] + g = f' && (o0_{qual[n]} === 1\'b1)' if n in qual else '' + tb.append(f' if ((o0_{n} !== o1_{n}){g}) begin') + tb.append(f' mismatches = mismatches + 1;') + tb.append(f' if (mismatches <= 50) $display("MISMATCH t=%0t port={n} orig=%h ip=%h", $time, o0_{n}, o1_{n});') + tb.append(' end') + tb += [ ' end', + f' initial begin repeat ({a.cycles}) @(posedge clk);', + f' fd = $fopen("cmp_result.txt", "w");', + ' if (mismatches == 0) begin $fdisplay(fd, "PASS"); $display("CMP PASS"); end', + ' else begin $fdisplay(fd, "FAIL %0d", mismatches); $display("CMP FAIL %0d", mismatches); end', + ' $fclose(fd); $finish;', + ' end', 'endmodule'] + open(os.path.join(outdir, 'tb_cmp.v'), 'w').write('\n'.join(tb) + '\n') + + vroot = _vivado_root() + xbin = os.path.join(vroot, 'bin') + glbl = os.path.join(vroot, 'data', 'verilog', 'src', 'glbl.v') + if not os.path.isfile(glbl): + print(f'WARNING: glbl.v not found under {vroot}\n' + f' set VIVADO_PATH (or XILINX_VIVADO) to your Vivado install, ' + f'its bin/, or bin/vivado.bat; run_cmp.bat will fail as written.') + bat = [f'@echo off', f'cd /d %~dp0', + f'call "{xbin}\\xvlog.bat" "{glbl}"', + f'for %%f in ("{hdlsrc}\\*.v") do if /I not "%%~nxf"=="{a.top}_tb.v" call "{xbin}\\xvlog.bat" "%%f"', + f'for %%f in ("{rtl_ip_dir}\\*.v") do call "{xbin}\\xvlog.bat" "%%f"'] + if a.ip_simdir: + bat.append(f'for /R "{a.ip_simdir}" %%f in (*.v) do call "{xbin}\\xvlog.bat" "%%f"') + # dist_mem_gen/blk_mem_gen sim models load .mif by BARE filename relative to + # the xsim run directory -- without this copy every ROM reads 0. + bat.append(f'for /R "{a.ip_simdir}" %%f in (*.mif) do copy /Y "%%f" . >nul') + bat += [f'call "{xbin}\\xvlog.bat" tb_cmp.v', + f'call "{xbin}\\xelab.bat" tb_cmp glbl -s cmp_sim -timescale 1ns/1ns ' + f'-L dist_mem_gen_v8_0 -L blk_mem_gen_v8_4 -L fifo_generator_v13_2 ' + f'-L cordic_v6_0_14 -L fir_compiler_v7_2_11 -L cic_compiler_v4_0_13 ' + f'-L xbip_utils_v3_0_8 -L xbip_utils_v3_0_9 -L xbip_pipe_v3_0_5 ' + f'-L unisims_ver -L unimacro_ver -L secureip', + f'call "{xbin}\\xsim.bat" cmp_sim -R', + f'type cmp_result.txt'] + open(os.path.join(outdir, 'run_cmp.bat'), 'w').write('\r\n'.join(bat) + '\r\n') + print(f'generated {outdir}\\tb_cmp.v ({len(ins)} in / {len(outs)} out ports) and run_cmp.bat') + print('NOTE: exclude the original *_tb.v from compile; for control-heavy tops replace the') + print(' LFSR stimulus with replayed HDL Coder testbench vectors (see SKILL.md).') + +if __name__ == '__main__': + main() diff --git a/rtl-ip-replace/scripts/gen_ip.tcl b/rtl-ip-replace/scripts/gen_ip.tcl new file mode 100644 index 0000000..09e334e --- /dev/null +++ b/rtl-ip-replace/scripts/gen_ip.tcl @@ -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 [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" diff --git a/rtl-ip-replace/scripts/incr_plan.py b/rtl-ip-replace/scripts/incr_plan.py new file mode 100644 index 0000000..d21e191 --- /dev/null +++ b/rtl-ip-replace/scripts/incr_plan.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""Incremental mode for rtl-ip-replace (Task12 rules 13-16 / Task12 §5). + +Baseline = the newest backup_/ under the fixed dir, i.e. the previous run's +ORIGINAL MATLAB RTL (src_orig/) + rewritten RTL (*.v) + ip/ + cmp/. + + plan [--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 [--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() diff --git a/rtl-ip-replace/scripts/initrom_to_ip.py b/rtl-ip-replace/scripts/initrom_to_ip.py new file mode 100644 index 0000000..052aefe --- /dev/null +++ b/rtl-ip-replace/scripts/initrom_to_ip.py @@ -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 = ; 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 (operates on /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. /vivado/rtl_ip/)') + 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() diff --git a/rtl-ip-replace/scripts/ipflow_common.py b/rtl-ip-replace/scripts/ipflow_common.py new file mode 100644 index 0000000..93f01ec --- /dev/null +++ b/rtl-ip-replace/scripts/ipflow_common.py @@ -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 /vivado/rtl_ip// ; 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 /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') diff --git a/rtl-ip-replace/scripts/namespace_ip.py b/rtl-ip-replace/scripts/namespace_ip.py new file mode 100644 index 0000000..0e5d521 --- /dev/null +++ b/rtl-ip-replace/scripts/namespace_ip.py @@ -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() diff --git a/rtl-ip-replace/scripts/ram_to_ip.py b/rtl-ip-replace/scripts/ram_to_ip.py new file mode 100644 index 0000000..55065c4 --- /dev/null +++ b/rtl-ip-replace/scripts/ram_to_ip.py @@ -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. /vivado/rtl_ip/)') + 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() diff --git a/rtl-ip-replace/scripts/rom_to_ip.py b/rtl-ip-replace/scripts/rom_to_ip.py new file mode 100644 index 0000000..2681e68 --- /dev/null +++ b/rtl-ip-replace/scripts/rom_to_ip.py @@ -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 into /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] _a; wire [W-1:0] _q; + assign _a = ; + u__r (.a(_a), .spo(_q)); + and rewrites `[]` -> `$signed(_q)` (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. /vivado/rtl_ip/)') + 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() diff --git a/rtl-ip-replace/scripts/rtl_ip_scan.py b/rtl-ip-replace/scripts/rtl_ip_scan.py new file mode 100644 index 0000000..23736a0 --- /dev/null +++ b/rtl-ip-replace/scripts/rtl_ip_scan.py @@ -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 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 [] [-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_ = 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 /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() diff --git a/sim-rtl-flow/SKILL.md b/sim-rtl-flow/SKILL.md index da6a13d..4139b8c 100644 --- a/sim-rtl-flow/SKILL.md +++ b/sim-rtl-flow/SKILL.md @@ -94,12 +94,18 @@ From `Notes/PR01.md` and `Notes/PR01-2.md`; the graph routes on these. nativize all submodules in one Simulink model. 3. **Never hand-edit MATLAB-generated RTL.** If IP replacement, Vivado timing, or HIL needs a change there, return `change_scope: "simulink_module"` + `affected_modules`. - The graph then forces a `modify_e:*` step — **edit the `-E.slx` and re-simulate - against sim-golden** — before any RTL is regenerated. -4. **Timing fixes change the RTL, not the constraints.** Prefer adding pipeline/delay + The graph then forces `modify_e:* → sync_sim:*` before any RTL is regenerated. +4. **Editing a `-E` model obliges you to sync the `-x`/`-q` model.** `modify_e:*` edits + only the `-E`; the follow-up `sync_sim:*` uses **`simulink-rtl-redev`** to carry the + same change into the simulation model and re-verify against sim-golden. The + derivation script does *not* translate native-core edits into the m implementation, + so changing one side alone means the RTL moved while the simulation still validates + the old behaviour. If they don't align, the graph goes back to `modify_e:*` — it will + **not** let you regenerate RTL. +5. **Timing fixes change the RTL, not the constraints.** Prefer adding pipeline/delay stages. Only touch timing constraints when the constraint itself is plainly wrong, and say why in `notes`. -5. **HIL is strictly read-only.** `hil:*` arrives with `allow_code_changes=false`. Run +6. **HIL is strictly read-only.** `hil:*` arrives with `allow_code_changes=false`. Run `fpga-imx6-flow` / `zynq-imx6-flow` for *diagnosis only*; the moment a code change is needed, stop that sub-flow and report it back here — the fix happens in the sim-rtl-flow main graph, then re-enters Vivado and HIL. @@ -183,9 +189,10 @@ keys with a default success — rehearsal only, and it refuses review keys. | `interfaces#n` | RTL↔SoC interface doc — register-config or data-stream only | write the doc under `work_dir` | | `review:functional_partition#n` | **ask the user** to sign off on the split + interfaces | — (relay to user) | | `module_plan#n` | split the RTL side into A/B/C…, give order + dependencies (**first pass only**) | inline | -| `native:#n` | nativize M into HDL-capable blocks, save `_M-E.slx`, generate RTL | request says `simulink-rtl-dev` — **not installed**; use MATLAB MCP + model-based-design-core | -| `ip_replace:#n` | swap in Xilinx IP, cycle-true compare vs MATLAB RTL | request says `rtl-ip-replace` — **not installed**; use `vivado-sim` + `vivado-mcp` | -| `modify_e:vivado#n` / `modify_e:hil#n` | **edit the named `-E.slx` and re-simulate** against sim-golden | MATLAB MCP + model-based-design-core | +| `native:#n` | nativize M into HDL-capable blocks, save `_M-E.slx`, generate RTL | `simulink_rtl_dev` | +| `ip_replace:#n` | swap in Xilinx IP, cycle-true compare vs MATLAB RTL | `rtl-ip-replace` | +| `modify_e:module#n` / `modify_e:vivado#n` / `modify_e:hil#n` | edit **only** the named `-E.slx` (cycle-true / timing / HIL driven) | `simulink_rtl_dev` | +| `sync_sim:#n` | carry that `-E` change into the matching `-x`/`-q` model and re-verify alignment | **`simulink-rtl-redev`** | | `vivado#n` | build the Vivado project, synth+impl+bitstream, check all timing | `vivado-prj-create`, `vivado-mcp` | | `hil:fpga-imx6-flow#n` / `hil:zynq-imx6-flow#n` | HIL debug, **read-only** | `fpga-imx6-flow` / `zynq-imx6-flow` | | `timing_rework#n` | fix non-Simulink RTL / SoC / project timing | inline | @@ -215,12 +222,17 @@ review: {"approved": true, "notes": "", "metadata": {}} Routing on `change_scope`: -- `simulink_module` → **`modify_e:*`** (edit `-E.slx` + re-simulate) → only the affected - modules re-run `native → ip_replace` → Vivado → HIL. +- `simulink_module` → **`modify_e:*`** (edit the `-E.slx`) → **`sync_sim:*`** (carry it + into `-x`/`-q`, re-verify) → only the affected modules re-run `native → ip_replace` → + Vivado → HIL. - `non_simulink_rtl` / `soc` / `hardware` → `timing_rework` (from Vivado) or `hil_rework` (from HIL) → Vivado. - The driver **rejects** `needs_code_change: true` with `change_scope: "none"`. +A failing `ip_replace:` (cycle-true mismatch) enters the same chain on its own: +`evaluate_modules → modify_e:module → sync_sim:module → native:`. A failing +`native:` does **not** — the `-E` isn't sound yet, so it just re-runs nativization. + ### 6. Parallel batches (verified) | stage | keys handed over at once | @@ -276,9 +288,14 @@ Global flags go **before** the subcommand: `--project`, `--work-dir`, `--thread- Mermaid can't show. Use `Notes/Result00.md` for the authoritative picture. - **`--max-attempts` must precede the subcommand** and stay the *same value* across `start` and every `resume` — it is runtime context, not persisted state. -- **The two skills the graph names don't exist.** `native:*` suggests `simulink-rtl-dev`, - `ip_replace:*` suggests `rtl-ip-replace`; neither is installed. Fall back to the MATLAB - MCP + `model-based-design-core:*` skills and `vivado-sim` / `vivado-mcp`. Don't abort. +- **The skill names the graph hands you are real and exact** — `simulink_rtl_dev` + (**underscores**, not dashes), `rtl-ip-replace`, `simulink-rtl-redev`. All three are + installed. Invoke the named one rather than improvising with MATLAB MCP directly: they + carry the project's own gates (`audit_hdl_blocks.m`, cycle-true compare, `check_sync.py`). +- **`sync_sim:*` is not optional bookkeeping.** It is the step that keeps the `-x`/`-q` + model telling the truth about the `-E` you just changed. Recording it as passed without + actually re-simulating means every later gate — Vivado, HIL, sim-golden — is validating + a model that no longer matches the RTL. - **`simulink_x`'s first artifact silently becomes sim-golden.** Record `artifacts: []` and the graph substitutes `-x.slx`. Always record the real filename. - **`module_plan` runs only on the first pass.** Rework rounds reuse the plan, so you diff --git a/simulink-rtl-redev/SKILL.md b/simulink-rtl-redev/SKILL.md new file mode 100644 index 0000000..f88769c --- /dev/null +++ b/simulink-rtl-redev/SKILL.md @@ -0,0 +1,237 @@ +--- +name: simulink-rtl-redev +description: > + 改动原生模型(xxx_h.slx)之后,如何保证仿真模型(xxx_hs.slx)同步、仿真结论仍然算数。 + 典型触发场景:Vivado 综合时序不满足要改流水、硬件调试发现问题要改 RTL、定点位宽调整。 + 核心:生成脚本不会把原生核的改动翻译成 m 代码,只改一侧 = RTL 已变而仿真还在验证旧行为。 + 提供两个工具:同步新鲜度闸门 check_sync.py(三条时序链,退出码即结论)、 + 延迟实测 measure_k.m(时域/频域/包络三个估计器互证,不一致返回 NaN 而非猜测值)。 + 另含改动分类、验证三步、环内/环外加流水的差别、六条禁令。 + 触发词:改了原生模型、同步仿真模型、xxx_h 和 xxx_hs 不一致、时序违例改流水、 + 硬件调试改RTL、仿真模型过时、等价闸门、cycle-true 同步、check_sync、measure_k、 + 测延迟、K 是多少、加流水延迟多少拍、原生模型 仿真模型 双轨、改完怎么验、 + makehdl 之前要做什么。 + 规范全文见工程 Notes/Task16-3.md。适用于任何"原生模型+仿真模型"双轨结构的工程。 +--- + +# simulink-rtl-redev — 原生模型改动后的仿真模型同步 + +## 这个 skill 解决什么 + +两个模型描述同一段算法: + +| 记号 | 名称 | 内容 | 用途 | +|---|---|---|---| +| `xxx_h.slx` | 原生模型(-e) | 目标模块全部用可综合原生块搭建 | **生成 RTL** | +| `xxx_hs.slx` | 仿真模型(-x/-q) | 由原生模型派生,跑不动的**原生核**换成等价的 **m 实现** | **跑场景仿真** | + +**核心原则:改了原生模型,必须同步改仿真模型,并重新验证两者行为一致。** + +原因:从原生模型派生仿真模型的脚本只做"摘掉原生核、换上 m 实现、接线", +**它不会把原生核的改动翻译成 m 代码**。只改原生模型再重跑生成脚本, +仿真模型不会跟着变——它只是把同一份未改动的 m 实现重新包一遍。 + +后果:**RTL 已经变了,而仿真还在验证旧行为**。板上出问题时仿真给不出线索, +因为它测的根本不是同一个设计。 + +## 什么时候需要手工同步 + +只看改动落在哪里: + +| 改动位置 | 是否需要手工同步 m 实现 | +|---|---| +| **原生核之内** | **需要**。这是两套实现的重叠区 | +| 原生核之外(其余链路) | 不需要。生成脚本原样继承,重生成即可 | + +## Agent 路径:先跑闸门 + +任何时候怀疑"仿真结论还算不算数",第一件事是跑同步新鲜度闸门。 + +### 一次性:写 manifest + +在工程里建一份 manifest(建议放 `Notes/sync_manifest_.json`), +声明这个模型的源与派生产物。**`root` 相对 manifest 所在目录**,其余路径相对 `root`: + +```json +{ + "model": "", + "root": "..", + + "native_model": "matlab//xxx_h.slx", + "build_scripts": ["matlab//build_core.m"], + "gen_scripts": ["matlab//make_sim.m"], + "mcode_impl": ["matlab//core_mcode.m"], + "runtime_deps": ["matlab//pscore.m", "matlab//coef.mat"], + + "sim_model": "matlab//xxx_hs.slx", + "rtl_source_model": "matlab//xxx_h__E.slx", + + "gate_result": "Notes/gate_.txt", + "gate_tools": ["matlab//gate.m", "matlab//cmp_core.m"] +} +``` + +`runtime_deps` 必填**运行时才从路径解析的 .m** 与运行时读取的数据文件 +(`.mat`、系数表、数据字典)。改它们时模型本身一个字节都不变,漏列等于没查。 +`rtl_source_model` 与 `gate_tools` 可省(省了就不查对应项)。 + +### 每次改完模型:跑闸门 + +``` +cd <工程根> +python "%USERPROFILE%\.claude\skills\simulink-rtl-redev\check_sync.py" Notes\sync_manifest_.json +``` + +Linux/macOS 把路径换成 `~/.claude/skills/simulink-rtl-redev/check_sync.py`。 + +**退出码即结论**:`0` 通过,`1` 有过时产物,`2` manifest 有问题(路径不存在等)。 +可直接串进脚本做前置门禁。 + +选项:`--quiet` 不打印文件清单,`--json` 输出机器可读结果。 + +它查三条时序链: + +``` +链 1(派生新鲜):max(源文件) < min(派生产物) +链 2(闸门新鲜):max(派生产物) < 闸门结果 +链 3(工具新鲜):max(闸门工具) < 闸门结果 +``` + +**链 1 是最容易漏的那条。** 只查"闸门晚于全部源文件"会放行下面这种情况: +闸门确实在改完源之后跑的,但仿真模型停留在改动之前。本工具在真实工程上正是这样 +抓出了一个已存在的漂移——链 2、链 3 都 PASS,只有链 1 FAIL。 + +## 同步流程 + +``` +[1] 改原生实现(改构建脚本,不直接手改 .slx) + | +[2] 把同样的改动落到 m 实现 <-- 漏掉这步是本 skill 存在的理由 + | +[3] 重新生成仿真模型 + | +[4] 若改动带来延迟变化:先实测新增拍数 K + | +[5] 跑等价闸门 --> 原生核 与 m 实现 是否一致 + | +[6] 跑短程冒烟 --> 链路是否还通 + | +[7] 跑全场景全长 --> 对照基线,指标是否达标 + | +[8] 从原生模型生成 RTL +``` + +任一步不过,回到 `[1]` 重走,不要从中间插入——仿真模型一旦重生成,后面的结果全部作废。 + +**`[4]` 必须在 `[5]` 之前**:延迟变了,闸门比对要带偏移,K 没测出来这一步做不了。 + +跑完 `[3]` 之后、进 `[5]` 之前,先用 `check_sync.py` 确认新鲜度,能省掉一次白跑的闸门。 + +## 三类改动的差别 + +| 类别 | 例 | m 实现要改吗 | 延迟 | 场景判据 | +|---|---|---|---|---| +| **不改延迟** | 资源共享、块重构、常量折叠 | 通常不用 | 不变 | 与基线**逐位相同** | +| **改延迟** | 插流水寄存器 | 补等量延迟 | +K | 吸收 K 后与基线**逐位相同** | +| **改定点** | 位宽、二进制点、饱和/舍入 | **必须改** | 不变 | 行为已变,**重新定性并建立新基线** | + +- **延迟必须实测,不能按设计意图推算。** 用 `measure_k.m`(下节),至少两种互不依赖的方法测出一致值。 +- **在反馈环内插流水,代价远大于环外。** 环外只是一个可被注入侧参数吸收的常数延迟; + 环内会改变环路群延迟,必须重新定标环路参数(步长、移位、门限),并先在单元台验证 + 阶跃响应再上全链。**能搬到环外就搬到环外。** + +## 实测新增延迟 K:`measure_k.m` + +流程 `[4]`。B 类(加流水)改动必须先测 K 才能进 `[5]`——闸门的判据本身要用 K 作偏移。 + +```matlab +addpath(fullfile(getenv('USERPROFILE'),'.claude','skills','simulink-rtl-redev')); +[K, R] = measure_k(ref, dut); % ref/dut 为实数或复数 IQ 向量,同采样率 +``` + +`ref` 是改动前(或参考抽头)的信号,`dut` 是改动后(滞后 K 拍)的信号。 +返回 `K` = 认定的整数延迟,**方法不一致时返回 `NaN`**,不给猜测值。 +`R` 里有各方法的分项结果。可选参数:`'MaxLag'`(4096)、`'OnsetTol'`(1e-3)、 +`'FracTol'`(0.05)、`'Verbose'`(true)。**只用基础 MATLAB**,不依赖 Signal Processing Toolbox。 + +三个估计器,前两个域完全不同、始终可用,第三个仅在信号有静默前导时适用: + +| 方法 | 域 | 说明 | +|---|---|---| +| 1 互相关 argmax | 时域 | 整数 | +| 2 互谱相位斜率 | **频域** | 分数;纯延迟使 `angle(Y·conj(X))` 对频率成直线,斜率即 −K | +| 3 起始点差 | 幅度包络 | 整数;阈值取各自**峰值**的比例,故 DUT 的噪声底不会误触发;连续激励信号判 `n/a` 而非失败 | + +判据:方法 1 与 2 必须一致;方法 3 适用时也必须一致;插值峰距整数不超过 `FracTol`。 + +**为什么非要两种。** 实测:两路**完全无关**的信号,相关器照样给出 `K = 241` +(|rho| 仅 0.0198 的假峰)——单靠它会把一个纯噪声峰当成延迟。方法 2 给出 −26.350, +两者互不相认,工具才判 FAIL。这就是"至少两种互不依赖的方法"的全部理由。 + +实测过的六种情形: + +| 情形 | 结果 | +|---|---| +| 干净整数延迟 40,复数 IQ | PASS,3 法一致 | +| 增益 0.37× + 全程噪声底 | PASS,2 法一致(方法 3 自动判 n/a) | +| 连续激励无静默段,延迟 17 | PASS,2 法一致 | +| **分数延迟 12.5 拍** | **FAIL**:方法 1 给 13、方法 2 给 12.500,且插值峰偏离整数 0.5 | +| **两路无关信号** | **FAIL**:241 vs −26.350 | +| 真延迟 40 + 3 阶非线性失真 | PASS,2 法一致(\|rho\| 0.989) | + +分数延迟那一条尤其要留意:它说明改动**没有引入干净的整数流水延迟**(碰到了分数内插/重采样 +路径)。这时不能四舍五入接着走,要回去查改的是不是环内。 + +## 等价闸门的语义(极易搞错) + +闸门比对的是**原生核与 m 实现这两套实现**,不是"新行为与旧行为"。因此: + +- 闸门对**所有类别**的改动都必须跑,判据都是"一致"; +- 即使行为相对上一版变了,只要两侧按同样意图改完,**两套实现对新行为的描述仍必须一致**; +- 改完两侧仍不一致,说明两边理解有分歧——这正是闸门要抓的。 + +**不要以"行为变了"为由跳过闸门。** 跳过它就等于放弃了对双实现是否同步的唯一检查。 + +闸门的局限:它只证明"在所用激励下一致",不证明全状态空间等价。 +状态机边界、饱和、复位时序这类角落必须补定向用例,不能指望随机激励覆盖到。 + +## 六条禁令 + +| # | 规矩 | 理由 | +|---|---|---| +| 1 | 仿真模型是**产物**,不能手改;要改就改生成脚本再重生成 | 手改必被下次重生成冲掉,git 上无法溯源 | +| 2 | 只改一侧的改动**视为未完成**,不得进入验证 | 两套实现已分叉 | +| 3 | 没跑过闸门,**不得跑场景仿真,也不得生成 RTL** | 前者测的可能是旧设计,后者错误直接进硅 | +| 4 | 旧结果**永不覆盖**,每次运行带唯一标签、落唯一目录 | 失败记录是"改动前后对比"的另一半;陈旧编译缓存会静默复现旧结果 | +| 5 | 判据不得为了通过而修改 | 一条带机理分析的 FAIL,价值高于一个来路不明的 PASS | +| 6 | 时序违例优先改模型,**禁止**用多周期豁免放宽慢角富余路径 | 布线器会吃满被放松的预算,表现为"报告通过、板上异常" | + +## Gotchas + +- **时间戳只是权宜之计。** `copy`/`touch`/版本签出都会扰动 mtime。 + `check_sync.py` 因此只能作自查,不是证明。更可靠的做法是让生成脚本与闸门自动记录 + 所用源码的校验和,运行前比对不符即拒绝开跑——那属于工具建设,本 skill 不含。 +- **"闸门比全部源文件新"不等于没问题。** 这正是链 1 存在的理由,见上文。 +- **`runtime_deps` 漏列 = 没查。** 运行时解析的 .m 和 `.mat`/系数表改动时, + 模型校验和纹丝不动。这类依赖必须显式声明,工具无法自动发现 + (`load('x.mat')` 这种字符串依赖追不到)。 +- **manifest 里每个声明的路径都必须存在**,否则 exit 2。这是故意的: + 路径写错而静默跳过,比报错危险得多。 +- **exit 2 与 exit 1 含义不同**:2 是 manifest 坏了(你的配置问题), + 1 是真的有过时产物(你的工程问题)。串脚本时别把两者混为一谈。 + +## Troubleshooting + +| 症状 | 原因 / 处置 | +|---|---| +| `ERROR: manifest refers to something that is missing -> : ` | 该路径不存在。exit 2。检查 `root` 与相对路径;`root` 是相对 manifest 所在目录的 | +| 链 1 FAIL | 派生产物早于源。**先**把原生核改动落到 m 实现,**再**重生成,**再**重跑闸门。只重生成不同步 m 实现无效 | +| 链 2 FAIL | 闸门跑在当前派生产物之前 —— 重跑闸门 | +| 链 3 FAIL | 闸门工具在记录的通过之后改过 —— 重跑闸门 | +| 三条链全 PASS 但板上仍与仿真不符 | 新鲜度没问题,问题在别处:查闸门覆盖(定向用例是否覆盖到出问题的状态),或查 `runtime_deps` 是否漏列 | +| `ERROR: manifest declares no sources / no derived products` | manifest 缺 `native_model` 一类的键,或值是空列表 | + +## 规范全文 + +本 skill 是 `Notes/Task16-3.md` 的可执行化。规范原文(含制定过程与十轮评审纠正的 +逻辑错误)见该文档;本 skill 只承载执行路径与工具。 diff --git a/simulink-rtl-redev/check_sync.py b/simulink-rtl-redev/check_sync.py new file mode 100644 index 0000000..c26f0a9 --- /dev/null +++ b/simulink-rtl-redev/check_sync.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Freshness gate for the native-model / sim-model dual-track flow (Task16-3 section 8). + + python check_sync.py [--json] [--quiet] + +Two models describe the same algorithm: + + xxx_h.slx native model -- the ONLY source RTL is generated from + xxx_hs.slx sim model -- derived from it, native core swapped for a + hand-written m implementation, runs the scenarios + +The m implementation is NOT derived from the native core: regenerating the sim +model just re-wraps the same unchanged m file. So a change to the native core +that is not mirrored into the m implementation leaves the RTL changed while the +simulation still validates the OLD behaviour -- silently. + +This tool checks the three ordering chains that catch that: + + chain 1 (derived fresh) : max(sources) < min(derived products) + chain 2 (gate fresh) : max(derived) < gate result + chain 3 (tools fresh) : max(gate tools) < gate result + +Chain 1 is the one people forget. "gate is newer than every source" alone +passes even when the sim model was built before the source changed. + +Exit code 0 = all chains hold, the sim evidence may be trusted. +Exit code 1 = a chain is broken (stale artefact) -- regenerate and re-run the gate. +Exit code 2 = manifest problem (missing file, bad JSON). + +Timestamps are a stopgap: copy/touch/checkout perturb them. The stronger form is +to have the generator and gate record checksums of the sources they used. This +tool is the cheap version that needs no changes to the existing scripts. +""" +import json +import os +import sys +from datetime import datetime + +# manifest key -> (role, which side of chain 1) +SOURCE_KEYS = [ + ('native_model', 'native model'), + ('build_scripts', 'build script'), + ('gen_scripts', 'generator script'), + ('mcode_impl', 'm implementation'), + ('runtime_deps', 'runtime dep (.m / .mat / config)'), +] +DERIVED_KEYS = [ + ('sim_model', 'sim model'), + ('rtl_source_model', 'RTL source model'), +] + + +def _as_list(v): + if v is None: + return [] + return v if isinstance(v, list) else [v] + + +def _resolve(root, p): + return p if os.path.isabs(p) else os.path.normpath(os.path.join(root, p)) + + +def collect(root, manifest, keys): + """-> list of (abspath, role, mtime) ; missing files raise.""" + out = [] + for key, role in keys: + for p in _as_list(manifest.get(key)): + ap = _resolve(root, p) + if not os.path.exists(ap): + raise FileNotFoundError(f'{key}: {ap}') + out.append((ap, role, os.path.getmtime(ap))) + return out + + +def ts(t): + return datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S') + + +def rel(root, p): + try: + return os.path.relpath(p, root) + except ValueError: + return p + + +def main(argv): + args = [a for a in argv if not a.startswith('--')] + flags = {a for a in argv if a.startswith('--')} + if not args: + sys.exit(__doc__) + + mf_path = os.path.abspath(args[0]) + try: + manifest = json.load(open(mf_path, encoding='utf-8-sig')) + except Exception as e: # noqa: BLE001 + print(f'ERROR: cannot read manifest {mf_path}: {e}') + return 2 + + # paths in the manifest are relative to "root", default = manifest's directory + root = manifest.get('root') or os.path.dirname(mf_path) + root = os.path.abspath(_resolve(os.path.dirname(mf_path), root)) + + try: + sources = collect(root, manifest, SOURCE_KEYS) + derived = collect(root, manifest, DERIVED_KEYS) + tools = collect(root, manifest, [('gate_tools', 'gate tool')]) + gate_p = manifest.get('gate_result') + if not gate_p: + raise KeyError('gate_result') + gate_ap = _resolve(root, gate_p) + if not os.path.exists(gate_ap): + raise FileNotFoundError(f'gate_result: {gate_ap}') + gate_t = os.path.getmtime(gate_ap) + except (FileNotFoundError, KeyError) as e: + print(f'ERROR: manifest refers to something that is missing -> {e}') + print(' every declared path must exist; fix the manifest or generate the file.') + return 2 + + if not sources: + print('ERROR: manifest declares no sources'); return 2 + if not derived: + print('ERROR: manifest declares no derived products'); return 2 + + newest_src = max(sources, key=lambda r: r[2]) + oldest_der = min(derived, key=lambda r: r[2]) + newest_der = max(derived, key=lambda r: r[2]) + newest_tool = max(tools, key=lambda r: r[2]) if tools else None + + chains = [ + ('1 derived fresh', newest_src[2] < oldest_der[2], + f'newest source {ts(newest_src[2])} {rel(root, newest_src[0])}', + f'oldest derived {ts(oldest_der[2])} {rel(root, oldest_der[0])}'), + ('2 gate fresh', newest_der[2] < gate_t, + f'newest derived {ts(newest_der[2])} {rel(root, newest_der[0])}', + f'gate result {ts(gate_t)} {rel(root, gate_ap)}'), + ] + if newest_tool: + chains.append(('3 tools fresh', newest_tool[2] < gate_t, + f'newest tool {ts(newest_tool[2])} {rel(root, newest_tool[0])}', + f'gate result {ts(gate_t)} {rel(root, gate_ap)}')) + + ok = all(c[1] for c in chains) + + if '--json' in flags: + print(json.dumps({ + 'model': manifest.get('model'), + 'ok': ok, + 'chains': [{'name': n, 'ok': o} for n, o, _, _ in chains], + 'gate_result_mtime': ts(gate_t), + }, ensure_ascii=False, indent=2)) + return 0 if ok else 1 + + print(f'check_sync: {manifest.get("model") or "(unnamed)"} root={root}') + if '--quiet' not in flags: + print('\n all declared files, oldest first:') + for ap, role, t in sorted(sources + derived + tools + [(gate_ap, 'gate result', gate_t)], + key=lambda r: r[2]): + print(f' {ts(t)} {role:<32} {rel(root, ap)}') + print() + for name, good, a, b in chains: + print(f' [{"PASS" if good else "FAIL"}] chain {name}') + print(f' {a}') + print(f' {b}') + if not good: + print(' -> the second is OLDER than the first, i.e. it is STALE') + print() + if ok: + print('VERDICT: PASS -- ordering holds; sim evidence may be trusted.') + else: + print('VERDICT: FAIL -- a stale artefact is in play.') + broken = {c[0][0] for c in chains if not c[1]} # '1' / '2' / '3' + if '1' in broken: + print(' chain 1: the derived model predates its source.') + print(' FIRST mirror any native-core change into the m implementation,') + print(' THEN regenerate the derived model(s), THEN re-run the gate.') + print(' Regenerating alone does NOT sync the m implementation.') + if '2' in broken: + print(' chain 2: the gate ran before the current derived model existed -- re-run the gate.') + if '3' in broken: + print(' chain 3: the gate tooling changed after the recorded pass -- re-run the gate.') + return 0 if ok else 1 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/simulink-rtl-redev/measure_k.m b/simulink-rtl-redev/measure_k.m new file mode 100644 index 0000000..d1da9e7 --- /dev/null +++ b/simulink-rtl-redev/measure_k.m @@ -0,0 +1,199 @@ +function [K, R] = measure_k(ref, dut, varargin) +%MEASURE_K Measure the integer delay K that lags , by TWO INDEPENDENT +% methods that must agree (Task16-3 section 5: "延迟必须实测,不能按设计意图推算. +% 用独立微台、至少两种互不依赖的方法测出一致值"). +% +% K = measure_k(ref, dut) +% [K, R] = measure_k(ref, dut, 'Name', value, ...) +% +% ref, dut : real or COMPLEX vectors, same sample rate. dut is the signal that +% comes out K samples later than ref. +% +% Options +% 'MaxLag' (default 4096) search window, in samples +% 'OnsetTol' (default 1e-3) onset threshold, RELATIVE to each signal's own +% RMS -- so the two signals may differ in gain +% 'FracTol' (default 0.05) how far the interpolated correlation peak may sit +% from an integer before K is declared non-integer +% 'Verbose' (default true) +% +% Returns +% K : the agreed integer delay, or NaN if the methods disagree +% R : struct with per-method results and the verdict -- +% .k_corr argmax of the normalised cross-correlation +% .k_onset difference of first-significant-sample indices +% .k_frac parabolic-interpolated peak (fractional) +% .frac_resid |k_frac - round(k_frac)| -- large => not a pure integer shift +% .rho |correlation| at the peak, 0..1 +% .agree k_corr == k_onset +% .pass agree && frac_resid <= FracTol +% +% WHY TWO METHODS: a correlation argmax alone will happily return a number for +% two unrelated signals. Onset detection is a structurally different estimator +% (amplitude threshold, not inner product), so agreement between them is real +% evidence. The fractional peak is the third check: it catches the case where +% the change did NOT introduce a clean integer pipeline delay (e.g. it altered a +% fractional interpolator), which an integer-only estimator would silently round. +% +% Base MATLAB only -- no Signal Processing Toolbox (no xcorr/finddelay). + +p = inputParser; +p.addParameter('MaxLag', 4096, @(x) isnumeric(x) && isscalar(x) && x > 0); +p.addParameter('OnsetTol', 1e-3, @(x) isnumeric(x) && isscalar(x) && x > 0); +p.addParameter('FracTol', 0.05, @(x) isnumeric(x) && isscalar(x) && x > 0); +p.addParameter('Verbose', true, @(x) islogical(x) || isnumeric(x)); +p.parse(varargin{:}); +o = p.Results; + +ref = ref(:); dut = dut(:); +n = min(numel(ref), numel(dut)); +if n < 16 + error('measure_k: need at least 16 samples, got %d', n); +end +ref = ref(1:n); dut = dut(1:n); + +maxlag = min(o.MaxLag, n - 1); + +% ---- method 1: normalised cross-correlation, argmax over 0..maxlag ----------- +% FFT-based so it stays base MATLAB and is fast for long vectors. +nfft = 2^nextpow2(2*n); +Rxy = ifft(fft(dut, nfft) .* conj(fft(ref, nfft))); % dut delayed by k => peak at k +c = abs(Rxy(1:maxlag+1)); +den = norm(ref) * norm(dut); +if den == 0 + error('measure_k: one of the signals is all zeros'); +end +c = c / den; +[rho, idx] = max(c); +k_corr = idx - 1; + +% ---- method 3 (on the same curve): parabolic interpolation of the peak ------- +% Done here because it needs c; it is reported separately and is NOT one of the +% "two independent methods" -- it only tests integer-ness. +if idx > 1 && idx < numel(c) + y0 = c(idx-1); y1 = c(idx); y2 = c(idx+1); + d = (y0 - y2) / (2*(y0 - 2*y1 + y2)); + if ~isfinite(d), d = 0; end + k_frac = k_corr + d; +else + k_frac = k_corr; +end +frac_resid = abs(k_frac - round(k_frac)); + +% ---- method 2: cross-spectral phase slope (FREQUENCY domain, always available) +% A pure delay of K samples makes angle(Y.*conj(X)) a straight line of slope -K +% against normalised frequency. Fitting that slope uses no argmax at all, so it +% is a genuinely different estimator from method 1 -- and it is fractional, so it +% doubles as an integer-ness check that cannot be fooled by rounding. +k_phase = local_phase_slope(ref, dut, nfft); + +% ---- method 3: onset of the first significant sample (only if one exists) ---- +% Threshold is a fraction of each signal's own PEAK envelope, not its RMS: a +% noise/quantisation floor in the DUT during the reference's silent lead-in must +% not trigger it. Returns NaN for continuously excited signals -- that is not a +% failure, it just means this method does not apply. +i_ref = local_onset(ref, o.OnsetTol); +i_dut = local_onset(dut, o.OnsetTol); +if isnan(i_ref) || isnan(i_dut) + k_onset = NaN; +else + k_onset = i_dut - i_ref; +end + +% ---- adjudication ----------------------------------------------------------- +% Require methods 1 and 2 (always available, different domains) to agree, and +% method 3 to agree as well WHEN it applies. +agree_phase = isfinite(k_phase) && abs(k_phase - k_corr) <= o.FracTol; +agree_onset = isnan(k_onset) || (k_onset == k_corr); % NaN = not applicable +n_methods = 2 + double(~isnan(k_onset)); +agree = agree_phase && agree_onset; +pass = agree && (frac_resid <= o.FracTol); +if pass, K = k_corr; else, K = NaN; end + +R = struct('k_corr', k_corr, 'k_phase', k_phase, 'k_onset', k_onset, ... + 'k_frac', k_frac, 'frac_resid', frac_resid, 'rho', rho, ... + 'agree_phase', agree_phase, 'agree_onset', agree_onset, ... + 'n_methods', n_methods, 'agree', agree, 'pass', pass, ... + 'n', n, 'maxlag', maxlag); + +if o.Verbose + fprintf('measure_k: n=%d maxlag=%d\n', n, maxlag); + fprintf(' method 1 xcorr argmax (time domain) K = %d |rho| = %.6f\n', k_corr, rho); + if isfinite(k_phase) + fprintf(' method 2 phase slope (freq domain) K = %.3f\n', k_phase); + else + fprintf(' method 2 phase slope (freq domain) K = n/a (not enough usable bins)\n'); + end + if isnan(k_onset) + fprintf(' method 3 onset diff (amplitude) K = n/a (no clear onset -- continuous signal)\n'); + else + fprintf(' method 3 onset diff (amplitude) K = %d\n', k_onset); + end + fprintf(' check interpolated peak %.3f (residual %.4f, tol %.4f)\n', ... + k_frac, frac_resid, o.FracTol); + if pass + fprintf(' VERDICT: PASS -- %d independent methods agree, K = %d\n', n_methods, K); + else + fprintf(' VERDICT: FAIL -- K NOT established\n'); + if ~agree_phase + fprintf(' methods 1 and 2 disagree (%d vs %.3f). Different domains giving\n', ... + k_corr, k_phase); + fprintf(' different answers means these are not a pure delayed copy of each\n'); + fprintf(' other -- check you compared the right taps.\n'); + end + if ~agree_onset + fprintf(' method 3 disagrees (%d vs %d). A noise/quantisation floor in the\n', ... + k_corr, k_onset); + fprintf(' DUT during the reference lead-in can do this: raise OnsetTol.\n'); + end + if frac_resid > o.FracTol + fprintf(' peak sits %.3f off an integer: the change did not introduce a\n', frac_resid); + fprintf(' clean integer pipeline delay (fractional/resampling path?).\n'); + end + fprintf(' Do NOT fall back to the design-intent value (Task16-3 section 5).\n'); + end +end +end + + +function kp = local_phase_slope(ref, dut, nfft) +%LOCAL_PHASE_SLOPE Delay from the slope of the cross-spectrum phase. +% Only bins where the reference has real energy are used, and the phase is +% unwrapped over those bins before a weighted least-squares line fit. +X = fft(ref, nfft); Y = fft(dut, nfft); +half = floor(nfft/2); +X = X(2:half); Y = Y(2:half); % drop DC and Nyquist +w = abs(X); +if isempty(w) || max(w) == 0, kp = NaN; return, end +keep = w > 0.10 * max(w); % usable bins only +if nnz(keep) < 16, kp = NaN; return, end +k = (1:numel(X)).'; +ph = unwrap(angle(Y(keep) .* conj(X(keep)))); +kk = k(keep); +ww = w(keep); +% weighted least squares: ph ~ a + b*(2*pi*kk/nfft), delay = -b +x = 2*pi*kk/nfft; +sw = sum(ww); +mx = sum(ww.*x)/sw; my = sum(ww.*ph)/sw; +b = sum(ww.*(x-mx).*(ph-my)) / sum(ww.*(x-mx).^2); +kp = -b; +end + + +function i0 = local_onset(x, tol) +%LOCAL_ONSET First index where the SMOOTHED envelope exceeds tol * its own PEAK. +% Threshold is relative to the peak, not the RMS, so a noise or quantisation +% floor well below the signal does not trigger it. Returns NaN when the signal +% is already at level from the first sample (continuously excited) -- that means +% "this method does not apply", not "failure". +a = abs(x); +w = 8; % short moving average +e = filter(ones(w,1)/w, 1, a); +pk = max(e); +if pk <= 0, i0 = NaN; return, end +thr = tol * pk; +i0 = find(e > thr, 1, 'first'); +if isempty(i0), i0 = NaN; return, end +% already hot at the very start => no usable onset +if i0 <= w, i0 = NaN; end +end diff --git a/simulink_rtl_dev/SKILL.md b/simulink_rtl_dev/SKILL.md new file mode 100644 index 0000000..499c62f --- /dev/null +++ b/simulink_rtl_dev/SKILL.md @@ -0,0 +1,154 @@ +--- +name: simulink_rtl_dev +description: > + Simulink→RTL 的设计规矩与闸门:凡是要生成 RTL 的子系统,它及其所有下级模块**必须用能直接 + 生成 RTL 的 Simulink 模块搭建,不允许用嵌入式 m 语言(MATLAB Function)实现**;确实非用 m 代码 + 不可的,必须**指定具体的 Xilinx IP 来替代,并通过仿真确认 cycle-true**,再登记豁免。提供审计脚本 + (audit_hdl_blocks.m 盘点 RTL 子树里的 m 代码块)、豁免闸门(check_exemptions.py)、原生模块查库 + (hdl_block_search.m)、HDL 兼容性检查(check_hdl_support.m)与 m 代码→原生模块对照表。 + 触发词:simulink 设计规范、要生成RTL的模块、MATLAB Function 能不能用、嵌入式m语言、HDL 子系统审计、 + m代码豁免、cycle-true 豁免、hdl_block_search、audit_hdl_blocks、makehdl 前检查、新建 Simulink 模块。 + 适用于任何 Simulink→RTL 工程。IP 替换与 cycle-true 对比由 rtl-ip-replace skill 执行。 +--- + +# simulink_rtl_dev — 要生成 RTL 的 Simulink 设计规矩 + +> 系统级 skill,适用于任何 Simulink→RTL 工程。 +> 本文中的模型名、子系统名一律是**占位示例**;各工程的具体规格、豁免登记与实测基线, +> 放在该工程自己的任务书与 Notes 下,不写进本 skill。 + +## 〇、本 skill 的安装位置 + +脚本位于: + +``` +~/.claude/skills/simulink_rtl_dev/scripts/ +``` + +Windows 上即 `%USERPROFILE%\.claude\skills\simulink_rtl_dev\scripts\`。 +下文所有 `addpath` 与 `python` 调用都按此路径给出。 + +## 一、两条铁律 + +1. **在 Simulink 设计中,如果明确指定某个模块是要生成 RTL 的,那么这个模块及下属的所有子模块, + 原则上都不允许使用嵌入式 m 语言的模块来实现,必须使用可以直接生成 RTL 的 Simulink 模块来搭建。** +2. **如果一定要使用嵌入式 m 语言来实现的模块,那么必须指定明确的 Xilinx 的 IP 模块来替代, + 并且在替换过程中一定要通过仿真来确认 cycle true。** + +推论(本 skill 的执行口径): + +- 铁律 1 的判定对象是**"要生成 RTL 的子系统"整棵子树**——`makehdl` 的目标以及它下面的一切, + 与之无关的仿真件(信道、PA 模型、激励源)不受此规矩约束。 +- 铁律 2 的"指定 Xilinx IP"不是口头指定:必须落到工程的豁免登记文件 + (建议 `Notes/_mcode_exemptions.json`),写明**哪个 IP**、**覆盖范围**、 + **cycle-true 证据文件**(`rtl-ip-replace` 的 `cmp_result.txt`,首行须为 PASS)、批准人与日期。 + 没有证据的豁免 = 违规。 +- 违规不是"警告",是**闸门**:`check_exemptions.py` 返回非 0 时不得 `makehdl`、不得进综合。 + +## 二、先确定"要生成 RTL 的子树" + +按优先级: + +1. 用户/任务书明确指定; +2. 工程脚本里的 `makehdl` 调用 —— `grep -rn "makehdl" matlab/**/*.m` + (形如 `makehdl('/','TargetLanguage','Verilog')`); +3. 模型参数 `hdlget_param(model,'HDLSubsystem')`; +4. 命名约定 `*_HDL` 子系统。 + +**不要**默认整个模型都是 RTL 目标。信道模型、功放模型、载波/延迟注入、激励源这类**纯仿真件** +天生允许用 m 代码,把它们算进审计范围只会制造假违规。 +接入本 skill 时,先在工程任务书里把"RTL 子树"与"纯仿真件"两份清单写清楚。 + +## 三、流程 A:新建 / 修改一个要生成 RTL 的模块(默认路径) + +1. **先查库,再动手**(MATLAB 里): + ```matlab + addpath(fullfile(getenv('USERPROFILE'),'.claude','skills','simulink_rtl_dev','scripts')); + hdl_block_search('lms') % 关键字搜 HDL Coder + DSP HDL + Wireless/Vision HDL 库 + ``` + 查到就必须用库里的模块;查不到再看 `reference/block_substitutions.md` 能不能用基础模块拼。 + **拼得出来就不许写 MATLAB Function。** +2. 搭好后跑 HDL 兼容性检查: + ```matlab + check_hdl_support('/', 'Out', 'hdl_support.txt') + ``` +3. 跑本 skill 的政策审计 + 闸门(见 §五)。 +4. 功能仿真复验(收敛/MER 等系统指标不劣化)→ `makehdl` → 交给 **rtl-ip-replace** 做 IP 化与 + cycle-true 对比 → 进综合。 + +## 四、流程 B:确实非 m 代码不可(铁律 2 的唯一合法出口) + +1. 写清**为什么原生模块搭不出来**(算法结构/时序/资源,不接受"太麻烦"); +2. **指定具体 Xilinx IP**(名字 + 版本 + 覆盖范围),而不是"以后再换"; +3. 用 **rtl-ip-replace** skill 完成替换,并跑双 DUT 逐拍对比,拿到 `cmp/cmp_result.txt` 首行 PASS; +4. 把上述内容登记进工程的豁免文件 + (格式见 `scripts/exemptions.example.json`,字段缺一不可); +5. 重跑闸门,必须显示 `[EXEMPT]` 且 `GATE: PASS`。 + +> 位级逐拍等价确实做不到时(典型:CORDIC 内部舍入),按 rtl-ip-replace 的**消费点契约**放宽, +> 但差异幅度与性能复验结论必须写进 Result 文档,并在豁免条目的 `evidence_kind` 里注明。 + +## 五、审计与闸门(每次改完模型、`makehdl` 之前跑) + +MATLAB 侧(经 matlab MCP 运行): +```matlab +addpath(fullfile(getenv('USERPROFILE'),'.claude','skills','simulink_rtl_dev','scripts')); +audit_hdl_blocks('/', ... + 'Exemptions','Notes/_mcode_exemptions.json', ... + 'Out','Notes/_hdl_audit.json'); +``` + +Shell 侧(闸门,退出码即结论): +``` +python "%USERPROFILE%\.claude\skills\simulink_rtl_dev\scripts\check_exemptions.py" ^ + Notes\_hdl_audit.json Notes\_mcode_exemptions.json +``` + +审计如何分级: + +| 级别 | 命中的东西 | 处理 | +|---|---|---| +| `VIOLATION` | MATLAB Function、Truth Table、MATLAB System、S-Function、`Fcn` | 必须改成原生模块,或走流程 B 豁免 | +| `EXEMPT` | 已登记且证据齐全的 m 代码块 | 放行(闸门仍会核对证据文件首行是否 PASS) | +| `WARN` | Stateflow Chart / State Transition Table | 允许,但**只能做控制**,不许放数据通路算术 | + +审计顺带给出迁移所需信息:脚本行数、是否有 `persistent`、是否有循环、输入输出端口数, +以及按关键字给出的**原生模块建议**(sin/cos→NCO、atan2→Complex to Magnitude-Angle、 +persistent 数组→RAM/Tapped Delay 等)。 + +MATLAB Function 块内部那个 ` SFunction ` 子块是实现细节,审计会自动折叠,不会重复报。 + +## 六、新工程接入本 skill + +1. 在工程任务书里写明**"RTL 子树"清单**与**"纯仿真件"清单**(§二); +2. 跑一次全量审计,把结果作为**基线**记进工程 Notes——**如实记录,不要粉饰**: + 历史欠账(早于本规矩写成的 m 代码块)照实列出,标明闸门当前是 BLOCKED 还是 PASS; +3. 对每个已有违规块,给出**原生模块出路**(查 `hdl_block_search` 与 `block_substitutions.md`) + 与**迁移优先级**——有现成库块的先换(如 LMS Filter、Farrow Rate Converter), + 最大最复杂的建议拆成几个子系统分批换; +4. 从此**新代码一律按铁律 1 办**,不许再往违规块里追加 m 代码; +5. 已完成的局部 IP 化可作为**部分豁免**依据,但 `ip_scope` 必须写清 + "IP 只覆盖这几处,其余 m 代码仍是欠账",不得整块标 PASS。 + +不在当前 `makehdl` 目标里的 m 代码块(如仅用于仿真的源),暂不受闸门约束; +哪天要给它生成 RTL,先跑一次本 skill 的审计。 + +## 七、脚本清单 + +| 文件 | 作用 | +|---|---| +| `scripts/audit_hdl_blocks.m` | 政策审计:遍历 RTL 子树,列出所有 m 代码块 + 迁移度量 + 原生模块建议,写 JSON;支持豁免文件 | +| `scripts/check_exemptions.py` | 铁律 2 闸门:每个 m 代码块必须有 IP 名 + cycle-true 证据(首行 PASS)+ 批准人/日期,否则退出码 1 | +| `scripts/hdl_block_search.m` | 在 HDL Coder / DSP HDL / Wireless HDL / Vision HDL 库里按关键字找可直接生成 RTL 的模块 | +| `scripts/check_hdl_support.m` | `checkhdl` 封装:抓出根本无法生成 RTL 的模块 | +| `scripts/exemptions.example.json` | 豁免登记模板(字段缺一不可) | +| `reference/block_substitutions.md` | m 代码惯用法 → 原生模块对照表(库名/块名在 R2025b 核对过) | + +## 八、与其它 skill 的分工 + +- **本 skill**:模型侧的规矩与闸门(不许写 m 代码;要写就得有 IP + cycle-true 证据)。 +- **rtl-ip-replace**:`makehdl` 之后 RTL 侧的 IP 化(ROM/RAM/CORDIC/FIFO/**定抽头**滤波器) + 与双 DUT cycle-true 对比 —— 铁律 2 的证据就是它产出的 `cmp_result.txt`。 +- **sim-rtl-flow**:把整条 Simulink→RTL→硬件的流程编排起来,本 skill 是其中"模块怎么搭"的一环。 +- 滤波器口径三者一致:**只有固定抽头的滤波器才做 IP 替换**;LMS/Farrow/运行时参考抽头的 + 相关器不替换 —— 但在模型侧,它们照样必须用原生模块(LMS Filter / Farrow Rate Converter)搭。 diff --git a/simulink_rtl_dev/reference/block_substitutions.md b/simulink_rtl_dev/reference/block_substitutions.md new file mode 100644 index 0000000..e98fdea --- /dev/null +++ b/simulink_rtl_dev/reference/block_substitutions.md @@ -0,0 +1,65 @@ +# m 代码惯用法 → 原生可生成 RTL 的 Simulink 模块 + +**用途**:Task12-1 铁律 1 要求"要生成 RTL 的模块及其所有子模块必须用能直接生成 RTL 的 Simulink +模块搭建"。下表是把 MATLAB Function 里的常见写法翻译成原生模块的对照表。 + +**库来源(本机 R2025b 实测存在)**: + +| 库 | `load_system` 名 | 内容 | +|---|---|---| +| HDL Coder 模块库 | `hdlsllib` | 基础集(241 个):延迟/RAM/FIFO/查找表/位操作/计数器/数学 | +| DSP HDL Toolbox | `dsphdlfiltering2` | Discrete FIR Filter、FIR Decimator/Interpolator/Rate Converter、CIC Decimator/Interpolator、Biquad、**LMS Filter**、Channelizer | +| | `dsphdlmathfun2` | **Complex to Magnitude-Angle**(CORDIC) | +| | `dsphdlxfrm2` | FFT、IFFT | +| | `dsphdlsigops2` | **NCO**、**Farrow Rate Converter**、Downsampler、Upsampler | +| Wireless HDL | `whdllib` | 调制、纠错编码、I/O | +| Vision HDL | `visionhdllib` | 图像流水线 | + +查库命令(skill 自带):`hdl_block_search('lms','farrow')` —— 关键字搜上述所有库。 + +## 对照表 + +| m 代码写法 | 原生模块 | 备注 | +|---|---|---| +| `persistent` 标量状态 | **Unit Delay** / **Delay** | 带复位/使能用 `Unit Delay Resettable Synchronous`、`Enabled Delay` | +| `persistent` 数组当延迟线 | **Tapped Delay** / `Delay`(Length>1) | 抽头要外露就用 Tapped Delay | +| `persistent` 数组当存储、按地址读写 | **Simple Dual Port RAM** / `Single Port RAM` / `Dual Port RAM` | 下游 `rtl-ip-replace` 会换成 `blk_mem_gen` | +| 常量表 `sinT/cosT` 查表相位旋转 | **NCO**(dsphdlsigops2)或 `Sine/Cosine HDL Optimized` | 表本身 → `Direct Lookup Table (n-D)` → 下游 `dist_mem_gen` | +| 任意常量表查表 | **Direct Lookup Table (n-D)** / `1-D Lookup Table` / `Prelookup` | 内容固定才算查找表 | +| `atan2(y,x)` / `abs(z)` 求角求模 | **Complex to Magnitude-Angle**(dsphdlmathfun2,CORDIC) | 下游可换 Xilinx `cordic` IP | +| `sqrt` | `Sqrt` / `Reciprocal Sqrt` | | +| 除法 / 倒数 | `Divide` / `Math Reciprocal` | 注意流水延迟,别在反馈环里放 | +| **定系数 FIR** | **Discrete FIR Filter** | 下游 `fir_compiler`(只有定系数才 IP 化) | +| 抽取 / 内插 FIR | **FIR Decimator** / **FIR Interpolator** / `FIR Rate Converter` | 大比例整数变速 → `CIC Decimator/Interpolator` | +| **LMS 自适应均衡**(EqCore 那种) | **LMS Filter**(dsphdlfiltering2) | 原生模块搭得出来 ⇒ 不需要豁免;IP 化不适用(抽头非常量) | +| **分数延时 / Farrow 重采样**(TxPreOffset 那种) | **Farrow Rate Converter**(dsphdlsigops2) | 同上:能用原生模块就不许写 m 代码 | +| FFT / IFFT | **FFT** / **IFFT**(dsphdlxfrm2) | | +| 计数器 `k = k+1; if k==N, k=0` | **HDL Counter** / `Counter Free-Running` / `Counter Limited` | | +| `if/elseif` 分支选值 | `Switch` / `Multiport Switch` / `Index Vector` | | +| `switch state` 控制状态机 | **Stateflow Chart(纯控制)** 或 `Switch Case Action Subsystem` | Chart 只放控制,**不许放数据通路算术**(审计报 WARN) | +| 饱和 / 量化 / 定点转换 | `Data Type Conversion`(saturate/round)、`Saturation` | 位宽与舍入方式在这里定死,便于逐拍对齐 | +| 移位、拼位、取位 | `Shift Arithmetic`、`Bit Concat`、`Bit Slice`、`Bit Set/Clear/Reduce` | | +| 复数乘加 | `Product`(复数信号)、`Multiply-Add`、`Multiply-Accumulate`、`Dot Product` | | +| 队列 / 跨速率缓冲 | **HDL FIFO** | 下游 `fifo_generator` | +| 相关器(参考抽头运行时捕获) | `Tapped Delay` + `Multiply-Accumulate` + RAM | 抽头非常量 ⇒ **不做 IP 替换**,但仍应用原生模块搭 | +| `max/min`、门限比较 | `MinMax`、`Compare To Constant`、`Relational Operator` | | +| 边沿检测 | `Detect Rise Positive` / `Detect Fall Negative` | | + +## 与 rtl-ip-replace 的对应关系 + +原生模块搭好 → `makehdl` 生成 RTL → `rtl-ip-replace` 再把其中四类结构换成 Xilinx IP: + +| 原生模块 | 生成的 RTL 结构 | 下游 IP | +|---|---|---| +| Direct Lookup Table (n-D) / 常量表 | 常量 wire 数组 | `dist_mem_gen` | +| Simple Dual Port RAM 等 | `SimpleDualPortRAM_generic` | `blk_mem_gen` | +| Complex to Magnitude-Angle | CORDIC 迭代锥 | `cordic` | +| HDL FIFO | reg 数组 + 指针 | `fifo_generator` | +| Discrete FIR Filter(**定系数**) | Coef 模块 + systolic 抽头链 | `fir_compiler` | +| LMS Filter / Farrow Rate Converter | 自适应/变系数结构 | **不替换**(抽头非常量) | + +## 判定口诀 + +1. 先 `hdl_block_search('关键字')`:库里有 → 必须用库里的,不许写 m 代码; +2. 库里没有、且能用基础模块(Delay/Product/Sum/Switch/Lookup/RAM)拼出来 → 拼出来; +3. 两者都不行 → 才轮到铁律 2:**指定具体的 Xilinx IP + 仿真证明 cycle-true + 登记豁免**。 diff --git a/simulink_rtl_dev/scripts/audit_hdl_blocks.m b/simulink_rtl_dev/scripts/audit_hdl_blocks.m new file mode 100644 index 0000000..0171ebd --- /dev/null +++ b/simulink_rtl_dev/scripts/audit_hdl_blocks.m @@ -0,0 +1,208 @@ +function rep = audit_hdl_blocks(target, varargin) +%AUDIT_HDL_BLOCKS Policy audit of an RTL-bound Simulink subtree (Task12-1). +% +% Rule 1: everything under a subsystem that is going to be turned into RTL must +% be built from Simulink blocks that generate RTL directly -- no embedded +% MATLAB (m) code. +% Rule 2: an m-code block is only acceptable if a specific Xilinx IP is named +% as its replacement AND the swap is proven cycle-true by simulation. Such +% blocks must be listed in the exemptions file, with evidence. +% +% rep = AUDIT_HDL_BLOCKS('model/HdlSubsystem') +% rep = AUDIT_HDL_BLOCKS(..., 'Exemptions', 'Notes/hdl_mcode_exemptions.json') +% rep = AUDIT_HDL_BLOCKS(..., 'Out', 'hdl_audit.json') +% +% The report (struct + JSON) lists every offending block with the metrics +% needed to plan its migration: script size, persistent state, loops, ports, +% and a suggested native-block replacement. +% +% Verdict: PASS no m-code in the subtree +% EXEMPT all m-code blocks are listed as exempted (evidence is +% checked separately by check_exemptions.py) +% VIOLATION at least one undocumented m-code block + +p = inputParser; +p.addParameter('Exemptions', ''); +p.addParameter('Out', ''); +p.parse(varargin{:}); +exFile = p.Results.Exemptions; +outFile = p.Results.Out; + +parts = strsplit(target, '/'); +model = parts{1}; +if ~bdIsLoaded(model) + load_system(model); +end + +opts = {'LookUnderMasks', 'all', 'FollowLinks', 'on'}; +blocks = find_system(target, opts{:}, 'Type', 'Block'); + +% ---- classification ------------------------------------------------------ +% VIOLATION: behaviour written in m code (or opaque to HDL Coder) +% WARN : Stateflow control constructs -- allowed for control only, must be +% justified; they are not the "embedded m language" this rule targets +% but they still hide code from review. +VIOL_SF = {'MATLAB Function', 'Truth Table'}; +WARN_SF = {'Chart', 'State Transition Table'}; +VIOL_BT = {'MATLABSystem', 'S-Function', 'M-S-Function', 'Fcn', 'MATLABFcn'}; + +exempt = local_read_exemptions(exFile); + +items = struct('block', {}, 'kind', {}, 'severity', {}, 'lines', {}, ... + 'persistent_state', {}, 'loops', {}, 'n_in', {}, 'n_out', {}, ... + 'suggestion', {}, 'exempt', {}, 'xilinx_ip', {}); + +% Stateflow blocks carry an internal ' SFunction ' child; report the owner, not +% its implementation detail. +sfOwners = {}; +for k = 1:numel(blocks) + if strcmp(get_param(blocks{k}, 'BlockType'), 'SubSystem') + try + t = get_param(blocks{k}, 'SFBlockType'); + if ~isempty(t) && ~strcmp(t, 'NONE'), sfOwners{end+1} = blocks{k}; end %#ok + catch + end + end +end + +for k = 1:numel(blocks) + blk = blocks{k}; + bt = get_param(blk, 'BlockType'); + sfType = ''; + if strcmp(bt, 'SubSystem') + try, sfType = get_param(blk, 'SFBlockType'); catch, sfType = ''; end + end + if isempty(sfType) && any(cellfun(@(o) startsWith(blk, [o '/']), sfOwners)) + continue % inside a Stateflow block already accounted for + end + + if any(strcmp(sfType, VIOL_SF)) + kind = sfType; sev = 'VIOLATION'; + elseif any(strcmp(sfType, WARN_SF)) + kind = sfType; sev = 'WARN'; + elseif any(strcmp(bt, VIOL_BT)) + kind = bt; sev = 'VIOLATION'; + else + continue + end + + [nl, hasPersist, hasLoop, script] = local_script_metrics(blk, kind); + ph = get_param(blk, 'PortHandles'); + isEx = any(strcmp(blk, {exempt.block})); + ip = ''; + if isEx + ip = exempt(strcmp(blk, {exempt.block})).xilinx_ip; + sev = 'EXEMPT'; + end + + items(end+1) = struct( ... + 'block', blk, 'kind', kind, 'severity', sev, 'lines', nl, ... + 'persistent_state', hasPersist, 'loops', hasLoop, ... + 'n_in', numel(ph.Inport), 'n_out', numel(ph.Outport), ... + 'suggestion', local_suggest(script), 'exempt', isEx, 'xilinx_ip', ip); %#ok +end + +nViol = sum(strcmp({items.severity}, 'VIOLATION')); +nWarn = sum(strcmp({items.severity}, 'WARN')); +nEx = sum(strcmp({items.severity}, 'EXEMPT')); +if nViol > 0 + verdict = 'VIOLATION'; +elseif nEx > 0 + verdict = 'EXEMPT'; +else + verdict = 'PASS'; +end + +rep = struct('target', target, 'model', model, ... + 'generated_at', datestr(now, 'yyyy-mm-dd HH:MM:SS'), ... + 'exemptions_file', exFile, 'n_blocks_scanned', numel(blocks), ... + 'n_violation', nViol, 'n_warn', nWarn, 'n_exempt', nEx, ... + 'verdict', verdict, 'items', {items}); + +% ---- report -------------------------------------------------------------- +fprintf('\n=== HDL m-code audit: %s ===\n', target); +fprintf('scanned %d block(s)\n', numel(blocks)); +for k = 1:numel(items) + it = items(k); + fprintf(' [%-9s] %-11s %s\n', it.severity, it.kind, it.block); + fprintf(' %d line(s), persistent=%d, loops=%d, %d in / %d out\n', ... + it.lines, it.persistent_state, it.loops, it.n_in, it.n_out); + if ~isempty(it.xilinx_ip) + fprintf(' exempt -> Xilinx IP: %s\n', it.xilinx_ip); + elseif ~isempty(it.suggestion) + fprintf(' suggest: %s\n', it.suggestion); + end +end +fprintf('VERDICT: %s (violation=%d warn=%d exempt=%d)\n\n', verdict, nViol, nWarn, nEx); + +if ~isempty(outFile) + fid = fopen(outFile, 'w'); + fwrite(fid, jsonencode(rep)); + fclose(fid); + fprintf('report -> %s\n', outFile); +end +end + +% ------------------------------------------------------------------------- +function ex = local_read_exemptions(f) +ex = struct('block', {}, 'xilinx_ip', {}); +if isempty(f) || ~isfile(f) + return +end +raw = jsondecode(fileread(f)); +if isfield(raw, 'exemptions') + raw = raw.exemptions; +end +if isstruct(raw) + raw = num2cell(raw); +end +for i = 1:numel(raw) + e = raw{i}; + ip = ''; + if isfield(e, 'xilinx_ip'), ip = e.xilinx_ip; end + ex(end+1) = struct('block', e.block, 'xilinx_ip', ip); %#ok +end +end + +function [nl, hasPersist, hasLoop, script] = local_script_metrics(blk, kind) +script = ''; +nl = 0; hasPersist = false; hasLoop = false; +if ~strcmp(kind, 'MATLAB Function') + return +end +try + c = sfroot().find('-isa', 'Stateflow.EMChart', 'Path', blk); + if isempty(c), return, end + script = c(1).Script; + nl = numel(strsplit(script, newline)); + hasPersist = ~isempty(regexp(script, '\', 'once')); + hasLoop = ~isempty(regexp(script, '^\s*(for|while)\>', 'once', 'lineanchors')); +catch +end +end + +function s = local_suggest(script) +%LOCAL_SUGGEST keyword -> native Simulink block hints (see reference/block_substitutions.md) +s = ''; +if isempty(script), return, end +hits = {}; +map = { ... + '\<(sin|cos)\s*\(', 'Sine/Cosine: NCO HDL Optimized or Direct Lookup Table (n-D)'; ... + '\', 'control FSM: Stateflow Chart (control only, no datapath math)'; ... + '\ + end +end +if isempty(hits) + s = 'no keyword match -- rebuild from primitives (Delay/Product/Sum/Switch/Lookup Table)'; +else + s = strjoin(hits, ' | '); +end +end diff --git a/simulink_rtl_dev/scripts/check_exemptions.py b/simulink_rtl_dev/scripts/check_exemptions.py new file mode 100644 index 0000000..2a0ad84 --- /dev/null +++ b/simulink_rtl_dev/scripts/check_exemptions.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Gate for rule 2 of Task12-1: an m-code block inside an RTL-bound subtree is +acceptable ONLY if a specific Xilinx IP is named as its replacement AND the swap +is proven cycle-true by simulation. + + python check_exemptions.py [] + +Exit code 0 = the subtree may go to makehdl / synthesis: + every m-code block is either gone, or exempted with (a) a named Xilinx IP and + (b) an evidence file whose first line is PASS (rtl-ip-replace cmp_result.txt). +Exit code 1 = blocked, with the reason printed per block. + +Evidence paths are resolved relative to the repo root (the directory containing +Notes/), or absolutely. +""" +import json, os, sys + +REQUIRED = ('block', 'reason', 'xilinx_ip', 'cycle_true_evidence', 'approved_by', 'date') + + +def repo_root(start): + d = os.path.abspath(start) + while True: + if os.path.isdir(os.path.join(d, 'Notes')) or os.path.isdir(os.path.join(d, '.git')): + return d + nd = os.path.dirname(d) + if nd == d: + return os.path.abspath(start) + d = nd + + +def evidence_ok(path, roots): + tried = [] + for r in ([''] if os.path.isabs(path) else roots): + p = path if os.path.isabs(path) else os.path.join(r, path) + tried.append(p) + if os.path.isfile(p): + first = open(p, errors='replace').readline().strip() + if not first.upper().startswith('PASS'): + return False, f'evidence is not a PASS: "{first}" ({p})' + return True, first + return False, 'evidence file not found: ' + ' | '.join(tried) + + +def main(): + argv = [a for a in sys.argv[1:] if not a.startswith('--root=')] + root_opt = [a.split('=', 1)[1] for a in sys.argv[1:] if a.startswith('--root=')] + if not argv: + sys.exit(__doc__) + audit = json.load(open(argv[0], encoding='utf-8-sig')) + exf = argv[1] if len(argv) > 1 else audit.get('exemptions_file') or '' + roots = [r for r in (root_opt + [repo_root(os.getcwd()), + repo_root(os.path.dirname(os.path.abspath(argv[0])) or '.'), + os.getcwd()]) if r] + + items = audit.get('items') or [] + if isinstance(items, dict): + items = [items] + mcode = [i for i in items if i['severity'] in ('VIOLATION', 'EXEMPT')] + warns = [i for i in items if i['severity'] == 'WARN'] + + ex = [] + if exf and os.path.isfile(exf): + raw = json.load(open(exf, encoding='utf-8-sig')) + ex = raw.get('exemptions', raw if isinstance(raw, list) else []) + by_block = {e.get('block'): e for e in ex} + + print(f"target : {audit.get('target')}") + print(f"root : {roots[0]}") + print(f"audit : {len(items)} flagged block(s) " + f"({len(mcode)} m-code, {len(warns)} stateflow-warn)") + print(f"exempt : {exf or '(none)'}\n") + + problems = [] + for it in mcode: + blk = it['block'] + e = by_block.get(blk) + if not e: + problems.append(f"{blk}\n m-code with no exemption entry -- rebuild it from " + f"native Simulink blocks, or file an exemption (rule 2).") + print(f" [BLOCKED ] {blk}") + continue + missing = [k for k in REQUIRED if not str(e.get(k, '')).strip()] + if missing: + problems.append(f"{blk}\n exemption is incomplete: missing {', '.join(missing)}") + print(f" [BLOCKED ] {blk} (incomplete exemption)") + continue + ok, info = evidence_ok(e['cycle_true_evidence'], roots) + if not ok: + problems.append(f"{blk}\n {info}") + print(f" [BLOCKED ] {blk} ({info})") + continue + print(f" [EXEMPT ] {blk}\n IP: {e['xilinx_ip']}\n" + f" cycle-true: {info} [{e['cycle_true_evidence']}]") + + for it in warns: + print(f" [WARN ] {it['block']} ({it['kind']}) -- control-only Stateflow is tolerated; " + f"no datapath arithmetic in it") + + stale = [b for b in by_block if b not in {i['block'] for i in items}] + for b in stale: + print(f" [STALE ] exemption for a block that is no longer m-code (or gone): {b}") + + if problems: + print(f'\nGATE: BLOCKED -- {len(problems)} problem(s)\n') + for p in problems: + print(' * ' + p) + print('\nRule (Task12-1): an RTL-bound subsystem and everything under it must be built ' + 'from Simulink blocks that generate RTL directly; embedded m code is allowed only ' + 'against a named Xilinx IP, proven cycle-true.') + sys.exit(1) + print('\nGATE: PASS -- no undocumented m code in the RTL-bound subtree') + + +if __name__ == '__main__': + main() diff --git a/simulink_rtl_dev/scripts/check_hdl_support.m b/simulink_rtl_dev/scripts/check_hdl_support.m new file mode 100644 index 0000000..b0318ed --- /dev/null +++ b/simulink_rtl_dev/scripts/check_hdl_support.m @@ -0,0 +1,64 @@ +function ok = check_hdl_support(target, varargin) +%CHECK_HDL_SUPPORT HDL Coder compatibility check for an RTL-bound subsystem. +% +% ok = CHECK_HDL_SUPPORT('model/HdlSubsystem') +% ok = CHECK_HDL_SUPPORT(..., 'Out', 'hdl_support.txt') +% +% Wraps checkhdl: catches blocks that cannot become RTL at all -- the other +% half of Task12-1 rule 1 ("must be built from blocks that generate RTL +% directly"). The m-code policy audit is audit_hdl_blocks.m; run both. + +p = inputParser; +p.addParameter('Out', ''); +p.parse(varargin{:}); + +model = strtok(target, '/'); +if ~bdIsLoaded(model) + load_system(model); +end + +txt = ''; +threw = false; +try + txt = evalc('checkhdl(target)'); +catch e + threw = true; + txt = sprintf('checkhdl threw: %s\n%s', e.identifier, e.message); +end + +% The summary line is localised ("0 个错误、2 个警告" / "0 errors, 2 warnings"), +% so count both spellings rather than grepping for the word "error". +nErr = local_count(txt, {'(\d+)\s*个错误', '(\d+)\s+errors?'}); +nWarn = local_count(txt, {'(\d+)\s*个警告', '(\d+)\s+warnings?'}); +ok = ~threw && nErr == 0; + +rep = regexp(txt, '(?<=file:///)[^''"<>]*_report\.html', 'match', 'once'); +fprintf('=== checkhdl %s : %s (%d error(s), %d warning(s)) ===\n', ... + target, string(ok), nErr, nWarn); +if ~isempty(rep) + fprintf('report: %s\n', strrep(rep, '/', filesep)); +end +disp(txt); +if nWarn > 0 + fprintf(['NOTE: warnings are not a pass/fail gate here, but read them -- they are usually\n' ... + ' "this block will cost you area/latency in RTL" hints.\n']); +end + +if ~isempty(p.Results.Out) + fid = fopen(p.Results.Out, 'w'); + fwrite(fid, txt); + fclose(fid); + fprintf('checkhdl log -> %s\n', p.Results.Out); +end +end + +function n = local_count(txt, pats) +n = 0; +for i = 1:numel(pats) + t = regexp(txt, pats{i}, 'tokens', 'once'); + if ~isempty(t) + n = str2double(t{1}); + return + end +end +end diff --git a/simulink_rtl_dev/scripts/exemptions.example.json b/simulink_rtl_dev/scripts/exemptions.example.json new file mode 100644 index 0000000..8ecc75e --- /dev/null +++ b/simulink_rtl_dev/scripts/exemptions.example.json @@ -0,0 +1,17 @@ +{ + "_README": "Rule 2 of Task12-1: m code inside an RTL-bound subtree is only acceptable against a NAMED Xilinx IP, proven cycle-true. Copy this file to Notes/_mcode_exemptions.json and fill it in. Every field is mandatory; check_exemptions.py rejects the entry otherwise.", + "model": "dpd_ntn_05", + "hdl_target": "dpd_ntn_05/DpdAlign_HDL", + "exemptions": [ + { + "block": "dpd_ntn_05/DpdAlign_HDL/RxAlign_HDL/RxAlignCore", + "reason": "为什么原生 Simulink 模块搭不出来(必须具体:算法结构/时序/资源,不接受“太麻烦”)", + "xilinx_ip": "cordic 6.0 (Arc_Tan) + dist_mem_gen sin/cos ROM + blk_mem_gen SDP", + "ip_scope": "IP 覆盖的范围;剩下仍由 m 代码实现的部分要写清楚,它们仍然是欠账", + "cycle_true_evidence": "vivado/rtl_ip/dpd_ntn_05/cmp/cmp_result.txt", + "evidence_kind": "rtl-ip-replace 双 DUT 逐拍对比(首行须为 PASS)", + "approved_by": "bowei", + "date": "2026-07-24" + } + ] +} diff --git a/simulink_rtl_dev/scripts/hdl_block_search.m b/simulink_rtl_dev/scripts/hdl_block_search.m new file mode 100644 index 0000000..4b62bb4 --- /dev/null +++ b/simulink_rtl_dev/scripts/hdl_block_search.m @@ -0,0 +1,50 @@ +function hits = hdl_block_search(varargin) +%HDL_BLOCK_SEARCH Find a native, RTL-generatable Simulink block (Task12-1 rule 1). +% +% hdl_block_search list every block in the HDL Coder library +% hdl_block_search('ram') blocks whose name matches a keyword +% hdl_block_search('sin','cos') several keywords (OR) +% +% Source of truth is the HDL Coder block library (hdlsllib) plus the HDL +% Optimized blocks of DSP HDL / Wireless HDL / Vision HDL: anything in there +% generates RTL directly, so it is a legal building block. Anything NOT in +% there needs checkhdl before you rely on it. + +% Sub-libraries are listed individually: the top-level DSP HDL library holds +% links, which find_system will not walk into. +libs = {'hdlsllib', ... % HDL Coder baseline set + 'dsphdlfiltering2', 'dsphdlmathfun2', ... % FIR/CIC/LMS/Biquad, CORDIC + 'dsphdlxfrm2', 'dsphdlsigops2', 'dsphdlsrcs2', ...% FFT/IFFT, Farrow/NCO/resample + 'whdllib', 'visionhdllib'}; +hits = {}; +for i = 1:numel(libs) + try + load_system(libs{i}); + catch + continue + end + blks = find_system(libs{i}, 'SearchDepth', 5, 'Type', 'Block'); + for k = 1:numel(blks) + nm = strrep(get_param(blks{k}, 'Name'), newline, ' '); + if isempty(varargin) || any(cellfun(@(p) ~isempty(regexpi(nm, p, 'once')), varargin)) + hits{end+1, 1} = nm; %#ok + hits{end, 2} = blks{k}; %#ok + end + end +end + +if isempty(hits) + fprintf(['no HDL library block matches %s\n' ... + '-> if you cannot build it from these blocks, rule 2 applies: name the Xilinx IP\n' ... + ' that will implement it, prove the swap cycle-true, and file the exemption.\n'], ... + strjoin(varargin, ', ')); + return +end + +[~, ia] = unique(hits(:, 1)); +hits = hits(sort(ia), :); +fprintf('%d HDL-generatable block(s):\n', size(hits, 1)); +for k = 1:size(hits, 1) + fprintf(' %-42s %s\n', hits{k, 1}, hits{k, 2}); +end +end