Yosys 入门:综合把 RTL 变成了什么
仿真回答「这份 RTL 行为对不对」,综合回答的是另一个问题:「它到底由多少个触发器和门电路实现」。开源 EDA 入门系列的前两站,iverilog 入门 与 Verilator 入门 已经让同一个 4 位计数器在两套仿真器里跑出了波形;本篇是第三站,用 Yosys 0.33 把这份验证过的 RTL 综合成门单元网表,顺着真实日志看清楚综合的输入、变换过程与产物去向。
本文要回答的问题
- 在从 RTL 到 GDS 的整条链路上,逻辑综合占哪一站,它和仿真的边界划在哪里?
- 一个只有四行的
synth.ys脚本,每一行各自触发了什么动作? stat报告里的 wires、wire bits、cells 分别怎么读,10 个单元对应计数器的哪些硬件?- 综合出来的网表文件里为什么是
$_SDFFE_PN0P_这样的怪名字,它和手写的 Verilog 有什么区别? - 通用单元网表为什么不能直接流片,它的下家是谁?
一、先定位:逻辑综合在 RTL 到 GDS 链路上站哪一站
数字芯片从代码到版图的主干可以粗分为「写 RTL、仿真验证、逻辑综合、布局布线、签核」几段,全景图见 从 RTL 到 GDS。仿真器把 RTL 当程序执行,脑子里只有事件和时间,不存在真实的门;综合器则相反,它不执行任何激励,而是做一次等价变换:把行为级的 always 块翻译成一张由触发器和组合门组成的连接清单,也就是网表(netlist)。
这一站的输入、变换与输出可以用一张分区卡概括。
读这张卡抓住一条主线:左边的 RTL 描述的是「每个时钟沿做什么」,右边的网表描述的是「用哪些器件、怎么连线才能做到这件事」。综合不判断你的计数顺序对不对——那是前两站仿真的职责;它默认 RTL 行为已经验证通过,只负责给出一份面积、时序可被下游工具继续处理的结构描述。仿真与综合的职责边界,在 第一次逻辑综合完整流程 里有更完整的展开。
二、四行脚本跑完一次综合:synth.ys 逐行拆解
Yosys 支持交互模式,也支持把命令写进脚本文件一次跑完。本实验的 synth.ys 只有四行,却覆盖了一次完整综合的最小闭环。
read_verilog rtl/counter.v
synth -top counter
stat
write_verilog -noattr -noexpr build/counter_netlist.v逐行看,每条命令的职责与日志里的对应动作如下。
| 行 | 命令 | 职责 | 日志中的对应过程 |
|---|---|---|---|
| 1 | read_verilog | Verilog 前端:语法解析、生成 AST,再转成 Yosys 的 RTLIL 中间表示 | Executing Verilog-2005 frontend,为模块 counter 生成 RTLIL |
| 2 | synth -top counter | 综合主命令:一个宏,串起层次管理、过程转换、多轮优化、技术映射、ABC 等二十余道 pass | 日志 2.1 到 2.24 的全部 pass,指定 counter 为顶层 |
| 3 | stat | 打印当前设计的 wires、cells 等统计数字 | === counter === 统计块,全篇出现两次且数字一致 |
| 4 | write_verilog -noattr -noexpr | Verilog 后端:把内存中的网表导出成文本文件 | Executing Verilog backend,落盘 build/counter_netlist.v |
synth 不是一道 pass,而是一组 pass 的编排宏。日志里能看到它内部的层次:先 HIERARCHY 确定顶层、PROC 系列把 always 过程转换成多路器和触发器(其中 PROC_DFF 明确记下 created $dff cell ... with positive edge clock),再反复跑 OPT_EXPR、OPT_CLEAN 做常量折叠与死逻辑消除,TECHMAP 把高层运算展开成基础单元,最后调 ABC 做门级映射。整条管道的数据流如下图。
蓝色是唯一的人工输入,三个橙色节点是 Yosys 的处理动作,绿色是只打印不落盘的统计,灰色是最终文件。注意 stat 和 write_verilog 是并列的两个消费者:它们面对的是 synth 结束后同一份内存设计,所以报告里的单元数与网表文件里的实例数必然对得上,第三节会实际核对一遍。
第四行的两个开关值得单独说。write_verilog 默认会把内部门单元「回写」成 assign 表达式,导出的文件看起来像手写的数据流代码,门单元实例被还原没了;加上 -noexpr 才保留 $_... 形式的通用单元实例,-noattr 则去掉综合器附加的 (* ... *) 属性,让网表更干净。想观察门单元本身,这两个开关缺一不可,网表输出的完整选项体系见 Yosys 网表输出流程。
三、读 stat 报告:8 根线、17 个 wire bit 与 10 个单元
脚本跑到 stat 时,终端打印出下面这段统计。这是本次实验的真实输出,CHECK pass 紧随其后。
报告分上下两半。上半是线网统计,逐行含义如下。
| 统计项 | 数值 | 含义 |
|---|---|---|
| Number of wires | 8 | 设计中命名线网的条数,总线只算一条 |
| Number of wire bits | 17 | 所有线网展开到位后的总位数,4 位总线贡献 4 bit |
| Number of public wires | 4 | 模块端口线网:clk、rst_n、enable、count |
| public wire bits | 7 | 端口总位数:三个 1 位输入加 4 位 count 输出 |
| memories / processes | 0 / 0 | 没有推断出存储器;行为级 process 已全部转换完毕 |
processes 为 0 是一个关键信号:它说明 always 块在 PROC 阶段已经被彻底消化,设计里不再残留任何行为级结构,剩下的全是单元和连线。
下半是单元统计,Number of cells: 10,共六类。
| 单元类型 | 数量 | 在计数器里承担的角色 |
|---|---|---|
$_SDFFE_PN0P_ | 4 | 4 个带使能、带同步复位的 D 触发器,对应 count 的 4 个寄存器位 |
$_XOR_ | 2 | 异或门,构成逐位加一的进位翻转逻辑 |
$_ANDNOT_ | 1 | 与非门的一种(A 与 非 B),参与高位进位 |
$_NAND_ | 1 | 与非门,产生低位进位信号 |
$_XNOR_ | 1 | 同或门,配合进位控制第 3 位翻转 |
$_NOT_ | 1 | 非门,实现最低位每拍取反 |
4 个时序单元加 6 个组合门,分工清晰:触发器保存 4 位状态,组合门实现 count + 1 的加法以及复位、使能对输入端的选择。日志里 OPT_DFF 那两行 Adding SRST signal 与 Adding EN signal,正是综合器识别出 RTL 中同步复位与使能语义、给触发器补上对应控制端的过程。stat 每个字段的深入读法在 Yosys stat 报告怎么读 中逐条展开。
报告末尾的 CHECK pass 给出 Found and reported 0 problems.。这道 pass 在综合后做一次结构性体检,查组合环路、悬空位宽、多驱动等明显错误;它不是形式验证,不保证功能正确,但 0 problems 至少说明网表在结构层面没有硬伤。同一份统计在脚本的独立 stat 命令处又打印了一次(日志 2.25 与第 3 步),两次数字完全一致,可以互相印证。
四、打开网表:通用门单元与 SDFFE 触发器长什么样
write_verilog 落盘的 build/counter_netlist.v 仍是合法 Verilog,但它的写法和 RTL 已经是两种语言。模块端口保持不变,内部却是四根自动命名的线网加十个单元实例。
module counter(clk, rst_n, enable, count);
wire _00_;
wire _01_;
wire [3:0] _02_;
wire [3:0] _03_;
input clk;
wire clk;
output [3:0] count;
wire [3:0] count;
input enable;
wire enable;
input rst_n;
wire rst_n;先看一个组合门和一个触发器实例的真实样子。
\$_NOT_ _04_ (
.A(count[0]),
.Y(_02_[0])
);
\$_SDFFE_PN0P_ \count_reg[0] /* _11_ */ (
.C(clk),
.D(_02_[0]),
.E(enable),
.Q(count[0]),
.R(rst_n)
);
assign _02_[3:1] = count[3:1];
assign _03_[0] = _02_[0];
endmodule最低位的翻转逻辑最直观:非门 _04_ 把 count[0] 取反送进 _02_[0],触发器 count_reg[0] 的 D 端接这个取反值,于是每个有效时钟沿最低位翻转一次。$_SDFFE_PN0P_ 这个名字可以拆开读:主体 SDFFE 表示带同步复位(S)的 D 触发器(DFF)加使能端(E);后缀 PN0P 是四个极性位,依次表示时钟上升沿有效、复位低有效、复位值为 0、使能高有效——正好对应 RTL 里的 posedge clk、!rst_n、复位到零和 if (enable)。五个端口的分工是:.C 时钟、.D 下一状态输入、.E 使能、.Q 当前状态输出、.R 同步复位。
网表里四个 $_SDFFE_PN0P_ 实例分别命名为 count_reg[0] 到 count_reg[3],名字里保留了 RTL 寄存器的来历。组合门则串成一条行波进位链:_00_、_01_ 是逐级进位,高位的 XOR、XNOR 依据进位决定该位是否翻转,这就是 count + 1'b1 综合后的硬件形态。文件尾部两行 assign 没有任何逻辑功能,只是位片别名——把总线上的若干位转接给不同单元的端口,是综合器布线时留下的连接声明。
必须强调这些 $_... 单元的身份:它们是 Yosys 定义的通用(generic)单元,只表达布尔功能,不绑定任何厂家的工艺库。换句话说,网表里没有面积、没有延迟、没有驱动强度,它是一份与工艺无关的结构描述,不能直接拿去流片。要变成真实芯片,还得经过工艺映射换成标准单元。
读者自己核对网表时,不必逐行读实例,用一条 grep 统计单元类型即可,本实验的 run.sh 第三步就是这么做的。
== 3. 网表中的通用单元统计 ==
1 $_ANDNOT_
1 $_NAND_
1 $_NOT_
4 $_SDFFE_PN0P_
1 $_XNOR_
2 $_XOR_
done六类单元的计数与 stat 报告完全一致:4 个触发器、6 个组合门,合计 10 个 cell。报告与文件互相对账,是确认综合产物没有被后续步骤意外改坏的基本手法。
五、网表交给谁:工艺映射、时序分析与门级仿真
通用网表只是中间产物,它的下游有三条主要去向,按「先落地、再验证」收进两个分组。
灰色是起点,两个浅蓝容器把下游收窄成「落地」与「验证」两组。其实 synth 内部已经调过一次 ABC:日志 2.22 节显示它把 6 个组合门提取成 BLIF,读入一个含 13 个门的 genlib 临时库做映射,ABC 结果行列出 NOT、NAND、XNOR、ANDNOT 各 1 个加 XOR 2 个,与通用单元一一对应。但这只是综合器自带的通用库演示;真实工程里要换成目标工艺的标准单元库,让每个门落到有确定面积和延迟的具体型号,这一步的完整做法见 Yosys 与 ABC 工艺映射。
映射之后进入验证组。OpenSTA 对网表做静态时序分析,在时钟周期约束下检查每条路径的建立时间与保持时间,Yosys 网表如何接入这条链路见 OpenSTA 读取 Yosys 网表。门级仿真则把网表当 DUT、沿用原来的 testbench 再跑一遍,确认综合没有改变行为;如果反标了延迟,还能暴露 RTL 仿真看不见的时序问题。两条路都通过,设计才有资格进入布局布线。
六、0.08 秒与 13 MB:综合耗时与资源占用
计数器是极小的设计,数据只用来建立量级感,不作为工具性能结论。日志结尾记录了这次运行的资源账。
End of script. Logfile hash: 53af642b8d, CPU: user 0.08s system 0.00s, MEM: 13.00 MB peak
Yosys 0.33 (git sha1 2584903a060)
Time spent: 32% 1x abc (0 sec), 16% 16x opt_expr (0 sec), ...整次综合用户态 CPU 时间只有 0.08 秒,峰值内存 13 MB,小到计时精度已经读不出单道 pass 的秒数。但 Time spent 的相对占比仍有信息量:ABC 一道 pass 占了 32% 的时间,opt_expr 跑了 16 次合计占 16%。这揭示了综合器的时间花在哪——不是解析 Verilog,而是反复的优化迭代与门级映射;设计规模上去之后,ABC 的映射策略和优化轮次会成为综合时长的主导因素,这也是大型工程要按模块分层综合的原因。
把本篇与前两站放在一起对比,三类工具的边界就非常清楚。
| 维度 | Icarus Verilog | Verilator | Yosys |
|---|---|---|---|
| 工具类别 | 事件驱动仿真器 | 周期级编译仿真器 | 逻辑综合器 |
| 输入 | RTL 加 testbench | RTL 加 testbench | 仅可综合 RTL |
| 核心动作 | 解释执行 vvp 字节码 | 转 C++ 后本机编译执行 | 等价变换为门单元网表 |
| 主要产物 | 日志、VCD 波形 | 日志、波形 | stat 统计、Verilog 网表 |
| 回答的问题 | 激励下行为是否正确 | 大设计下的高吞吐行为验证 | 由多少触发器和门实现 |
| 本例量级 | 编译中位 7 ms | 编译约 3.6 s | 综合 0.08 s、峰值 13 MB |
仿真与综合不是替代关系,而是前后工序:先用仿真器把行为钉死,再让综合器放手做结构变换。三者在同一个 counter 工程上的实测对照,正好构成开源数字前端的最小闭环。
实验环境
- 机器/系统:Docker Ubuntu 24.04.5 容器(linux/amd64),宿主机 Intel i7-9750H,容器可见 12 vCPU;纯命令行环境,无 GUI。
- 工具版本:Yosys 0.33 (git sha1 2584903a060)。
- 日期:2026-11-14。
- 完整命令:
bash run.sh,脚本内依次执行yosys -s synth.ys、打印build/counter_netlist.v、用grep统计网表中的通用单元类型。
代码获取:本实验完整代码(
rtl/counter.v、synth.ys、run.sh)与实测输出见公开仓库 derekzhuo/geek-doc.cn 的experiments/yosys-first-lesson/。克隆后进入该目录执行bash run.sh即可复现本篇全部数字;同一份 RTL 的仿真实验在同仓experiments/iverilog-verilator-first-lesson/。
这一篇在知识体系里的位置
本篇是开源 EDA 入门系列的综合入口,向上承接 从 RTL 到 GDS 的流程全景。横向看,同一份计数器 RTL 的两种仿真形态在 iverilog 入门 与 Verilator 入门,本篇是它们通过之后的下一步。继续沿综合链路深入,完整流程读 第一次逻辑综合完整流程,报告解读读 Yosys stat 报告怎么读,网表导出读 Yosys 网表输出流程,工艺落地读 Yosys 与 ABC 工艺映射,时序签核读 OpenSTA 读取 Yosys 网表。
总结
Yosys 入门的核心不是记住 synth.ys 的四行命令,而是理解一次等价变换:read_verilog 把 RTL 送进工具,synth 经过过程转换、多轮优化、技术映射与 ABC,把行为级 always 块变成 4 个 SDFFE 触发器加 6 个组合门、合计 10 个通用单元的网表,stat 与 write_verilog 再从同一份内存结构分别产出统计报告和网表文件。 本次计数器实验综合耗时 0.08 秒、峰值内存 13 MB、CHECK 报告 0 个问题;这份与工艺无关的通用网表还不能流片,它要先经 ABC 换成标准单元,再交给 OpenSTA 做时序签核、交给门级仿真复核行为——看清这条「仿真验证行为、综合生成结构、映射落地工艺」的交接链,就跨过了逻辑综合的第一道门槛。