sync: SHANGHAI-U9 2026-07-27 三个 Simulink→RTL skill 转系统级 + 新增 simulink-rtl-redev

- simulink_rtl_dev / rtl-ip-replace: 从工程级 .claude/skills 迁到用户级并去工程耦合
  (模型名/Notes 文档依赖/工程实测基线移出; addpath 与脚本路径改用户级)
- rtl-ip-replace/scripts/gen_cmp_tb.py: Vivado 路径不再写死, 改读 VIVADO_PATH /
  XILINX_VIVADO (兼容 install root / bin / bin\vivado.bat 三种写法), 回落时提示
  去设 skills_env.md 的 VIVADO_PATH
- simulink-rtl-redev: 新增。原生模型改动后的仿真模型同步规范, 含两个实测过的工具
  check_sync.py (三条时序链新鲜度闸门) 与 measure_k.m (时域/频域/包络三估计器互证)
- sim-rtl-flow/SKILL.md: 加入 sync_sim:* 步骤, 引用上述三个 skill
This commit is contained in:
bowei
2026-07-27 10:39:53 +08:00
parent 6dddab8923
commit ea4541ee90
23 changed files with 3171 additions and 12 deletions
+217
View File
@@ -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/<model>/(含 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。
换机器/换版本只需改环境变量。
**实施对象(`<model>`)不绑定任何默认值,必须由用户在每次调用时明确指定**
(如 "在 `<model>` 上实施");未指定时先问用户,不得自行猜测。
"查找表"指**固定内容**的表(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/<model>/`。
**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_<YYYYMMDD_HHMMSS>/`
子目录,然后全新生成(`backup_outdir.py` 完成,backup_* 目录本身不重复嵌套)。
5. 接入综合前 lint:`.xpr` 中不得再有指向 hdlsrc 的**综合**源(检查:grep hdlsrc *.xpr)。
## 适用范围(HDL Coder 25.2 生成代码的习惯用法,实测归纳)
| 结构 | 生成代码特征 | 目标 IP | cycle-true 约束 |
|---|---|---|---|
| 常量查找表 (ROM) | `wire signed [W-1:0] <tbl> [0:D-1];` + D 条 `assign <tbl>[i] = W'sb...;` + 组合读 `<tbl>[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/<model>`),
`OUT = vivado/rtl_ip/<model>`(固定目录,见上节规定),脚本在 `~/.claude/skills/rtl-ip-replace/scripts/`。
### 0. 备份(强制第一步)
```
python scripts/backup_outdir.py <OUT>
```
备份后的 `backup_<ts>/` 同时含**上次的原始 MATLAB RTL**(`src_orig/`)与**上次改写后的 RTL**,
即下一步增量判定的基线。
### 0.5 增量判定(Task12 规定 13~16;要全量复核时加 `--full`)
```
python scripts/incr_plan.py plan <HDLSRC> <OUT> --apply
```
- 把新 hdlsrc 与基线 `src_orig/` 逐文件比 sha1:`same` 的文件**直接取回上次改写后的 .v**;
- 打印 `ALL MATLAB sources unchanged` 时,**第 2 步(transform)整步可跳过**;
- 同时把本次源码快照写进 `<OUT>/src_orig/`(下次的基线),计划落 `<OUT>/incr_plan.json`。
### 1. 扫描盘点
```
python scripts/rtl_ip_scan.py <HDLSRC> <OUT>
```
manifest 落在 `<OUT>/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(另存到 <OUT>,不动原始文件)
```
python scripts/initrom_to_ip.py <HDLSRC> <OUT> # isempty-init 表(sinT/cosT/atanT)
python scripts/rom_to_ip.py <HDLSRC> <OUT> # 剩余纯常量 ROM
python scripts/ram_to_ip.py <HDLSRC> <OUT> # SimpleDualPortRAM → blk_mem_gen
python scripts/fir_to_ip.py <HDLSRC> <OUT> [--rate N ...] # 定系数 FIR → .coe + fir_compiler 登记
# + FIR_PROPOSAL_*.md(不改 RTL)
#(CORDIC/FIFO/滤波器手术引导式手改 <OUT> 里的副本,见下方契约;所需 IP 手工追加进 ip_manifest)
python scripts/namespace_ip.py <OUT> # 最后统一加 _IP 后缀(幂等)
```
产物:改写后 `*.v`、去重 `*.coe`、`ip_manifest.json/.tcl`、`PROVENANCE.log`、`src_orig/`(自动)。
### 2.5 增量结算(决定第 3、4 步能不能省)
```
python scripts/incr_plan.py finalize <HDLSRC> <OUT>
```
生成 `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 <OUT>/ip_manifest.tcl <OUT>/ip
```
### 4. cycle-true 对比仿真(闸门,不 PASS 不准上综合) — `sim_inherit: YES` 时跳过
```
python scripts/gen_cmp_tb.py <top_module> <HDLSRC> --rtl-ip <OUT>
<OUT>/cmp/run_cmp.bat
```
- 双 DUT:原始 `<top>`(从 HDLSRC 编译,仅用于仿真)vs `<top>_IP`(从 <OUT> 编译);
复位释放后逐拍比较全部输出;CORDIC 输出用 `--compare-at OUT=done信号` 限定提交拍。
- 激励:默认 LFSR;控制密集顶层优先复用 HDL Coder `<top>_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 <OUT>/ip`)。
- 判据:`cmp_result.txt` 首行 **PASS(0 失配)**;FAIL 时禁止接入综合。
### 5. 接入综合
- 工程源:`<OUT>/*.v` + `<OUT>/ip/**/*.xci`,顶层实例化 `<top>_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 化从根上解决。接入后优先扫这一类。
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Step 0 of every rtl-ip-replace run (MANDATORY, user directive 2026-07-23):
move ALL existing content of the fixed output dir into backup_<timestamp>/,
so the run always generates everything fresh while keeping full history.
Usage: python backup_outdir.py <outdir>
Existing backup_* subdirectories are kept in place (not re-nested).
"""
import os, shutil, sys, time
def main():
if len(sys.argv) != 2:
sys.exit('usage: backup_outdir.py <outdir>')
d = os.path.abspath(sys.argv[1])
os.makedirs(d, exist_ok=True)
items = [x for x in os.listdir(d) if not x.startswith('backup_')]
if not items:
print('nothing to back up in', d)
return
bdir = os.path.join(d, 'backup_' + time.strftime('%Y%m%d_%H%M%S'))
os.makedirs(bdir)
for x in items:
shutil.move(os.path.join(d, x), os.path.join(bdir, x))
print(f'backed up {len(items)} item(s) -> {bdir}')
if __name__ == '__main__':
main()
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Filters -> Xilinx FIR Compiler (Task12 §六, requirement 4b).
GUIDED, by design: this script does the mechanical half (pull the integer taps
out of the HDL Coder coefficient module, write the .coe, register the IP in
ip_manifest, and write a surgery proposal with the exact latency-alignment
arithmetic). It NEVER rewrites the RTL by itself -- the structural surgery and
the delay alignment are reviewed with the user first (Task12 §6.3).
python fir_to_ip.py <HDLSRC> <OUT> [--manifest m.json]
[--only MODULE ...] [--filter-type Single_Rate|Interpolation|Decimation]
[--rate N] [--data-width W --data-frac F] [--channels N] [--paths N]
[--reload] # switching between SEVERAL FIXED coefficient
# sets; NOT an escape hatch for adaptive taps
[--no-aclken]
Outputs in <OUT>:
fir_<module>_<hash>.coe integer taps, radix 10 (no re-quantisation by the IP)
ip_manifest.json/.tcl += {type fir_compiler ...}
FIR_PROPOSAL_<module>.md what to cut, what to wire, how to align the latency
"""
import argparse, hashlib, json, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def coe_path(outdir, module, coeffs, width, frac):
h = hashlib.sha1((f'{width}/{frac}:' + ','.join(map(str, coeffs))).encode()).hexdigest()[:10]
name = f'fir_{module}_{h}'
p = os.path.join(outdir, name + '.coe')
with open(p, 'w') as f: # ASCII only: .coe is read by Vivado, not by people
f.write('; rtl-ip-replace: taps copied verbatim from the HDL Coder constant module\n')
f.write('; (Task12 6.2.1 -- integer coefficients, the IP must not re-quantise)\n')
f.write('radix = 10;\ncoefdata =\n')
f.write(',\n'.join(str(c) for c in coeffs) + ';\n')
return name, p
PROPOSAL = """# FIR IP 替换方案 — `{module}` (Task12 §六,引导式,待确认)
来源:`{file}` Source Path:`{src}`
IP:`{ipname}`(fir_compiler 7.2) — 系数 `.coe`:`{coe}`
## 1. 系数(契约 §6.2.1)
- 抽头数 **{taps}**,系数位宽 **{cw}**,小数位 **En{cf}**,对称:{sym}
- `.coe` 为**整数**系数、逐个抄自 `{coef_signal}_i` 常量,IP 侧 `Quantization=Integer_Coefficients`,
不做二次量化。改系数 = 改 .coe + 重生成 IP。
## 2. IP 配置(gen_ip.tcl 已按此写入)
| 项 | 值 | 理由 |
|---|---|---|
| Filter_Type | {ftype} | {rate_note} |
| Data_Width | {dw}(原 RTL 二进制点 En{df}) | 位宽照抄;**小数位一律填 0** |
| Coefficient_Width | {cw}(原 En{cf}) | 同上 |
| Output_Rounding_Mode | Full_Precision | 契约 §6.2.2:截位放到 IP 外面做 |
| RateSpecification / SamplePeriod | Input_Sample_Period / 1 | 禁止硬件折叠,延迟才是确定值 |
| Has_ACLKEN | {aclken} | 用 `enb` 门控,与原 RTL 的使能节奏逐拍一致 |
| Coefficient_Reload | {reload} | {reload_note} |
| M_DATA_Has_TREADY / S_DATA_Has_FIFO | false / false | 纯流水,不引入可变延迟 |
**二进制点填 0 的理由**:core 做的是整数 MAC,二进制点只影响 GUI 显示;填 0 既与生成 RTL 的
整数运算完全一致,又避开 `Data_Fractional_Bits` 的合法区间(0..16)限制。
**IP 硬限制(2026-07-24 在 Vivado 2018.1 实测)**:`Coefficient_Width∈[8,49]`、
`Data_Width∈[2,47]`、`Data_Fractional_Bits∈[0,16]`。本例:系数 {cw} 位、数据 {dw} 位 → **{limits}**。
超限时不得硬塞 IP,按 §一.12 双向调整原则回 Simulink 重新量化(HDL Coder 的 full-precision
51 位抽头本来就没有工程意义),并复验性能。
## 3. 手术步骤(需人工确认后执行)
1. 在 `<OUT>/{file}` 中圈出滤波器边界:输入 `{din}`、输出 `{dout}`、有效 `{valid}`;
复数({cplx})时每个实部/虚部各一路 IP(或 Number_Paths=2 走同一套系数)。
2. 例化 `{ipname}`:`aclk=clk`、`aclken=enb`、`s_axis_data_tvalid=<原输入有效拍>`、
`s_axis_data_tdata=<输入,符号扩展到 8 的倍数位>`。
3. **延迟对齐(契约 §6.2.3,最容易翻车的一步)**:
- 量 IP 延迟 L:综合后看 `m_axis_data_tvalid` 相对 `s_axis_data_tvalid` 的拍数
(或 xsim 打一个脉冲量);
- 量原 RTL 延迟 L0:数原来的 delay line + 乘加流水寄存器;
- **L ≤ L0**:在 IP 输出后补 `L0-L` 拍移位寄存器;
- **L > L0**:不许偷偷改节奏——按 §一.12 双向调整原则改 Simulink 源(整体延迟增加),
并用模型仿真复验 MER/收敛不劣化,结论写进 Result 文档。
4. 截位:把原 RTL 对乘加结果做的右移/饱和原样接到 `m_axis_data_tdata` 之后
(IP 输出是 Full Precision,位宽 = {out_width_hint})。
5. `m_axis_data_tvalid` 的翻转拍必须与原输出有效拍逐拍一致(契约 §6.2.4)。
## 4. 验收
`gen_cmp_tb.py` 双 DUT 逐拍对比,对齐后 **0 失配**方为 PASS(契约 §6.2.5);
不 PASS 不得接入综合(§三)。
"""
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc'); ap.add_argument('outdir')
ap.add_argument('--manifest')
ap.add_argument('--only', nargs='*', default=None, help='limit to these module names')
ap.add_argument('--filter-type', default=None,
choices=['Single_Rate', 'Interpolation', 'Decimation'])
ap.add_argument('--rate', type=int, default=1)
ap.add_argument('--data-width', type=int, default=None)
ap.add_argument('--data-frac', type=int, default=None)
ap.add_argument('--channels', type=int, default=1)
ap.add_argument('--paths', type=int, default=1)
ap.add_argument('--reload', action='store_true', help='adaptive taps via Coefficient_Reload')
ap.add_argument('--no-aclken', action='store_true')
a = ap.parse_args()
out = os.path.abspath(a.outdir)
man = json.load(open(a.manifest or os.path.join(out, 'manifest.json')))
filters = man.get('filter', [])
banks = [f for f in filters if f['kind'] == 'fir_coef_bank']
blocks = [f for f in filters if f['kind'].startswith('fir_block')]
adaptive = [f for f in filters if f['kind'] == 'adaptive_fir_candidate']
if a.only:
banks = [b for b in banks if b['module'] in a.only]
if not banks:
print('no fixed-coefficient FIR coefficient bank in the manifest '
'-- nothing in scope for fir_compiler.')
for f in adaptive:
print(f"INVENTORY ONLY (not replaced): {f['module']}.{f['tap_regs']} "
f"({f['taps']} runtime taps, delay line {f['delay_line']} deep {f['delay_depth']}). "
f"Per SKILL scope: Farrow/LMS/correlator filters keep their RTL.")
for f in [x for x in filters if x['kind'] == 'fir_block_candidate']:
print(f"INVENTORY ONLY (not replaced): {f['module']} [{f['file']}] -- "
f"coefficients are not constants ({len(f.get('coef_ports', []))} coef port(s)).")
ipman_p = os.path.join(out, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
done = 0
for b in banks:
# the FIR block that consumes this coefficient module (same Simulink parent path)
parent = next((x for x in blocks
if b['source_path'].startswith(x['source_path'])
and x['source_path'] and x['module'] != b['module']), None)
din = sorted(parent['data_in']) if parent else []
dout = sorted(parent['data_out']) if parent else []
dinfo = (parent['data_in'][din[0]] if din else {})
dw = a.data_width or dinfo.get('width') or 16
df = a.data_frac if a.data_frac is not None else (dinfo.get('frac') or 0)
ftype = a.filter_type or ({'interpolation': 'Interpolation',
'decimation': 'Decimation'}.get(
parent['rate_hint'] if parent else '', 'Single_Rate'))
if ftype != 'Single_Rate' and a.rate == 1:
print(f" !! {b['module']}: rate change inferred from the module name but no --rate "
f"given; falling back to Single_Rate. Confirm the factor in the Simulink block.")
ftype = 'Single_Rate'
ipname, coe = coe_path(out, b['module'], b['coeffs'], b['coef_width'], b['coef_frac'] or 0)
# FIR Compiler 7.2 hard limits (probed on 2026-07-24, Vivado 2018.1):
# Coefficient_Width 8..49, Data_Width 2..47, Data_Fractional_Bits 0..16.
# The binary point is documentation only -- the core does integer MACs, and
# feeding frac=0 keeps us inside the legal range AND matches the generated
# RTL, which is plain integer arithmetic on the raw bits.
limits = []
if not 8 <= b['coef_width'] <= 49:
limits.append(f"coefficient width {b['coef_width']} outside 8..49")
if not 2 <= dw <= 47:
limits.append(f"data width {dw} outside 2..47")
if limits:
print(' !! ' + '; '.join(limits) +
' -- fir_compiler cannot take this as-is: re-quantise the Simulink source '
'(Task12 rule 12, bidirectional adjustment) or keep the RTL.')
entry = {'type': 'fir_compiler', 'name': ipname, 'coe': os.path.abspath(coe),
'coef_width': b['coef_width'], 'coef_frac': 0,
'width': dw, 'frac': 0,
'coef_binpt_rtl': b['coef_frac'], 'data_binpt_rtl': df,
'limits_ok': 'no' if limits else 'yes', 'filter_type': ftype,
'interp': a.rate if ftype == 'Interpolation' else 1,
'decim': a.rate if ftype == 'Decimation' else 1,
'zero_pack': 1, 'channels': a.channels, 'paths': a.paths,
'aclken': 'false' if a.no_aclken else 'true',
'reload': 'true' if a.reload else 'false',
'taps': b['taps']}
ipman['ips'] = [i for i in ipman['ips'] if i.get('name') != ipname]
if limits:
print(' -> NOT registered in ip_manifest (would fail create_ip); '
'proposal written so the source can be fixed first.')
else:
ipman['ips'].append(entry)
done += 1
prop = os.path.join(out, f"FIR_PROPOSAL_{b['module']}_{b['coef_signal']}.md")
open(prop, 'w', encoding='utf-8').write(PROPOSAL.format(
module=b['module'], file=(parent or b)['file'], src=b['source_path'],
ipname=ipname, coe=os.path.basename(coe), taps=b['taps'],
cw=b['coef_width'], cf=b['coef_frac'], sym='是' if b['symmetric'] else '否',
coef_signal=b['coef_signal'], ftype=ftype,
rate_note=('抽取/内插比必须与 Simulink 块一致(本次取 %d)' % a.rate)
if ftype != 'Single_Rate' else '单速率',
dw=dw, df=df, aclken='false' if a.no_aclken else 'true',
reload='true' if a.reload else 'false',
reload_note='系数运行时更新(AXI-Stream config 通道)' if a.reload else '定系数,无需重载',
din=', '.join(din) or '(未定位,人工确认)', dout=', '.join(dout) or '(未定位,人工确认)',
valid='validIn/validOut' if (parent and parent.get('valid_in')) else '(人工确认)',
cplx='是' if (parent and parent.get('complex')) else '否',
limits='; '.join(limits) + ' —— 超限,先改源头' if limits else '均在范围内',
out_width_hint=f"{dw}+{b['coef_width']}+ceil(log2({b['taps']})) 位左右"))
print(f"FIR {b['module']}: {b['taps']} taps x {b['coef_width']}b (En{b['coef_frac']}) "
f"-> {ipname} [{os.path.basename(coe)}]\n proposal: {os.path.basename(prop)}")
done += 1
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(out, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()))
print(f'done: {done} FIR IP(s) registered; RTL untouched (guided surgery, Task12 6.3)')
if __name__ == '__main__':
main()
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""Generate the dual-DUT cycle-true comparison testbench + xsim run script.
python gen_cmp_tb.py <top_module> <hdlsrc_dir> [--outdir DIR] [--cycles N]
[--ip-simdir DIR] [--compare-at PORT=SIGNAL ...]
- DUT0 = <top> compiled from <hdlsrc>/*.v (original HDL Coder RTL)
- DUT1 = <top>_IP compiled from <hdlsrc>/rtl_ip/*.v (+ IP simulation netlists)
- identical stimulus into both: clk; reset (asserted RST_CYCLES then deasserted);
enable-like inputs (enb/clk_enable/*_en names) held 1; every other input driven
from a per-port LFSR (deterministic, seeded by port name hash).
!! For control-heavy tops, replace the LFSR block with replayed vectors from
the HDL Coder <top>_tb.v -- see SKILL.md.
- compare: every output port, every posedge after both-ready; mismatches counted
and logged; result file first line = PASS or "FAIL <n>".
- --compare-at OUT=QUALIFIER limits comparison of OUT to cycles where the named
qualifier signal (an output of the DUTs, e.g. a done/valid strobe) is 1 --
the CORDIC consumption-point contract.
"""
import argparse, hashlib, os, re, sys
# Vivado install root, for the generated run_cmp.bat (xvlog/xelab/xsim + glbl.v).
# Machine-specific, so it comes from the environment, never from a literal path.
# VIVADO_PATH may point at the install root, its bin/, or bin/vivado.bat --
# all three are accepted. XILINX_VIVADO (set by Vivado's settings64) also works.
VIVADO_FALLBACK = r'D:\Xilinx\Vivado\2018.1'
def _vivado_root():
v = (os.environ.get('VIVADO_PATH') or os.environ.get('XILINX_VIVADO') or '').strip().strip('"')
if not v:
print(f'NOTE: neither VIVADO_PATH nor XILINX_VIVADO is set -- falling back to\n'
f' {VIVADO_FALLBACK}\n'
f' That literal is one machine\'s install. Set VIVADO_PATH to this\n'
f' machine\'s Vivado (see the VIVADO_PATH entry in ~/skills_env.md)\n'
f' or run_cmp.bat may silently use the wrong version.')
return VIVADO_FALLBACK
p = v.rstrip('\\/')
if p.lower().endswith(('.bat', '.exe')) or os.path.isfile(p):
p = os.path.dirname(p) # ...\bin\vivado.bat -> ...\bin
if os.path.basename(p).lower() == 'bin':
p = os.path.dirname(p) # ...\bin -> install root
return p or VIVADO_FALLBACK
PORT_RE = re.compile(r'^\s*(input|output)\s+(signed\s+)?(\[[^\]]+\]\s*)?(\w+)\s*[,;)]', re.M)
def parse_ports(vfile, top):
txt = open(vfile, encoding='utf-8', errors='replace').read()
m = re.search(r'module\s+%s\b(.*?)endmodule' % re.escape(top), txt, re.S)
if not m:
sys.exit(f'module {top} not found in {vfile}')
body = m.group(1)
ports = []
for pm in PORT_RE.finditer(body):
d, signed, rng, name = pm.group(1), pm.group(2), (pm.group(3) or '').strip(), pm.group(4)
ports.append({'dir': d, 'signed': bool(signed), 'range': rng, 'name': name})
return ports
def lfsr_seed(name):
return int(hashlib.sha1(name.encode()).hexdigest()[:8], 16) | 1
def main():
ap = argparse.ArgumentParser()
ap.add_argument('top'); ap.add_argument('hdlsrc')
ap.add_argument('--rtl-ip', required=True, help='FIXED rtl_ip dir (vivado/rtl_ip/<model>)')
ap.add_argument('--outdir', default=None)
ap.add_argument('--cycles', type=int, default=200000)
ap.add_argument('--rst-cycles', type=int, default=20)
ap.add_argument('--ip-simdir', default='', help='dir with generated IP sim .v files (globbed recursively)')
ap.add_argument('--compare-at', nargs='*', default=[], metavar='OUT=QUAL')
a = ap.parse_args()
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip_dir = os.path.abspath(a.rtl_ip)
outdir = a.outdir or os.path.join(rtl_ip_dir, 'cmp')
os.makedirs(outdir, exist_ok=True)
qual = dict(kv.split('=') for kv in a.compare_at)
src = None
for fn in os.listdir(hdlsrc):
if fn.endswith('.v') and re.search(r'^\s*module\s+%s\b' % re.escape(a.top),
open(os.path.join(hdlsrc, fn), encoding='utf-8', errors='replace').read(), re.M):
src = os.path.join(hdlsrc, fn); break
if not src:
sys.exit(f'{a.top} not declared in {hdlsrc}/*.v')
ports = parse_ports(src, a.top)
ins = [p for p in ports if p['dir'] == 'input']
outs = [p for p in ports if p['dir'] == 'output']
def is_clk(n): return n in ('clk', 'clock')
def is_rst(n): return 'reset' in n or n == 'rst'
def is_en(n): return n in ('enb', 'clk_enable') or n.endswith('_en') or n.endswith('_enable')
tb = ['`timescale 1 ns / 1 ns', 'module tb_cmp;',
' reg clk = 0; always #2 clk = ~clk;',
f' reg rst = 1; initial begin repeat ({a.rst_cycles}) @(posedge clk); rst <= 0; end',
' integer mismatches = 0; integer fd;']
conn0, conn1 = [], []
for p in ins:
n, rng = p['name'], p['range']
if is_clk(n):
conn0.append(f'.{n}(clk)'); conn1.append(f'.{n}(clk)'); continue
if is_rst(n):
conn0.append(f'.{n}(rst)'); conn1.append(f'.{n}(rst)'); continue
if is_en(n):
tb.append(f' wire {rng} {n} = 1\'b1;')
else:
w = 32
m = re.match(r'\[(\d+):0\]', rng or '')
if m: w = int(m.group(1)) + 1
tb.append(f' reg [31:0] lfsr_{n} = 32\'h{lfsr_seed(n):08x};')
tb.append(f' always @(posedge clk) lfsr_{n} <= {{lfsr_{n}[30:0], lfsr_{n}[31]^lfsr_{n}[21]^lfsr_{n}[1]^lfsr_{n}[0]}};')
tb.append(f' wire {rng} {n} = lfsr_{n}[{min(w,32)-1}:0];')
conn0.append(f'.{n}({n})'); conn1.append(f'.{n}({n})')
for p in outs:
n, rng = p['name'], p['range']
tb.append(f' wire {rng} o0_{n}, o1_{n};')
conn0.append(f'.{n}(o0_{n})'); conn1.append(f'.{n}(o1_{n})')
tb += [f' {a.top} dut0 ({", ".join(conn0)});',
f' {a.top}_IP dut1 ({", ".join(conn1)});',
f' reg armed = 0; initial begin repeat ({a.rst_cycles + 5}) @(posedge clk); armed <= 1; end',
' always @(posedge clk) if (armed) begin']
for p in outs:
n = p['name']
g = f' && (o0_{qual[n]} === 1\'b1)' if n in qual else ''
tb.append(f' if ((o0_{n} !== o1_{n}){g}) begin')
tb.append(f' mismatches = mismatches + 1;')
tb.append(f' if (mismatches <= 50) $display("MISMATCH t=%0t port={n} orig=%h ip=%h", $time, o0_{n}, o1_{n});')
tb.append(' end')
tb += [ ' end',
f' initial begin repeat ({a.cycles}) @(posedge clk);',
f' fd = $fopen("cmp_result.txt", "w");',
' if (mismatches == 0) begin $fdisplay(fd, "PASS"); $display("CMP PASS"); end',
' else begin $fdisplay(fd, "FAIL %0d", mismatches); $display("CMP FAIL %0d", mismatches); end',
' $fclose(fd); $finish;',
' end', 'endmodule']
open(os.path.join(outdir, 'tb_cmp.v'), 'w').write('\n'.join(tb) + '\n')
vroot = _vivado_root()
xbin = os.path.join(vroot, 'bin')
glbl = os.path.join(vroot, 'data', 'verilog', 'src', 'glbl.v')
if not os.path.isfile(glbl):
print(f'WARNING: glbl.v not found under {vroot}\n'
f' set VIVADO_PATH (or XILINX_VIVADO) to your Vivado install, '
f'its bin/, or bin/vivado.bat; run_cmp.bat will fail as written.')
bat = [f'@echo off', f'cd /d %~dp0',
f'call "{xbin}\\xvlog.bat" "{glbl}"',
f'for %%f in ("{hdlsrc}\\*.v") do if /I not "%%~nxf"=="{a.top}_tb.v" call "{xbin}\\xvlog.bat" "%%f"',
f'for %%f in ("{rtl_ip_dir}\\*.v") do call "{xbin}\\xvlog.bat" "%%f"']
if a.ip_simdir:
bat.append(f'for /R "{a.ip_simdir}" %%f in (*.v) do call "{xbin}\\xvlog.bat" "%%f"')
# dist_mem_gen/blk_mem_gen sim models load .mif by BARE filename relative to
# the xsim run directory -- without this copy every ROM reads 0.
bat.append(f'for /R "{a.ip_simdir}" %%f in (*.mif) do copy /Y "%%f" . >nul')
bat += [f'call "{xbin}\\xvlog.bat" tb_cmp.v',
f'call "{xbin}\\xelab.bat" tb_cmp glbl -s cmp_sim -timescale 1ns/1ns '
f'-L dist_mem_gen_v8_0 -L blk_mem_gen_v8_4 -L fifo_generator_v13_2 '
f'-L cordic_v6_0_14 -L fir_compiler_v7_2_11 -L cic_compiler_v4_0_13 '
f'-L xbip_utils_v3_0_8 -L xbip_utils_v3_0_9 -L xbip_pipe_v3_0_5 '
f'-L unisims_ver -L unimacro_ver -L secureip',
f'call "{xbin}\\xsim.bat" cmp_sim -R',
f'type cmp_result.txt']
open(os.path.join(outdir, 'run_cmp.bat'), 'w').write('\r\n'.join(bat) + '\r\n')
print(f'generated {outdir}\\tb_cmp.v ({len(ins)} in / {len(outs)} out ports) and run_cmp.bat')
print('NOTE: exclude the original *_tb.v from compile; for control-heavy tops replace the')
print(' LFSR stimulus with replayed HDL Coder testbench vectors (see SKILL.md).')
if __name__ == '__main__':
main()
+169
View File
@@ -0,0 +1,169 @@
# =============================================================================
# gen_ip.tcl -- create the Vivado IPs listed in rtl_ip/ip_manifest.tcl
# usage: vivado -mode batch -source gen_ip.tcl -tclargs <ip_manifest.tcl> <ip_out_dir> [part]
# Managed-IP style: .xci + synthesis + simulation targets are generated so the
# cores can be (a) added to the synthesis project and (b) compiled for xsim.
# =============================================================================
set manifest [lindex $argv 0]
set ipdir [file normalize [lindex $argv 1]]
set part xc7z100ffg900-2
if {[llength $argv] > 2} { set part [lindex $argv 2] }
file mkdir $ipdir
create_project -in_memory -part $part
set_property target_language Verilog [current_project]
set ips {}
source $manifest ;# fills $ips: list of {type {..} name {..} width {..} depth {..} coe {..} mode {..}}
# manifest field with a default (older manifests do not carry the newer keys)
proc opt {arrname key def} {
upvar 1 $arrname c
if {[info exists c($key)] && $c($key) ne {}} { return $c($key) }
return $def
}
foreach ip $ips {
array unset c; array set c $ip
set name $c(name)
if {[file exists $ipdir/$name/$name.xci]} {
puts "###### SKIP $name (exists)"
continue
}
switch -- $c(type) {
dist_mem_gen {
create_ip -name dist_mem_gen -vendor xilinx.com -library ip -version 8.0 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.memory_type {rom} \
CONFIG.data_width $c(width) \
CONFIG.depth $c(depth) \
CONFIG.coefficient_file $c(coe) \
] [get_ips $name]
}
blk_mem_gen {
# SDP, common clock, READ_FIRST on the write port, no output register,
# ENA/ENB pins -- matches SimpleDualPortRAM_generic semantics exactly.
create_ip -name blk_mem_gen -vendor xilinx.com -library ip -version 8.4 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.Memory_Type {Simple_Dual_Port_RAM} \
CONFIG.Assume_Synchronous_Clk {true} \
CONFIG.Use_Byte_Write_Enable {false} \
CONFIG.Write_Width_A $c(width) \
CONFIG.Write_Depth_A $c(depth) \
CONFIG.Read_Width_A $c(width) \
CONFIG.Write_Width_B $c(width) \
CONFIG.Read_Width_B $c(width) \
CONFIG.Operating_Mode_A {READ_FIRST} \
CONFIG.Enable_A {Use_ENA_Pin} \
CONFIG.Enable_B {Use_ENB_Pin} \
CONFIG.Register_PortB_Output_of_Memory_Primitives {false} \
CONFIG.Register_PortB_Output_of_Memory_Core {false} \
] [get_ips $name]
}
fifo_generator {
# guided: mode field carries standard|fwft; depth/width from manifest
create_ip -name fifo_generator -vendor xilinx.com -library ip -version 13.2 \
-module_name $name -dir $ipdir
set rm {Standard_FIFO}
if {[info exists c(mode)] && $c(mode) eq "fwft"} { set rm {First_Word_Fall_Through} }
set_property -dict [list \
CONFIG.Fifo_Implementation {Common_Clock_Block_RAM} \
CONFIG.Performance_Options $rm \
CONFIG.Input_Data_Width $c(width) \
CONFIG.Input_Depth $c(depth) \
CONFIG.Output_Data_Width $c(width) \
CONFIG.Output_Depth $c(depth) \
] [get_ips $name]
}
cordic {
# guided: Arc_Tan phase-seed kernel. Coarse rotation = full-quadrant atan2
# (bidirectional-adjustment principle: better than the FSM's RHP quirk);
# Radians 3QN phase; Truncate rounding; ACLKEN so enb gating stays exact;
# parallel/maximum pipelining -> latency ~input_width+4, well under the
# 64-cycle commit budget.
create_ip -name cordic -vendor xilinx.com -library ip -version 6.0 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.Functional_Selection {Arc_Tan} \
CONFIG.Input_Width $c(width) \
CONFIG.Output_Width $c(out_width) \
CONFIG.Data_Format {SignedFraction} \
CONFIG.Phase_Format {Radians} \
CONFIG.Coarse_Rotation {true} \
CONFIG.Round_Mode {Truncate} \
CONFIG.ACLKEN {true} \
CONFIG.Pipelining_Mode {Maximum} \
] [get_ips $name]
}
fir_compiler {
# guided (Task12 §六). Cycle-true-critical settings:
# Integer_Coefficients : taps go in as integers, the IP must NOT re-quantise
# Full_Precision output : truncation stays OUTSIDE, done the way the RTL did it
# Input_Sample_Period/1 : one sample per clock, no hardware folding
# -> the latency is a fixed number we can align to
# ACLKEN : gated by enb exactly like the generated RTL
# no TREADY / no input FIFO: pure pipeline, no variable-latency handshake
create_ip -name fir_compiler -vendor xilinx.com -library ip -version 7.2 \
-module_name $name -dir $ipdir
set_property -dict [list \
CONFIG.Filter_Type [opt c filter_type {Single_Rate}] \
CONFIG.CoefficientSource {COE_File} \
CONFIG.Coefficient_File $c(coe) \
CONFIG.Quantization {Integer_Coefficients} \
CONFIG.Coefficient_Width $c(coef_width) \
CONFIG.Coefficient_Fractional_Bits [opt c coef_frac 0] \
CONFIG.Coefficient_Sign {Signed} \
CONFIG.Coefficient_Structure [opt c coef_structure {Inferred}] \
CONFIG.Data_Width $c(width) \
CONFIG.Data_Fractional_Bits [opt c frac 0] \
CONFIG.Data_Sign {Signed} \
CONFIG.Output_Rounding_Mode {Full_Precision} \
CONFIG.RateSpecification {Input_Sample_Period} \
CONFIG.SamplePeriod 1 \
CONFIG.HardwareOversamplingRate 1 \
CONFIG.Interpolation_Rate [opt c interp 1] \
CONFIG.Decimation_Rate [opt c decim 1] \
CONFIG.Zero_Pack_Factor [opt c zero_pack 1] \
CONFIG.Number_Channels [opt c channels 1] \
CONFIG.Number_Paths [opt c paths 1] \
CONFIG.Coefficient_Reload [opt c reload {false}] \
CONFIG.Num_Reload_Slots [opt c reload_slots 1] \
CONFIG.Has_ACLKEN [opt c aclken {true}] \
CONFIG.Has_ARESETn [opt c aresetn {false}] \
CONFIG.M_DATA_Has_TREADY {false} \
CONFIG.S_DATA_Has_FIFO {false} \
] [get_ips $name]
}
cic_compiler {
# guided: only for large integer rate changes (CIC), fixed rate so the
# decimation/interpolation cadence is static and comparable cycle by cycle.
create_ip -name cic_compiler -vendor xilinx.com -library ip -version 4.0 \
-module_name $name -dir $ipdir
set rate [opt c rate 1]
set_property -dict [list \
CONFIG.Filter_Type [opt c filter_type {Decimation}] \
CONFIG.Number_Of_Stages [opt c stages 3] \
CONFIG.Differential_Delay [opt c diff_delay 1] \
CONFIG.Sample_Rate_Changes {Fixed} \
CONFIG.Fixed_Or_Initial_Rate $rate \
CONFIG.Minimum_Rate [opt c min_rate $rate] \
CONFIG.Maximum_Rate [opt c max_rate $rate] \
CONFIG.Input_Data_Width $c(width) \
CONFIG.Quantization {Full_Precision} \
CONFIG.RateSpecification {Sample_Period} \
CONFIG.SamplePeriod 1 \
CONFIG.Number_Of_Channels [opt c channels 1] \
CONFIG.Use_Xtreme_DSP_Slice {false} \
CONFIG.HAS_ACLKEN [opt c aclken {true}] \
CONFIG.HAS_ARESETN [opt c aresetn {false}] \
CONFIG.HAS_DOUT_TREADY {false} \
] [get_ips $name]
}
default { puts "###### UNKNOWN IP TYPE: $c(type) ($name)"; continue }
}
generate_target {instantiation_template synthesis simulation} [get_ips $name]
puts "###### GENERATED $name ($c(type))"
}
puts "###### DONE [llength $ips] ip(s) in $ipdir"
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""Incremental mode for rtl-ip-replace (Task12 rules 13-16 / Task12 §5).
Baseline = the newest backup_<ts>/ under the fixed dir, i.e. the previous run's
ORIGINAL MATLAB RTL (src_orig/) + rewritten RTL (*.v) + ip/ + cmp/.
plan <HDLSRC> <OUT> [--apply] [--full] [--baseline DIR]
Run right after backup_outdir.py. Classifies every source file against the
baseline snapshot; --apply copies back the rewritten .v of files whose
MATLAB source is byte-identical (nothing to redo for them).
finalize <HDLSRC> <OUT> [--baseline DIR]
Run after the transform scripts (step 2), BEFORE create_ip / cmp sim.
Writes TRANSFORM.diff, compares it + ip_manifest + .coe + scripts_sha1
against the baseline, and decides whether step 3 (IP gen) and step 4
(cycle-true sim) may be skipped. Decisions land in incr_plan.json and are
appended to PROVENANCE.log.
Skipping the sim is only allowed when the rewrite this run performed is
byte-for-byte the same rewrite the baseline PASS was proven on (Task12 §5.4).
"""
import argparse, json, os, shutil, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ipflow_common import (SRC_ORIG, TRANSFORM_DIFF, append_provenance, # noqa: E402
scripts_sha1, sha1_file, snapshot_sources, src_files)
PLAN = 'incr_plan.json'
def find_baseline(out, explicit=None):
if explicit:
return os.path.abspath(explicit)
cands = sorted(d for d in os.listdir(out)
if d.startswith('backup_') and os.path.isdir(os.path.join(out, d)))
for d in reversed(cands): # newest usable one
if os.path.isdir(os.path.join(out, d, SRC_ORIG)):
return os.path.join(out, d)
return None
def read_sha(p):
return sha1_file(p) if os.path.exists(p) else None
def baseline_cmp_pass(base):
p = os.path.join(base, 'cmp', 'cmp_result.txt')
if not os.path.exists(p):
return False, 'no cmp_result.txt in baseline'
first = open(p, errors='replace').readline().strip()
return first.startswith('PASS'), first
def baseline_scripts_sha1(base):
p = os.path.join(base, 'PROVENANCE.log')
if os.path.exists(p):
for ln in open(p, errors='replace'):
if ln.startswith('scripts_sha1:'):
return ln.split(':', 1)[1].strip()
return None
def save(out, plan):
json.dump(plan, open(os.path.join(out, PLAN), 'w'), indent=2)
def load(out):
p = os.path.join(out, PLAN)
return json.load(open(p)) if os.path.exists(p) else {}
def cmd_plan(a):
hdlsrc, out = os.path.abspath(a.hdlsrc), os.path.abspath(a.outdir)
os.makedirs(out, exist_ok=True)
snapshot_sources(hdlsrc, out) # this run's baseline-to-be (rule 14)
base = None if a.full else find_baseline(out, a.baseline)
plan = {'baseline': base, 'incremental': bool(base), 'files': {}, 'reasons': []}
if not base:
plan['reasons'].append('forced --full' if a.full else 'no usable baseline (backup_*/src_orig)')
save(out, plan)
print('FULL run: ' + plan['reasons'][0])
return
bsrc = os.path.join(base, SRC_ORIG)
new, old = src_files(hdlsrc), src_files(bsrc)
for fn in new:
if fn not in old:
plan['files'][fn] = 'new'
elif read_sha(os.path.join(hdlsrc, fn)) == read_sha(os.path.join(bsrc, fn)):
plan['files'][fn] = 'same'
else:
plan['files'][fn] = 'changed'
for fn in old:
if fn not in new:
plan['files'][fn] = 'removed'
same = [f for f, c in plan['files'].items() if c == 'same']
all_same = bool(same) and all(c == 'same' for c in plan['files'].values())
plan['all_same'] = all_same
applied = []
if a.apply:
for fn in same:
src = os.path.join(base, fn) # the REWRITTEN file
if os.path.exists(src):
shutil.copy(src, os.path.join(out, fn))
applied.append(fn)
# .coe / manifests are only safe to take wholesale when NOTHING changed;
# otherwise the transform scripts must re-derive them (a stale .coe of a
# table that moved would silently survive).
extra = (['ip_manifest.tcl', 'ip_manifest.json', 'manifest.json']
+ [f for f in sorted(os.listdir(base)) if f.endswith('.coe')]) if all_same else []
for fn in extra:
s, d = os.path.join(base, fn), os.path.join(out, fn)
if os.path.exists(s) and not os.path.exists(d):
shutil.copy(s, d)
applied.append(fn)
plan['reused_from_baseline'] = applied
save(out, plan)
ok, first = baseline_cmp_pass(base)
print(f'baseline: {base}')
print(' cmp_result: ' + (first if first else 'MISSING'))
for c in ('same', 'changed', 'new', 'removed'):
fs = [f for f, k in plan['files'].items() if k == c]
if fs:
print(f' {c:8s}: {" ".join(sorted(fs))}')
if applied:
print(f' applied {len(applied)} file(s) from baseline (skip transform for them)')
if plan['all_same']:
print(' ALL MATLAB sources unchanged -> step 2 (transform scripts) can be skipped entirely')
if not ok:
print(' NOTE: baseline has no PASS -- cycle-true sim CANNOT be inherited')
def cmd_finalize(a):
from ipflow_common import write_transform_diff
hdlsrc, out = os.path.abspath(a.hdlsrc), os.path.abspath(a.outdir)
write_transform_diff(out)
plan = load(out)
if plan and not plan.get('incremental'):
base = None # a full run was decided in `plan` -- honour it
else:
base = plan.get('baseline') or find_baseline(out, a.baseline)
blockers, ip_reuse, sim_inherit = [], False, False
if not base:
blockers.append('full run (no baseline / --full): everything regenerated')
else:
if any(c in ('new', 'removed') for c in plan.get('files', {}).values()):
blockers.append('source file set changed (new/removed)')
d_new = read_sha(os.path.join(out, TRANSFORM_DIFF))
d_old = read_sha(os.path.join(base, TRANSFORM_DIFF))
if d_old is None:
blockers.append('baseline has no TRANSFORM.diff')
elif d_new != d_old:
blockers.append('TRANSFORM.diff differs from baseline (rewrite is not the same one)')
m_new = read_sha(os.path.join(out, 'ip_manifest.tcl'))
m_old = read_sha(os.path.join(base, 'ip_manifest.tcl'))
coe_new = {f: read_sha(os.path.join(out, f)) for f in os.listdir(out) if f.endswith('.coe')}
coe_old = {f: read_sha(os.path.join(base, f)) for f in os.listdir(base) if f.endswith('.coe')}
ip_same = (m_new is not None and m_new == m_old and coe_new == coe_old)
if not ip_same:
blockers.append('ip_manifest/.coe differ from baseline')
have_ip = os.path.isdir(os.path.join(out, 'ip')) or os.path.isdir(os.path.join(base, 'ip'))
if ip_same and not have_ip: # Task12 §5.5: an incomplete baseline may not be inherited
ip_same = False
blockers.append('baseline has no ip/ directory')
s_now, s_old = scripts_sha1(), baseline_scripts_sha1(base)
if s_old != s_now:
blockers.append(f'skill scripts changed ({s_old} -> {s_now})')
ok, first = baseline_cmp_pass(base)
if not ok:
blockers.append('baseline cmp_result is not PASS')
ip_reuse = ip_same and not any(b.startswith(('no baseline', 'source file set')) for b in blockers)
sim_inherit = not blockers
if ip_reuse and not os.path.isdir(os.path.join(out, 'ip')) \
and os.path.isdir(os.path.join(base, 'ip')):
shutil.copytree(os.path.join(base, 'ip'), os.path.join(out, 'ip'))
for f in ('ip_manifest.json',):
if os.path.exists(os.path.join(base, f)) and not os.path.exists(os.path.join(out, f)):
shutil.copy(os.path.join(base, f), os.path.join(out, f))
plan.update({'ip_reuse': ip_reuse, 'sim_inherit': sim_inherit, 'blockers': blockers})
save(out, plan)
files = plan.get('files', {})
reused = sorted(f for f, c in files.items() if c == 'same')
regen = sorted(f for f, c in files.items() if c != 'same')
append_provenance(out, [
'',
'# --- incremental run (Task12 rules 13-16) ---',
'incremental: ' + ('on' if base else 'off'),
'baseline: ' + (os.path.basename(base) + '/' if base else '-'),
'reused_files: ' + (' '.join(reused) or '-'),
'regenerated_files: ' + (' '.join(regen) or '-'),
'ip_reuse: ' + ('full' if ip_reuse else 'none'),
'cmp_sim: ' + (f'INHERITED PASS from {os.path.basename(base)}/cmp/cmp_result.txt'
if sim_inherit else 'MUST RUN'),
'blockers: ' + ('; '.join(blockers) or '-'),
])
print('ip_reuse :', 'YES (skip step 3 create_ip)' if ip_reuse else 'no (run step 3)')
print('sim_inherit :', 'YES (skip step 4 cycle-true sim, PASS inherited from %s)'
% os.path.basename(base) if sim_inherit else 'no (run step 4)')
for b in blockers:
print(' blocker:', b)
def main():
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = p.add_subparsers(dest='cmd', required=True)
for name in ('plan', 'finalize'):
s = sub.add_parser(name)
s.add_argument('hdlsrc')
s.add_argument('outdir')
s.add_argument('--baseline', help='explicit baseline dir (default: newest backup_*)')
if name == 'plan':
s.add_argument('--apply', action='store_true',
help='copy back rewritten files whose source is unchanged')
s.add_argument('--full', action='store_true', help='force a full run')
a = p.parse_args()
(cmd_plan if a.cmd == 'plan' else cmd_finalize)(a)
if __name__ == '__main__':
main()
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Replace the HDL Coder `persistent isempty-init` table idiom with dist ROM IP.
Idiom (MATLAB `persistent T; if isempty(T), T = <const>; end`):
wire [..] P [0:D-1]; + D const assigns (the constant image)
wire [..] T_1 [0:D-1]; generate: T_1[i] = P[i];
reg [..] T_reg_reg [0:D-1]; always@(posedge clk/reset):
reset -> 0; enb -> T_reg_reg <= T_reg_reg_next
wire [..] T_reg_reg_next [0:D-1]; generate: T[i] = T_reg_reg[i];
T_reg_reg_next[i] = TMP[i];
reg [..] TMP [0:D-1]; always@*: TMP = (S) ? T_1 : T; // S = first-call flag
Net semantics: T reads 0 until the first (enb && S) tick, then the constant
image forever. TMP reads (S || loaded) ? const : 0.
Replacement (bit- and cycle-true):
dist_mem_gen ROM (content = P) per read site
reg T__ipinited: reset->0, (enb && S)->1
read T[e] -> (T__ipinited ? ROM[e] : 0)
read TMP[e] -> ((S || T__ipinited) ? ROM[e] : 0)
all five arrays + three blocks deleted.
Usage: python initrom_to_ip.py <hdlsrc> (operates on <hdlsrc>/rtl_ip copies;
run AFTER rtl_ip_scan + source copy [rom_to_ip.py --copy-only or manual],
BEFORE rom_to_ip's rewriting of any remaining plain ROMs, BEFORE namespace_ip.)
"""
import argparse, hashlib, json, os, re, shutil, sys
CONST_ASSIGN = re.compile(r"^\s*assign\s+(\w+)\[(\d+)\]\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_]+)\s*;\s*$", re.M)
def to_bits(radix, lit, width):
lit = lit.replace('_', '')
v = int(lit, {'b': 2, 'd': 10, 'h': 16}[radix]) & ((1 << width) - 1)
return format(v, '0%db' % width)
def find_block(lines, i0, opener):
"""Return (start,end) line span of the statement block whose header line index
is i0 (an `always`/`generate` line); tracks begin/end + generate/endgenerate."""
depth, i = 0, i0
started = False
while i < len(lines):
t = lines[i]
depth += len(re.findall(r'\bbegin\b|\bgenerate\b|\bcase\b', t))
depth -= len(re.findall(r'\bend\b(?!module|generate|case)|\bendgenerate\b|\bendcase\b', t))
if depth > 0:
started = True
if started and depth <= 0:
return i0, i
i += 1
sys.exit(f'unbalanced block at line {i0}: {lines[i0]}')
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', help='FIXED output dir (e.g. <repo>/vivado/rtl_ip/<model>)')
a = ap.parse_args()
from ipflow_common import copy_sources
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip = os.path.abspath(a.outdir)
copy_sources(hdlsrc, rtl_ip)
ipman_p = os.path.join(rtl_ip, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
coe_of_hash = {ip.get('content_hash'): ip['name'] for ip in ipman['ips'] if 'content_hash' in ip}
for fn in [f for f in os.listdir(rtl_ip) if f.endswith('.v')]:
path = os.path.join(rtl_ip, fn)
txt = open(path, encoding='utf-8', errors='replace').read()
# locate idiom instances: assign T_1[gv] = P[gv]; inside a generate loop
pairs = []
for m in re.finditer(r'assign\s+(\w+)_1\[(\w+)\]\s*=\s*(\w+)\[\2\]\s*;', txt):
T, P = m.group(1), m.group(3)
if f'{T}_reg_reg' in txt:
pairs.append((T, P))
if not pairs:
continue
print(f'[{fn}] isempty-init tables: {pairs}')
for T, P in pairs:
# ---- table content ----
vals, W = {}, None
for m in CONST_ASSIGN.finditer(txt):
if m.group(1) == P:
W = W or len(to_bits(m.group(4), m.group(5), int(m.group(3))))
vals[int(m.group(2))] = to_bits(m.group(4), m.group(5), int(m.group(3)))
D = len(vals)
if D == 0 or set(vals) != set(range(D)):
print(f' SKIP {T}: const image {P} incomplete'); continue
AW = max(1, (D - 1).bit_length())
content = '\n'.join(vals[i] for i in range(D))
h = hashlib.sha1((f'{W}x{D}\n' + content).encode()).hexdigest()[:10]
if h not in coe_of_hash:
ipname = f'rom_{W}x{D}_{h}'
with open(os.path.join(rtl_ip, ipname + '.coe'), 'w') as f:
f.write('memory_initialization_radix=2;\nmemory_initialization_vector=\n')
f.write(',\n'.join(vals[i] for i in range(D)) + ';\n')
ipman['ips'].append({'type': 'dist_mem_gen', 'name': ipname, 'width': W, 'depth': D,
'coe': os.path.abspath(os.path.join(rtl_ip, ipname + '.coe')),
'content_hash': h})
coe_of_hash[h] = ipname
ipname = coe_of_hash[h]
lines = txt.split('\n')
# ---- discover TMP name + guard signal from the mux always block ----
mm = re.search(r'(\w+)\[\w+\]\s*=\s*%s_1\[\w+\]\s*;' % re.escape(T), txt)
if not mm:
print(f' SKIP {T}: mux TMP not found'); continue
TMP = mm.group(1)
gm = None
for i, ln in enumerate(lines):
if re.search(r'\bif\s*\(\s*(\w+)\s*==\s*1\'b0\s*\)', ln) is not None:
# candidate: block must contain TMP writes
s2, e2 = find_block(lines, max(0, i - 1), 'always')
body = '\n'.join(lines[s2:e2 + 1])
if f'{TMP}[' in body and f'{T}_1[' in body:
gm = re.search(r'\bif\s*\(\s*(\w+)\s*==\s*1\'b0\s*\)', ln).group(1)
break
if not gm:
print(f' SKIP {T}: guard signal not found'); continue
S = gm
# ---- delete the three blocks (reg process, copy generate, next generate, mux always) ----
kill = set()
for i, ln in enumerate(lines):
if (re.search(r'begin\s*:\s*%s_reg_process' % re.escape(T), ln)
or re.search(r'begin\s*:\s*%s_1_gen' % re.escape(T), ln)
or re.search(r'begin\s*:\s*%s_reg_reg_next_gen' % re.escape(T), ln)):
j = i
while j > 0 and not re.search(r'^\s*(always|generate|genvar|\s*for)', lines[j]):
j -= 1
# generate blocks start at 'generate'/'genvar'; always at 'always'
while j > 0 and re.match(r'^\s*for\b', lines[j]):
j -= 1
if re.match(r'^\s*genvar', lines[j - 1] if j else ''):
j -= 1
s2, e2 = find_block(lines, j, None)
# extend to endgenerate if present
while e2 + 1 < len(lines) and re.match(r'^\s*endgenerate', lines[e2 + 1]):
e2 += 1
kill.update(range(s2, e2 + 1))
if re.match(r'^\s*always\s*@\*', ln):
s2, e2 = find_block(lines, i, None)
body = '\n'.join(lines[s2:e2 + 1])
if f'{TMP}[' in body and f'{T}_1[' in body and S in body:
kill.update(range(s2, e2 + 1))
# delete array declarations
for i, ln in enumerate(lines):
if re.search(r'\b(%s|%s_1|%s_reg_reg|%s_reg_reg_next|%s)\s*\[0:%d\]\s*;'
% tuple(map(re.escape, (T, T, T, T, TMP))) % () if False else
r'\b(' + '|'.join(map(re.escape, (T, T + '_1', T + '_reg_reg',
T + '_reg_reg_next', TMP))) + r')\s+\[0:' + str(D - 1) + r'\]\s*;', ln):
kill.add(i)
if re.match(r'^\s*assign\s+%s\[\d+\]' % re.escape(P), ln):
kill.add(i)
if re.search(r'\b%s\s+\[0:%d\]\s*;' % (re.escape(P), D - 1), ln):
kill.add(i)
survivors = [ln for i, ln in enumerate(lines) if i not in kill]
txt = '\n'.join(survivors)
# ---- rewrite remaining real read sites ----
inited = f'{T}__ipinited'
inst, k = [], 0
zeros = f"{W}'sb" + '0' * W
for arr, gate in ((T, inited), (TMP, f'({S} || {inited})')):
for m in list(re.finditer(re.escape(arr) + r'\[([^\]\n]+)\]', txt)):
expr = m.group(1)
if expr.isdigit():
continue
aq, qq = f'{T}_ipa{k}', f'{T}_ipq{k}'
inst.append(f' wire [{AW-1}:0] {aq};\n wire [{W-1}:0] {qq};\n'
f' assign {aq} = {expr};\n'
f' {ipname} u_{T}_ip{k} (.a({aq}), .spo({qq}));\n')
txt = txt.replace(f'{arr}[{expr}]', f'({gate} ? $signed({qq}) : {zeros})')
k += 1
flag = (f' reg {inited};\n'
f' always @(posedge clk or posedge reset)\n'
f' if (reset) {inited} <= 1\'b0;\n'
f' else if (enb && ({S})) {inited} <= 1\'b1;\n')
txt = txt.replace('endmodule',
f' // ---- rtl-ip-replace(initrom): {T} -> {ipname} ({k} read site(s)) ----\n'
+ flag + ''.join(inst) + '\nendmodule', 1)
print(f' {T} ({W}x{D}) -> {ipname}, {k} read site(s), guard={S}')
open(path, 'w', encoding='utf-8').write(txt)
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(rtl_ip, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()
if k != 'content_hash'))
print('ip_manifest updated')
if __name__ == '__main__':
main()
+110
View File
@@ -0,0 +1,110 @@
"""Shared helpers: source copying + PROVENANCE.log (mandatory traceability).
RULE (user directive 2026-07-23): script-processed RTL lives ONLY in the fixed
directory <repo>/vivado/rtl_ip/<model>/ ; the Vivado project may reference RTL
ONLY from there, NEVER from the MATLAB hdlsrc directly. PROVENANCE.log in the
fixed directory records the absolute path of the original MATLAB RTL.
Generated Xilinx IPs live in <fixed_dir>/ip/.
"""
import difflib, hashlib, os, re, shutil, time
SRC_ORIG = 'src_orig' # verbatim snapshot of the MATLAB RTL this run is based on
TRANSFORM_DIFF = 'TRANSFORM.diff'
def src_files(d):
return [fn for fn in sorted(os.listdir(d))
if fn.endswith('.v') and not fn.endswith('_tb.v')]
def sha1_file(p):
return hashlib.sha1(open(p, 'rb').read()).hexdigest()
def copy_sources(hdlsrc, outdir, force=False):
os.makedirs(outdir, exist_ok=True)
copied = []
for fn in src_files(hdlsrc):
dst = os.path.join(outdir, fn)
if force or not os.path.exists(dst):
shutil.copy(os.path.join(hdlsrc, fn), dst)
copied.append(fn)
snapshot_sources(hdlsrc, outdir)
write_provenance(outdir, hdlsrc)
return copied
def snapshot_sources(hdlsrc, outdir):
"""Task12 rule 14: keep the ORIGINAL MATLAB RTL this run is based on, so the
next run can diff against it and reuse unchanged work (incremental mode)."""
d = os.path.join(outdir, SRC_ORIG)
os.makedirs(d, exist_ok=True)
for fn in src_files(hdlsrc):
dst = os.path.join(d, fn)
if not os.path.exists(dst) or sha1_file(dst) != sha1_file(os.path.join(hdlsrc, fn)):
shutil.copy(os.path.join(hdlsrc, fn), dst)
return d
_HUNK = re.compile(r'^@@ -\d+(,\d+)? \+\d+(,\d+)? @@')
def write_transform_diff(outdir):
"""src_orig/*.v -> rewritten *.v, as removed/added lines only (n=0) with hunk
line numbers normalised away: two runs whose scripts performed the SAME
rewrite produce byte-identical files, even if the MATLAB source changed
elsewhere (that is exactly the class-B case of Task12 §5.2)."""
orig = os.path.join(outdir, SRC_ORIG)
out = []
for fn in src_files(orig):
mod = os.path.join(outdir, fn)
if not os.path.exists(mod):
out.append(f'=== {fn}: MISSING in outdir')
continue
a = open(os.path.join(orig, fn), errors='replace').read().splitlines()
b = open(mod, errors='replace').read().splitlines()
d = [_HUNK.sub('@@', ln) for ln in
difflib.unified_diff(a, b, fromfile=f'orig/{fn}', tofile=f'ip/{fn}',
n=0, lineterm='')]
if d:
out.extend(d)
p = os.path.join(outdir, TRANSFORM_DIFF)
open(p, 'w').write('\n'.join(out) + '\n')
return p
def scripts_sha1(script_dir=None):
"""Fingerprint of the skill itself -- changed scripts forbid inheriting a
previous cycle-true PASS (Task12 §5.4/5.5)."""
d = script_dir or os.path.dirname(os.path.abspath(__file__))
h = hashlib.sha1()
for fn in sorted(os.listdir(d)):
if fn.endswith(('.py', '.tcl')):
h.update(fn.encode())
h.update(open(os.path.join(d, fn), 'rb').read())
return h.hexdigest()[:12]
def write_provenance(outdir, hdlsrc):
hdlsrc = os.path.abspath(hdlsrc)
lines = ['# PROVENANCE.log -- generated by rtl-ip-replace, do not edit',
'# RULE: the Vivado project must use RTL from THIS directory only;',
'# direct use of the MATLAB-generated hdlsrc below is FORBIDDEN.',
'source_hdlsrc: ' + hdlsrc,
'updated_at: ' + time.strftime('%Y-%m-%d %H:%M:%S'),
'ip_subdir: ip/',
'files:']
for fn in src_files(hdlsrc):
p = os.path.join(hdlsrc, fn)
h = sha1_file(p)[:12]
mt = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(p)))
lines.append(f' {fn} src_sha1={h} src_mtime={mt}')
lines.append('src_snapshot: ' + SRC_ORIG + '/ # original MATLAB RTL kept for the next incremental run')
lines.append('scripts_sha1: ' + scripts_sha1())
open(os.path.join(outdir, 'PROVENANCE.log'), 'w').write('\n'.join(lines) + '\n')
def append_provenance(outdir, lines):
"""Append the incremental-run block (Task12 §5.6). Called last, after the
transform scripts have rewritten PROVENANCE.log."""
with open(os.path.join(outdir, 'PROVENANCE.log'), 'a') as f:
f.write('\n'.join(lines) + '\n')
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Suffix every module in rtl_ip/ with _IP so original and IP-based RTL can
coexist in one xsim `work` library for the dual-DUT cycle-true comparison.
- renames `module X` -> `module X_IP` for every module DECLARED in rtl_ip/*.v
- rewrites instantiations of those modules accordingly (word-boundary match)
- IP core instances (dist_mem_gen/blk_mem_gen/... names from ip_manifest) are
left untouched -- they are not declared here.
Run LAST, after rom_to_ip.py / ram_to_ip.py / any hand edits (CORDIC, FIFO).
Idempotent: modules already ending in _IP are skipped.
"""
import argparse, os, re
def main():
ap = argparse.ArgumentParser()
ap.add_argument('rtl_ip_dir')
a = ap.parse_args()
d = os.path.abspath(a.rtl_ip_dir)
files = [os.path.join(d, f) for f in os.listdir(d) if f.endswith('.v')]
mods = []
for p in files:
for m in re.finditer(r'^\s*module\s+(\w+)', open(p, encoding='utf-8', errors='replace').read(), re.M):
if not m.group(1).endswith('_IP'):
mods.append(m.group(1))
mods = sorted(set(mods), key=len, reverse=True)
print('modules:', mods)
for p in files:
txt = open(p, encoding='utf-8', errors='replace').read()
for mod in mods:
txt = re.sub(r'\b%s\b(?!_IP)' % re.escape(mod), mod + '_IP', txt)
open(p, 'w', encoding='utf-8').write(txt)
print(f'suffixed {len(mods)} modules across {len(files)} files')
if __name__ == '__main__':
main()
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Replace SimpleDualPortRAM_generic (HDL Coder) with blk_mem_gen SDP instances.
Semantics of the original (must be preserved exactly):
- single clock, `enb` gates BOTH write and read registration
- read latency 1 cycle; write/read same address -> read returns OLD data (read-first)
=> blk_mem_gen: Simple Dual Port, common clock, Port-A write mode READ_FIRST,
ENA/ENB pins used, NO output register.
The wrapper keeps the module name/ports/params; a generate-if selects the
per-(AddrWidth,DataWidth) IP; unlisted combos fall back to the original
behavioral body (so simulation still works, and synthesis of an unlisted combo
is loudly visible in the utilization report instead of silently wrong).
Run AFTER rom_to_ip.py (needs rtl_ip/ copies), BEFORE namespace_ip.py.
"""
import argparse, json, os, re
WRAP = '''// regenerated by ram_to_ip.py -- blk_mem_gen SDP wrappers (read-first, 1-cycle, common clk)
`timescale 1 ns / 1 ns
module {mod}
(clk, enb, wr_din, wr_addr, wr_en, rd_addr, dout);
parameter integer AddrWidth = 1;
parameter integer DataWidth = 1;
input clk;
input enb;
input signed [DataWidth - 1:0] wr_din;
input [AddrWidth - 1:0] wr_addr;
input wr_en;
input [AddrWidth - 1:0] rd_addr;
output signed [DataWidth - 1:0] dout;
generate
{cases} begin: g_behav // fallback: original behavioral RAM (unlisted param combo)
reg [DataWidth - 1:0] ram [2**AddrWidth - 1:0];
reg [DataWidth - 1:0] data_int;
integer i;
initial begin
for (i=0; i<=2**AddrWidth - 1; i=i+1) ram[i] = 0;
data_int = 0;
end
always @(posedge clk)
if (enb == 1'b1) begin
if (wr_en == 1'b1) ram[wr_addr] <= wr_din;
data_int <= ram[rd_addr];
end
assign dout = data_int;
end
endgenerate
endmodule
'''
CASE = ''' if (AddrWidth == {aw} && DataWidth == {dw}) begin: g_a{aw}d{dw}
wire [{dwm}:0] doutb_i;
{ip} u_bram (
.clka(clk), .ena(enb), .wea(wr_en), .addra(wr_addr), .dina(wr_din),
.clkb(clk), .enb(enb), .addrb(rd_addr), .doutb(doutb_i));
assign dout = doutb_i;
end else
'''
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', help='FIXED output dir (e.g. <repo>/vivado/rtl_ip/<model>)')
ap.add_argument('--manifest', default=None)
a = ap.parse_args()
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip = os.path.abspath(a.outdir)
man = json.load(open(a.manifest or os.path.join(rtl_ip, 'manifest.json')))
combos = sorted({(r['AddrWidth'], r['DataWidth']) for r in man['ram']
if r['AddrWidth'] and r['DataWidth']})
if not combos:
print('no SimpleDualPortRAM instantiations found'); return
ipman_p = os.path.join(rtl_ip, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
have = {ip['name'] for ip in ipman['ips']}
cases = ''
for aw, dw in combos:
ip = f'bram_sdp_a{aw}d{dw}'
cases += CASE.format(aw=aw, dw=dw, dwm=dw - 1, ip=ip)
if ip not in have:
ipman['ips'].append({'type': 'blk_mem_gen', 'name': ip, 'width': dw,
'depth': 2 ** aw, 'mode': 'sdp_readfirst_commonclk'})
# rewrite every SimpleDualPortRAM_*.v module file present in rtl_ip
n = 0
for fn in os.listdir(rtl_ip):
if re.match(r'SimpleDualPortRAM_\w+\.v$', fn):
mod = fn[:-2]
open(os.path.join(rtl_ip, fn), 'w').write(WRAP.format(mod=mod, cases=cases))
print('wrapped', mod, 'combos:', combos)
n += 1
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(rtl_ip, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()))
print(f'done: {n} RAM module(s) wrapped, {len(combos)} blk_mem_gen config(s)')
if __name__ == '__main__':
main()
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Replace fully-constant wire-array ROMs with Vivado dist_mem_gen instances.
- Copies every non-tb .v from <hdlsrc> into <hdlsrc>/rtl_ip/ (first run only, or --force).
- For each fully-constant ROM in the manifest:
* extracts the table into a .coe (dedup by content hash: identical tables share one IP config)
* deletes the wire-array declaration and the constant assigns from the copy
* per READ SITE inserts: wire [AW-1:0] <name>_a<k>; wire [W-1:0] <name>_q<k>;
assign <name>_a<k> = <read_expr>;
<ip> u_<name>_r<k> (.a(<name>_a<k>), .spo(<name>_q<k>));
and rewrites `<name>[<read_expr>]` -> `$signed(<name>_q<k>)` (or unsigned).
- Appends dist_mem_gen entries to ip_manifest.json / ip_manifest.tcl.
dist_mem_gen is ASYNC read => bit- and cycle-true against the original combinational read.
Run AFTER rtl_ip_scan.py, BEFORE namespace_ip.py.
"""
import argparse, hashlib, json, os, re, shutil, sys
def to_bits(radix, lit, width):
lit = lit.replace('_', '')
base = {'b': 2, 'd': 10, 'h': 16}[radix]
v = int(lit, base) & ((1 << width) - 1)
return format(v, '0%db' % width)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', help='FIXED output dir (e.g. <repo>/vivado/rtl_ip/<model>)')
ap.add_argument('--manifest', default=None)
ap.add_argument('--force', action='store_true')
a = ap.parse_args()
from ipflow_common import copy_sources
hdlsrc = os.path.abspath(a.hdlsrc)
rtl_ip = os.path.abspath(a.outdir)
os.makedirs(rtl_ip, exist_ok=True)
man = json.load(open(a.manifest or os.path.join(rtl_ip, 'manifest.json')))
# 1. copy sources + PROVENANCE.log
copy_sources(hdlsrc, rtl_ip, force=a.force)
ipman_p = os.path.join(rtl_ip, 'ip_manifest.json')
ipman = json.load(open(ipman_p)) if os.path.exists(ipman_p) else {'ips': []}
coe_of_hash, roms_done = {}, 0
CONST_ASSIGN = re.compile(r"^\s*assign\s+(\w+)\[(\d+)\]\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_]+)\s*;\s*$", re.M)
by_file = {}
for rom in man['const_rom']:
if not rom.get('fully_const'):
print('SKIP (partial const):', rom['name']); continue
by_file.setdefault(rom['file'], []).append(rom)
for fn, roms in by_file.items():
path = os.path.join(rtl_ip, fn)
txt = open(path, encoding='utf-8', errors='replace').read()
for rom in roms:
name, W, D, signed = rom['name'], rom['width'], rom['depth'], rom['signed']
AW = max(1, (D - 1).bit_length())
# collect table
bits = {}
for m in CONST_ASSIGN.finditer(txt):
if m.group(1) == name:
bits[int(m.group(2))] = to_bits(m.group(4), m.group(5), W)
if len(bits) != D:
print(f'ERROR {name}: found {len(bits)}/{D} const assigns -- skipped'); continue
content = '\n'.join(bits[i] for i in range(D))
h = hashlib.sha1((f'{W}x{D}\n' + content).encode()).hexdigest()[:10]
if h not in coe_of_hash:
ipname = f'rom_{W}x{D}_{h}'
coe = os.path.join(rtl_ip, ipname + '.coe')
with open(coe, 'w') as f:
f.write('memory_initialization_radix=2;\nmemory_initialization_vector=\n')
f.write(',\n'.join(bits[i] for i in range(D)) + ';\n')
coe_of_hash[h] = ipname
ipman['ips'].append({'type': 'dist_mem_gen', 'name': ipname, 'width': W,
'depth': D, 'coe': os.path.abspath(coe)})
ipname = coe_of_hash[h]
# remove decl + assigns
txt = re.sub(r'^\s*wire\s+(signed\s+)?\[%d:0\]\s+%s\s+\[0:%d\]\s*;.*\n'
% (W - 1, re.escape(name), D - 1), '', txt, flags=re.M)
txt = re.sub(r"^\s*assign\s+%s\[\d+\]\s*=\s*[^;]+;\s*\n" % re.escape(name), '', txt, flags=re.M)
# rewrite read sites (unique exprs; every occurrence of same expr shares one instance)
inst_lines, k = [], 0
for expr in dict.fromkeys(rom['read_exprs']): # preserve order, dedup
aq, qq = f'{name}_a{k}', f'{name}_q{k}'
inst_lines.append(f' wire [{AW-1}:0] {aq};\n wire [{W-1}:0] {qq};\n'
f' assign {aq} = {expr};\n'
f' {ipname} u_{name}_r{k} (.a({aq}), .spo({qq}));\n')
rd = f'$signed({qq})' if signed else qq
txt = txt.replace(f'{name}[{expr}]', rd)
k += 1
# place instances right before `endmodule`
txt = txt.replace('endmodule', ' // ---- rtl-ip-replace: %s -> %s (%d read site(s)) ----\n%s\nendmodule'
% (name, ipname, k, ''.join(inst_lines)), 1)
roms_done += 1
print(f'ROM {name} ({W}x{D}, {k} reads) -> {ipname} [{fn}]')
open(path, 'w', encoding='utf-8').write(txt)
json.dump(ipman, open(ipman_p, 'w'), indent=1)
with open(os.path.join(rtl_ip, 'ip_manifest.tcl'), 'w') as f:
for ip in ipman['ips']:
f.write('lappend ips {%s}\n' % ' '.join(f'{k} {{{v}}}' for k, v in ip.items()))
print(f'done: {roms_done} ROMs, {len(coe_of_hash)} unique tables; ip_manifest updated')
if __name__ == '__main__':
main()
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""Scan HDL Coder generated Verilog for IP-replaceable structures.
Emits a JSON manifest of:
const_rom : `wire [W:0] name [0:D];` fully populated by constant assigns -> dist/blk ROM IP
reg_table : `reg` arrays (runtime-written tables) -> review by hand (RAM or leave)
ram : SimpleDualPortRAM_generic instantiations (AddrWidth/DataWidth combos) -> blk_mem_gen
fifo : modules/instances whose name contains 'fifo' -> fifo_generator (guided)
cordic : files with the cx/cy/cz + atanT iterative kernel -> cordic IP (guided)
filter : FIR filters (Task12 §六) -> fir_compiler / cic_compiler (guided)
fir_coef_bank : HDL Coder <Name>Coef module, constant CoefOut_i taps
fir_block : module whose header says "Discrete FIR Filter"
adaptive_fir_candidate: hand-rolled tap regs + delay line + MAC (LMS etc.)
Usage: python rtl_ip_scan.py <hdlsrc_dir> [<outdir>] [-o manifest.json]
"""
import argparse, json, os, re, sys
ROM_DECL = re.compile(r'^\s*wire\s+(signed\s+)?\[(\d+):0\]\s+(\w+)\s+\[0:(\d+)\]\s*;', re.M)
REG_DECL = re.compile(r'^\s*reg\s+(signed\s+)?\[(\d+):0\]\s+(\w+)\s+\[0:(\d+)\]\s*;', re.M)
CONST_ASSIGN = re.compile(r"^\s*assign\s+(\w+)\[(\d+)\]\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_xzXZ]+)\s*;", re.M)
RAM_INST = re.compile(r'SimpleDualPortRAM_\w+\s*#\s*\(((?:[^()]|\([^()]*\))*)\)\s*(\w+)\s*\(', re.S)
PARAM = re.compile(r'\.(\w+)\s*\(\s*(\d+)\s*\)')
# ---- filter (Task12 §六) ----------------------------------------------------
MODULE = re.compile(r'^[ \t]*module\s+(\w+)\b(.*?)^\s*endmodule', re.M | re.S)
DECL = re.compile(r'^\s*(input|output|inout|wire|reg)\s+(signed\s+)?(?:\[(\d+):0\]\s+)?(\w+)\s*;'
r'(?:[ \t]*//[ \t]*([usq]fix\d+(?:_En-?\d+)?|\w+))?', re.M)
CONST_SIG = re.compile(r"^\s*assign\s+(\w+)\s*=\s*(\d+)'s?([bdh])([0-9a-fA-F_]+)\s*;", re.M)
BASE_IDX = re.compile(r'^(.*?)_?(\d+)$')
DLY_CHAIN = re.compile(r'^\s*(\w*?)(\d+)_next(_re|_im)?\s*=\s*\1(\d+)(?:_re|_im)?\s*;', re.M)
FIXPT = re.compile(r'([us])fix(\d+)(?:_En(-?\d+))?')
def to_signed(width, radix, digits):
v = int(digits.replace('_', ''), {'b': 2, 'd': 10, 'h': 16}[radix])
return v - (1 << width) if v >> (width - 1) else v
def split_modules(txt):
"""[(name, body, header_comment_block)] -- HDL Coder writes the Simulink block
kind ('Discrete FIR Filter') and Source Path in the comment above the module."""
out = []
for m in MODULE.finditer(txt):
out.append((m.group(1), m.group(2), txt[max(0, m.start() - 1200):m.start()]))
return out
def decls_of(body):
d = {}
for m in DECL.finditer(body):
d[m.group(4)] = {'dir': m.group(1), 'signed': m.group(2) is not None,
'width': int(m.group(3)) + 1 if m.group(3) else 1,
'type': m.group(5) or ''}
return d
def fixpt_of(type_comment, width):
m = FIXPT.search(type_comment or '')
if not m:
return {'width': width, 'frac': None, 'signed': None}
return {'width': int(m.group(2)), 'frac': int(m.group(3) or 0), 'signed': m.group(1) == 's'}
def const_groups(body, decls):
"""constant `assign base_<i> = W'sh...;` families, index-complete from 0."""
vals = {}
for m in CONST_SIG.finditer(body):
name, w, radix, digits = m.group(1), int(m.group(2)), m.group(3), m.group(4)
vals[name] = (w, to_signed(w, radix, digits))
groups = {}
for name, (w, v) in vals.items():
bm = BASE_IDX.match(name)
if bm:
groups.setdefault((bm.group(1), w), {})[int(bm.group(2))] = v
# HDL Coder names the first element without an index (CoefData, CoefData_1, ...)
for (base, w), members in groups.items():
if 0 not in members and base in vals and vals[base][0] == w:
members[0] = vals[base][1]
out = []
for (base, w), members in groups.items():
n = len(members)
if n < 4 or sorted(members) != list(range(n)):
continue
coeffs = [members[i] for i in range(n)]
ex = decls.get(base + '_0') or decls.get(base + '0') or decls.get(base) or {}
out.append({'base': base, 'n': n, 'width': w, 'coeffs': coeffs,
'frac': fixpt_of(ex.get('type', ''), w)['frac'],
'symmetric': coeffs == coeffs[::-1]})
return out
def scan_filters(path, txt):
"""fir_coef_bank / fir_block / adaptive_fir_candidate (guided replacement)."""
found = []
fn = os.path.basename(path)
for name, body, hdr in split_modules(txt):
decls = decls_of(body)
is_fir_hdr = 'Discrete FIR Filter' in hdr
srcpath = (re.search(r'//\s*Source Path:\s*(\S+)', hdr) or [None, ''])[1] \
if re.search(r'//\s*Source Path:\s*(\S+)', hdr) else ''
ports = {k: v for k, v in decls.items() if v['dir'] in ('input', 'output')}
coef_in = [p for p in ports if re.match(r'coef', p, re.I) and ports[p]['dir'] == 'input']
for g in const_groups(body, decls):
# A constant family is only a TAP BANK if it is named/exported as one, or
# if it is fed to multipliers inside a FIR module. Without this guard every
# HDL Coder state/parameter constant family (state_1, mu_2, ...) looks like
# a filter -- 5 false positives on RxAlignCore alone.
exports = [p for p in ports if ports[p]['dir'] == 'output'
and re.search(r'coef|tap', p, re.I)]
named = re.search(r'coef|tap', name, re.I) or re.search(r'coef|tap', g['base'], re.I)
multiplied = any(re.search(r'\b%s(?:_\d+)?\s*\*|\*\s*%s(?:_\d+)?\b'
% (re.escape(g['base']), re.escape(g['base'])), body)
for _ in (0,)) and (is_fir_hdr or re.search(r'fir|filt', name, re.I))
if not (exports or named or multiplied):
continue
found.append({'file': fn, 'module': name, 'kind': 'fir_coef_bank',
'source_path': srcpath, 'coef_signal': g['base'],
'taps': g['n'], 'coef_width': g['width'], 'coef_frac': g['frac'],
'symmetric': g['symmetric'], 'coeffs': g['coeffs'],
'exports_taps': bool(exports),
'note': 'fixed coefficients -> fir_compiler (.coe), guided (Task12 §6.2)'})
din = [p for p in ports if re.match(r'(data|d)?in', p, re.I) and ports[p]['dir'] == 'input'
and ports[p]['width'] > 1]
dout = [p for p in ports if re.search(r'out', p, re.I) and ports[p]['dir'] == 'output'
and ports[p]['width'] > 1]
if is_fir_hdr or (re.search(r'fir|filt', name, re.I) and din and dout):
e = {'file': fn, 'module': name,
'kind': 'fir_block' if is_fir_hdr else 'fir_block_candidate',
'source_path': srcpath,
'complex': any(p.endswith('_re') for p in din),
'data_in': {p: fixpt_of(ports[p]['type'], ports[p]['width']) for p in sorted(din)},
'data_out': {p: fixpt_of(ports[p]['type'], ports[p]['width']) for p in sorted(dout)},
'valid_in': any(re.match(r'valid|.*vld', p, re.I) and ports[p]['dir'] == 'input'
for p in ports),
'valid_out': any(re.match(r'valid|.*vld', p, re.I) and ports[p]['dir'] == 'output'
for p in ports),
'coef_ports': sorted(coef_in),
'adaptive': bool(coef_in),
'rate_hint': ('interpolation' if re.search(r'interp|up', name, re.I) else
'decimation' if re.search(r'decim|down', name, re.I) else 'single_rate?'),
'note': 'rate/phase must be confirmed against the Simulink block before IP config'}
found.append(e)
# hand-rolled adaptive FIR: tap regs + delay-line chain + multiplies
chains = {}
for m in DLY_CHAIN.finditer(body):
if int(m.group(2)) == int(m.group(4)) + 1:
chains.setdefault(m.group(1), set()).add(int(m.group(2)))
chain = max(((b, len(s)) for b, s in chains.items()), key=lambda x: x[1], default=None)
# tap registers: indexed reg group named like weights/taps. Do NOT require the
# reg itself to sit next to a `*` -- HDL Coder feeds multipliers through operand
# temporaries (da0_re * db0_re), so that test misses every real case (EqCore).
tapregs = {}
for rn, d in decls.items():
bm = BASE_IDX.match(re.sub(r'_(re|im)$', '', rn))
if d['dir'] == 'reg' and d['width'] > 1 and bm \
and re.fullmatch(r'w\w*|tap\w*|coef\w*|h', bm.group(1), re.I):
tapregs.setdefault(bm.group(1), set()).add(int(bm.group(2)))
taps = max(((b, len(s)) for b, s in tapregs.items()), key=lambda x: x[1], default=None)
if chain and chain[1] >= 3 and taps and taps[1] >= 3 and '*' in body:
found.append({'file': fn, 'module': name, 'kind': 'adaptive_fir_candidate',
'source_path': srcpath, 'tap_regs': taps[0], 'taps': taps[1],
'delay_line': chain[0], 'delay_depth': chain[1] + 1,
'note': 'runtime-updated taps: fir_compiler Reloadable, or KEEP the RTL '
'with a written reason (Task12 §6.1)'})
return found
KINDS = ('const_rom', 'reg_table', 'ram', 'fifo', 'cordic', 'filter')
def scan_file(path):
txt = open(path, encoding='utf-8', errors='replace').read()
out = {k: [] for k in KINDS}
# constant assigns grouped by array name
assigns = {}
for m in CONST_ASSIGN.finditer(txt):
assigns.setdefault(m.group(1), {})[int(m.group(2))] = (m.group(3), m.group(4), m.group(5))
for m in ROM_DECL.finditer(txt):
signed, msb, name, dmax = m.group(1) is not None, int(m.group(2)), m.group(3), int(m.group(4))
depth = dmax + 1
vals = assigns.get(name, {})
# read sites: name[expr] not on its own assign-constant lines
reads = []
for rm in re.finditer(re.escape(name) + r'\[([^\]\n]+)\]', txt):
idx = rm.group(1)
if idx.isdigit(): # constant index = the table-fill assigns
continue
reads.append(idx.strip())
entry = {'file': os.path.basename(path), 'name': name, 'width': msb + 1,
'depth': depth, 'signed': signed, 'n_const': len(vals),
'fully_const': len(vals) == depth, 'read_exprs': reads}
if len(vals) == depth and reads:
out['const_rom'].append(entry)
elif len(vals) > 0:
entry['note'] = 'PARTIAL const fill -- inspect by hand'
out['const_rom'].append(entry)
for m in REG_DECL.finditer(txt):
out['reg_table'].append({'file': os.path.basename(path), 'name': m.group(3),
'width': int(m.group(2)) + 1, 'depth': int(m.group(4)) + 1,
'note': 'runtime-written reg array: treat as RAM or leave as-is'})
for m in RAM_INST.finditer(txt):
params = dict((k, int(v)) for k, v in PARAM.findall(m.group(1)))
out['ram'].append({'file': os.path.basename(path), 'inst': m.group(2),
'AddrWidth': params.get('AddrWidth'), 'DataWidth': params.get('DataWidth')})
for m in re.finditer(r'^\s*module\s+(\w*[fF][iI][fF][oO]\w*)', txt, re.M):
out['fifo'].append({'file': os.path.basename(path), 'module': m.group(1)})
if re.search(r'\bcx\b', txt) and re.search(r'\bcz\b', txt) and 'atanT' in txt:
out['cordic'].append({'file': os.path.basename(path),
'note': 'iterative CORDIC kernel (cx/cy/cz + atanT) -- guided replacement'})
out['filter'] = scan_filters(path, txt)
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument('hdlsrc')
ap.add_argument('outdir', nargs='?', default=None,
help='FIXED output dir; manifest goes to <outdir>/manifest.json')
ap.add_argument('-o', '--out', default=None)
a = ap.parse_args()
if a.outdir and not a.out:
a.out = os.path.join(a.outdir, 'manifest.json')
manifest = dict({'hdlsrc': os.path.abspath(a.hdlsrc), 'files': {}}, **{k: [] for k in KINDS})
for fn in sorted(os.listdir(a.hdlsrc)):
if not fn.endswith('.v') or fn.endswith('_tb.v'):
continue
r = scan_file(os.path.join(a.hdlsrc, fn))
for k in KINDS:
manifest[k].extend(r[k])
# a Discrete FIR Filter block expands into a tree of helper modules (Filter,
# subFilter, FilterTapSystolic*): keep the block, drop its own guts.
tops = [f['source_path'] for f in manifest['filter']
if f['kind'] == 'fir_block' and f.get('source_path')]
kept = []
for f in manifest['filter']:
inner = any(f.get('source_path', '').startswith(t + '/') for t in tops)
if inner and f['kind'] == 'fir_block_candidate':
continue # internal plumbing of a detected FIR block
if inner and f['kind'] == 'fir_coef_bank':
f['belongs_to'] = next(t for t in tops if f['source_path'].startswith(t + '/'))
kept.append(f)
manifest['filter'] = kept
outp = a.out or os.path.join(a.hdlsrc, 'rtl_ip', 'manifest.json')
os.makedirs(os.path.dirname(outp), exist_ok=True)
json.dump(manifest, open(outp, 'w'), indent=1)
print('manifest ->', outp)
for k in KINDS:
print(f' {k}: {len(manifest[k])}')
for e in manifest[k]:
brief = {kk: (f'[{len(vv)} values]' if kk == 'coeffs' else vv)
for kk, vv in e.items() if kk != 'read_exprs'}
print(' ', brief,
f"reads={len(e.get('read_exprs', []))}" if 'read_exprs' in e else '')
if __name__ == '__main__':
main()
+29 -12
View File
@@ -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. nativize all submodules in one Simulink model.
3. **Never hand-edit MATLAB-generated RTL.** If IP replacement, Vivado timing, or HIL 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`. 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 The graph then forces `modify_e:* → sync_sim:*` before any RTL is regenerated.
against sim-golden** — before any RTL is regenerated. 4. **Editing a `-E` model obliges you to sync the `-x`/`-q` model.** `modify_e:*` edits
4. **Timing fixes change the RTL, not the constraints.** Prefer adding pipeline/delay 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, stages. Only touch timing constraints when the constraint itself is plainly wrong,
and say why in `notes`. 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 `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 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. 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` | | `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) | | `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 | | `module_plan#n` | split the RTL side into A/B/C…, give order + dependencies (**first pass only**) | inline |
| `native:<M>#n` | nativize M into HDL-capable blocks, save `<project>_M-E.slx`, generate RTL | request says `simulink-rtl-dev` — **not installed**; use MATLAB MCP + model-based-design-core | | `native:<M>#n` | nativize M into HDL-capable blocks, save `<project>_M-E.slx`, generate RTL | `simulink_rtl_dev` |
| `ip_replace:<M>#n` | swap in Xilinx IP, cycle-true compare vs MATLAB RTL | request says `rtl-ip-replace` — **not installed**; use `vivado-sim` + `vivado-mcp` | | `ip_replace:<M>#n` | swap in Xilinx IP, cycle-true compare vs MATLAB RTL | `rtl-ip-replace` |
| `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 | | `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:<source>#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` | | `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` | | `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 | | `timing_rework#n` | fix non-Simulink RTL / SoC / project timing | inline |
@@ -215,12 +222,17 @@ review: {"approved": true, "notes": "", "metadata": {}}
Routing on `change_scope`: Routing on `change_scope`:
- `simulink_module` → **`modify_e:*`** (edit `-E.slx` + re-simulate) → only the affected - `simulink_module` → **`modify_e:*`** (edit the `-E.slx`) → **`sync_sim:*`** (carry it
modules re-run `native → ip_replace` → Vivado → HIL. 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` - `non_simulink_rtl` / `soc` / `hardware` → `timing_rework` (from Vivado) or `hil_rework`
(from HIL) → Vivado. (from HIL) → Vivado.
- The driver **rejects** `needs_code_change: true` with `change_scope: "none"`. - The driver **rejects** `needs_code_change: true` with `change_scope: "none"`.
A failing `ip_replace:<M>` (cycle-true mismatch) enters the same chain on its own:
`evaluate_modules → modify_e:module → sync_sim:module → native:<M>`. A failing
`native:<M>` does **not** — the `-E` isn't sound yet, so it just re-runs nativization.
### 6. Parallel batches (verified) ### 6. Parallel batches (verified)
| stage | keys handed over at once | | 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. Mermaid can't show. Use `Notes/Result00.md` for the authoritative picture.
- **`--max-attempts` must precede the subcommand** and stay the *same value* across - **`--max-attempts` must precede the subcommand** and stay the *same value* across
`start` and every `resume` — it is runtime context, not persisted state. `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`, - **The skill names the graph hands you are real and exact** — `simulink_rtl_dev`
`ip_replace:*` suggests `rtl-ip-replace`; neither is installed. Fall back to the MATLAB (**underscores**, not dashes), `rtl-ip-replace`, `simulink-rtl-redev`. All three are
MCP + `model-based-design-core:*` skills and `vivado-sim` / `vivado-mcp`. Don't abort. 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: []` - **`simulink_x`'s first artifact silently becomes sim-golden.** Record `artifacts: []`
and the graph substitutes `<project_name>-x.slx`. Always record the real filename. and the graph substitutes `<project_name>-x.slx`. Always record the real filename.
- **`module_plan` runs only on the first pass.** Rework rounds reuse the plan, so you - **`module_plan` runs only on the first pass.** Rework rounds reuse the plan, so you
+237
View File
@@ -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_<model>.json`),
声明这个模型的源与派生产物。**`root` 相对 manifest 所在目录**,其余路径相对 `root`:
```json
{
"model": "<model>",
"root": "..",
"native_model": "matlab/<proj>/xxx_h.slx",
"build_scripts": ["matlab/<proj>/build_core.m"],
"gen_scripts": ["matlab/<proj>/make_sim.m"],
"mcode_impl": ["matlab/<proj>/core_mcode.m"],
"runtime_deps": ["matlab/<proj>/pscore.m", "matlab/<proj>/coef.mat"],
"sim_model": "matlab/<proj>/xxx_hs.slx",
"rtl_source_model": "matlab/<proj>/xxx_h_<Mod>_E.slx",
"gate_result": "Notes/gate_<model>.txt",
"gate_tools": ["matlab/<proj>/gate.m", "matlab/<proj>/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_<model>.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 -> <key>: <path>` | 该路径不存在。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 只承载执行路径与工具。
+185
View File
@@ -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 <manifest.json> [--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:]))
+199
View File
@@ -0,0 +1,199 @@
function [K, R] = measure_k(ref, dut, varargin)
%MEASURE_K Measure the integer delay K that <dut> lags <ref>, 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
+154
View File
@@ -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/<model>_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('<model>/<HDL 顶层>','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('<model>/<HDL 顶层>', '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('<model>/<HDL 顶层>', ...
'Exemptions','Notes/<model>_mcode_exemptions.json', ...
'Out','Notes/<model>_hdl_audit.json');
```
Shell 侧(闸门,退出码即结论):
```
python "%USERPROFILE%\.claude\skills\simulink_rtl_dev\scripts\check_exemptions.py" ^
Notes\<model>_hdl_audit.json Notes\<model>_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)搭。
@@ -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 + 登记豁免**。
+208
View File
@@ -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<AGROW>
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<AGROW>
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<AGROW>
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, '\<persistent\>', '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)'; ...
'\<atan2?\s*\(', 'atan/atan2: Complex to Magnitude-Angle HDL Optimized (CORDIC)'; ...
'\<(sqrt|abs)\s*\(', 'sqrt/abs: Sqrt HDL Optimized / Complex to Magnitude-Angle HDL Optimized'; ...
'\<(filter|conv)\s*\(', 'FIR: Discrete FIR Filter (HDL) -- keeps taps as a block parameter'; ...
'persistent\s+\w*(buf|mem|ram|dl)', 'sample memory: Simple Dual Port RAM / Delay / Tapped Delay'; ...
'\<(fft|ifft)\s*\(', 'FFT: FFT HDL Optimized'; ...
'switch\s+\w*(state|st)\>', 'control FSM: Stateflow Chart (control only, no datapath math)'; ...
'\<mod\s*\(|\+\s*1\s*;.*count', 'counter: HDL Counter / Counter Free-Running'};
for i = 1:size(map, 1)
if ~isempty(regexp(script, map{i,1}, 'once'))
hits{end+1} = map{i,2}; %#ok<AGROW>
end
end
if isempty(hits)
s = 'no keyword match -- rebuild from primitives (Delay/Product/Sum/Switch/Lookup Table)';
else
s = strjoin(hits, ' | ');
end
end
@@ -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 <hdl_audit.json> [<exemptions.json>]
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()
@@ -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
@@ -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/<model>_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"
}
]
}
@@ -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<AGROW>
hits{end, 2} = blks{k}; %#ok<AGROW>
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