我最早用Genus跑第一次综合,是在一个很小的流水线模块上。当时以为把RTL丢给工具、跑完命令就能拿到网表,结果光库文件就折腾了两天,后来才发现是环境变量和库版本对不上。这篇随笔是数字IC设计随笔系列的第九篇,主要记录我学习GENUS基本综合流程的完整思路:库文件准备、RTL读入、SDC约束、综合策略和报告解读。如果你已经开始写RTL,但还没有完整跑过一次综合,这篇文章应该能帮你把流程串起来,也能理解每一步到底在解决什么问题。
我会按我自己跑通一个模块的实际顺序来讲,不按工具手册的顺序。因为很多手册把命令堆在一起,却没有告诉你哪些地方容易翻车、哪些报告值得先看。下面进入正题。
1. 综合流程全景:从RTL到门级网表到底发生了什么
1.1 Genus的基本命令骨架
Genus的基本综合流程,说到底就是五件事:读RTL、细化设计、定约束、做综合、出报告。用命令写出来大致是下面这个样子:
# 1. 读入RTL read_hdl -sv ./rtl/top.sv ./rtl/alu.v ./rtl/fft_ctl.sv # 2. 细化设计,把模块层次真正展开 elaborate # 3. 初始化设计并引入约束 init_design read_sdc ./constraints/top.sdc # 4. 综合与优化 syn_gen # 5. 报告与输出 report_qor report_timing -nworst 2 write_hdl > ./outputs/top_netlist.v write_sdc > ./outputs/top_syn.sdc这里我强烈建议你先分清read_hdl和elaborate的区别。read_hdl只负责把RTL文本读进工具,它不会真正展开模块例化,也不会对parameter做全面求值。真正把模块层次、generate块、参数传递确定下来的是elaborate。很多第一次用Genus的同学跑完read_hdl就急着看时序,结果发现报告里全是空的,回头才发现是elaborate没执行。
命令语法在我手头的版本上是这样,不同版本有小差异,但逻辑顺序不变。我习惯把这五步写成一段tcl脚本,然后交给Genus批量执行,这样每次流程都可复现,不会因为手动敲命令漏掉哪一步。
1.2 综合过程中的逻辑优化与技术映射
刚接触综合时,我总觉得工具是在"翻译"RTL,实际上它的动作比翻译多得多。综合工具内部要做三件事:先把你写的HDL转换成一种内部的功能描述,再做逻辑层面的化简与重定时,最后把化简后的逻辑映射成工艺库里的标准单元。
这个过程可以类比成做菜:RTL是菜谱,综合工具先把文字描述整理成明确的备料清单,然后根据冰箱里实际有的食材调整方案,最后把每道菜具体到用什么锅铲、什么火候。技术映射就是在问:这个逻辑用二输入与非门实现,还是用AOI复合门实现更省面积?这个加法器用行波进位结构还是超前进位结构?
理解这一点之后,你就不容易对综合结果产生"工具是不是把我的代码改坏了"的怀疑。综合工具在优化时确实会改变逻辑结构,但只要约束合理、代码风格可综合,它总能找到一个满足时序的等价映射。
1.3 为什么我建议先拿小模块练手
我的第一个综合对象是一个带状态机和外部FIFO接口的小控制模块,大概两千行RTL。当时跑出来的违例报告有几十条,我看得一头雾水,根本分不清是约束写错、代码写差还是库配置有问题。后来换了一个只有几百行的小模块重跑,一眼就发现是时钟约束定得太紧,和库单元能力完全不匹配。
先拿小模块练手的价值就在这里:变量少,问题暴露得直接。大设计里问题会被层层路径平均掉,新手很难判断优先级。小模块跑通之后,你会形成一种"综合到底想要什么"的直觉,再上大设计就不会对着报告干瞪眼。
2. 库文件与环境准备:翻车率最高的起步环节
2.1 工艺库、标准单元与延迟模型
综合工具必须知道目标工艺下有哪些门、每个门多快、多大、多费电,这些信息全部来自工艺库。常见的有文本格式的.lib,也有二进制格式的.db,内容是一个个标准单元的功能描述和时序弧。
我第一次自己搭综合环境时,把工艺库路径写错了一个层级,Genus直接报了一堆乱码一样的单元缺失错误。后来才明白,工具分"target library"和"link library"两个概念:target library负责告诉工具能映射到什么单元;link library负责把已经存在的IP、宏单元、库电池引进来做链接。概念上一混淆,后面全部白搭。
延迟模型方面,现代库一般使用NLDM或CCS模型。NLDM用查找表描述单元延时与输入转换时间、输出负载的关系;CCS用电流源模型描述得更准,对低压工艺更友好。对我们跑学习流程来说,不需要手动选,直接用库默认模型就好,但至少要知道库里面那些PKG、INDEX是什么意思,不然看时序报告时一头雾水。
2.2 一个可复用的启动脚本骨架
我把自己常用的Genus环境初始化脚本放在下面,每次换工程只改路径:
# 初始化路径 set_db init_lib_search_path "/path/to/stdcell_lib /path/to/macro_lib" set_db init_hdl_search_path "./rtl" # 读入库 set_db library [list /path/to/stdcell_lib/xxx_stdcell.lib \ /path/to/macro_lib/xxx_macro.lib] # 设置顶层例化时的设计名称 set_design_name top # 读入RTL文件 read_hdl -sv ./rtl/*.sv # 细化设计 elaborate这个骨架有几个地方值得注意。第一,init_lib_search_path只影响后续read_lib或set_db library时的相对路径查找,没有它你的相对路径很容易失效。第二,我在脚本开头用相对路径,再用变量统一管理,避免每次手改一长串绝对路径。第三,如果把read_hdl写成read_hdl ./rtl/*.sv,不同版本的Genus对通配符的支持不完全一致,我建议先用ls检查文件列表再读入。
环境准备的另一个坑是版本管理。工艺库更新、EDA工具更新、RTL改动这三者经常是不同步的。我用了一个笨办法:在输出网表的文件头里写下综合日期、Genus版本、库文件路径,这样以后再查网表问题时能快速定位是哪一套环境产生的。
2.3 库文件相关的三个典型报错
我实际遇到最多的库相关报错有三种,这里把排查思路列出来。
第一种报错是"unknown library cell",工具在映射时找不到某个单元。原因通常是link library没有包含某个宏单元库,或者库文件版本太老缺了部分单元。排查时用report_lib_cell这类命令确认单元是否存在于库中即可。
第二种是"timing library not found",几乎永远是路径问题。检查set_db library的路径是不是真的指向了包含时序信息的库,而不是只有一个空的符号库。
第三种比较隐蔽:RTL里某个模块名字和工艺库里的标准单元名字重了,工具读入RTL后把库单元当成了设计模块,综合结果完全不对。解决办法是给工艺库单元加前缀,或者修改RTL模块命名,尽量避开常见门名称。
3. 读设计与约束:让综合工具认识你的设计边界
3.1 RTL读入和elaborate阶段的常见问题
read_hdl阶段一般很少出错,错也多是语法层面;真正暴露设计问题的常常是elaborate。这个阶段会做层次展开、参数求值、端口连接检查,很多在仿真里完全没有暴露的问题会在这里浮现。
最常见的错误包括:某个实例对应的module不存在、总线位宽不匹配被截断、parameter传递成了负值、generate块里例化的模块没有定义。这些在仿真里可能不会出现直接报错,但综合阶段不允许含糊。
我的习惯是读入RTL之后,先用report_design或类似命令看一下当前设计里有哪些顶层端口、多少个层次实例,确认工具眼里的设计和自己预期一致。这一步只要几十秒,但能省掉后面排查问题的几个小时。
另外,RTL里尽量别用太长的模块名和端口名,也避免使用带中括号、反斜杠等特殊字符的命名。综合工具虽然能处理一部分,但网表输出、后端布局布线阶段遇到特殊字符的概率会成倍增加。
3.2 SDC约束怎么写:时钟、I/O时序与例外
SDC约束是综合里最影响结果的部分。约束代表的是"这个设计在真实系统中如何被使用",不是拍脑袋写死的一串数字。最基础的几条是时钟定义、输入输出延迟和时序例外。
# 时钟定义 create_clock -name clk -period 10 [get_ports clk] set_clock_uncertainty 0.1 [get_clocks clk] # 输入输出延迟 set_input_delay -max 2 [get_ports data_in] -clock clk set_output_delay -max 2 [get_clocks clk] [get_ports data_out] # 时序例外 set_false_path -from [get_ports rst_n] set_multicycle_path -setup 2 -to [get_clocks clk]这里必须理解一个原则:set_input_delay和set_output_delay定义的是端口外部路径的延迟,不是端口内部延迟。写input_delay = 2ns的意思是输入信号在时钟沿前2ns到达端口,这个2ns代表了上一级芯片或逻辑的延迟。如果你不理解这一点,很容易把外部延迟写成内部延迟,导致综合工具拼命优化本来不该优化的路径。
时序例外的滥用也是一个重灾区。set_false_path的本质含义是"这条路径不需要做时序检查",但它不是用来掩盖违例的。如果路径上真的有寄存器接收数据,乱设false_path会让后端在签核阶段收到一堆真实的时序失败。正确的做法是把约束当成一份给后端同事的设计说明书,每一条例外都要能讲清楚为什么。
3.3 约束与代码的矛盾要怎么判断
跑完综合看到几十条违例,先别急着改RTL,先看约束。我遇到过一种情况:模块内部有一个同步FIFO指针,仿真完全正常,综合却报出几百条路径违例。查了半天才发现,约束文件里把整个模块的输入端口误设了frontier delay,相当于在外界假设了一个非常苛刻的数据到达时间,工具为了满足这个不合理的约束,把大量逻辑反复优化,面积也跟着暴涨。
判断约束是否合理有一个简单的办法:把RTL放到一个空旷的测试环境里,用同样的时钟频率做行为仿真,看设计是否存在至少一个可解释的裕量。如果行为级仿真在10ns周期下跑得很稳,约束却报告一堆0.5ns的违例,大概率是约束定义有问题,而不是逻辑本身有致命问题。
所以我的建议是:违例报告出来以后,先花半小时逐条读约束,再花十分钟看RTL。大部分初学者都喜欢反向操作,结果浪费一整天在改代码上。
4. 综合策略与迭代:跑通第一版结果之后的事
4.1 综合层次与流程选择
模块规模一大,top-down综合就会很吃力。因为顶层路径上跨越多个子模块,任何一块的小改动都会影响整个设计的迭代。规模到几百万门的时候,更常见的做法是bottom-up,先对各子模块分别综合,再在顶层做整合。
但我强烈建议新手不要一上来就搞自底向上。第一次学习时,先用一个小设计把top-down跑通,理解"覆盖""约束传递""结果整合"这些概念之后,再涉及分层综合。否则你会同时遇到工具问题、约束问题和设计问题,三件事搅在一起完全无法定位。
另外,综合策略里还有快速模式、探索模式等区别。快速模式适合验证流程是否跑通,优化程度较低但速度快;探索模式会尝试多种优化方向,结果更接近最佳但耗时更长。我的习惯是流程调试阶段用快速模式,确认无误后再切探索模式做最终综合。
4.2 时序违例的第一次迭代
第一次看到真实的时序违例,很多人会慌。这里给你一个自己常用的迭代顺序:
第一步,看WNS和TNS,判断违例是集中在一条路径还是分散在很多路径。集中在一条路径说明是某个结构性问题,分散说明约束或库配置可能整体不合理。
第二步,从report_timing里挑最差的路径,算一下logic depth。如果一条路径有十几级组合逻辑,哪怕每个门只延迟0.1ns也可能超时。这时先想有没有办法在RTL里插流水线或并行化,而不是继续让工具优化。
第三步,检查路径两端是不是异步时钟域或复位释放路径,如果属于已声明false_path的路径,需要回去查 SDC 是否真的声明了。
我自己的一个经历是64位加法器,综合后关键路径延迟特别长,改约束怎么都下不去。后来把加法器改成流水结构,在中间插了两级寄存器,问题马上解决。工具能做的优化有物理上限,代码结构决定了天花板。
4.3 面积与性能的权衡
综合工具的默认行为是在满足时序目标的前提下尽量减面积和功耗。所以如果你约束给得很松,它会优先用最省面积的低阈值单元;如果时序紧,它会大面积使用高性能单元,面积功耗都会涨。
多阈值单元的使用是一个例子。低阈值单元漏电大但速度快,高阈值单元漏电小但速度慢。综合工具会根据路径的时序需求自动混用。我们自己不需要手动指定,但要看懂报告里单元数量的变化:如果面积突然翻倍,大概率是某条路径时序压力过大,工具在疯狂堆高速单元。
还有一点要提醒:综合报告里的面积不是芯片最终面积。它只是标准单元总面积,不包括布线资源和宏单元布局的额外开销。所以综合阶段面积利用率做到85%以上就要警惕后端拥塞风险,留出足够余量给布线阶段。
5. 解读综合报告:从日志和报告里发现设计问题
5.1 时序报告:WNS、TNS与关键路径
每次综合完,我都养成了先看三个数字的习惯:WNS、TNS和违例路径数量。WNS是最差负时序余量,TNS是所有负余量路径的累加。只看WNS会漏掉很多小违例,通常会同时看TNS和违例条数来判断严重程度。
看report_timing时,重点不是看最后一个数字,而是看路径结构。我一般会看四样东西:startpoint是不是符合预期;endpoint的时钟是不是被正确约束;路径中间有没有特别慢的单元或特别长的连线;最终slack与库单元本身的极限差多少。看到一条路径里有一个单元延迟占了一半,就要想是不是结构问题。
下面是我常看的几个字段:
| 字段 | 含义 | 我的使用方式 |
|---|---|---|
| WNS | 最差负时序余量 | 判断整体最差情况 |
| TNS | 总负时序余量 | 判断违例的覆盖范围 |
| Slack | 路径时序余量 | 判断路径能否继续优化 |
| Logic Depth | 组合逻辑级数 | 判断是否需要改RTL结构 |
| Cell Count | 路径上的单元数量 | 辅助判断拥塞风险 |
5.2 面积利用与拥塞隐患
综合阶段就要开始想后端的问题。面积利用率过高是拥塞的直接根源。我遇过一次综合报告显示面积利用率92%,当时觉得没问题,结果后端布局布线时整个布线资源严重不足,修了将近一周才压下来。
后来我形成的习惯是:综合阶段就把面积利用率控制在80%到85%以下,如果超过就要回看代码里是否存在大量冗余逻辑、重复算法或过于复杂的case语句。比如某个模块里用了多个64位比较器的组合逻辑,其实可以用编码后的状态比较来代替,面积立刻下降一截。
同时,也可以借助工具自带的congestion估计或密度图粗略判断。虽然最终结果以后端布线为准,但综合阶段的趋势预判能帮你提前避开大坑。
5.3 常见问题排查清单
这段时间我自己整理了一张排查清单,遇到综合问题先照表过一遍:
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 时钟违例多且分散 | 时钟约束过紧或库能力不足 | 先检查周期和uncertainty是否合理 |
| 个别路径极差 | 逻辑级数过深 | 考虑插入流水线或并行化 |
| 面积异常偏大 | 约束太紧导致工具堆高速单元 | 放宽非关键路径约束 |
| 库单元缺失 | link library未包含对应库 | 检查library设置 |
| 报告里大量x态 | RTL存在未初始化信号 | 先修RTL再综合 |
| 输出网表和RTL功能不一致 | 代码存在不可综合写法 | 逐条检查可综合性 |
上面这些问题的修复方式,大部分不是靠换个综合命令能解决的,需要回到RTL和约束层面去思考。
6. 跑完一趟Genus综合后,我给自己留的几个检查习惯
这一篇从库文件一直讲到报告解读,基本流程已经串完了。最后想分享几个我自己坚持了很久的小习惯,不算什么高深技巧,但确实帮我省了非常多的时间。
第一个习惯是综合前给RTL做一次可综合性自检:有没有initial赋值、有没有task里使用延时控制、有没有循环上界不确定的for语句。这些问题仿真不一定暴露,综合时一定会。
第二个习惯是每一轮综合都保留日志和命令记录。我之前调试一个面积异常问题,因为没有留存之前的命令日志,完全没法判断是哪一次设置产生了影响,只能全部推倒重来。后来不管多忙,每次跑综合都会把log归档,按日期命名。
第三个习惯是把常用报告输出成文件,快速浏览WNS、TNS、面积三个数字,形成模块的"体检表"。每个模块每次改动,这三个数字的变化趋势就是最好的回退判断依据。改完一版代码,看这三个数就知道是变好还是变差。
最后想说的是,综合流程本身并不复杂,真正复杂的是背后对时序、面积、功耗、约束的权衡理解。把这些权衡想明白了,你再用Genus跑任何设计,都会觉得工具其实只是一个放大器——把你在RTL和约束里埋下的每一个决策,都放大成可量化的数字。