2. RTL设计与综合约束基础

综合工具并不会“理解”设计意图,它只根据两样东西工作:RTL 代码所描述的硬件结构,以及 SDC 约束所描述的时序目标。前者决定电路“长什么样”,后者决定工具“按什么标准去优化”。因此,在真正跑 dc_shell 之前,必须先建立一套可综合的编码习惯和一套语义正确的约束文件。本节围绕这两条主线展开:先讲 RTL 中哪些写法能被映射成触发器、组合逻辑和连线,再讲时序路径如何分类、时钟如何定义、输入输出延迟如何描述。

一、可综合 RTL 的编码边界

可综合的本质是“代码中的每一个赋值都能对应到某种硬件元件”。综合工具支持的 RTL 子集大致包括:连续赋值 assign、always 组合块、always_ff 时序块、模块例化、参数化位宽、有限状态机、算术与逻辑运算、条件与分支。与之相对,initial 块(除 FPGA 初始化外)、#delay、while 无限循环、fork/join、动态数组、类、断言中的复杂时序检查等,通常不可综合或只能被忽略。

时序逻辑必须使用非阻塞赋值 <=,组合逻辑必须使用阻塞赋值 =。这不是风格偏好,而是仿真语义与综合语义一致性的要求。若在同一个 always 块中混用,或在时序块中用阻塞赋值,仿真结果可能与综合后网表不一致,形成“仿真通过、上板失败”的经典陷阱。

另一个高频问题是敏感列表不完整。组合逻辑应写成 always @(*) 或 always_comb,让工具自动推导敏感信号;若手写敏感列表而漏掉某个输入,综合工具会推断出锁存器(latch),这是设计中应当避免的。判断方法很简单:如果组合块中某条分支没有给输出赋值,就会产生锁存器。例如:

// 会产生锁存器:else 分支缺失
always @(*) begin
    if (sel)
        y = a;
end

// 无锁存器:所有分支都赋值
always @(*) begin
    if (sel)
        y = a;
    else
        y = b;
end

此外,复位策略需要统一。同步复位在时钟沿采样,综合为触发器数据端的逻辑;异步复位直接连到触发器的异步复位端,但释放时必须同步化,否则可能产生亚稳态。一个模块内应保持复位方式一致,避免同一路径上混用同步与异步复位导致时序分析复杂化。

二、时序路径的四种类型

静态时序分析(STA)把设计中的所有路径归为四类,理解这四类是读懂时序报告的前提:

  • 输入到寄存器(input-to-reg):从芯片输入端口经组合逻辑到第一个触发器,受输入延迟约束。
  • 寄存器到寄存器(reg-to-reg):从触发器输出经组合逻辑到下一个触发器,是时序优化的核心对象。
  • 寄存器到输出(reg-to-output):从触发器输出经组合逻辑到芯片输出端口,受输出延迟约束。
  • 输入到输出(input-to-output):纯组合路径,从输入端口直接到输出端口,受输入延迟与输出延迟共同约束。

这四类路径的起点和终点不同,约束方式也不同。reg-to-reg 路径由时钟周期直接约束;input-to-reg 和 reg-to-output 需要额外描述外部器件的延迟;input-to-output 则完全由输入输出延迟决定。综合工具在优化时,会优先满足 reg-to-reg 路径,因为它是芯片内部可控的部分。

四类时序路径与 SDC 约束对应关系 输入端口 组合逻辑 触发器 组合逻辑 触发器 输出端口 组合逻辑 ① input-to-reg ② reg-to-reg ③ reg-to-output ① set_input_delay 约束外部到达时间 ② create_clock 定义周期,工具优化组合逻辑延迟 ③ set_output_delay 约束外部建立/保持要求 ④ input-to-output 由输入输出延迟共同约束

三、时钟定义:create_clock 的语义

时钟是时序分析的基准。没有时钟定义,STA 无法计算任何建立/保持关系。最基本的写法是:

create_clock -name clk_core -period 5.0 [get_ports clk]
create_clock -name clk_ddr  -period 2.5 [get_ports clk_ddr]

其中 -period 5.0 表示周期为 5 ns,对应 200 MHz。-name 给时钟命名,后续约束通过名字引用。[get_ports clk] 指定时钟源端口。若时钟来自内部 PLL 或分频器,则用 create_generated_clock 描述其与源时钟的关系:

create_generated_clock -name clk_div2 \
    -source [get_ports clk] -divide_by 2 [get_pins u_div/Q]

时钟定义后,工具会自动推导所有由该时钟驱动的触发器之间的 reg-to-reg 路径。需要注意的是,create_clock 只定义时钟的周期和波形,不定义时钟树延迟;时钟树延迟由 CTS 阶段处理,综合阶段通常假设理想时钟(零偏斜)。若设计中存在多个时钟域,必须分别定义,并用 set_clock_groups 声明异步关系,否则工具会尝试分析跨时钟域路径,产生大量无意义的违例。

四、输入输出延迟:描述芯片外部世界

综合工具不知道芯片外部连接了什么器件,因此需要设计者用 set_input_delay 和 set_output_delay 告诉工具:外部信号相对于时钟沿到达或离开的时间是多少。

set_input_delay 的语义是:输入信号在时钟沿之后多久才稳定到达芯片输入端口。例如,外部器件在时钟上升沿后 2 ns 输出数据,那么:

set_input_delay -clock clk_core -max 2.0 [get_ports data_in]
set_input_delay -clock clk_core -min 0.5 [get_ports data_in]

-max 用于建立时间分析,-min 用于保持时间分析。若只写一个值,工具会同时用于两者,这通常不够精确。

set_output_delay 的语义是:输出信号必须在时钟沿之前多久稳定,以满足外部器件的建立时间。例如,外部器件要求数据在时钟沿前 1.5 ns 到达:

set_output_delay -clock clk_core -max 1.5 [get_ports data_out]
set_output_delay -clock clk_core -min 0.3 [get_ports data_out]

这两个约束共同决定了 input-to-reg、reg-to-output 和 input-to-output 路径的时序预算。一个常见的错误是忘记加 -clock 选项,导致约束被忽略或报错。另一个错误是输入输出延迟之和超过时钟周期,使工具无法满足时序,此时需要检查外部器件的时序参数是否合理。

实践建议:在综合前,先用 check_timing 检查是否有未约束的路径、未定义的时钟、未设置的输入输出延迟。一个干净的约束文件应当让 check_timing 报告零警告。对于跨时钟域路径,用 set_clock_groups -asynchronous 明确声明,避免工具浪费优化资源。

把以上内容串起来:可综合 RTL 决定了电路的物理结构,四类时序路径决定了 STA 的分析框架,create_clock 建立了时间基准,set_input_delay 与 set_output_delay 补全了芯片与外部世界的接口时序。这四者缺一不可,共同构成后续综合实验的约束基础。