Yosys 综合后设计为空:模块被优化清理的根因与 stat、assert 两种判空语义
更新时间:2026-11-11。长尾排查篇,属于 EDA 报错排查系列,汇总入口见 开源 EDA 报错排查导航。Verilog 文件里明明写了两个 module,
synth跑完却只剩一个;更隐蔽的是模块名还在、stat也不报错,Number of cells却是 0。本文用三个最小工程把「模块整个消失」和「模块空心化」两条路径分别跑透,并解释为什么 CI 只看退出码会漏掉空设计。
首屏答案:Yosys 综合以 -top 指定的模块为根做层次可达性分析,从根出发沿实例化边不可达的模块会在 hierarchy 阶段被整体移除,日志留下 Removing unused module;模块虽然被选为 top,但输出绑常量、内部逻辑悬空时,模块外壳与端口保留,逻辑却被 opt 各轮优化成零元件。普通 stat 对空设计只打印软 Warning 且退出码为 0,只有 select -assert-min 这类断言检查才把「0 个元件」升级成硬 ERROR、退出码 1。把逻辑真正接到顶层端口后复跑,Number of cells: 1,设计恢复非空。
这张流程卡对应本文的叙事顺序:先看清模块在哪个 pass 被删,再区分两种空设计的成因,最后用退出码语义解释为什么流水线需要显式断言。跳过 hierarchy 日志直接怀疑综合器 bug,是这类问题最常见的弯路。
本文要回答的问题
- 一个写得好好的 module,为什么
synth之后在设计里彻底找不到了,日志依据是哪一行? - 模块名还在、端口还在,
Number of cells却是 0,这种「空心模块」是怎么产生的? stat选不中模块时为什么只警告不失败,select -assert-min又凭什么把同一件事判成 ERROR?- 怎样改 RTL 才能让元件在优化后存活,复跑时看哪几个数字确认修复?
一、完整报错原文
最小工程 two_mod.v 里有两个模块:top 是一个与门,orphan 是一个反相器,但 top 内部从未实例化 orphan,两者在层次上毫无关系。
module orphan(input a, output y);
assign y = ~a;
endmodule
module top(input a, input b, output y);
assign y = a & b;
endmodule执行 yosys -p "read_verilog two_mod.v; synth -top top",前端阶段两个模块都被正常读入,随后 hierarchy pass 以 top 为根分析层次,orphan 因为不可达被直接移除。日志原样摘录如下:
1. Executing Verilog-2005 frontend: two_mod.v
Generating RTLIL representation for module `\orphan'.
Generating RTLIL representation for module `\top'.
Successfully finished Verilog frontend.
2.1.2. Analyzing design hierarchy..
Top module: \top
Removing unused module `\orphan'.
Removed 1 unused modules.如果脚本后面再接一句 select -assert-min 1 orphan/*,要求该选择至少命中一个元件,综合器就会在收尾阶段抛出硬错误:
ERROR: Assertion failed: selection contains 0 elements, less than the minimum number 1: orphan/*
Selection contains:
exit code: 1读这两段输出抓住三点。第一,orphan 不是读入失败,前端明确为它生成了 RTLIL,它死在 hierarchy 而非 frontend;第二,判据是 unused 即「从 top 不可达」,与模块内部逻辑写得对不对无关;第三,ERROR 文案里的 selection contains 0 elements 说明断言检查的是选择结果的元件数,空选择本身才是被拦截的对象。
二、工具与版本
本次实验全部在 Docker 镜像 geek-eda:0.2 内完成,日志头部三行固定记录了运行环境,版本只以日志为准:
# command: bash run.sh(容器 geek-eda:0.2 内执行,Yosys 0.33)
# date: 2026-11-11
# host-toolchain: Docker geek-eda:0.2 / Yosys 0.33 / Debian bookworm综合器具体版本为 Yosys 0.33(git sha1 2584903a06)。hierarchy 的移除文案、stat 的警告措辞与 select -assert-min 的断言格式都随版本演进过,本文的输出只对 0.33 负责;换大版本复现时应保留当次完整日志再对照,不能凭文案反推行为。
三、根本原因:可达性决定电路的生死
3.1 模块的存亡由实例化边决定
Yosys 读入 Verilog 时并不区分模块轻重,所有 module 一律进设计库;真正做裁剪的是 hierarchy pass。它从 -top 指定的根出发,沿模块内部的实例化边做可达性遍历,走到的保留、走不到的判为 unused 并移除。two_mod.v 里 top 与 orphan 之间没有任何实例化语句,这条边从一开始就不存在,orphan 的命运在层次分析时已经确定,与后续 opt 无关。
图中蓝色是输入动作,紫色是判定中枢,绿色是存活分支,红色是被清理的结局,橙色是另一种容易混淆的场景。对照看图:多模块工程的空设计死于不可达,单模块工程的空设计则死于逻辑悬空,两者的 pass 与日志特征完全不同。
3.2 空心模块:人还在,电路没了
第二个工程 dangling.v 只有一个模块,不存在可达性问题,但输出被常量钉死,内部与门的结果谁也不用:
module dangling(input wire a, input wire b, output wire y);
wire internal;
assign internal = a & b;
assign y = 1'b0;
endmoduley 恒为 0,internal 又没有任何负载,opt 各轮先删无驱动负载的悬空线,再把与门当作死逻辑删掉。综合结束时模块外壳和三个端口都在,统计却触目惊心:
=== dangling ===
Number of wires: 3
Number of wire bits: 3
Number of public wires: 3
Number of public wire bits: 3
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 0这正是 Yosys stat area 0 速查 讨论的零元件现象:Number of cells: 0 不等于综合失败,而是综合器忠实地表达了「这份 RTL 化简后不需要任何硬件」。模块没被删,是因为它本身就是 top;top 不受 unused 规则约束,哪怕里面空无一物也会保留。
3.3 软警告与硬错误:同一件事的两种退出码
关键区分在判空手段。对已经被移除的 orphan 执行 stat orphan,Yosys 只给一条选择失配的警告,脚本正常走完,退出码 0;而 select -assert-min 1 orphan/* 显式声明「这里至少要有一个元件」,零选择直接触发断言失败,退出码 1。这意味着 CI 如果只靠进程退出码判断成败,空设计会从眼皮底下溜过去——没有断言的综合脚本对空设计天然失明。同类「不报错但逻辑被悄悄优化光」的场景还有 Yosys synth 未指定 top 模块,根因都在层次与可达性,值得连读。
四、排查清单:先定根,再查边,最后数元件
图中红色是入口现象,三个蓝色节点依次是排查动作,橙色对应补层次关系,绿色对应修接线。落地时按四步走:
- 回看 hierarchy 段日志:搜
Removing unused module,被点名的模块就是不可达的死者,再去 RTL 里找谁本该实例化它。 - 核对 top 选择:多模块工程确认
-top没拼错、没指到空壳;top 选错时整个工程的可达集都会错位。 - 核对输出与负载:单模块出现 cells 为 0 时,逐级检查输出是否被常量赋值、内部信号是否悬空,悬空线的警告语义可参考 Yosys undriven signal 警告排查。
- 给流水线加断言:在综合脚本尾部追加
select -assert-min 1 <模块>/*,把空设计从软警告升级为硬失败,退出码 1 才能让 CI 真正拦住问题。
五、修复代码与复跑确认
修复 dangling.v 不需要加模块,只要让与门的输出真正驱动顶层端口,internal 随之有了负载,优化器便没有理由删它:
module connected(input wire a, input wire b, output wire y);
assign y = a & b;
endmodule一次 bash run.sh 顺序跑出五段记录。第二段对已清理模块执行 stat orphan,只有软警告、退出码 0,终端实录如下:
第三段换成 select -assert-min 1 orphan/*,同一事实被断言拦截,硬错误与退出码 1 的实录如下:
第五段对修复后的 connected.v 复跑同样的综合与统计,元件被保留下来,日志原样摘录:
=== connected ===
Number of wires: 3
Number of wire bits: 3
Number of public wires: 3
Number of public wire bits: 3
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 1
$_AND_ 1修复闭环有三处证据要一起读。其一,Number of cells: 1 且明确列出 $_AND_ 一个与门单元,说明逻辑在全部优化轮次后存活;其二,对照第四段 dangling 的两次统计均为 Number of cells: 0 且退出码 0,证实「模块在但电路空」时普通统计不会失败;其三,第三段断言以退出码 1 中止,给出了 CI 可依赖的拦截信号。断言在 Yosys 读取、综合、验证各阶段的语义差异,另见 Yosys 断言被忽略的排查。
实验环境
- 机器/系统:macOS 宿主机启动 Docker,容器内为 Debian bookworm 系工具链
- 工具版本:镜像
geek-eda:0.2,Yosys 0.33(git sha1 2584903a06) - 日期:2026-11-11
- 完整命令:在实验目录执行
bash run.sh;脚本依次运行五段yosys -p命令,分别覆盖two_mod.v的完整综合、stat orphan软警告、select -assert-min 1 orphan/*硬错误、dangling.v零元件统计与connected.v修复确认,每段打印退出码。
代码获取
完整源码(two_mod.v、dangling.v、connected.v)与五段复现脚本位于公开仓:https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-yosys-design-empty-after-opt。本地取得 geek-eda:0.2 镜像后,在仓库上级目录执行以下命令即可原样复现全部五段运行:
docker run --rm -v "$PWD":/work -w /work/ts-yosys-design-empty-after-opt geek-eda:0.2 bash run.sh这一篇在知识体系里的位置
本篇属于 开源 EDA 报错排查导航 的 Yosys 综合分支,回答「模块为什么被优化干净、空设计为什么不一定失败」。零元件统计的速查口径见 Yosys stat area 0 速查;top 选择错误导致逻辑被悄悄优化光的姐妹场景见 Yosys synth 未指定 top 模块;悬空与无驱动信号的警告语义见 Yosys undriven signal 警告排查;assert 单元在各阶段的行为见 Yosys 断言被忽略的排查。几篇合起来覆盖综合阶段「层次裁剪、死逻辑消除、警告与断言分级」三个故障面。
总结
Yosys 综合后设计为空有两条成因:一是模块从 -top 根沿实例化边不可达,hierarchy 阶段直接 Removing unused module 整体移除;二是模块虽为 top,但输出绑常量、内部逻辑悬空,被 opt 逐轮化简到 Number of cells: 0,只剩端口外壳。普通 stat 对选不中的模块和零元件设计只给软 Warning、退出码仍是 0,CI 单看退出码必然漏检;select -assert-min 1 把零选择升级为 ERROR、退出码 1,才是流水线可用的判空手段。把逻辑真正接到顶层端口后复跑,统计回到 Number of cells: 1 并列出 $_AND_ 单元,修复闭环成立。