Yosys 网表输出流程:write_verilog 门级网表与 write_json 结构化网表怎么选
更新时间:2026-11-03。综合是什么 讲过,综合的产物是一张「元件清单加接线表」式的门级网表。用开源综合器 Yosys 时,这张网表在内存里(RTLIL 中间表示)只有一份,但落盘时有三种常见写法:
write_verilog、write_json、write_blif。本篇用 Yosys 0.33 把同一个小设计真跑一遍,对照三种文件里到底装了什么、各自交给谁消费。
本文要回答的问题
- 综合跑完后,
write_verilog和write_json导出的东西有什么本质不同? write_verilog -noattr -noexpr -nodec这几个开关各改了什么,什么场景必须加?- JSON 网表里的 modules / cells / netnames 和 bit 编号怎么读?
write_blif在流程里处于什么位置,什么时候才用它?- 为什么有人把综合前的 RTL 直接
write_verilog出来,得到的还是行为代码?
先给一张两栏对照卡,后面逐段展开:
读这张卡抓住一个前提:三个 write_* 命令都不改设计,只是把 Yosys 内存里的同一份 RTLIL 按不同语法序列化。所以三个文件应当互相一致——cell 数量、连接关系对不上,才是流程出了问题。
一、这一步在流程里的位置
网表输出是综合的收尾动作。上游是 read_verilog → hierarchy → synth(内部含化简、映射到 Yosys 通用门 $_* 单元),下游则因产物格式而异:Verilog 网表去门级仿真和后端实现,JSON 去你自己写的分析脚本,BLIF 去学术工具或 FPGA 流程。
读图注意分叉发生在写盘之后、而非综合阶段:无论导几种格式,synth 只跑一次。还有一个容易忽略的前提——若目标是 ASIC 标准单元,synth 之后还要经 abc -liberty 加 techmap 映射到工艺库单元再导出,否则网表里全是 $_NAND_ 这类 Yosys 通用门,厂家后端工具不认识。本篇聚焦输出格式本身,停在通用门这一级。
二、命令逐段解读:三个 write 命令与常用选项
完整脚本只有八行,核心是四条输出命令:
read_verilog counter4.v
hierarchy -top counter4
synth -flatten -top counter4
stat
write_verilog netlist_default.v
write_verilog -noattr -noexpr -nodec netlist_clean.v
write_json design.json
write_blif design.blifwrite_verilog 的选项直接决定文件长相,对照 0.33 版帮助逐个讲:
| 选项 | 作用 | 不加会怎样 |
|---|---|---|
| (默认) | 内部 $_* 单元折回 Verilog 表达式与 always 块,输出更可读 | 看不到显式门实例,但属性、来源注释都保留 |
-noattr | 不输出 (* ... *) 属性(src 行号、top、force_downto 等) | 属性泄漏源码路径,部分挑剔的后端会告警 |
-noexpr | 内部单元不折成表达式,逐门输出 cell 实例化 | 组合逻辑是 assign y = ~(a & b); 而非一个 $_NAND_ 实例 |
-nodec | 常量一律按位模式写二进制(如 4'h0 风格),不写十进制 | 32 位常量默认写成十进制,跨工具解析偶尔踩坑 |
-noexpr 是理解两种 Verilog 产物差异的关键:默认模式面向「读」,把门重新表达成人熟悉的 assign/always;加 -noexpr 面向「交接」,网表里每一行实例化都与内存 cell 一一对应,做门级计数、功耗注入、形式核对时不会丢结构。注意 -flatten 加在 synth 上而不是 write_* 上——展平层次是改设计,必须在导出前做。
write_json design.json 基本不需要选项:它把端口、cell、线网、属性全量序列化,还能用 read_json 重新读回 Yosys,往返检查的边界见 Yosys JSON 网表回读。write_blif 同理,常用一个 -gates 声明哪些单元是合法基元;它写的是 Berkeley 逻辑交换格式,组合逻辑用真值表(.names)、时序单元用 .subckt,是 ABC、VPR、不少 FPGA 教学流程的通用语,但不是给人读的。
三、输入输出文件:真实片段对照
输入 counter4.v 是 510 字节的带使能 4 位计数器(复位清零、使能加一、q==15 时拉高 carry)。同一次综合后四个产物的真实大小是:默认 Verilog 1699 字节、干净 Verilog 1440 字节、JSON 8822 字节、BLIF 1334 字节。
默认 Verilog 网表里,门已经被折回表达式,但每个对象都带着源码溯源属性:
(* top = 1 *)
(* src = "counter4.v:2.1-17.10" *)
module counter4(clk, rst_n, en, q, carry);
(* src = "counter4.v:7.23-7.28" *)
output carry;
assign carry = en & ~(_02_);
(* src = "counter4.v:9.5-14.8" *)
always @(posedge clk, negedge rst_n)
if (!rst_n) q[0] <= 1'h0;
else if (en) q[0] <= _04_[0];加上 -noattr -noexpr -nodec 后,同一段电路变成逐门实例化,属性消失、结构与 stat 报告的 13 个 cell 严格对应:
\$_NAND_ _07_ ( .A(q[1]), .B(q[0]), .Y(_00_) );
\$_ANDNOT_ _10_ ( .A(en), .B(_02_), .Y(carry) );
\$_DFFE_PN0P_ \q_reg[0] /* _15_ */ (
.C(clk), .D(_04_[0]), .E(en), .Q(q[0]), .R(rst_n)
);\$_DFFE_PN0P_ 这个名字自带语义:DFF + Enable,P/N/0/P 依次表示时钟上升沿、复位低电平有效、复位值为 0、异步复位。四个触发器对应四位寄存器,与 JSON 里四个 DFF cell 完全一致。
JSON 则是另一套世界观——它不靠名字连线,靠 bit 编号。每个模块下有四块:ports(端口方向与 bit 列表)、cells(每个 cell 的类型和连接)、netnames(具名线网映射到哪些 bit)、attributes。真实片段:
"$auto$ff.cc:266:slice$111": {
"hide_name": 1,
"type": "$_DFFE_PN0P_",
"port_directions": { "C": "input", "D": "input", "E": "input",
"Q": "output", "R": "input" },
"connections": { "C": [ 2 ], "D": [ 10 ], "E": [ 4 ],
"Q": [ 5 ], "R": [ 3 ] }
}端口区里 "q": { "direction": "output", "bits": [5,6,7,8] },所以这个触发器的 Q:[5] 正是 q[0];"carry" 占 bit 9。写脚本数触发器,只需遍历 modules.*.cells 统计 type 以 $_DFF 开头的条目,比正则解析 Verilog 可靠得多。常量 0/1/不定态在 bit 编号里有保留值(0、1 与字符串 "x"/"z"),这也是 JSON 适合程序消费的原因。BLIF 里同一电路又换成语义等价的真值表写法:
.names en $abc$172$new_n14_ carry
10 1
.subckt $_DFFE_PN0P_ C=clk D=... E=en Q=q[0] R=rst_n.names 行的 10 1 是覆盖条件——仅当第一输入为 1、第二输入为 0 时输出 1,即 carry = en & ~n14,与 Verilog 网表那条 $_ANDNOT_ 是同一个门。
四、常见错与检查点
| 现象 | 先查什么 | 纠正 |
|---|---|---|
写出的「网表」里全是 always/if,跟 RTL 几乎一样 | 是否在 write_verilog 前只做了 read_verilog,没跑 synth/proc | write 只负责序列化;先跑综合流程再导出 |
后端/门仿报 Unknown module: $_NAND_ | 是否直接把通用门网表交了出去 | 用目标库 abc -liberty xxx.lib + techmap 映射后再写,FPGA 走 synth_ice40 等专用脚本 |
JSON 用 read_verilog 读不回来 | 文件格式与读入命令是否配套 | JSON 必须 read_json;Verilog 与 JSON 的往返差异见 JSON 网表回读 |
| 下游抱怨网表里有源码路径和行号 | 默认导出带着 (* src=... *) 属性 | 交付版加 -noattr(或 -attr2comment 降级成注释) |
一个通用核对动作:导出后对照 stat 的 cell 计数。本次报告 13 个 cell,干净网表里就应数到 13 个实例——-noexpr 模式下这件事可以用 grep 直接验证,比肉眼读表达式可靠。
五、真实跑一遍:一次综合导出三种格式
在 geek-eda:0.3-sta 容器里执行 make run(即 yosys -s run.ys),统计与四个后端顺序执行的真实输出如下:
实录里 stat 给出终态账:11 根线、20 个 wire bit、13 个 cell(4 个 $_DFFE_PN0P_ 触发器加 9 个组合门);紧接着第 5、6 节是两次 Verilog backend(默认版与 -noattr -noexpr -nodec 版),第 7、8 节依次是 JSON 与 BLIF backend,日志以 End of script 正常收尾。末尾的 wc -c 五行就是第三节引用的文件大小:510 / 1699 / 1440 / 8822 / 1334 字节,四个产物全部落盘。
把 JSON 喂回 Yosys 还能做闭环自检:read_json design.json 后再 stat,cell 计数应仍是 13——这证明序列化过程没有丢结构。想继续往 FPGA 走,把 synth -flatten 换成 synth_ice40 后再 write_json,cells 里的类型就会从 $_* 变成 SB_DFF、SB_LUT4 这类厂商原语,格式读法完全相同。
终端卡是写网表的执行过程,跑完后各产物的文件体积对账单原样如下,可以直观对比 Verilog 与各类网表的体量:
Yosys 0.33 (git sha1 2584903a060)
Time spent: 33% 1x abc (0 sec), 15% 16x opt_expr (0 sec), ...
--- file sizes ---
510 counter4.v
1699 netlist_default.v
1440 netlist_clean.v
8822 design.json
1334 design.blif
13805 total实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon),容器镜像 geek-eda:0.3-sta(Docker)
- 工具版本:Yosys 0.33(git sha1 2584903a060),容器内
yosys -V照抄 - 日期:2026-11-03
- 完整命令:
docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta yosys -V→docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta yosys -s run.ys(等价于make run)→wc -c counter4.v netlist_default.v netlist_clean.v design.json design.blif
代码获取
完整 RTL、Yosys 脚本与 Makefile 已同步到公开仓库:geek-doc.cn/experiments/flow-yosys-write-netlist,克隆后进入该目录执行 make run 即可在 geek-eda:0.3-sta 容器中复现四种产物,make clean 删除全部生成文件。
这一篇在知识体系里的位置
- 想先弄清「综合」整体在干什么、门级网表为什么长这样:读 综合是什么;
- 关心网表作为数据交接物的契约(门仿、ECO、网表交接):读 网表交接:门级仿真与 ECO 与 RTL 与网表数据格式;
- 想验证 JSON 写出去再读回来会丢什么:读 Yosys JSON 网表回读。
总结
Yosys 网表输出流程的要点是「一次综合、多种导出」:synth 跑完后内存里只有一份 RTLIL 设计,write_verilog 把它写成 EDA 工具能吃的门级 Verilog——默认折成带 (* src *) 属性的表达式,交付门仿和后端时加 -noattr -noexpr -nodec 得到逐门实例化、属性干净、与 stat 计数一一对应的网表;write_json 写出 modules/cells/netnames/ports 四段结构、靠整数 bit 编号连线的 JSON,适合自定义脚本、统计和可视化,并可用 read_json 回读核对;write_blif 写出真值表加 .subckt 的交换格式,主要对接 ABC 等学术与 FPGA 工具。记住两个检查点:导出前必须真跑过综合,且通用门 $_* 网表在交给厂商后端前要先映射到目标单元库。