GLS 门级后仿真实战:yosys 出门级网表 + iverilog 双跑逐行比对
更新时间:2026-10-28。本文是「集成电路大类 · 芯片设计」中间层(intermediate)的一篇实验文。概念篇 门级仿真、SDF 反标与 X 传播 已经把 GLS(Gate-Level Simulation,门级仿真)的定位讲清楚了:流片前在综合后的门级网表上再跑一遍仿真,抓住 RTL 前仿真看不见的初始化与 x 传播问题。概念归概念,很多读者卡在一个更实际的问题上:手边没有商业仿真器,这条链路用开源工具能不能真跑起来? 本篇给出肯定答案:用 yosys 把一个 8 位 ALU 综合成通用门级网表,再用 iverilog 让 RTL 源码与门级网表分跑同一份 testbench,两侧输出逐行比对。iverilog 跑 RTL 的基本功可参考 同步 FIFO Verilog 实现:iverilog 回归实测,本篇在它基础上往前再走一步——被测对象从 RTL 换成了综合后的网表。
本文要回答的问题
- 一份 RTL 从
yosys synth到「能被 iverilog 仿真的门级网表」,中间要经过哪些环节? - 同一份 testbench 不加改动地驱动 RTL 和门级网表,需要满足什么前提?
- 本篇实测两侧输出逐行一致——这是「GLS 没必要」的证据,还是恰恰说明实验设计里没埋 x 源?
- GLS 独有的 x 传播语义和 RTL 仿真的 x 乐观主义,差异具体长什么样?
先放结论对照卡,四个要点都来自后文真实日志:
这张卡的读法是:左右两列不是「谁比谁好」,而是同一份功能在两种抽象层级上的两次确认。左侧 RTL 仿真确认「我写的逻辑对不对」,右侧 GLS 确认「综合器翻译成门之后逻辑还对不对」。本次实验两侧输出完全一致,原因不是 GLS 多余,而是这个 ALU 是纯组合逻辑、testbench 激励全部是确定的 0/1——设计里没有任何 x 源可传播。这个「一致」本身的信息量,下文第四节展开。
一、实验设计:一个 ALU、一份 TB、两次仿真
被测对象是复用本站逻辑等价检查实验的 simple_alu:8 位输入 a/b,2 位操作码 op 选加/减/与/或,输出结果 y 和零标志 zero。全部组合逻辑,没有时钟没有复位——这是刻意为之,先把「综合等价性」这一件事孤立出来验证。选 ALU 而不是更简单的单门电路,理由是它的四条操作路径功能性质完全不同:加减走算术进位链、与或走纯位逻辑,zero 标志再对 y 做一层归约——一个模块里同时存在算术、位操作、归约三类逻辑,综合器对它们的优化策略各不相同,比对覆盖面比一个孤零零的加法器宽得多。
// simple_alu.v —— 8 位 ALU,4 种操作,纯组合
always @(*) begin
case (op)
2'd0: y = a + b;
2'd1: y = a - b;
2'd2: y = a & b;
default: y = a | b;
endcase
end
assign zero = (y == 8'h00);testbench 的关键设计是自检 + 打印。8 组激励覆盖四条操作路径外加四个边角:零输入出零标志、加法进位环绕(0xFF+0x01 回绕成 0x00)、相等相减出零、按位与出零。每组激励用 !== 全等比较——包括 x 在内的逐位比对,任何一位是 x 都会判 MISMATCH,这就是自检 TB 比普通 $display 人工目测强的地方:
task check(input [7:0] ta, input [7:0] tb, input [1:0] top, input [7:0] exp);
begin
a = ta; b = tb; op = top; #10;
if (y !== exp) begin
$display("MISMATCH a=%h b=%h op=%0d y=%h exp=%h", ta, tb, top, y, exp);
errors = errors + 1;
end else
$display("OK a=%h b=%h op=%0d y=%h zero=%b", ta, tb, top, y, zero);
end
endtaskTB 能用同一份代码驱动两种 DUT,前提是端口契约不变:yosys 综合保留模块名与端口名,simple_alu dut (.a(a), .b(b), .op(op), .y(y), .zero(zero)); 这一行实例化对 RTL 和网表都成立。这是 GLS 工程实践里最重要的一条纪律——testbench 面向接口写,不面向内部信号写;凡是 TB 里引用 dut.xxx 内部层次信号的写法,一到网表上全部报废,因为内部信号名早被综合器重命名成了自动生成的编号。真需要观察内部行为时,正确姿势是把观察点提到端口或用 bind/探针文件,而不是在 TB 里硬戳层次路径。
二、yosys 综合:从 case 语句到 121 个门单元
综合命令一行:
yosys -p "read_verilog simple_alu.v; synth -top simple_alu; write_verilog alu_gates.v"synth 是 yosys 的通用综合宏命令,内部串了 proc(过程转网表)、opt(常量折叠与死代码消除)、techmap(映射到通用门)、abc(逻辑优化与工艺映射)等二十多个 pass。日志里能看到这条流水线对这段 ALU 做的几件具体的事:PROC_MUX 把 case 语句先译成决策树再变成两级 mux 结构;PROC_DLATCH 专门检查有没有意外推断出锁存器——日志里明确写着「No latch inferred」,这是组合逻辑 always 块最容易翻车的点,漏写分支就会默默出 latch;ALUMACC 把 a+b、a-b 识别成两个 ALU 单元而不是通用加法树;最后 ABC 把 165 个门的中间网络优化收敛。对一个 8 位 ALU 全程不到一秒,但日志里有一段信息值得单独看——ABC 优化后的单元统计:
ABC RESULTS: AND cells: 10
ABC RESULTS: ANDNOT cells: 20
ABC RESULTS: AOI3 cells: 6
ABC RESULTS: AOI4 cells: 7
ABC RESULTS: MUX cells: 8
ABC RESULTS: XOR cells: 18
ABC RESULTS: internal signals: 157
=== simple_alu ===
Number of cells: 121读这段数字抓住一个对应关系:RTL 里一句 case (op) 四选一,到了门级变成 8 个 MUX 加一堆与或门搭的选择逻辑;a + b 的加法器被拆成 XOR/AND 组成的全加器链。行为的「意图」消失了,剩下的只有结构。这正是需要 GLS 的根本原因:你写的 case 有 default 兜底,综合后的门海没有「兜底」这个概念,每一根线在任何输入组合下都必须算出正确的 0/1——综合器有没有把你的意图翻译错,只有拿网表真跑一遍才算数。
顺带说明 write_verilog alu_gates.v 产出的是什么:一个与 simple_alu 同名同端口的模块,内部全部是 $_AND_、$_MUX_ 这类 yosys 通用门原语的实例和连线。它不是任何真实工艺库的标准单元网表——那是 abc 挂接具体 PDK 的 liberty 文件之后的事——但它具备了门级仿真的全部语义要素:纯结构、无行为语句、原语级粒度。拿它练通 GLS 链路,换到真实工艺库网表时流程一字不变,只是把 simlib.v 换成 Foundry 提供的 Verilog 仿真模型。综合原理本身在 逻辑综合 有系统讲解,这里不展开。
三、iverilog 双跑:RTL 与网表同 TB 逐行比对
GLS 侧编译比 RTL 侧多一个文件:门单元的仿真模型库。yosys 自带的通用门模型在 $(yosys-config --datdir)/techlibs/common/simlib.v,本次实验先按惯性写了 /usr/share/yosys/... 的硬编码路径,shell 直接报 No such file or directory——simlib 路径不要猜,用 yosys-config 问出来,这是开源链路上的常见坑。修正后两侧命令对称。注意 GLS 侧编译单元变成了三个:TB、门级网表、门模型库——simlib.v 里是每个 $_AND_、$_MUX_ 原语的行为级 Verilog 模型,iverilog 靠它才知道这些单元怎么算,缺了它编译会直接报 unknown module:
# RTL 前仿真
iverilog -g2012 -o sim_rtl.out tb_alu.v simple_alu.v && vvp sim_rtl.out
# GLS 门级后仿真
SIMLIB=$(yosys-config --datdir)/techlibs/common/simlib.v
iverilog -g2012 -o sim_gls.out tb_alu.v alu_gates.v $SIMLIB && vvp sim_gls.outRTL 侧的真实输出:
逐行看:第 5 行 a=ff b=01 op=0 y=00 是加法进位环绕——8 位加法 0xFF+0x01 溢出回绕为 0x00,zero 标志随之拉高;第 8 行 a=55 b=aa op=2 y=00 是按位与出零的经典对(0101 与 1010 逐位与得全零)。这两组是专门冲着「边界条件」去的,平坦输入测不出来的进位链错误在这里现形。
GLS 侧的真实输出:
两侧输出放在一起 diff,结论一句话:8 行 OK 逐字节相同,两侧都是 errors=0、TB_RESULT: PASS。diff 的做法也值得说一句:两次 vvp 的标准输出各重定向到文件,直接 diff rtl.txt gls.txt,空输出即一致——不要只比最后一行的 PASS,逐行比才能抓出「都 PASS 但中间过程不同」的情况,比如某组激励下 y 碰巧对但 zero 标志翻错。本次比对逐字节为空,对功能等价这件事,yosys 的综合结果通过了行为级黄金参考的检验。工程上更系统的做法是把这个比对交给 LEC(逻辑等价检查)工具做形式证明——逻辑等价检查 LEC/Formality 走的正是那条路,仿真比对与形式证明是互补关系:仿真快、直观但只覆盖跑过的激励,LEC 慢、抽象但穷举全部输入空间。
四、一致是因为没埋 x 源:GLS 独有的 x 传播语义
现在回答开头那个尖锐问题:两侧输出一致,GLS 是不是白跑了?不是。本次实验里 x 根本没有产生的机会——组合逻辑、激励全确定、无复位无上电过程。而 GLS 真正的杀手锏场景恰恰是 x 传播差异。看一张波形对比,同一个二选一逻辑,选择信号 sel 因未初始化而为 x 时的两侧表现:
读这张图抓住分歧行:RTL 写法 case(sel) 0: y=d0; default: y=d1; 里,sel=x 不匹配任何分支,落到 default 输出 d1=0xA5——仿真器替你做了一个确定性的选择,把 x「洗」掉了;而综合后的门级实现是 y = (sel & d1) | (~sel & d0) 这类结构,x 与任何数相与都是 x,x 顺着门一级级传到输出,GLS 的 y 如实显示 x。前者叫 x 乐观主义(x-optimism):RTL 仿真里 x 经常被 default、if-else、=== 比较等语法悄悄消解, bug 被掩盖;后者叫 x 悲观主义的如实呈现——真实硅片上这根线不会输出「半个电平」,它是不确定的 0 或 1,门级仿真里的 x 正是在提醒你「这里硅片上的行为你没控制住」。
这个差异在三种工程场景里会咬人:寄存器未复位就上电运行(RTL 里 initial 给了初值而硅片没有)、低功耗域掉电后隔离信号缺失、多时钟域异步采到亚稳态。商业仿真器为此专门提供 x-propagation 运行模式(VCS 的 xprop、Xcelium 的 xfile),把 RTL 仿真的 x 语义向门级靠拢;开源 iverilog 没有等价开关,这恰恰是「在网表上真跑一遍」在开源流程里更不可替代的原因——没有运行模式能替你补,只能换被测对象本身。门级仿真、SDF 反标与 X 传播 对这些 x 源有逐一清单。所以本篇的「逐行一致」要这样定性:它证明了这个纯组合 ALU 的综合等价性,不构成「GLS 可以省」的证据——一旦设计里有时序单元和复位时序,GLS 抓到的就往往是 RTL 仿真永远看不到的问题。
把常见语法在两边的 x 行为列成一张对照表,写 TB 时对照自查:
| 语法结构 | RTL 仿真里的 x 行为 | 门级网表里的 x 行为 |
|---|---|---|
case 带 default | x 落进 default,输出确定值 | 门结构如实传播 x |
if (sig) 判断 | x 按假处理,走 else 分支 | 选择逻辑输出 x 或毛刺样不确定 |
!= 比较 | 含 x 返回 x,被当假吞掉 | 同左,比较器本身是 x 传播的 |
!== 全等比较 | 含 x 判不等,MISMATCH 现形 | 同左,是 TB 自检的正确写法 |
| 寄存器上电初值 | initial 给什么就是什么 | 触发器模型上电即 x,等复位 |
这张表也解释了为什么「同一份 TB」在网表上反而更严格:TB 写法不变,但被测对象从「会替你擦屁股的语义」换成了「有一说一的语义」。
五、把链路变成可复用的 checklist
整条开源 GLS 链路沉淀成四步清单,以后每个模块过一遍:
| 步骤 | 命令/动作 | 通过判据 |
|---|---|---|
| 1. 综合 | yosys -p "read_verilog x.v; synth -top x; write_verilog x_gates.v" | 日志末尾 CHECK 报 0 problems |
| 2. 备模型 | SIMLIB=$(yosys-config --datdir)/techlibs/common/simlib.v | 路径真实存在,不硬编码 |
| 3. 双跑 | RTL 与网表各编译一次,同一份自检 TB | 两侧 TB_RESULT 均 PASS |
| 4. 比对 | 两次 $display 输出做 diff | 逐行一致,且激励含边界用例 |
两个容易翻车的点:一是 TB 必须面向端口写(第一节的纪律),引用内部层次信号会在网表上编译即失败;二是自检比较用 !== 而不是 !=——后者遇到 x 会返回 x 被 if 当假处理,等于把 MISMATCH 吞了,GLS 场景下这是致命的习惯。
清单之外还有一条更高层的取舍要说清:本篇这条链路是「零延迟门级仿真」,不带 SDF 时序反标。它回答的是「综合有没有改坏功能」,回答不了「真实延迟下时序还成不成立」——后者要拿到布局布线后的网表和 SDF 文件才能做,而且其时序正确性大头已由 STA 静态签核兜住,时序仿真主要补异步、复位、多周期这些 STA 覆盖姿势别扭的场景。对开源练习链路而言,零延迟 GLS 加 STA 的组合已经覆盖了门级验证的大部分价值。
实验环境
- 机器/系统:云主机 / Ubuntu 22.04(eda-golden-v1 镜像)
- 工具版本:Yosys 0.9 (git sha1 1979e0b)、Icarus Verilog 11.0(照抄工具自报版本)
- 日期:2026-10-28
- 完整命令:
yosys -p "read_verilog simple_alu.v; synth -top simple_alu; write_verilog alu_gates.v"
iverilog -g2012 -o sim_rtl.out tb_alu.v simple_alu.v && vvp sim_rtl.out
SIMLIB=$(yosys-config --datdir)/techlibs/common/simlib.v
iverilog -g2012 -o sim_gls.out tb_alu.v alu_gates.v $SIMLIB && vvp sim_gls.out代码获取:本文全部源码(simple_alu.v、tb_alu.v、运行脚本与原始日志)在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/gls-netlist-simulation-iverilog,装好 yosys 与 iverilog 后执行目录下的 run 脚本即可复现双跑比对。
这一篇在知识体系里的位置
本文是芯片设计验证线「RTL 仿真 → 门级仿真 → 形式等价」三角中的 GLS 落地点:概念底座是 门级仿真、SDF 反标与 X 传播(三个仿真层级与 x 源清单),仿真器基本功见 同步 FIFO Verilog 实现:iverilog 回归实测,综合原理见 逻辑综合,等价性的形式证明路线见 逻辑等价检查 LEC/Formality。往后延伸的方向是带 SDF 延迟的时序仿真——那才是 GLS 在真实流片流程里的完整形态。
一句话总结
GLS 门级后仿真用开源工具链完全可以落地:yosys 一行 synth 把 RTL 综合成通用门级网表,iverilog 加上 simlib.v 就能让同一份自检 testbench 双跑——本篇 8 组激励两侧输出逐行一致,证明了这个纯组合 ALU 的综合等价性;而真正让 GLS 不可替代的是它对 x 的如实传播,RTL 仿真的 default 分支会把 x 悄悄洗掉,门级网表不会替你做选择,这份「不乐观」正是流片前最后一道防线的价值。