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:
@@ -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 化从根上解决。接入后优先扫这一类。
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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')
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 只承载执行路径与工具。
|
||||||
@@ -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:]))
|
||||||
@@ -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
|
||||||
@@ -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 + 登记豁免**。
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user