
数字前端这条线上逻辑综合算是第一个真正意义上的分水岭。写RTL的时候你关心的是功能对不对、波形跑不跑得通一旦进入综合视角就得切换到这段代码最终会变成多大面积、能跑到多少频率、后端能不能收敛。我见过太多人RTL写得挺漂亮一到综合就满屏警告timing report一片红最后返工改架构前面几天全白干。这篇东西面向的是刚接触综合的在校生、转岗的数字设计工程师以及需要看懂综合报告的验证和版图同事。我会把综合这件事从是什么讲到怎么跑包括库和约束的基本概念、综合引擎内部的三步走、脚本怎么写、report怎么看还有我自己踩过的那些坑。不会讲得太玄乎都是能落到键盘上的操作看完你应该能独立跑通一个小模块的综合并且知道哪些数值得盯着看。1. 逻辑综合到底在做什么——RTL到门级网片的翻译逻辑1.1 一个类比把设计意图翻成施工图纸你可以把RTL当成一份用图纸语言写的建筑方案——它描述的是这里要有个加法器那里判断一下是否大于阈值。但工地施工队看不懂这种描述他们需要的是用几号钢筋、几根、怎么搭接。逻辑综合干的就是这份翻译工作把行为级/寄存器传输级的Verilog或VHDL翻译成由标准单元与门、或门、D触发器、多路选择器这些和它们之间的连线组成的门级网表。关键点在于这个翻译不是一对一的。一行a b c在不同约束下可能被翻成行波进位加法器也可能被翻成超前进位加法器面积差好几倍。综合工具手里有一整个工艺库的单元可选它要做的就是在满足时序约束的前提下找出面积和功耗都尽量小的那个组合。所以综合不是一个纯编译过程它是一个带约束的优化问题这一点想清楚了后面很多行为就都能理解了。1.2 综合在流程里的位置上游是RTL下游是版图放到完整流程里看逻辑综合的输入输出非常明确。上游给你的是经过仿真验证的RTL代码加上工艺库文件通常来自代工厂和时序约束文件SDC一般由你自己写或者从架构预算里继承。综合工具输出的是门级网表.v、时序报告、面积报告以及一份后续要交给后端的约束文件。下游的布局布线PR会拿着这份网表去摆位置、连金属线。这里有个细节很多人忽略综合阶段工具并不知道真实的走线长度它用的是线负载模型或者零线负载去估算互连延迟。这就导致综合报出来的时序和最终签核的时序经常对不上通常需要给后端留一定的余量行内叫时序预算或者derate。我个人的习惯是在综合阶段把目标频率压到最终目标的90%左右去跑给物理实现留空间这个比例具体多少要看工艺节点和设计规模。1.3 为什么不直接手写门级网表刚入行的人有时会问既然综合出来的东西也是网表我直接拿库里的单元搭不就行了理论上可以但实际完全不现实。一个中等规模的设计动辄几十万甚至上百万个实例手工连接根本不可能维护改一个功能要动的地方太多而且无法快速探索不同的实现方案。更重要的是综合工具能做的优化远超手工水平。它会做逻辑化简、公用子表达式提取、寄存器复制、门控时钟插入、状态机重编码等等。这些优化手工做一遍要几周工具几分钟跑完而且它遍历的解空间比人脑大得多。所以正确的心态是把综合工具当成一个需要用约束指挥的强执行力工匠你的水平体现在给的约束准不准、策略调得对不对而不是替代它去连门。1.4 一个容易混淆的边界综合不管什么要说明的是综合阶段不做时钟树插入CTS也不做真实的布线。它假设时钟到达各个寄存器的偏差是一个估计值这个值来自你对时钟不确定性的设定。真实的时钟树是后端做的做到那一步时钟偏差才能算准。另外综合一般不处理模拟模块、不处理存储器阵列的内部结构这些通常以宏单元或者IP的形式被例化进来也不管测试逻辑之外的DFT结构——扫描链插入通常是另一道工序。把这些边界记清楚你就不会在综合报错的时候去错误的方向找原因。2. 开跑之前必须备齐的三样东西库、约束、时序模型2.1 工艺库文件里到底装了什么库文件常见后缀 .lib编译后是 .db是整个综合的物料清单。它定义了每一颗标准单元的全部属性我按重要性列一下功能描述这颗单元是几输入与门还是带使能的D触发器输出和输入的逻辑关系是什么。面积单位通常是平方微米综合工具靠它算总面积。引脚与时序弧每个输入到输出之间的延迟模型通常用非线性延迟表NLDM给出输入转换时间和输出负载是两个查表变量。约束信息比如建立/保持时间、最小脉宽、最大转换时间、最大电容负载。功耗信息内部功耗、开关功耗、漏电功耗。不同的库会对应不同的工艺角corner比如典型、快、慢还有不同的温度电压条件。综合时一般用慢角查建立时间用快角查保持时间。选错库是新手非常常见的错误表现是报出来的时序和预期差很多或者某些单元根本找不到。2.2 时序约束综合的军令状约束文件SDCSynopsys Design Constraints用的是Tcl语法它告诉工具这个设计要跑多快。核心内容就几类定义时钟、定义输入输出外部延迟、定义时序例外、定义设计规则约束最大转换时间、最大扇出、最大电容。我常跟新人打这个比方RTL是设计者的想法库是可用材料约束是工期和预算。工具会拼尽全力满足约束但你如果给错了约束它也会一丝不苟地按错误目标优化最后结果同样不可用。约束给得松综合出来的电路又大又慢给得紧工具可能反复优化还是收敛不了编译时间暴涨。所以约束的准确性直接决定了综合质量的上限。2.3 互连延迟模型的三个层次综合工具需要估算走线延迟主流有三种做法模型说明适用场景零线负载假设线延迟为0仅算单元延迟早期快速评估、教学线负载模型按扇出数查表估线电容传统流程、中小设计物理感知综合先做粗略布局再估算先进节点、高性能设计线负载模型是很多老流程的默认选择但它的精度在28nm以下就明显不够了。现在主流做法是用带物理感知的综合工具先做一次快速布局拿到真实的线长估计再优化。这一步的差异对高频设计是决定性的我做过一个对比同一个设计用线负载模型跑出来的时序比物理感知乐观将近15%直接导致后端返工。注意库文件、约束文件、RTL三者的版本必须配套管理。换个库版本时序结果可能大变一定要在脚本里明确记录用的哪个库、哪个版本。3. 时序约束怎么写才不会被后端打回3.1 时钟定义一切时序的起点最基础的一条约束是时钟。假设系统主频100MHz时钟从某个端口进来create_clock -name clk -period 10 -waveform {0 5} [get_ports clk]-period 10表示周期10ns即100MHz-waveform {0 5}表示上升沿在0ns、下降沿在5ns占空比50%。这两项看起来简单但实际项目里坑不少。如果时钟是内部PLL产生的就不要在端口上定义而应该在PLL输出引脚上定义时钟并把PLL输入时钟作为它的源。还要记得设置时钟的不确定性uncertainty它代表时钟抖动加上一部分预留的偏差set_clock_uncertainty -setup 0.3 [get_clocks clk] set_clock_uncertainty -hold 0.15 [get_clocks clk]这个0.3ns不是随便定的。它通常等于PLL抖动比如0.1ns加上后端时钟树偏差的预估值比如0.2ns。给太小综合乐观后端收不住给太大综合过度悲观面积白花。我一般会跟后端同事对齐一个值两边用同一套数。3.2 输入输出延迟别让边界成为盲区很多新手只定义了时钟忘了端口的外部延迟结果工具认为端口进来的数据随时可能到于是把输入路径优化得极其激进面积爆炸或者干脆不约束报告里一堆unconstrained path。输入延迟的意思是数据从外部器件发出经过板级走线到达本芯片端口这段延迟折算到时钟周期里是多少。举个例子上游器件时钟到输出的延迟是2ns板级走线延迟1ns那么set_input_delay -clock clk -max 3.0 [get_ports data_in*] set_input_delay -clock clk -min 1.0 [get_ports data_in*]最大值3.0ns用于建立时间检查最小值1.0ns用于保持时间检查。输出延迟同理是芯片输出到下游器件的时间。这两个值一般来自系统级的时序预算文档不是设计者拍脑袋定的。3.3 例外路径把不该算的路径放过工具默认对每一对寄存器之间的路径都做时序检查但实际有些路径不需要按单周期检查主要有三类伪路径逻辑上永远不会同时有效的路径比如多路选择器的两个输入来自互斥的控制信号。用set_false_path声明。多周期路径需要多个时钟周期才能算完的路径比如乘法器流水线。用set_multicycle_path声明。最大/最小延迟对异步接口、跨时钟域握手路径直接指定延迟上限。set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b] set_multicycle_path 2 -setup -from [get_pins mult_reg*/D] set_max_delay 5.0 -from [get_ports async_in] -to [get_pins sync_reg*/D]这里有个坑要提醒多周期路径必须setup和hold成对设置只设setup不设hold工具会默认hold检查也在第二周期导致保持时间被过度放宽甚至产生错误。标准写法是setup设2hold设1两个都要写。注意伪路径要慎用。我见过有人为了消掉违例把真实路径标成false path结果芯片回来功能错了。每一条false path都要能说清楚为什么这两个信号不会同时有效说不清楚就别标。4. 综合引擎内部的三步走翻译、优化、映射4.1 第一步翻译与elaborate综合工具读入RTL后第一步是把它转成一种与工艺无关的中间表示业内常叫通用技术门。这一步会做语法检查、参数传递、模块展开、生成语句展开。你会看到类似Elaborating design的日志。这一步最常见的报错来自语法和例化问题端口宽度不匹配、参数没传、模块找不到、多次驱动同一根线。这些错误其实在仿真阶段就该发现但仿真和综合的检查严格程度不同有些写法仿真能过综合报错比如对一个wire用always块赋值。我建议读入之后先跑一次check_design把结构性问题一次性揪出来。4.2 第二步逻辑优化工具真正发力的时候进入优化阶段工具开始做与工艺无关的逻辑变换主要包括常量传播把恒为0或1的信号化简掉删掉死逻辑。逻辑化简用卡诺图或者更高级的布尔方法压缩逻辑级数。公用子表达式提取多个地方用到的相同逻辑只实现一份共享结果。结构调整在逻辑级数和扇出之间做平衡减少关键路径深度。寄存器优化寄存器复制降低扇出、状态机重编码降低译码复杂度。这一步的优化质量很大程度上取决于你的约束。工具会认为关键路径就是时序最紧的那些路径你把哪些路径标成关键它就往哪些地方使劲。4.3 第三步工艺映射落到具体单元优化完之后工具会拿着中间表示去库里挑单元。这一步要解决的是这个逻辑功能用哪几个单元拼最合适。同一个二输入与库里可能有面积小的低驱动版本也有面积大但驱动强、延迟小的版本选哪个取决于下游负载和时序要求。映射完成后工具会做最后一次时序分析和修复迭代。如果还有违例它会尝试换单元、插入缓冲器、调整逻辑结构能修就修修不了就在报告里报出来。所以编译结束还有违例说明约束本身可能就不合理这时候不要再盲目加编译选项回头看看约束。4.4 整个流程的时间都花在哪有个现象值得说一下综合跑一次几个小时很常见大设计甚至一整天。时间大致分布上elaborate通常几分钟真正的优化和映射占大头反复的时序分析迭代也很耗时。想缩短时间可以先用较低的努力级别跑一版看趋势确认约束合理后再用高努力级别精跑。别一上来就开最高档白等半天可能约束本来就是错的。5. 综合策略取舍面积、时序、功耗的三角博弈5.1 三种优化目标的相互关系面积、时序、功耗这三者不可能同时最优这是综合阶段最核心的权衡。想跑得快往往要并行化、加驱动、加大单元面积和动态功耗都上去想省面积逻辑级数增加时序变差想省功耗降电压降频率或者用高阈值单元速度又下来。工具提供的策略选择通常包括以时序优先、以面积优先、以及各种平衡档位。在主流工具里compile是基础策略compile_ultra会启用更激进的时序驱动优化和边界优化代价是编译时间更长。我的经验是首次编译用标准策略看基线确认约束没问题后再上高级策略精修这样出问题容易定位。5.2 层次化展开flatten还是保留层次设计通常有层次结构综合时可以全部展平flatten也可以按模块分别综合再拼起来hierarchical或者只展开部分模块ungroup。全部展平的好处是工具能跨模块优化时序和面积通常更好坏处是编译时间长、内存占用大、出了问题难定位而且模块的接口边界没法单独约束。保留层次则相反模块可以并行综合方便复用和团队协作但跨边界优化受限。我一般对性能敏感的模块做局部展平对控制逻辑和大块重复的模块保留层次。比如把数据通路展平让工具自由重组把控制状态机单独综合。这个取舍没有标准答案得看具体设计的瓶颈在哪。5.3 面积优化的几个实用手段如果面积是主要矛盾除了工具选项还可以从RTL层面配合复用运算单元让多个操作分时共享一个乘法器或加法器。避免不必要的位宽扩展很多人习惯性写32位实际8位就够。减少流水线级数寄存器本身占面积。检查是否有多余的复位逻辑异步复位同步释放的电路比全同步复位复杂。反过来说如果时序是主要矛盾就要反其道而行加流水线、打拍、复制高扇出寄存器、用低阈值单元。这些手段能换频率但要付出面积和功耗的代价。5.4 功耗优化的基本抓手功耗分动态和静态两部分。动态功耗主要是翻转引起的抓手是门控时钟——当一段逻辑暂时不工作时把它的时钟关掉。综合工具能自动插入门控时钟前提是你在RTL里写了使能条件比较规整工具识别得出来。写成一堆零散的if判断工具往往识别不出来。静态功耗主要是漏电抓手是换高阈值单元。库里同一功能通常有高阈值、标准阈值、低阈值三种版本高阈值慢但漏电小。综合时可以设定一个策略只在关键路径上用低阈值单元其他都走高阈值这个思路叫多阈值优化工具一般支持。提示功耗优化要有取舍意识。把时钟门控加得太碎控制逻辑本身的面积和功耗可能就把省下来的吃回去了。一般按功能模块粒度做比较划算。6. 一份可复用的综合脚本与关键参数计算6.1 脚本骨架下面这份骨架是精简过的逻辑上覆盖了综合的主要步骤你可以按自己的工具微调命令名。核心顺序一定是建库环境 → 读RTL → 约束 → 检查 → 编译 → 报告 → 输出。# 1. 设置库搜索路径和目标库 set search_path [list . ./lib] set target_library slow.db set link_library * slow.db set symbol_library generic.sdb # 2. 读入RTL analyze -format verilog [list ./rtl/top.v ./rtl/datapath.v] elaborate top -architecture verilog -update # 3. 读入约束 source ./constraints/top.sdc # 4. 结构检查 check_design ./report/check_design.rpt link # 5. 编译 compile_ultra -timing -area # 6. 输出报告 report_timing -max_paths 20 -nworst 5 ./report/timing.rpt report_area -hierarchy ./report/area.rpt report_constraint -all_violators ./report/violators.rpt # 7. 输出结果 write -format verilog -hierarchy -output ./netlist/top.v write_sdc -output ./constraints/top_out.sdc write_sdf -output ./netlist/top.sdf这里要说明几个命令的意图。analyze只做语法解析不建电路elaborate才真正展开设计分开做的好处是语法错误能在analyze阶段快速暴露。link是把例化的子模块和库单元全部解析出来如果这里报找不到模块说明路径或库设置有问题。write_sdc输出的是综合认为的实际约束下游后端要用这份。6.2 时序参数的计算过程约束里的数值不能拍脑袋我给你推一遍最基本的两条公式。建立时间检查要求数据在捕获时钟沿到来前足够早到达T_clk_period T_clk_to_q T_comb_max T_setup T_uncertainty - T_skew假设时钟周期10ns触发器时钟到输出延迟0.5ns建立时间0.2ns不确定性0.3ns时钟偏差0.1ns那么留给组合逻辑的最大延迟就是T_comb_max 10 - 0.5 - 0.2 - 0.3 0.1 9.1ns保持时间检查要求数据不能在捕获沿之后太快变化T_clk_to_q T_comb_min T_hold T_skew假设保持时间0.15ns偏差0.1ns那么组合逻辑最小延迟不能小于T_comb_min 0.15 0.1 - 0.5 -0.25ns结果是负数说明触发器本身的输出延迟就足够满足保持要求这条路径不会出现保持违例。但如果时钟偏差是负的或者线路延迟特别小就可能需要插延迟单元。把这两个公式记住你就明白为什么综合报告里setup和hold要分开看也明白为什么时钟偏差和不确定性这两个值那么重要。它们直接吃掉了你的时序预算。6.3 编译时的努力级别与增量编译工具一般提供几档编译努力级别从低到高耗时递增。跑完一版之后如果只有少量违例可以用增量编译在原有结果基础上局部修不用重新从头跑一遍能省不少时间。增量编译的思路是保留已经满足约束的部分只对违例路径重新优化。但这里有个坑增量编译会继承上一版的优化结果如果上一版的约束有问题增量也修不好。所以改约束之后最好重新完整编译一次别偷懒。7. 综合报告怎么读怎么判断结果能不能用7.1 必须看的几份报告跑完综合下面这几份报告我是必看的顺序也有讲究报告关注内容判断标准check_design多驱动、未连接、组合环、latch推断组合环和latch必须为零report_timing路径延迟、slack、逻辑级数最差slack应≥0留余量report_area总面积、层次面积分布与预算对比看瓶颈模块report_constraint所有违例汇总转换时间、扇出、电容是否超限report_power动态/静态功耗估计与功耗预算对比先看check_design这是结构健康度。组合逻辑环路和意外的锁存器推断是两个危险信号前者会导致时序分析无法收敛后者会让后续流程非常难处理。这两个问题一定要在综合阶段解决带到后端就是大麻烦。7.2 逻辑级数比slack更能说明问题的指标report_timing里除了slack还有一个值值得重点看逻辑级数。它表示一条路径上经过的门有多少级。同样的slack逻辑级数差别很大说明优化空间不同。我的经验判断标准大致是这样一条跨越一个时钟周期的路径逻辑级数在10到20级之间比较正常超过30级说明组合逻辑太深即使当前频率能过往后提频或者换工艺节点时会很痛苦低于5级但slack还是很紧说明可能是线延迟占主导问题在物理实现那边。这个指标可以帮你判断设计的体质。逻辑级数合理的设计后端跑起来也顺级数畸高的设计往往是RTL架构问题得回去改代码而不是在综合选项上折腾。7.3 时序余量到底留多少综合报出slack为正不代表就万事大吉。前面说过综合用的线延迟是估算的跟真实布线有差距。到底留多少余量我的经验是要看几个因素工艺节点越先进线延迟占比越高余量要给得越多。设计规模越大时钟树越复杂偏差预估值越高。综合用的库角跟签核库角的差距差距越大余量越多。一般28nm及以下我会要求综合阶段的最差slack至少留出时钟周期的10%。16nm以下可能要到15%到20%。这个数字不是死的最好和后端同事一起定两边用一致的标准避免互相甩锅。7.4 面积数字的正确解读report_area给出的总面积一般不包括布线面积和电源网络面积。它主要由标准单元面积加上宏单元面积组成。看这个数的时候要注意组合逻辑面积和时序逻辑面积的比例。时序逻辑占比过高说明寄存器太多可能是流水线级数过多或者位宽浪费。目录层次分布找出面积大户。往往是几个模块占了大头优化要 prioritize。和工艺库里的门数做个换算理解这个设计规模大概相当于多少门。一个常见误区是只看总数不看分布。总数超标时你得知道从哪下手。层次面积报告就是干这个的。8. 常见问题与排查实录8.1 报错与警告速查表下面这张表是我这些年攒下来的高频问题几乎每次带新人都会遇到现象可能原因处理方式找不到模块/单元link_library或search_path没设对检查库路径确认模块名大小写推断出锁存器if或case分支覆盖不全补全default检查敏感列表组合逻辑环路组合逻辑反馈或变量未初始化打断环路初始化变量多驱动网络同一信号被两处赋值检查端口连接和赋值语句时序不收敛约束过紧或逻辑级数过深先松约束验证再看架构端口未约束忘设input/output delay补SDC或标false path这些问题的共同点是它们几乎都能在综合前通过一次静态检查提前发现。养成拿到RTL先跑一遍lint和check_design的习惯能省掉大量来回。8.2 时序违例的排查思路遇到setup违例我一般按这个顺序查确认这条路径是不是真的需要单周期检查。如果是跨时钟域或者假路径漏标约束就是根因。看路径的起点和终点。如果是输入端口到寄存器检查input delay是否给得过大。看逻辑级数和路径组成。级数畸高说明组合逻辑太深考虑插流水线。看是不是高扇出导致。一个寄存器驱动成千上万个负载延迟会很大考虑复制寄存器。以上都排除再看是不是库角选错了或者约束周期填错。hold违例相对少见而且主要是后端修的。综合阶段如果报hold违例通常是因为数据路径太短。处理方法是在短路经上插缓冲器或者调整时钟偏差。综合阶段一般不做激进修复报出来交给后端。8.3 那些文档不写但很要命的经验第一约束要版本化管理。我见过因为SDC改了一行没记录导致两周后复现不出问题。约束文件必须和RTL一起进版本控制每次改动写清楚原因。第二报告要存归档。每次综合的报告都留档包括用的库版本、工具版本、脚本版本。出问题回溯的时候这些记录是救命的。第三别迷信工具的乐观报告。工具报的时序是基于模型的跟实测有差距。综合过了不代表后端能过一定要跟后端对齐签核标准。第四警惕假收敛。有时候约束给得太松工具轻松报全绿你以为没问题其实频率根本没跑上去。定期用目标频率严格验证一次别一直用宽松约束自欺欺人。第五elaborate的速度和编译速度要区分看。大设计elaborate慢往往是RTL规模问题编译慢往往是优化空间大或者约束太紧。把这两段时间分开记录能帮你快速定位性能瓶颈在哪。8.4 一个真实的小案例我之前接手过一个模块综合总是差0.3ns收敛不了试了各种编译选项都没用。后来把report_timing拉出来细看发现最差路径的起点是一个复位信号而这个复位信号被当成普通数据路径在检查。原来SDC里用的是同步复位但RTL里实际写的是异步复位工具按同步逻辑检查路径自然对不上。改了一下复位处理方式把复位路径声明成false path问题当场解决。这个事的教训是约束和RTL必须语义一致。工具不会读你的心你怎么写它就怎么算。所以每次改架构记得回头检查约束是不是还匹配。我个人在实际操作中的体会是逻辑综合这件事工具的命令就那么几十条真正拉开差距的是对约束的理解和对报告数据的敏感度。同样一个设计约束给得准的人跑出来的结果和后端配合起来顺畅得多返工也少。建议新手别急着学各种高级选项先把基础约束吃透把时序公式推一遍把每份报告的数字都能讲出个所以然这一步扎实了后面再学物理综合、低功耗流程都是顺水推舟。