4. 综合阶段优化:逻辑综合中的时序优化策略
综合阶段,说白了就是把 RTL 代码翻译成门级网表。这一步做得好不好,直接决定了后端布局布线能不能收时序。我见过太多项目,综合时图省事,结果后端跑几个月都收不了时序,最后只能回头改综合脚本——那叫一个痛苦。
今天咱们就聊聊综合阶段那些实打实的优化手段。我个人习惯把综合优化分成四个维度:策略选择、重定时、资源共享与复制、以及脚本配置。一个一个来。
4.1 逻辑综合中的时序优化策略
综合工具(比如 Synopsys DC、Cadence Genus)在做时序优化时,本质上是在做三件事:
- 结构优化:把 RTL 代码转换成工具内部的布尔表达式,然后做化简、公因子提取。
- 门级优化:选择合适的标准单元,比如用大驱动能力的 buffer 替换小 buffer,或者用低阈值电压单元(LVT)来提速。
- 时序驱动优化:根据你设定的时钟周期,工具会优先优化那些时序紧张的路径。
这里有个坑,我刚开始做设计时踩过:不要盲目相信工具的默认策略。默认策略往往偏保守,它要保证各种场景下都能编译通过。但你的项目可能有特殊需求,比如面积优先、功耗优先、或者极端性能优先。
核心原则:综合策略的选择,本质上是在面积、功耗、时序三者之间做 trade-off。没有银弹,只有最适合你当前项目的配置。
举个例子,我在一个 AI 加速器项目中,数据通路特别长,默认策略下时序根本收不了。后来我强制工具使用 compile_ultra -timing_high_effort,并且打开了 -retime 选项,才把关键路径从 2.1ns 压到 1.6ns。代价是面积增加了 15%,但项目保住了。
4.2 重定时与流水线插入
重定时(Retiming),说白了就是在不改变功能的前提下,把寄存器在组合逻辑之间挪一挪。你想想看,如果一段组合逻辑太长,时序肯定紧张。那能不能把这段逻辑切成两段,中间插一组寄存器?这就是重定时的核心思想。
工具做重定时有两种方式:
- 前向重定时:把寄存器从输入端往输出端挪,缩短输入到寄存器的路径。
- 后向重定时:把寄存器从输出端往输入端挪,缩短寄存器到输出的路径。
但要注意,重定时不是万能的。它要求你的设计是寄存器传输级(RTL)的,而且不能有反馈环路(或者环路必须被正确处理)。
我的经验:在综合脚本里加上 set_optimize_registers true 或者 compile_ultra -retime,工具会自动做重定时。但如果你设计里有跨时钟域(CDC)路径,一定要用 set_false_path 或者 set_clock_groups 告诉工具别动那些路径,否则会出大问题。
至于流水线插入,这个更直接——就是在长组合逻辑中间手动插入寄存器。我建议在 RTL 设计阶段就规划好流水线级数,而不是等到综合阶段让工具去猜。工具能做的流水线优化其实很有限,它只能挪寄存器,不能凭空变出寄存器来。
我曾经在一个视频编解码项目中,核心计算单元有 32 级流水线。最开始只规划了 16 级,综合后时序惨不忍睹。后来我手动插了 16 级寄存器,重新综合,时序一下子就过了。嗯,这里要注意:流水线插入会增加 latency,如果你的设计对延迟敏感,需要提前评估。
4.3 资源共享与逻辑复制
这两个策略听起来矛盾,但其实各有各的适用场景。
资源共享:把多个功能模块共用的运算单元合并成一个。比如两个模块都要做乘法,如果它们不会同时使用,就可以共用一个乘法器。这样做的好处是面积小、功耗低。但代价是——时序可能会变差。因为共享逻辑需要额外的 MUX 选择器,这些 MUX 会引入延迟。
逻辑复制:恰恰相反,把一个大扇出的逻辑复制成多份,每份驱动一部分负载。这样做的好处是减少扇出,改善时序。但代价是面积变大、功耗变高。
怎么选?我个人的判断标准很简单:
- 如果设计面积紧张,优先考虑资源共享。但要注意,共享后的路径延迟不能超标。
- 如果设计时序紧张,优先考虑逻辑复制。尤其是那些扇出很大的信号,比如时钟使能、复位信号、或者控制信号。
避坑指南:我曾经在一个项目中,为了省面积,把多个模块的加法器共享了。结果综合后时序分析发现,共享加法器前面的 MUX 路径成了关键路径。最后不得不把共享去掉,重新综合。所以我的建议是:资源共享要谨慎,最好在综合前用脚本估算一下共享带来的延迟增加。
4.4 综合策略文件配置
综合策略文件(通常叫 .sdc 或者 .tcl 脚本)是综合阶段的灵魂。没有好的策略文件,工具就像没头苍蝇一样乱撞。
我一般会在综合脚本里配置以下几类内容:
| 配置项 | 说明 | 我的建议 |
|---|---|---|
| 时钟约束 | 定义时钟周期、占空比、抖动 | 时钟周期留 10%-15% 的余量 |
| 输入输出延迟 | 定义端口相对于时钟的延迟 | 根据芯片外部接口时序来设,别乱猜 |
| false path | 告诉工具哪些路径不用分析 | 跨时钟域、异步复位、测试模式路径 |
| multicycle path | 告诉工具某些路径需要多个周期 | 比如慢速外设接口、握手信号 |
| 面积/功耗约束 | 设定面积和功耗的目标 | 如果时序紧张,先放宽面积约束 |
下面是一个我常用的综合脚本片段,你可以参考:
# 时钟约束
create_clock -name clk -period 2.0 [get_ports clk]
set_clock_uncertainty -setup 0.15 [get_clocks clk]
set_clock_transition -rise 0.1 [get_clocks clk]
# 输入输出延迟
set_input_delay -max 0.5 -clock clk [get_ports data_in*]
set_output_delay -max 0.5 -clock clk [get_ports data_out*]
# 例外路径
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]
set_multicycle_path -setup 2 -from [get_pins u_fifo/rd_en_reg/CK] -to [get_pins u_fifo/rd_data_reg/D]
# 综合策略
set_optimize_registers true
compile_ultra -timing_high_effort -retime
小技巧:综合完成后,一定要检查 report_timing 和 report_area。我习惯把综合后的时序报告和面积报告保存下来,跟后端布局布线后的结果做对比。如果综合阶段时序就很紧张,那后端基本没戏。
最后,我想强调一点:综合阶段的优化,不是一次就能搞定的。我通常要迭代 3-5 轮:第一轮用默认策略看看 baseline,第二轮根据时序报告调整约束,第三轮尝试不同的优化策略,第四轮做最终收敛。每一轮迭代,我都会把综合脚本和报告保存下来,方便回溯。
好了,综合阶段的优化策略就聊到这里。下一节咱们聊聊布局布线阶段的时序收敛方法,那又是另一番天地了。
可可精品资料库