Verilator MODDUP 同名模块重复定义排查:文件去重与改名修复
更新时间:2026-10-30。FAQ 速查页,属于 EDA 报错排查系列。Verilator MODDUP 对应
Duplicate declaration of module:同一次 lint 读入的两个源文件都声明了同名module,工具无法让两份实现共用一个设计名字。相邻问题可对照 Verilator DECLFILENAME 文件名与模块名不一致 与 Verilator 找不到模块文件。
首屏答案:看 MODDUP 给出的两个位置。同一文件出现两次,先给命令行、.f 文件列表和 `include 路径去重;不同文件各有定义,内容相同就淘汰旧副本,内容不同就给其中一套模块及例化统一改名。--bbox-unsup 只把不支持的结构处理成黑盒,不解决名字冲突,也不是通用消重开关。
本文要回答的问题
- MODDUP 报错里的原始定义和重复定义分别在哪里?
- 文件列表重复、旧副本混入与不同 IP 撞名怎么区分?
- 为什么
--bbox-unsup和简单消音不是正确修复? - 去重或改名之后,怎样复跑确认问题闭环?
排查动线只有四层:先拿两个定义位置,再判断是不是同一份代码,随后选择去重或改名,最后用原参数复跑。不要把「警告消失」置于「实例选中了正确实现」之前。
这张图的关键是第二层分流:文件重复与模块撞名不是一回事。前者修构建输入,后者改设计命名;若不比较两份定义就随手删一个文件,可能把真正要用的实现删掉。
一、完整报错:Verilator MODDUP 指向两个定义
最小工程包含顶层 top、当前实现 pulse_dup.v 和旧实现 legacy_pulse.v。两个被读入的文件都写了 module pulse_dup,只是时序逻辑给 pulse 的值不同。旧文件混入 broken.f 后就足以复现,不需要仿真激励。
# broken.f
top.v
pulse_dup.v
legacy_pulse.vverilator --lint-only -Wall --top-module top -f broken.fVerilator 5.020 的实际输出如下,卡片保留了报错版与修复版的退出码。
报错的主位置是 legacy_pulse.v:1:8,指出后读到的重复声明;缩进的 Location of original declaration 则指向 pulse_dup.v:1:8。这两个位置比错误名本身更有用。-Wall 下这条 warning 按错误收口,所以末尾出现 %Error: Exiting due to 1 warning(s),实测退出码为 1;修复版无告警输出,退出码为 0。
二、版本与根因:设计命名空间不接受两份实现
本篇使用 Debian 打包的 Verilator 5.020。MODDUP 是 module duplicate 的缩写,即模块重复声明。Verilog 的模块名是设计单元在命名空间里的身份,顶层例化 pulse_dup u_pulse (...) 时只能绑定到一份定义。即使两个文件文本完全相同,也不能据此假定「重复无害」;它说明构建边界已经失控,下次其中一份变化就会静默改变设计来源。
图中红色是报错入口,蓝色是定位与比较,橙色是两种根治动作,绿色是复验通过。注意绿色之前没有「加黑盒参数」这条捷径:黑盒改变的是工具如何处理模块内部,不会把两个同名声明变成一个。
三、排查顺序:先找定义,再比较输入路径
在项目根目录用模块名查定义,不要只查同名文件。模块可能放在名字完全不同的文件中,也可能经 `include 被文本展开。
rg -n '^\s*module\s+pulse_dup\b' .
rg -n 'pulse_dup\.v|legacy_pulse\.v' --glob '*.f' --glob 'Makefile*' .
diff -u pulse_dup.v legacy_pulse.v第一条命令应命中报错中的两处;第二条负责追查是谁把旧文件加入构建;第三条帮助判断保留哪一份。如果工程不用 rg,可换成递归 grep,但搜索范围要包含 file list、构建脚本和 include 目录。
| 两个定义的特征 | 常见根因 | 正确处理 |
|---|---|---|
| 同一文件、同一行 | 文件列表或命令行重复传入 | 对输入路径去重 |
| 不同文件,内容相同 | 旧副本、生成目录与源目录同时入列 | 保留权威副本,移除另一输入 |
| 不同文件,逻辑不同 | 两个 IP 或新旧实现撞名 | 重命名一套定义及其例化 |
| 报错文件之外还有 include | 文本包含后又单独编译 | include 与独立编译二选一 |
--bbox-unsup 的全称语义是 black-box unsupported constructs,即把工具不支持的结构黑盒化。它适用于明确接受「不看内部实现」的场景;MODDUP 发生在模块声明层面,黑盒化内部语句不会消除命名冲突。把该选项加到同一条 broken 命令,实测仍在原位置报错:
=== bbox-unsup: not a duplicate-module switch ===
$ verilator --lint-only -Wall --bbox-unsup --top-module top -f broken.f
%Warning-MODDUP: legacy_pulse.v:1:8: Duplicate declaration of module: 'pulse_dup'
1 | module pulse_dup (
| ^~~~~~~~~
pulse_dup.v:1:8: ... Location of original declaration
%Error: Exiting due to 1 warning(s)
bbox-unsup exit=1这段对照说明退出码没有因黑盒参数改变。类似地,全局 -Wno-MODDUP 或大面积 lint_off 只是在掩盖输入冲突,可能让后续工具选到非预期实现;除非有经过审查的特殊生成流程,不应把它当修复。
四、修复与复跑:让文件列表只保留当前实现
本例确认 pulse_dup.v 是当前实现,legacy_pulse.v 是旧副本,所以不用改 RTL,只修改文件列表:
top.v
pulse_dup.v
-legacy_pulse.v复跑原命令:
verilator --lint-only -Wall --top-module top -f fixed.f实测没有任何告警,随后输出 fixed exit=0 和 PASS。这里的 PASS 不只代表文本匹配成功,还由脚本检查了三项退出码:broken 非零、--bbox-unsup 对照非零、fixed 为零。
若两份是都要保留的不同实现,例如 vendor A 与 vendor B 各自提供 pulse_dup,应把其中一套改成 pulse_dup_a,同步更新它的 endmodule 前声明、所有例化、层次路径、约束及验证环境引用,并给另一个实例换用明确的新名字。只改文件名无效,因为 MODDUP 比较的是 module 后的设计名,不是磁盘文件名。
收尾再检查三项:file list 没有由变量展开后再次拼入旧路径;生成文件目录没有与源码目录一起被通配符读入;所有顶层和子模块都完成同一参数复跑。这样修掉的是构建输入与设计命名,而不是一条日志。
实验环境
- 机器/系统:Linux 容器环境(Docker 镜像
geek-eda:0.2) - 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1)
- 日期:2026-10-30
- 完整命令:
bash run.sh;脚本依次运行verilator --lint-only -Wall --top-module top -f broken.f、增加--bbox-unsup的同口径对照,以及verilator --lint-only -Wall --top-module top -f fixed.f,并记录各自退出码。
代码获取
最小 RTL、broken.f、fixed.f 和复跑脚本位于公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-verilator-warn-moddup。进入该目录执行 bash run.sh,可复现 MODDUP、验证黑盒参数无效,并确认去重后 PASS。
这一篇在知识体系里的位置
本篇处理同名设计单元被重复读入的问题。Verilator MODDUP 与 DECLFILENAME 的分工在于:前者是模块名冲突,后者只是文件名与模块名不一致;Verilator MODDUP 之外的模块找不到问题则要补搜索路径或文件列表。返回 EDA 报错排查系列 可按工具继续定位。
总结
Verilator MODDUP 的判断标准很明确:同一次编译出现了两个同名 module 声明。按报错给出的两个位置查定义,同文件重复就去重输入,不同文件的相同副本就淘汰旧文件,不同实现则同步重命名定义与例化;--bbox-unsup 不处理模块命名冲突,全局消音也不能代替修复。用原参数复跑,无 MODDUP 且退出码为 0,才算闭环。