
1. 时序约束里最容易被忽视的那条命令搞FPGA或者ASIC后端的人多少都有过这种经历综合布线跑完了时序报告一打开setup slack是正的hold也过了心里正美结果板子一上电接在输出端口上的那颗ADC或者DAC就是采不对数。回头查了半天发现不是逻辑写错了也不是时钟频率太高而是set_output_delay这条约束压根没写或者写了个拍脑袋的数。set_output_delay是SDCSynopsys Design Constraints里专门用来描述本芯片输出端口到下游器件输入端口之间那段外部路径时序的命令。它跟set_input_delay是一对孪生兄弟一个管进一个管出。很多人学时序约束的时候把大量精力花在create_clock、set_clock_groups、set_false_path这些命令上对set_output_delay往往就是抄个模板填个数字了事。但恰恰是这个数字决定了你的输出信号在真实PCB上能不能被正确采样。这篇文章面向的是已经写过一些时序约束、但对外部接口时序还停留在“抄模板”阶段的FPGA/ASIC工程师也适合正在做RGMII、SPI、并行总线这类接口调试的朋友。我会从这条命令到底在描述什么讲起把max和min的物理含义、数值怎么算、不同接口场景下怎么用、以及实际项目中踩过的坑一条一条掰开说清楚。看完之后你至少能做到拿到一个输出接口知道该不该写set_output_delay该写多少以及写错了会出什么问题。2. set_output_delay到底在约束什么2.1 从“芯片内部”到“芯片外部”的边界时序约束的本质是告诉EDA工具我的信号在什么时间窗口内到达工具你要保证在这个窗口内把数据稳定地送出去或者采进来。对于芯片内部的路径工具自己知道走线延迟、逻辑延迟它能算。但一旦信号出了芯片的引脚外面的PCB走线多长、下游器件建立保持时间多少、有没有串联电阻这些工具一概不知道。所以必须由工程师通过set_input_delay和set_output_delay手动告诉工具。set_output_delay描述的是从FPGA输出引脚出发经过外部走线到达下游器件输入引脚时相对于某个参考时钟沿数据到达的时间偏移。注意这里的参考时钟通常是FPGA输出出去的时钟或者是与数据同源的某个时钟。打个比方FPGA内部逻辑就像一个工厂输出引脚是工厂大门。set_output_delay不是在说工厂内部货物从仓库搬到大门要多久而是在说货物出了大门之后送到客户手里这段路要花多长时间客户要求什么时候必须收到。工具需要知道这个“门外的时间”才能反推工厂内部最晚什么时候必须把货送到大门口。2.2 max和min分别代表什么set_output_delay通常成对出现一个-max一个-min。这两个值不是随便填的它们对应着外部路径的两种极端情况。-max对应的是建立时间setup检查。它表示外部路径在最坏情况下走线最长、下游器件建立时间最大、时钟偏斜最不利数据到达下游器件的时间相对于时钟沿的最大延迟。工具会用这个值来反推FPGA内部输出路径必须多快完成才能保证数据在时钟沿之前足够早地到达下游器件并被稳定采到。-min对应的是保持时间hold检查。它表示外部路径在最好情况下走线最短、下游器件保持时间最小数据到达下游器件的时间相对于时钟沿的最小延迟。工具用这个值来反推FPGA内部输出路径不能太快否则数据变化太早会把下游器件上一个时钟沿要采的数据冲掉。这里有个容易搞混的点-max和-min的数值本身是相对于时钟沿的偏移量可以是正数也可以是负数。正数表示数据在时钟沿之后到达负数表示数据在时钟沿之前到达。具体怎么算下一节详细说。2.3 为什么不能只写一个值有些新手会问我就写一个-max行不行答案是对于纯组合逻辑输出或者对保持时间不敏感的接口可能侥幸能过但一旦下游器件的保持时间要求比较严或者PCB走线长短差异大只写-max就会导致hold违例被漏掉。工具在没有-min约束时会默认外部最小延迟为0这在实际中几乎不可能因为走线再短也有延迟。所以规范的做法永远是max和min成对写。3. 数值怎么算从下游器件手册反推3.1 建立时间侧的计算方法假设FPGA通过一个输出时钟clk_out向下游器件发送数据下游器件用同一个时钟的某个边沿来采样。我们要算-max的值。外部路径的最大延迟包括PCB走线最大延迟T_pcb_max下游器件的建立时间T_su时钟路径上的偏斜T_skew如果时钟和数据走线长度不同那么set_output_delay -max的值就是-max T_pcb_max T_su - T_skew这里的T_skew符号要看具体情况。如果时钟走线比数据走线长时钟到达下游器件更晚相当于给数据留了更多时间T_skew取正值减掉反之取负值加上。实际项目中通常会让时钟和数据走线等长T_skew近似为0简化计算。举个例子下游ADC的建立时间是2nsPCB走线最大延迟是0.5ns时钟和数据等长。那么-max 0.5 2 2.5ns。意思是数据从FPGA引脚出来最多允许2.5ns之后必须到达ADC并被采到。3.2 保持时间侧的计算方法-min的计算类似但用的是最小延迟和保持时间-min T_pcb_min - T_h其中T_pcb_min是PCB走线最小延迟T_h是下游器件的保持时间。注意这里是减号因为保持时间要求数据在时钟沿之后还要保持一段时间相当于数据不能太早变化。继续上面的例子PCB走线最小延迟0.3nsADC保持时间0.5ns。那么-min 0.3 - 0.5 -0.2ns。负值表示数据在时钟沿之前0.2ns就已经到达并稳定这是合理的因为保持时间要求数据在时钟沿之后0.5ns内不能变而数据在时钟沿前0.2ns到达到时钟沿时已经稳定了0.2ns满足要求。3.3 一个完整的计算实例假设一个RGMII接口FPGA输出TXC时钟和TXD[3:0]数据PHY芯片在TXC的上升沿和下降沿都采样数据DDR。PHY手册给出建立时间1.2ns保持时间0.8ns。PCB走线长度数据组内等长最大延迟0.6ns最小延迟0.4ns。时钟走线比数据走线长0.2ns。先算时钟偏斜时钟走线长0.2ns意味着时钟到达PHY比数据晚0.2ns。对于建立时间检查时钟晚到对数据有利所以T_skew取-0.2ns相当于减掉一个负值变成加0.2ns。对于保持时间检查时钟晚到对保持不利T_skew取0.2ns。建立侧-max 0.6 1.2 0.2 2.0ns保持侧-min 0.4 - 0.8 0.2 -0.2ns这两个值就是最终写到SDC里的数字。当然实际项目中还要考虑时钟抖动、电压温度变化等因素通常会留10%~20%的余量。注意以上计算假设下游器件用FPGA输出的时钟采样。如果下游器件用的是自己的本地时钟那就涉及跨时钟域set_output_delay的参考时钟要选对通常需要用-clock指定下游器件实际使用的时钟。4. 不同接口场景下的实战写法4.1 源同步接口RGMII与DDR输出源同步接口是set_output_delay最典型的应用场景。FPGA输出数据和时钟下游器件用这个时钟采样。RGMII就是其中的代表TXC频率125MHzDDR模式下等效250Mbps每根数据线。对于RGMII输出SDC通常这样写create_clock -name tx_clk -period 8.0 [get_ports TXC] set_output_delay -clock tx_clk -max 2.0 [get_ports {TXD[*]}] set_output_delay -clock tx_clk -min -0.2 [get_ports {TXD[*]}] set_output_delay -clock tx_clk -max 2.0 [get_ports {TXD[*]}] -clock_fall -add_delay set_output_delay -clock tx_clk -min -0.2 [get_ports {TXD[*]}] -clock_fall -add_delay注意最后两行因为RGMII是DDR下降沿也采样所以要对-clock_fall再写一遍并且加-add_delay否则后写的会覆盖前面的。这是很多人容易漏掉的地方漏掉之后工具只检查上升沿下降沿的时序就完全没约束。4.2 系统同步接口SPI与并行总线系统同步接口的特点是下游器件用的时钟不是FPGA输出的而是板上另一个时钟源提供的。这时候set_output_delay的参考时钟要选那个系统时钟而不是FPGA自己的时钟。比如一个SPI MasterFPGA输出SCLK、MOSI从器件用SCLK采样。虽然SCLK是FPGA输出的但从器件的采样时刻相对于SCLK有固定延迟所以参考时钟还是SCLK。但如果是从器件自己产生时钟来采样FPGA输出的数据那就得用从器件的时钟作为参考。对于并行总线比如FPGA向SRAM写数据地址、数据、片选、写使能都要约束。通常会把它们分成一组统一写set_output_delay因为它们的PCB走线通常要求等长。4.3 纯组合输出与虚拟时钟有些输出信号没有伴随时钟比如一个简单的使能信号或者中断信号下游器件用它的本地时钟采样。这时候FPGA侧没有对应的输出时钟就需要创建一个虚拟时钟virtual clock作为参考。create_clock -name virt_clk -period 10.0 set_output_delay -clock virt_clk -max 3.0 [get_ports INT_N] set_output_delay -clock virt_clk -min 1.0 [get_ports INT_N]虚拟时钟不对应任何实际端口只用于时序分析。它的周期要设成下游器件实际采样时钟的周期。这种写法在跨时钟域接口中很常见。4.4 多比特信号与组约束对于多位宽的总线比如16位数据线通常会对整个总线写一条约束set_output_delay -clock tx_clk -max 2.0 [get_ports {DATA[*]}]但前提是这些信号在PCB上走线等长且下游器件对它们的采样要求一致。如果总线内不同比特的走线长度差异很大就需要分组约束把走线长度接近的分为一组分别写不同的delay值。实际项目中DDR类接口通常要求组内等长误差在5mil以内普通并行总线可以放宽到50mil。5. 常见问题与排查技巧实录5.1 时序报告里output delay违例怎么看跑完时序分析后报告里会出现output delay相关的路径。如果setup违例说明-max给大了或者FPGA内部逻辑太慢。如果hold违例说明-min给小了太负或者FPGA内部逻辑太快。排查顺序先确认set_output_delay的值是否算对再检查时钟约束是否正确最后看内部逻辑是否需要加流水线。很多时候问题不在约束本身而在于内部组合逻辑路径太长导致输出寄存器到引脚的延迟过大。5.2 为什么加了约束反而违例更多有一种情况之前没写set_output_delay时序全过写了之后反而出现违例。这不是约束写错了而是之前工具根本没检查外部路径现在检查了暴露了真实问题。这时候应该高兴因为问题在流片前被发现了而不是在板子上。5.3 下降沿约束漏写导致的诡异现象前面提到RGMII要写-clock_fall。如果漏写上升沿检查通过下降沿没检查。实际板子上表现为高速传输时偶发误码降频后正常。因为下降沿的时序余量可能已经是负的只是工具没报。这种问题在调试时非常难查因为逻辑仿真完全正常只有上板才暴露。5.4 虚拟时钟周期设错虚拟时钟的周期必须和下游器件实际采样时钟一致。如果设错了比如下游是100MHz虚拟时钟设成了50MHz那约束就完全失效了。工具会按照50MHz去算余量看起来很大实际板子上100MHz根本跑不通。5.5 常见问题速查表现象可能原因排查方法上板数据偶发错误降频正常下降沿约束漏写或min值不对检查SDC是否有-clock_fall和-add_delay时序报告output delay setup违例-max值过大或内部逻辑太慢重新计算-max检查输出寄存器到引脚路径时序报告output delay hold违例-min值过负或内部逻辑太快重新计算-min考虑加输出缓冲约束写了但工具不检查参考时钟未定义或端口名写错用report_clocks和get_ports确认多比特总线部分比特违例组内走线不等长分组约束或要求PCB等长实操心得每次写完set_output_delay一定要用report_timing -to [get_ports 输出端口]单独看一下这条路径的时序报告确认工具确实在检查并且slack计算符合预期。不要只看summarysummary会掩盖很多细节。6. 从约束到PCB跨团队协作的注意事项set_output_delay的数值不是FPGA工程师一个人拍脑袋决定的它依赖PCB走线延迟和下游器件手册。实际项目中这三个信息往往掌握在不同人手里FPGA工程师知道内部逻辑PCB工程师知道走线长度硬件工程师知道下游器件选型。我踩过的一个坑是PCB工程师在布局时把FPGA和下游器件放得比较远走线长度比预估的长了30%但没人通知FPGA侧更新约束。结果时序报告是过的板子却工作不稳定。后来复盘发现-max的值需要从2.0ns改成2.6ns内部逻辑必须再优化才能满足。所以规范的做法是在PCB布局布线完成后让PCB工程师导出关键网络的实际走线长度换算成延迟通常FR4板材信号传播速度约6.5ps/mm即1mm约6.5ps1000mil约165ps然后更新SDC中的set_output_delay值重新跑时序。这个迭代过程至少要做一轮高速接口要做两到三轮。另外下游器件手册里的建立保持时间通常是在特定测试条件下的典型值实际应用中要考虑电压、温度、工艺角的变化。保守的做法是取手册中的最大值再留20%余量。如果手册只给了典型值可以按1.5倍估算。7. 写在最后的一点个人体会set_output_delay这条命令语法简单到五分钟就能学会但真正用对、用准需要的是对接口时序的完整理解。我见过太多项目内部逻辑写得漂漂亮亮时序约束却一塌糊涂最后靠降频或者加等待周期来“解决”问题。这其实是在用性能换稳定性代价很大。我的习惯是每定义一个输出接口先画一张时序图把时钟沿、数据有效窗口、下游器件建立保持要求都标出来然后从这张图直接推导出max和min的值。图画清楚了约束自然就对了。如果图画不出来说明对这个接口的理解还有盲区这时候写出来的约束大概率是错的。还有一点SDC文件一定要纳入版本管理每次修改都要写清楚原因和计算依据。过半年回头看你还能知道当时为什么填这个数。我见过有人把set_output_delay的值改了没留记录后来板子出问题查了一周才发现是约束被误改。这种坑完全可以通过规范流程避免。