
我第一次接触DC工具的时候心里其实挺没底的。RTL代码写得满满当当可把命令敲进dc_shell回车那一瞬间我完全不知道接下来会发生什么。只看到终端里刷过一屏又一屏的信息最后吐出一个网表、一份时序报告我盯着上面的负slack半天没想明白这个数字是怎么算出来的。后来自己带人、帮别人排查flow才慢慢意识到DC工具本身不难难的是知道每条命令到底在解决什么问题。如果你也卡在“命令会敲但不太理解”这个阶段这篇应该对你有用。DC工具是Synopsys Design Compiler的惯用叫法做数字IC设计的工程师几乎没有不知道它的。它做的是逻辑综合把可综合的RTL代码在时序、面积、功耗这些约束的引导下映射成基于特定工艺标准单元的门级网表。简单说RTL描述的是“电路应该做什么”门级网表描述的是“用这个工艺库里哪些单元、怎么连起来做这件事”。这篇是DC工具基本使用系列的第一篇面向刚接触数字IC流程的读者我会从综合的基本原理讲起把读入设计、加约束、跑综合、看报告、踩坑这整条链路完整走一遍。内容不追求把所有命令都罗列一遍而是把最常用、最容易理解错的部分讲透。1. 综合到底在干嘛从RTL到门级网表的“翻译”过程很多新手以为综合就是把RTL“变”成网表这个说法太笼统。DC工具内部其实做了三件不同性质的事转换、优化、映射。把这三件事分开理解你就能看懂DC工具为什么需要工艺库为什么要加约束以及为什么同一段RTL在不同库、不同约束下会得到完全不同的网表。1.1 三个动作转换、优化、映射“转换”是把RTL语言描述的行为级逻辑变成某种与工艺无关的通用布尔逻辑结构。你可以把它理解成翻译a b c这种写法先被翻译成底层逻辑表达式这时候还没有和任何具体工艺库扯上关系。“优化”是在这个通用逻辑结构上做等价变换比如把冗余项消掉、把公共因子提出来、把串行计算改成并行结构目的是减少逻辑级数和面积。“映射”才是真正和工艺库打交道的步骤把优化后的通用逻辑替换成目标库里真实存在的标准单元比如AND门、OR门、MUX、DFF。替换的同时DC工具会再次根据时序和面积约束做调整选驱动强度合适的单元必要时插入buffer。我用一个生活化类比转换是写出一份普通话提纲优化是反复删减句子让表达更精炼映射是找一个母语者把提纲说成地道英文。同一个意思母语者不同、口音要求不同、时间限制不同最终说出来的话就不一样。DC工具也是这样库不同、约束不同网表就不同。1.2 DC工具的两大输入和一个隐藏前提做一次综合表面上看需要两个输入RTL文件和约束。RTL文件是你要实现的功能约束是你对性能、面积、功耗的要求。但还有一个隐藏前提常常被新手忽略标准单元工艺库。没有工艺库DC工具只能在通用逻辑层做转换和优化到了映射这一步就直接卡死。这也是为什么你第一次打开DC工具如果target_library没有设好compile几乎必报错。约束的作用很多人理解也不够深。RTL本身不能告诉DC工具“这个电路要跑多快”只有约束能。时钟周期设成10ns还是20ns对网表结构的影响是巨大的周期紧工具会插入更多并行结构、用更大驱动强度的cell来缩短关键路径周期松工具会倾向选面积更小、速度更慢的单元。所以约束不是事后检查的指标而是综合过程中驱动每一个优化决策的方向盘。1.3 输出三件套网表、约束、报告综合跑完之后DC工具会产出三类东西。第一是门级网表通常写成Verilog格式这是后续布局布线工具的输入。第二是重新写出的SDC约束文件注意这个文件不是你手写的那份的复制品而是DC工具在综合过程中更新过、为后端工具准备的版本。第三是报告包括时序报告、面积报告、功耗报告、QoR报告等。很多人只关心网表忽略DDC文件这是不对的。DDC是DC工具的数据库格式保存了当前设计的综合后信息包括网表、约束、属性、逻辑结构。后续做形式验证、ECO、或者用IC Compiler做后端时DDC都是很省事的交接文件。我的建议是每次综合结束网表要写、SDC要写、DDC也顺手写出来反正就是一条命令的事后面能省掉很多麻烦。2. 开工前的准备工作RTL、工艺库和启动环境DC工具本身不校验你的代码风格好不好它只负责处理它能处理的代码。很多新手第一次跑综合不是死在命令上而是死在RTL本身不可综合。所以开工之前先把RTL这关过了。2.1 可综合的RTL先让代码能过编译可综合RTL基本遵循这么几条原则时序逻辑用always (posedge clk)或带异步复位的always (posedge clk or negedge rst_n)来写组合逻辑用assign或always (*)来写。initial、#10、wait、fork/join这些带有仿真语义的语句综合工具要么警告后忽略要么直接不支持。循环写不写genvar、generate也有讲究因为DC工具只在特定语法下做块展开。还有一个特别常见的坑是组合逻辑latch。case语句不写default或者if语句没有配套的else综合工具认为存在分支没有指定输出就可能推断出latch。latch在时序分析里很不讨喜不仅面积大还容易产生时序收敛问题。我的习惯是代码里组合逻辑的case永远补defaultif永远补else。这不是风格洁癖而是省得在综合报告里看到一堆不应出现的latch warning。2.2 target_library和link_library的区别DC工具有几个重要的库变量target_library和link_library是最容易搞混的。target_library是综合时用于映射的目标工艺库DC工具会从里面挑选标准单元来实现设计。link_library则更宽泛它用于解析设计中所有实例化的单元包括RTL中直接例化的宏单元、IP、Memory模型以及DesignWare库里用到的运算单元。有一个反直觉的点link_library里通常会加一个*。这个星号代表“内存中已经存在的设计”也就是你刚刚通过analyze和elaborate读入的RTL模块。如果不加*DC工具Link时会发现你例化的子模块“找不到”然后报出一堆undefined design错误。这个坑我在6.2节还会展开说。简单记住link_library至少要包含*、target_library里的库文件以及用到的Memory或IP库。2.3 启动方式与环境DC工具的启动方式有两种主流用法。一种是交互式Tcl模式终端里敲dc_shell -tcl进去一条一条敲命令适合改约束、看报告、调试。另一种是脚本模式把整个综合流程写成一个Tcl脚本然后dc_shell -tcl -f run.tcl一次跑完适合回归和批量跑版。新手我建议先交互式跑一遍把每步输出都看明白再转成脚本。环境配置通常写在.synopsys_dc.setup文件里。DC工具启动时会按固定路径搜索这个文件常见的是用户目录下和当前项目目录下。里面配置search_path、target_library、link_library这些变量。一个最小可用的配置大概长这样set_app_var search_path [list . ./rtl ./scripts ./db] set_app_var target_library {my_stdcell_tt.db} set_app_var link_library {* my_stdcell_tt.db dw_foundation.sldb} set_app_var symbol_library {my_stdcell.sdb}my_stdcell_tt.db只是占位符实际项目中替换成你工艺库提供的db文件。注意.db和.lib的区别.lib是文本格式库.db是编译后的二进制格式DC工具默认用.db。如果只有.lib可以用read_lib命令先读入并生成.db。3. 第一次跑通最简洁的dc_shell-t综合流程现在进入正题。假设你手头有一个简单的设计顶层叫top_module底下有两个子模块ctrl和datapath时钟端口叫clk异步复位端口叫rst_n。我们要把这段RTL从读入一直跑到网表输出。3.1 方式一analyzeelaborate分步读入DC工具读RTL有两种常见姿势。第一种是analyze加elaborate分两步走。analyze会对RTL做语法检查把代码解析成DC工具内部库里的中间文件elaborate再把顶层和子模块例化起来生成一个完整的设计对象。好处是出错时能清楚知道是语法问题还是例化问题。set TOP top_module set RTL_FILES [list \ ./rtl/ctrl.v \ ./rtl/datapath.v \ ./rtl/top.v \ ] analyze -format verilog $RTL_FILES elaborate $TOPelaborate完成之后当前设计会被自动设置为顶层。如果设计里有多个顶层或者你想切换后面可以再手动current_design。这种分步方式也方便你在elaborate之后用list_designs看一眼当前内存里有哪些设计确认读进来的结构和预期一致。3.2 方式二read_file/read_verilog一把梭第二种方式是直接用read_file或者更新版本支持的read_verilog一次性读入。这种方式更简单适合文件不多、层级不复杂的场景。但我觉得如果项目模块多还是analyze加elaborate更稳因为DC工具会明确告诉你哪个文件语法出错而不是笼统地失败。read_file -format verilog [list \ ./rtl/ctrl.v \ ./rtl/datapath.v \ ./rtl/top.v \ ]读完文件后最重要的动作是current_design。这个命令指定当前要综合哪个设计。因为一个文件里可能有好几个module或者你的工作目录里有多个设计不指定顶层DC工具不知道接下来该对谁操作。current_design $TOP3.3 检查与例化link和check_design接下来做两步检查link和check_design。link的作用是把当前设计里所有例化的子模块、宏单元、IP在link_library里找到对应定义并绑起来。如果报了Cant find the design xxx大概率是某个子模块没读入或者link_library里缺了对应库。check_design则是做一致性检查比如有没有未连接的端口、有没有悬空输入、有没有组合逻辑环。这些检查不是走流程而是帮你把明显的低级问题在综合前拦下来。有时候check_design报一堆warning新手容易直接忽略但我建议至少扫一遍有没有Error和Latch相关warning。3.4 最小约束集时钟必须最先定义约束是整个综合里最影响结果的部分。新手入门先牢牢记住一个顺序时钟约束永远最先定义。为什么因为输入输出延时、false path这些约束通通要挂在某个时钟上。时钟都没定义其他约束就没有参考点。一个最小可用的约束集合大概长这样create_clock -period 20 -waveform {0 10} [get_ports clk] set_clock_uncertainty 0.5 [get_clocks clk] set_clock_transition 0.2 [get_clocks clk] set_input_delay 2 -clock clk [remove_from_collection [all_inputs] [get_ports clk]] set_output_delay 2 -clock clk [all_outputs] set_max_area 0这段约束干了四件事定义了20ns周期也就是50MHz的时钟给时钟不确定性留了0.5ns告诉工具时钟沿的过渡时间是0.2ns输入输出分别给了2ns的接口延时。最后set_max_area 0的意思是面积越优化越好不设一个具体上限。第4章我会专门讲这里每条命令的含义先直接跑通。3.5 compile/compile_ultra与输出网表约束加完之后执行综合。对多数数字IC设计来说compile_ultra是更推荐的命令它在时序优化、功耗优化方面比普通compile更强尤其适合时序压力比较大的设计。如果只是跑通流程验证功能compile也够用。compile_ultra综合过程可能要跑几分钟到几十分钟取决于设计规模和约束松紧。日志里会不断输出优化进度。跑完之后把结果写出来write -format verilog -hierarchy -output ./output/top_netlist.v write_sdc ./output/top.sdc write -format ddc -hierarchy -output ./output/top.ddc到这一步你已经有了一份门级网表。但注意这只是综合后的网表还没有时钟树、还没有布线后端流程还要继续处理。DC工具输出的SDC文件要保存好后端工具要根据它来做时钟树综合和时序收敛。4. SDC约束里最容易理解错的三类命令SDC里的命令很多但真正在一堂入门课里必须彻底搞明白的其实就三类时钟定义、接口延时、例外路径。这三类理解透了大部分综合问题你都能自己分析。4.1 create_clock周期别拍脑袋create_clock最基础也是最重要。它的核心参数是-period和-waveform。-period 20代表20ns周期50MHz。-waveform {0 10}表示上升沿在0ns下降沿在10ns也就是50%占空比的方波。如果你的时钟不是50%占空比比如上升沿在0ns、下降沿在8ns波形就写成{0 8}。时钟端口名必须和RTL顶层端口严格一致。如果你给顶层起的时钟端口叫sys_clk命令行里写[get_ports clk]DC工具会直接报错说找不到这个端口。这种错不难处理但新手经常因为大小写或名字不一致卡住。记住DC工具区分大小写CLK和clk是两个完全不同的名字。还有一个实操经验要不要在综合时给时钟周期额外加margin。我的做法是如果后端的时序收敛压力大会在规格周期基础上留1%到3%的余量相当于让DC工具提前按更紧的目标做优化。但余量不是越大越好周期设太紧面积和功耗会明显上涨后端CTS反而更难做。折腾过两个项目后你会发现时钟周期的设定本身就是一次权衡。4.2 input/output delay接口时序的换算套路set_input_delay和set_output_delay是新手最容易算错的部分。set_input_delay描述的是相对于时钟边沿数据从外部到达芯片输入引脚需要多长时间。它通常包括上游器件的时钟到输出延时Tco加上PCB板级走线延时。比如上游芯片的Tco是1.5ns走线0.5ns那input_delay设2ns就合理。set_output_delay正好相反它描述的是下游器件的数据捕获要求。一般等于下游芯片的建立时间加上板级走线延时。比如下游建立时间1.5ns走线0.5ns那output_delay设2ns。这个2ns对DC工具的含义是输出数据必须在时钟边沿前2ns就稳定以此来保证下游器件能采到正确数据。设置时有一个几乎所有人都会踩一次的细节all_inputs包含了时钟端口。如果直接对整个集合设input_delay时钟端口也被当成数据端口算了一遍这会干扰时序报告。所以要用remove_from_collection把时钟端口从集合里剔除代码在3.4节已经给了。这个细节不处理报告会很奇怪而且你很难排查出来。4.3 false_path与multicycle_path动它们之前先问三个问题set_false_path和set_multicycle_path是例外约束作用很大但乱用的后果也很严重。前者告诉DC工具“这条路径不需要检查时序”后者告诉它“这条路径允许用N个时钟周期来传输数据”。false_path一般用于异步时钟之间的跨时钟域路径、复位释放逻辑、静态配置信号。使用前先问三个问题这条路径真的不需要时序检查吗跨时钟域有没有做同步处理这个例外会不会因为名字匹配太宽而误伤其他路径有些新手为了让时序报告好看把违例路径全设成false_path这种操作等于掩耳盗铃后端跑出来的芯片大概率工作不正常。set_multicycle_path则更精细。比如一个数据路径设计上允许两个周期完成你可以设set_multicycle_path 2 -setup -from A -to B。但注意设置了setup方向的多周期约束后通常还要配合设置hold方向否则hold检查可能被过度约束导致DC工具盲目插入大量buffer。这块的细节比较深入门阶段记住一点例外约束是工具很信任你的话说错了它真照做不会帮你把关。5. 综合报告到底看什么数字背后的物理含义综合跑完新手习惯看最后几屏有没有“All violations”这种字眼。但真正有用的习惯是打开报告文件从最差路径开始一条一条看。报告不是用来“通过/不通过”的它是你判断下一版该怎么改的唯一依据。5.1 report_timing永远从slack为负的路径开始看report_timing是时序报告的核心命令。我一般这样敲report_timing -path full -delay max -max_paths 20 -nworst 5然后从最差的slack开始看。时序报告的核心逻辑是对每一条从起点到终点的路径DC工具计算数据到达时间arrival time和数据必须就绪时间required time两者之差就是slack。slack为负说明数据来不及到达这条路径违例slack为正说明还有余量。新手看报告时容易迷失在密密麻麻的数字里。我提供一个简单的阅读顺序先看最上面几行确认检查的是哪个时钟沿、哪条路径再看报告末尾的slack是正还是负最后回到中间找关键路径上组合逻辑总延时是多少。如果组合逻辑延时过大说明这段逻辑本身要优化或者逻辑级数太多了如果组合逻辑延时不大但slack还是负那就要往约束是否太紧、单元驱动强度是否足够的方向排查。5.2 report_area面积不只是门数report_area看起来简单但有几个数字要分清。报告里通常有combinational area和noncombinational area前者是组合逻辑面积后者是寄存器、锁存器等时序单元面积。两者相加再算上net area才是设计总面积。实际上DC工具给的面积是基于库中单元的面积属性算出来的不是真实的版图面积。真实面积要等布局布线后才知道。所以综合阶段的面积报告更多是让你看个量级和趋势一版综合比上一版面积涨了20%是约束变紧了还是RTL改动导致的这个趋势比绝对值重要。如果面积异常膨胀先检查是不是生成了大量latch再检查时钟约束是不是过紧最后看综合日志里有没有插入几千个buffer。很多时候面积“爆了”不是RTL代码量的问题而是工具为满足过紧的时序约束在做等价交换。5.3 report_constraint与DRV违例DRV优先于时序综合完成后的第一份全局报告我会先跑report_constraint -all_violators。它会列出所有未满足的约束包括时序违例和DRV违例。DRV指的是设计规则约束比如max_transition、max_capacitance、max_fanout。这些规则来自工艺库是物理实现层面的底线要求。一个常见误区是只看setup时序违例不管DRV违例。实际上DRV违例会直接影响后端布局布线的信号完整性和时序如果综合阶段就存在大量max_transition违例后面CTS会更痛苦。我的处理顺序是先修DRV再修时序。DRV是工具眼中的“规则”时序是“目标”规则必须优先满足。新版本DC工具通常还可以用report_qor看到一份综合质量总结worst slack、TNS、总面积、总功耗都在上面适合快速判断这一版综合整体是否健康。跑完整流程后先看QoR再深入看具体路径效率会高很多。6. 第一次跑DC最容易踩的坑最后这章是我认为全篇最值钱的。这些坑不是从手册里抄的是我自己带项目、帮别人擦屁股擦出来的。每一个都真实发生过而且每一个都会让你白跑好几个小时。6.1 时钟忘了约束工具以为你只要一个理想网络有的新手嫌麻烦综合前不加时钟约束直接compile。DC工具不会报Error它会把所有寄存器当作没有时钟约束来处理优化的时候完全没有时序目标。最后出来的网表可能功能没问题但时序乱七八糟后端一接就崩。这种坑最恶心的地方在于它不是立刻报错的综合过程看起来一切正常报告里甚至可能看不到明显违例因为工具压根没在检查时序。所以我反复强调时钟约束是综合的第一步没有时钟就不要跑compile。写完脚本后检查一遍report_clock能看到时钟才允许自己往下走。6.2 link报undefined八成是link_library没配全link命令报Cant find the design xxx是很常见的综合启动期错误。我第一次遇到时以为是RTL文件没读进去把analyze重复跑了好几遍还是找不到。最后发现是.synopsys_dc.setup里link_library没有加*导致DC工具只去库里找设计不认内存里已经elaborate好的模块。解决方法是把link_library配成类似这样的形式set_app_var link_library {* my_stdcell_tt.db dw_foundation.sldb}。加了这个星号DC工具就会优先在内存设计里找例化模块。如果你还例化了Memory或其他IP确认对应库的.db也都在link_library列表里。6.3 uncertainty设成0后仿真必然教你做人set_clock_uncertainty用来模拟时钟偏斜、抖动以及留给后端的余量。有的新手不理解直接把uncertainty设成0觉得这样“约束更松更好做”。综合阶段确实更容易收敛了但后端做完时钟树后真实时钟偏斜远大于0后仿真随机就冒出setup违例到时候返工的成本比现在高十倍。我的习惯是综合阶段按周期的2%到5%估计uncertainty至少不低于0.3ns。后端CTS后有更精确的时钟偏斜数据那时候会重新约束。综合阶段留一点余量本质上是把后端可能出现的时序恶化提前考虑进去这不是保守是正常工程做法。6.4 写完网表忘了写SDC等于白做最后这个坑看起来蠢但它出现频率特别高。综合跑完网表写出来了报告也看了人已经累了文件夹里只有一份top_netlist.v没有top.sdc。等你把网表交给后端后端同事第一句话就是SDC呢后端做时钟树综合、时序收敛靠的就是综合后的SDC约束文件。没有它只有网表后端工具不知道时钟频率是多少、输入输出延时是多少完全没办法开工。所以综合流程脚本里write_sdc要和write -format verilog写在一起。我自己的习惯是把输出文件清单写在脚本开头网表、SDC、DDC三件套缺一不可跑完再ls确认一下产物齐全。如果你能把这几个坑提前避开第一次跑DC工具的综合流程会顺畅很多。这个系列我打算继续往下写后面会聊聊多时钟域约束、门控时钟的处理以及综合阶段怎么平衡时序和面积。先把基础流程跑通后面那些高级操作才会有意义。