做DFT的工程师,谁没在Scan Shift模式的时序约束上吃过亏?我最早接触SDC(业界通用的设计约束文件格式)的时候,一直以为把功能模式那一套约束改改时钟周期就能直接用。结果第一次做某项目的扫描链测试仿真,shift阶段跑出一堆莫名其妙的保持时间违例,修了一个多星期才发现问题根本不在设计,而是约束模板本身就没把shift模式的特殊工况说清楚。后来我整理了一套“基于DFT的Scan Shift模式SDC模板”,从那以后,不管后端工具怎么换、芯片规模怎么变,这套模板的基本思路都能直接套用。
这篇博客就把这套模板里的核心命令、参数选择逻辑、以及实战中容易翻车的地方完整拆开讲。适合刚接触DFT的后端工程师、想搞懂scan模式约束逻辑的验证工程师,以及所有被扫描链STA时序逼疯过的同行。你会发现,与其说这是一份模板,不如说是一份“怎么让时序工具在测试模式下别乱报错”的说明书。
1. Scan Shift模式为什么需要专门一套SDC模板?
1.1 先搞清楚Scan Shift到底在干什么
Scan Shift(扫描移位)是芯片可测试性设计里最基础也最关键的一个测试阶段。当scan_en置为1时,芯片内部所有扫描触发器会被首尾相连,形成一条或者多条移位寄存器链。测试向量从scan_in端口一个周期一个周期地串行灌进去,同时已经存在的响应数据从scan_out端口串行吐出来。
你可以把扫描链理解成一条很长的流水线,每一次时钟沿,所有节点的数据都往下一格挪。这个过程本身并不“测”什么逻辑功能,它的目的只有一个:把数据高效地搬运到每个触发器里,为后续的capture(捕获)阶段做准备。因为整条链在shift阶段每个时钟沿都在整体翻转,瞬间动态功耗极高,而且ATE(自动测试设备)能提供的时钟频率通常也不高,所以shift阶段的测试时钟普遍很慢,一般在10MHz到100MHz这个范围。
1.2 功能模式的SDC为什么在这里不适用
功能模式下,芯片内部有复杂的PLL、分频器、时钟门控,各个时钟域之间还有跨时钟域处理。这些在Scan Shift模式下几乎全部失效。DFT测试时,时钟通常直接由外部测试端口引入,内部时钟门控被强制打开或者绕过,分频器也不参与工作。整个芯片看到的只有一个外部慢速测试时钟,所有扫描单元都在同一个时钟沿上采样。
更关键的是路径结构完全不一样。功能模式下数据从组合逻辑来,扫描链在shift阶段的数据路径就是前一级触发器的Q端直接或者经过很少的组合逻辑接到后一级触发器的D端。路径极短,setup问题基本不存在,真正的麻烦是hold time——只要相邻触发器时钟偏斜稍大,数据就可能提前或落后一个周期到达。功能模式SDC里的时钟树复杂度和uncertainty设置根本覆盖不了这种场景。
1.3 谁最需要这份模板
如果你的工作内容涉及DFT流程、后端时序收敛、或者ATE测试向量生成,那你早晚会跟这份模板打交道。DFT工程师需要用它检查扫描链插入后的时序;后端工程师需要在signoff前确认shift模式约束没有时序违例;验证工程师编写测试仿真脚本时,也要理解SDC中哪些约束会影响门级仿真行为。说白了,项目里的每一批扫描链测试pattern能不能在测试机上跑通,很大程度取决于这份SDC写没写对。
2. 模板里的核心约束模块拆解
2.1 先按死模式信号:set_case_analysis
几乎所有DFT SDC的第一步,都是把测试模式和扫描使能信号固定住。用set_case_analysis命令让STA工具知道:当前分析场景是测试模式,而且scan_en固定为1。
set_case_analysis 1 test_mode set_case_analysis 1 scan_en不要小看这两行。如果不固定scan_en,时序工具会同时把shift和capture两条路径都拿去做检查,结果就是你在shift这个低速场景里还得满足capture那套快到离谱的约束,报出大量根本不该存在的violation。用set_case_analysis而不是“设false path”,是因为我们不是要完全忽略这些路径,而是希望工具在“scan_en恒为1”这个前提下老老实实去计算时序。一旦条件固定,工具就会自动把capture相关的路径排除掉。
这里有一个容易踩坑的点:有些工程师图省事,只用set_case_analysis 1 test_mode,忘了scan_en。这在单条扫描链、capture时钟和shift时钟完全相同的简单设计里可能侥幸能过,但只要设计里存在测试压缩逻辑,或者capture时钟和shift时钟不同频,立刻就会报出一堆让人头大的虚假违例。
2.2 让工具认识测试时钟:create_clock与时钟属性
Shift模式下,芯片内部一般不再需要复杂的时钟生成逻辑。你在SDC里需要做的,就是把外部测试时钟端口定义成一个干净的主时钟,并且告诉工具这个时钟的周期、占空比、不确定度和跳变时间。
create_clock -name test_clk -period 100.0 -waveform {0 50} [get_ports tck] set_clock_uncertainty 2.0 [get_clocks test_clk] set_clock_transition 1.0 [get_clocks test_clk]为什么period要用100ns,也就是10MHz?因为shift阶段全芯片几乎所有触发器同时翻转,如果频率太高,瞬时电流会直接击穿电源网络的承受极限,造成IR Drop,甚至让芯片在测试时直接逻辑错误。10MHz是一个非常常见的入门值,具体频率需要根据ATE能力、芯片功耗模型、扫描链长度综合评估。如果扫描链很长,还可能需要降低到5MHz甚至更低。
set_clock_uncertainty里既包含时钟偏斜预算,也包含jitter预算,它会同时影响setup和hold检查。模板里给2ns是一个相对保守的数值,如果设计本身的时钟树做得比较紧,可以压到1ns甚至更低,但我不建议一开始就压得很激进的,先跑一版看趋势再说。set_clock_transition模拟的是外部时钟进入芯片管脚之后的边沿斜率,一般给1ns左右。给得太小,噪声会让时钟沿发生抖动;给得太大,时序结果会过于悲观,明明能收敛的设计也会被拖累。
2.3 复位和异步信号的处理
Scan Shift阶段,芯片的复位、置位这些异步控制信号默认是不该工作的。它们如果不受约束,STA工具会认认真真地去检查异步复位从端口到每个触发器的时序关系,跟扫描时钟去比,结果就是成千上万条假违例。
处理方式同样用set_case_analysis固定住:
# 低有效复位:固定为1表示取消复位 set_case_analysis 1 rst_n # 或高有效复位:固定为0表示取消复位 set_case_analysis 0 rst这个约束必须跟测试pattern对应上。如果测试pattern在shift阶段没有刻意去复位芯片,那SDC里固定复位信号就是对的;如果pattern是边复位边shift,那SDC的约束就需要相应调整。实战中我最常遇到的问题是复位信号在网表里经过了很多级buffer,导致get_pins定位不到想要的节点,这时候建议直接约束在顶层端口上,简单直接。
异步信号(比如外部异步中断)如果没有参与扫描测试,可以建set_false_path。但注意,复位不推荐用false path,因为复位和时钟之间的异步关系在capture阶段还是很重要的,用case_analysis固定条件比一刀切屏蔽更稳妥。
2.4 把非扫描组合路径整体屏蔽掉
Scan Shift模式下,我们真正关心的路径只有三条:scan_in端口到第一级触发器的D端、扫描链内部相邻触发器Q到D的路径、最后一级触发器Q端到scan_out端口。除此之外,所有纯组合的端口到端口路径,功能模式下需要检查,但shift模式下没有意义。
一个在很多教程里出现的“偷懒”做法是直接写:
set_false_path -from [all_inputs] -to [all_outputs]很多人担心这会把scan_in到scan_out的链路也屏蔽了,其实不会。因为扫描链是一条穿过寄存器的路径,sta工具在切分路径时,会把它拆成“输入到寄存器”和“寄存器到输出”两段,起终点都不再同时是input和output,所以这条全局false path并不会误伤扫描链。真正被屏蔽的,是那些从某个功能输入经过一堆组合逻辑直接跑到某个功能输出的路径,这些在shift模式下本来就不关心。
不过我还是建议把这条命令写得稍微保守一点,至少把测试时钟和扫描端口明确排除,避免将来工具版本升级、STA路径切分规则变化时出现意外:
set_false_path -from [remove_from_collection [all_inputs] [get_ports {tck scan_in}]] \ -to [remove_from_collection [all_outputs] [get_ports scan_out]]这段代码的意思是:除了tck和scan_in之外的所有输入,到除了scan_out之外的所有输出,都设为false path。这样既屏蔽了功能组合路径,又给扫描链留了一条完整的检查通道。
2.5 lockup latch的反相时钟定义
扫描链很长时,相邻扫描单元在物理上可能距离很远,时钟偏斜会非常明显,hold time极难满足。解决的常见方案是在长路径上插入lockup latch(锁定锁存器)。这个锁存器的时钟通常接主时钟的反相,利用锁存器电平敏感的特征,在时钟相位差太大时给数据留出一个安全的保护窗口。
SDC模板里必须让STA工具“看懂”这个反相时钟。如果lockup latch的时钟端直接由一个反相器驱动,可以在反相器输出端创建一个反相生成时钟:
create_generated_clock -name test_clk_inv \ -source [get_ports tck] \ -divide_by 1 -invert \ [get_pins lockup_cell/clk]定义好之后,STA工具就能正确计算锁存器的透明时间和保持窗口。这里要注意:如果设计里已经插入了多个lockup latch,别给它们各起不同的时钟名,最好统一使用同一个反相时钟对象,否则工具会把这些latch当成跨时钟域路径处理,检查不出来真正的hold风险,反而会报一堆没有意义的违例。
3. 一份可以直接上手的Scan Shift SDC模板
3.1 模板全貌
下面这份模板是我在实际项目中反复打磨过的一个基础版本,删除了具体项目名和端口细节,只留下了通用骨架。它假设设计里只有一条扫描链,一个测试时钟端口,一个低有效复位。多扫描链、多时钟的复杂设计,只要在这个基础上扩展循环就行。
# ------------------------------------------------------------ # Scan Shift Mode SDC Template # 适用场景:scan_en=1,外部慢速测试时钟,全扫描链移位 # ------------------------------------------------------------ # 1. 固定测试模式与扫描使能 set_case_analysis 1 test_mode set_case_analysis 1 scan_en # 2. 定义外部测试时钟 # period=100ns 对应 10MHz,需要根据ATE与功耗调整 create_clock -name test_clk -period 100.0 -waveform {0 50} [get_ports tck] set_clock_uncertainty 2.0 [get_clocks test_clk] set_clock_transition 1.0 [get_clocks test_clk] # 3. 固定复位:低有效复位拉高,取消复位 set_case_analysis 1 rst_n # 4. 屏蔽非扫描组合路径 set_false_path -from [remove_from_collection [all_inputs] [get_ports {tck scan_in}]] \ -to [remove_from_collection [all_outputs] [get_ports scan_out]] # 5. 如果使用lockup latch,定义反相时钟 # create_generated_clock -name test_clk_inv \ # -source [get_ports tck] \ # -divide_by 1 -invert \ # [get_pins lockup_cell/clk] # 6. 其他异常路径单独补充(见下文)3.2 每条命令背后的参数选择逻辑
先解释一下-waveform {0 50}的含义。这个参数表示时钟在0ns时由低变高,在50ns时由高变低,也就是占空比50%,上升沿在0时刻,下降沿在50ns。如果你的ATE只支持非标准占空比,比如60%,那就要写成-waveform {0 60}。这个值影响锁存器和下降沿采样逻辑,不能随便抄。
set_clock_uncertainty一般按照周期的2%到5%来取。10MHz时钟周期100ns,取2ns就是2%,偏保守但合理。如果项目里时钟树修得很好,可以缩小到1ns;如果芯片工作环境比较恶劣、片上噪声大,建议不要低于1.5ns。这里有一个实操技巧:先把uncertainty设得宽松一点,让整体布局布线能有个大概轮廓,等设计稳定后再逐步收紧,避免一开始就被逼到绝路。
时钟端口定义上,SDC里用get_ports而不是get_pins,因为测试时钟是从端口进来的。如果测试时钟在芯片内部经过了一个门控单元,那你还需要在门控输出端创建生成时钟,但shift模式下我倾向于直接约束原始端口,内部门控保持透明,更干净。
关于扫描链的约束,有人还会在模板里加:
set_multicycle_path 1 -hold -from [get_ports scan_in] -to [all_registers]这个做法在一些老流程里出现过,目的是放宽scan_in到第一级触发器之间因为端口外部电路造成的hold约束。但我个人的建议是:除非你非常清楚外部ATE驱动电路的实际偏斜,否则不要乱加多周期路径。管脚约束本来就归芯片级IO建模管,随便放宽会让测试机台侧的真实延迟被忽略,反而可能把真正的hold问题掩盖掉。
3.3 多扫描链如何扩展
实际芯片不可能只有一条扫描链,动辄几十条。你不需要在SDC里给每条链分别写一遍约束,可以用循环。比如扫描输入端口列表是scan_in[0]到scan_in[15],扫描输出是scan_out[0]到scan_out[15]:
for {set i 0} {$i < 16} {incr i} { set_false_path -from [remove_from_collection [all_inputs] \ [get_ports [list tck scan_in[$i]]]] \ -to [remove_from_collection [all_outputs] \ [get_ports scan_out[$i]]] }不过要小心:remove_from_collection如果传入一个端口名不存在的列表,工具会直接报错。所以在循环之前,最好先确认一下这些端口在当前网表里真实存在,不要图省事拿通配符硬套。另一个扩展点是多时钟域。有的芯片有多个测试时钟端口,比如高链和低链分开测试,那你就需要把每个时钟端口分别定义成不同名字的时钟,然后根据扫描链归属情况设置时钟组关系。大多数情况下,shift模式下各条扫描链共享同一个测试时钟,但如果两个时钟在物理上完全不相交,可以设成逻辑互斥时钟组,减少不必要的跨域路径检查。
4. 实战中容易翻车的地方与排查技巧
4.1 一跑STA就报几百条hold violation
这是我在项目里遇到最多的情况。Shift模式下setup违例相对少见,hold违例才是主角。典型报法是:某条扫描链上相邻两个触发器之间,数据到达时间太早,hold check失败。原因基本可以锁定在两个方面:一是两个触发器之间的距离物理上太远,时钟偏斜大;二是数据路径上几乎没有组合逻辑,延迟太短。
从SDC侧你能做的,首先是检查set_clock_uncertainty是否给得太大。不要把设计的问题全部用uncertainty来“背锅”。我见过有项目把uncertainty拉到4ns来消除所有hold违例,结果跑到ATE上一测,hold问题原形毕露。正确做法是分别设置setup和hold的不确定度,比如:
set_clock_uncertainty -setup 2.0 [get_clocks test_clk] set_clock_uncertainty -hold 0.5 [get_clocks test_clk]hold的uncertainty只保留基本的jitter预算,时钟偏斜应该靠时序收敛去解决,而不是在约束里一刀切掉。如果uncertainty已经合理,那大概率是物理设计层面需要插入lockup latch或delay buffer。SDC模板能做的,就是确保lockup latch的反相时钟定义正确,让工具给你一个真实的检查结果,而不是把问题藏起来。
4.2 scan_out前几拍就冒出X态
仿真的时候发现scan_out在头几个周期就出现X,并且一路往下传播,导致整个pattern全部废掉。这跟SDC的关系往往不在STA检查,而在于你的约束条件跟仿真行为不一致。最常见的原因是:测试bench没有在shift阶段开始之前给芯片做一次复位,而你在SDC里用了set_case_analysis 1 rst_n,让STA只看到复位无效的工况,但门级仿真里每个触发器上电时的初始状态却是随机的。
解决办法不是改SDC,而是在测试bench里先给一个至少几个周期的复位脉冲,等所有触发器都被初始化了再拉高复位、开始shift。这里有一个容易被忽略的细节:如果复位信号在网表里是经过buffer树后才到各个触发器的,你需要在testbench里让复位保持足够长的时间,确保最远端触发器也真正被复位到。你可以用STA的时序报告查一下复位树的延迟,把这个延迟换算成多少个测试时钟周期,再多留2-3个周期的余量。
4.3 时钟树上有反相器或门控,创建时钟总报错
有些设计里,测试时钟不是直接从顶层端口进入扫描单元,而是经过了内部的反相器、或门、甚至多路选择器。你在SDC里只定义了顶层端口时钟,结果工具报一堆unconstrained pin,或者时钟根本传不到扫描链上。
碰上这种情况,最简单的做法是在内部关键节点用create_generated_clock把真实驱动扫描单元的时钟重新定义出来,并且用-master_clock指明它的源头。拿反相时钟举例:
create_generated_clock -name test_clk_inv \ -source [get_ports tck] \ -divide_by 1 -invert \ [get_pins mux_out/CK]注意-source指定的是master时钟的源头在哪里,这里是顶层tck端口;最后面的[get_pins mux_out/CK]才是generated clock要定义的位置。如果你在多个层次都有反相时钟,建议每个都单独命名,不要复用同一个名字,否则工具会认为它们是完全同相的时钟,导致hold检查错误。
4.4 常见问题速查表
| 现象 | 可能原因 | 优先检查项 |
|---|---|---|
| 大量hold violation | 相邻扫描单元时钟偏斜大,或data path延迟过短 | clock uncertainty、lockup latch定义 |
| 大量setup violation | 测试时钟周期设得过小,或capture路径被误检 | create_clock period、scan_en case_analysis |
| unconstrained path | 测试时钟没有定义,或端口误被mask | create_clock、get_ports准确性 |
| scan_out出现X | testbench未先复位,或复位约束与仿真不一致 | testbench复位时序、SDC复位case_analysis |
| 工具报时钟互相穿越 | 多个测试时钟被当成异步时钟处理 | set_clock_groups、生成时钟命名 |
| create_generated_clock失败 | 指定的pin不可综合或网表层次变化 | get_pins路径是否存在、是否在时钟树上 |
4.5 用report_timing验证模板有没有生效
写完了模板,别急着交出去,先跑一条路径检查一下。STA工具里最常用的命令就是:
report_timing -from [get_ports scan_in] -to [get_pins FF1/D] -through [get_pins FF1/CK]这条命令会把scan_in到第一级触发器D端的完整路径拉出来,包括时钟路径和数据路径。你重点看两点:第一,路径的起点、终点是不是预期对象;第二,边界条件(uncertainty、transition)是不是模板里设置的值。如果起点终点完全对不上,基本可以断定是端口名写错或者网表层次看错了。这时候不要继续往下查violation,先回SDC里把端口定义修对了再说。
我个人的习惯是每次写完模板,会把所有扫描链首尾路径都抽查一遍,至少保证每条链的头、中、尾三段路径是受约束的。这个过程看着繁琐,但比起一条条去追那些不明不白的违例,效率高得多。
做DFT约束这行时间久了,你会发现真正的难点不是某条命令怎么写,而是搞清楚每一条约束在当前这个设计里到底在屏蔽什么、放开什么。网上各种模板一抓一大把,但拿过来直接跑,十有八九都要根据自己项目的扫描链结构、时钟方案、ATE能力去重新调。上面这套模板是基础,真正值钱的其实是我在第四部分说的那些排查思路——你知道了为什么会报这些错,才知道怎么去改自己的约束。最后再分享一个小技巧:每次改完SDC,记得把old版本和new版本跑出来的时序违例数量做个对比,如果某条新约束让违例数量断崖式下降,先别高兴,回头确认一下它是不是把不该屏蔽的路径也屏蔽了。宁可多花半天把约束看透,也别等后端来帮你填坑。