
FPGA 圈子里有个说法叫“综合跑完只是走完一半路”剩下那一半里把综合结果导成门级网表算是绕不过去的一步。Vivado 的综合Synthesis会把 RTL 代码翻译成 LUT、FF、BRAM、DSP 这些器件原语但很多人跑完综合就直奔实现Implementation去了等到需要交付、需要做等价性检查、需要给别人做后综合仿真的时候才发现门级网表这个东西得单独导出来。这篇就聊聊我从零开始把 Vivado 综合后的门级网表导出来的完整过程包括 GUI 和 Tcl 两条路、网表格式怎么选、SDF 怎么带、以及一堆踩过的坑。适合刚上手 Vivado 的在校同学也适合需要往第三方工具链交付网表的工程岗。文章里所有命令和参数我都实际跑过器件用的是 7 系列 Artix其他系列同样适用。1. 先把“门级网表”这件事讲透1.1 门级网表到底是个什么产物刚接触这个概念的人容易把它和“比特流”混在一起。比特流bitstream是最终烧进芯片的配置数据人基本读不懂门级网表gate-level netlist还停留在“电路描述”这一层它用 Verilog 或 VHDL 的语法把设计写成一个个实例instance和连线net实例的名字是LUT6、FDRE、RAMB36E1、DSP48E1这类器件原语。你可以把它理解成一张“已经把乐高积木拼好、但还没粘到板子上的组装图”每一块积木型号、每个接口连到了哪里全都写清楚了。和 RTL 最大的区别在于抽象层级。RTL 里写的是always (posedge clk) q d;工具知道这是个触发器但具体用哪种触发器、带不带复位、复位是高有效还是低有效是综合阶段才定的。门级网表里就直接是FDRE #(.INIT(1b0)) u_ff (.C(clk), .CE(1b1), .R(1b0), .D(d), .Q(q));这种写法。LUT 的查找表内容会以INIT参数的形式出现一个六输入 LUT 的INIT是 64 位十六进制数这就是真值表的完整表达。看到INIT(64h00000000FFFFFF00)这种长串说明这个 LUT 实现的是一个具体的组合逻辑函数。提示门级网表里的原语名和芯片型号强相关7 系列的FDRE到了 UltraScale 还是FDRE但 BRAM 就从RAMB36E1变成RAMB36E2。跨器件复用网表基本等于重做综合。1.2 为什么非得在综合后导网表需求可以归成四类我按实际遇到的频率排一下。第一类是交付与保护。你写的 RTL 是核心资产客户或者合作方只想要一个能集成进他们系统的东西不想看到源码。综合后的 EDIF 网表是个不错的选择它是器件原语级别的描述虽然理论上能被逆向但门槛比给 RTL 高得多。Vivado 还支持对 EDIF 做加密进一步抬高门槛。第二类是后综合功能仿真。RTL 仿真过了不代表综合后没问题。综合器的优化动作很多比如资源共享、寄存器复制、FSM 状态编码重排、移位寄存器推断这些都可能引入 RTL 仿真里看不到的行为差异。跑一次综合后网表的功能仿真能抓出不少这类问题。要注意这里说的是“功能仿真”时序部分后面单独讲。第三类是形式等价性检查。用第三方工具比如做 LEC 的去比对 RTL 和综合后网表确认综合没有改变电路功能。这类工具吃的输入就是门级网表和 RTL。第四类是给第三方工具链喂数据。功耗分析、静态时序分析STA、甚至某些定制流程都需要网表输入。1.3 综合后网表和布局布线后网表差在哪这个区别很多人第一次做会搞混直接导致仿真结果对不上。综合后网表post-synthesis netlist只反映逻辑映射结果用的是 UNISIM 原语不含任何物理延时信息走线延时、布线拥塞带来的影响一概没有。布局布线后网表post-implementation netlist已经把布局和布线都做完用的是 SIMPRIM 原语配合 SDF 文件能反标真实的路径延时。对比维度综合后网表布局布线后网表生成阶段synth_design之后place_designroute_design之后原语库UNISIMunisims_verSIMPRIMsimprims_ver延时信息无或仅有估算值有完整 SDF 反标能否做时序签核不能能主要用途功能仿真、交付、等价性检查时序仿真、时序签核我那会儿第一次做拿了综合后的网表去跑时序仿真发现时序全过差点以为设计稳了。后来才知道综合阶段的 SDF 是估算出来的所谓“过”毫无意义。真正的时序结论只能看report_timing_summary在 route 之后的结果。2. 准备工作别急着敲命令2.1 版本与器件库要先对齐开工之前有两件事必须先确认。一是 Vivado 版本网表生成命令在不同版本间有过细节调整尤其是write_verilog的-mode参数可选值2014.x 时代和 2020.x 之后有差异。我用的 2022.2 是相对稳定的选择推荐新项目直接上 2020.2 之后的版本。二是器件家族的仿真库。如果你只是要导出网表文件本身不跑仿真那库的事情可以放一放但只要涉及仿真就必须先用compile_simlib把仿真库编译好或者让 Vivado 在启动仿真时自动编译。这个编译过程第一次要花十几到几十分钟编译产物放在工程目录下后续复用。# 手动编译仿真库指定输出目录和目标仿真器 compile_simlib -directory ./sim_lib -simulator modelsim -family all -language all -library all注意compile_simlib会吃掉好几个 GB 的磁盘空间提前确认盘符剩余容量。中途磁盘满了会导致库文件损坏现象是仿真时报某个 unisim 模块找不到很难往磁盘方向想。2.2 工程模式还是非工程模式Vivado 支持两种流程Project 模式有 .xpr 工程文件GUI 友好和 Non-Project 模式纯 Tcl 脚本驱动适合 CI。两种都能导网表命令略有不同。工程模式下你需要先综合完成然后open_run synth_1把综合结果打开再执行写网表命令。非工程模式下synth_design执行完设计就处于打开状态可以直接写。我个人在一次性探索和调试时用工程模式因为波形和报告看起来方便固定下来的交付流程一律用非工程模式方便脚本化和版本管理。下面两种都会给。2.3 综合时就要为网表埋好伏笔有几个综合选项会直接影响网表长什么样最好在综合阶段就定好。-flatten_hierarchy控制层次保留策略。full完全展平得到一张平坦网表none保留原 RTL 层次rebuilt默认重建层次兼顾面积和可读性。如果下游工具需要按模块定位选none或rebuilt。-keep_equivalent_registers是否保留等价寄存器。默认不保留综合器会把功能相同的寄存器合并。如果你的网表要跟 RTL 做逐点比对保留它们能让对应关系更清晰。-fsm_extraction状态机抽取方式。改成one_hot或sequential会让网表里的状态寄存器编码方式改变做等价性检查时要注意两边一致。-shreg_min_size移位寄存器的最小推断长度。默认是 3调大能减少 SRL 数量但会影响网表的结构。# 非工程模式下带这些选项综合 synth_design -top top -part xc7a35tfgg484-2 \ -flatten_hierarchy rebuilt \ -keep_equivalent_registers \ -fsm_extraction auto \ -directive Default-directive我一般保持Default。有些项目为了追频率会改PerformanceOptimized但那个会显著改变网表结构不利于后面比对。3. 两条路径把网表导出来3.1 GUI 方式适合先摸清楚有哪些产物如果你刚开始接触先用 GUI 走一遍能直观看到 Vivado 到底给你生成什么文件。流程是这样综合跑完左侧 Flow Navigator 里的 SYNTHESIS 分组下点Open Synthesized Design。菜单File→Export→Export Netlist。弹出的对话框里选输出格式Verilog / VHDL / EDIF选输出目录和文件名。需要仿真网表的话勾上下面的选项Vivado 会一并生成带_timesim后缀的文件。这里有个容易忽略的细节Export Netlist 对话框里对 Verilog 输出的选项和 Tcl 命令的-mode是一一对应的。你在 GUI 里选“Verilog”默认给的是design模式的网表它不含仿真信息拿去做仿真会发现时序块specify block全没了。想要仿真用的得选带时间信息的那个选项。提示GUI 导出一次之后Vivado 会在 Tcl Console 里打印出它实际执行的命令。把这个命令抄下来就是你后续脚本化的起点比翻文档快得多。3.2 Tcl 方式工程模式下的标准写法工程模式最顺手的写法是这样的# 打开工程 open_project ./my_proj.xpr # 等待综合完成并打开综合结果 open_run synth_1 -name synth_1 # 1) 导出普通门级网表给下游工具/交付用 write_verilog -force -mode design ./out/top_synth.v # 2) 导出仿真用网表 write_verilog -force -mode timesim ./out/top_timesim.v # 3) 导出 EDIF 网表 write_edif -force ./out/top.edif # 4) 单独导出 SDF write_sdf -force ./out/top_timesim.sdfopen_run这一步不能省。有人直接open_project之后就调write_verilog会报“no open design”。因为工程打开只是加载了工程对象设计还没进内存。3.3 Tcl 方式非工程模式下的完整脚本非工程模式适合放进脚本流程从头到尾一条龙# 读源码 read_verilog ./rtl/*.v read_xdc ./constr/top.xdc # 综合 synth_design -top top -part xc7a35tfgg484-2 -flatten_hierarchy rebuilt # 时序约束和报告可选先看综合估算时序 # report_timing_summary -file ./report/synth_timing.rpt # 导网表 write_verilog -force -mode timesim -sdf_anno true -sdf_file top_timesim.sdf ./out/top_timesim.v write_verilog -force -mode design ./out/top_synth.v write_edif -force ./out/top.edif # 单独 SDF 也来一份方便分开使用 write_sdf -force ./out/top_timesim.sdf puts netlist export done注意write_verilog -mode timesim里带了-sdf_anno true和-sdf_file这个组合会在网表顶部自动插入一段$sdf_annotate语句仿真器读到它就会自动去找对应的 SDF 文件并反标延时。这个细节后面第 4 节会详说。3.4 write_verilog 的关键参数逐个拆这是整个流程里参数最绕的命令我整理了一张对照表日常查这个就够了。参数作用常见取值与说明-mode决定输出网表类型design结构网表timesim仿真网表synth_stub空壳port仅端口default默认-force覆盖同名文件不加的话文件存在会报错停下-sdf_anno网表里是否插入 SDF 标注语句true/false-sdf_file指定 SDF 文件路径搭配-sdf_anno true用-include_xilinx_libs把 Xilinx 库模块也打进网表慎用网表会暴涨-rename_top重命名顶层模块交付时改名避免和客户设计冲突-keep_vcc_gnd保留 VCC/GND 原语某些工具链需要-noloc去掉//LOC之类的物理约束注释交付更干净-mode的语义值得单独说几句。design是最“干净”的结构网表只有实例和连线适合给 STA 或等价性检查工具。timesim会额外生成specify延时块、INIT初始化、以及存储器初始化数据是给仿真器看的。synth_stub只留一个模块声明和端口内部全空用在先占位后填实现的场景。port更极端连端口方向信息都是最简形式。我踩过的最大的坑就是拿design模式的网表去跑功能仿真。仿真器编译能过但跑起来所有 FF 的初值都是 X波形一片红查了半天才发现是网表里没有初始化信息。后来换timesim模式就正常了。注意-include_xilinx_libs别乱加。它会把用到的那一大堆 unisim 原语定义直接塞进你的网表文件里几万个模块文件能到几百 MB编译仿真器的时候能把内存吃光。4. SDF 反标与后综合仿真的坑4.1 SDF 是什么、后综合的 SDF 能不能信SDFStandard Delay Format是一份标准格式的延时描述文件里面按层次列出了每个单元cell和每条线网net的延时值仿真器读进来之后把延时加到对应的实例和连线上仿真波形就会带上延时。$sdf_annotate就是仿真器接受这份文件的入口。关键结论再强调一次综合后生成的 SDF 是估算值不能用于时序签核。综合阶段布局布线都还没做工具只能用统计模型估算线延时所谓的零线载模型这个值和真实布线差得很远。所以后综合仿真结果只能看功能对不对时序过不过看它没有意义。那为什么还要生成它一是方便做“功能正确性”验证时排除明显的组合环路和短路径问题二是很多第三方工具偏偏要求有 SDF 才肯吃网表。也就这两种场景值得用。4.2 仿真库怎么配、网表怎么加载仿真器这边分两种。用 Vivado 自带的 xsim它会自动去找已经编译好的库只要compile_simlib或之前的仿真跑过一次基本不用手动配。用第三方仿真器比如 ModelSim 或 Questa就需要在编译网表前先把 unisims_ver 和 simprims_ver 库映射好。# ModelSim 里映射 Vivado 仿真库 vmap unisims_ver {C:/sim_lib/unisims_ver} vmap simprims_ver {C:/sim_lib/simprims_ver} vmap xpm {C:/sim_lib/xpm}后综合网表用到的原语主要来自 unisims_ver但有些情况下工具会引到 simprims_ver 里的模型两个库都映射上最稳妥。如果设计里用了 XPM参数化宏xpm 库也不能少。编译顺序上一般是先编译仿真库 → 再编译 IP 的仿真模型 → 最后编译网表。IP 那一步很容易漏尤其是 FIFO、BRAM、DSP 这类 IP如果它们是以.dcp或加密网表形式集成的仿真时必须把 IP 对应的仿真文件一起加进来否则网表里只能看到一个黑盒实例端口拉不出内部行为波形全 X。# ModelSim 下的编译顺序示意 vlog -sv incdir./sim_lib ./sim_lib/glbl.v vlog -sv ./ip/fifo_generator_0/fifo_generator_0_sim_netlist.v vlog -sv ./out/top_timesim.v vlog -sv ./tb/tb_top.v vsim -novopt -L unisims_ver -L simprims_ver -L xpm work.tb_top work.glblglbl这个模块老生常谈但必须提。它负责驱动全局复位/置位信号GSR和GTS这些全局网络的默认值。忘记加 glbl仿真一开始所有寄存器就是 X而且不会报错只是波形不对新人在这上面卡半天很正常。4.3 一个能跑起来的最小仿真验证光有网表不够还要有个 testbench 把它包起来。后综合仿真和 RTL 仿真在 testbench 上有一点不同RTL 仿真可以直接给clk计时、给复位网表版同样可以但要注意网表里的端口名可能和 RTL 不完全一致尤其是顶层端口加了前缀或者大小写变了。timescale 1ns / 1ps module tb_top; reg clk 0; reg rst_n 0; reg [7:0] din 0; wire [7:0] dout; always #5 clk ~clk; // 100MHz initial begin rst_n 0; #200 rst_n 1; #100 din 8h5A; #200 $display(dout%h, dout); #500 $finish; end top u_top (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout)); endmodule仿完之后对比 RTL 仿真的输出如果 RTL 和网表两个结果一致说明综合没有引入功能偏差这一关就算过了。提示后综合仿真尽量别用那种跑几百万周期的长测试用例网表级仿真速度比 RTL 慢一两个数量级。挑能覆盖关键功能路径的短用例就行。真要跑长用例考虑只对重点模块导网表而不是整个顶层。5. 常见问题与排查实录5.1 网表导出来了但仿真编译不过最常见的原因是库没映射或映射错了。报错信息一般是Module LUT6 is not defined这类。排查顺序先确认compile_simlib有没有成功跑完再看仿真器里vmap是不是指向了正确目录最后检查库的版本和 Vivado 版本是不是同一套。用错版本的库某些原语名会对不上。还有一种情况是网表用 Verilog-2001 语法写出来但仿真器按 SystemVerilog 编译报错或者反过来。加-sv参数让仿真器按 SV 处理一般能解决。5.2 综合报错导致网表根本没生成write_verilog之前必须先有综合结果。如果综合阶段就挂了后面所有命令都会失败。综合常见报错有几类缺少 XDC 文件里的时钟定义只是警告不阻塞、模块例化名不匹配阻塞、器件型号写错阻塞。先看synth_design的 log确认综合是synth_design completed successfully再往下一步走。5.3 网表里 IP 全变黑盒如果 IP 是用 IP Catalog 生成并以.dcp形式集成正常情况下综合会把它的网表展开进顶层网表。但如果 IP 是以“网表模式”交付且 OOCOut-of-Context综合顶层网表里可能只保留一个封装好的模块引用。这时候要么在综合设置里关掉 OOC要么把 IP 的网表文件一起打包交付并在文档里说明依赖关系。这个坑在客户集成时特别容易翻车。5.4 端口名和 RTL 对不上Vivado 综合可能会把某些端口名做大小写归一化或者给保留字加后缀比如 RTL 里叫input的端口网表里可能变成input_0。做等价性检查或者 TB 例化时要注意。可以在综合前用KEEP_HIERARCHY属性把关键模块锁住减少改名情况。5.5 问题速查表现象最可能原因处理办法仿真报原语未定义仿真库未映射重新vmap并确认版本一致所有 FF 波形为 X缺少glbl模块仿真时加上work.glbl网表里找不到内部模块综合展平了层次综合时-flatten_hierarchy none时序全过但实际板子不稳用了后综合 SDF 判断时序改看 routed 后的report_timing_summary网表文件过大加了-include_xilinx_libs去掉该参数导出报无设计打开漏了open_run先open_run synth_1IP 显示为黑盒IP 为 OOC 或加密网表关 OOC 或一并交付 IP 网表5.6 几个私藏的操作习惯我自己固定下来的几个做法分享给同样折腾这块的人。第一网表导出后立刻做一次尺寸和模块数检查用wc -l和grep -c ^ 之类的粗糙办法估一下异常偏小往往说明导出的是 stub 而不是真网表。这个检查花不到十秒能省掉后面一堆麻烦。第二每次导出都带上一个版本标记目录比如./out/netlist_v3_20240510/网表一旦改了就很难用 diff 看出改了什么只能靠版本目录管理。EDIF 是二进制格式更需要靠目录区分。第三做交付时把 SDF、网表、以及一份说明依赖库的 README 放在一起。接收方最怕的是拿到网表不知道要哪几个库一份简短的 README 能省掉好几轮来回沟通。6. 从导出到交付我踩过的几件事第一次做网表交付的时候我以为把.v发过去就完事了结果对方集成时发现 IP 的仿真网表没打包进去一编译全是黑盒。第二次我学乖了把 IP 网表也打包结果又忘了告诉他们需要 unisims_ver 和 xpm 两个库。第三次我写了个交付清单从网表、SDF、IP 网表到库依赖说明全列上这回总算一次通过。这三次折腾下来我最大的体会是网表导出本身只是一个write_verilog命令真正花时间的是把它变成一个别人能直接用起来的东西。另外分享一个关于-mode的小技巧。我现在的习惯是一次性导两份design模式给下游 STA 和等价性检查工具timesim模式给仿真。两份文件放不同子目录命名上统一加_synth和_timesim后缀这样谁拿到都不会搞混。虽然多花几秒但省下的沟通成本远大于这点时间。最后提一个后面可以继续深挖的方向如果设计规模大到单张网表几万行等价性检查工具会很吃力这时候可以按层次分别导出子模块网表分块做检查再把结果汇总。这个方法在大型 SoC 集成验证里挺常见用 Vivado 的-flatten_hierarchy none配合link_design分模块处理就能实现下次可以单独展开聊聊这块。