多时钟域scan chain最气人的一点是:单时钟域的流程你跑得再熟,跨到两个以上时钟域依然会翻车,而且故障通常不会在DFT阶段立刻暴露,而是等到后端做完时序收敛、甚至流片回来测试时才集中爆发。我第一次独立做一颗带三个异步时钟域芯片的DFT时,insert_dft和dft_drc全绿,TetraMAX报的测试覆盖率也能看,结果后端做完时钟树综合,shift测试在波形里直接乱套,扫描链上相邻的两个register,launch和capture完全对不上,整个pattern变成一锅粥。后来复盘,问题根子几乎都出在移位阶段时钟关系、异步时钟域的处理策略和工具默认配置这三件事上。
这篇文章把我用Synopsys DFT Compiler做多时钟域scan chain设计的实战经验完整梳理了一遍,读者定位是正在做DFT、或者刚接手多时钟域芯片后端与验证的工程师,内容偏向能直接落地的细节:时钟梳理、dft信号定义、命令参数含义、真实翻车案例和对应排查链路,最后附一张长期沉淀下来的自查清单。你可以把这篇当作多时钟域scan chain的“实战笔记”来用,不必按教科书从头啃。
1. 多时钟域scan chain为什么总在最后关头翻车:三个绕不开的矛盾
1.1 移位阶段谁在驱动数据:功能时钟与测试时钟的冲突
scan chain的移位本质上是一个时钟周期一个时钟周期地把数据从scan_in推到scan_out。单时钟域里,所有触发器被同一个时钟沿驱动,只要保持时间足够,链上数据就像传送带一样稳定前进。多时钟域一出现,问题就来了:每个寄存器在功能模式下受自己的功能时钟控制,这些时钟频率不同、相位不同、上升沿到达时间也不同。如果让功能时钟直接参与shift,跨在两个时钟域边界的相邻寄存器之间,数据到达时刻和时钟到达时刻完全没有对齐关系,hold violation几乎是必然的。
所以真实项目的标准做法是在每个时钟的根部做测试时钟mux,把功能时钟切到统一的测试时钟上,让全链在shift阶段用同一个低频时钟沿驱动。但这里面有一个非常容易被忽略的点:测试时钟本身的频率、占空比,以及DFT阶段被识别为ScanClock的view,决定了STA阶段这条链以什么样的时钟关系去做时序检查。如果你在DFT脚本里只定义了功能时钟,没有把测试时钟通过set_dft_signal -view existing_dft的方式描述清楚,工具会默认拿功能时钟的沿来推断shift关系,这对异步时钟域来说几乎等于没切。
我个人习惯是在任何多时钟域项目里,先统一约定:shift阶段全芯片只存在一个有效时钟沿。这个约定写进DFT脚本、写进后端约束、写进STA检查条目,比任何一条命令都重要。
1.2 异步时钟域之间的寄存器链条:功能上的伪路径在测试里变成了真实路径
功能模式下,两个异步时钟域之间通常不需要做时序收敛,SDC里用set_clock_groups -asynchronous一划就完事,跨时钟域的握手逻辑、同步器链在时序分析时都可以当伪路径处理。但在scan chain的shift阶段,物理连接关系完全变了:链上相邻的两个触发器,不管它们之间在功能上有多少异步逻辑,测试时它们就是一条真实的数据通路。那层“异步伪路径”的保护伞,在移位模式下是不存在的。
这个问题最典型的表现就是:后端用功能模式SDC跑STA时,跨时钟域路径被当作false path,一点问题都没有;可一旦切到shift模式,工具开始检查扫描路径上的时序,跨域边界立刻冒出一堆hold violation。很多工程师第一反应是“是不是scan cell选得不对”“是不是后端修hold的buffer没插”,但真正的根因往往在更早的环节——DFT阶段有没有想清楚跨时钟域寄存器之间以什么方式互联,有没有在移位模式下给这条“伪路径”补上真实路径该有的时序保护。
1.3 工具默认配置距离多时钟域最优解还有不少距离
Synopsys DFT Compiler的默认配置是保守的。老实说,这个保守设计对单时钟域、单边沿的设计是安全的,但遇到多时钟域场景,默认配置往往不是最合适的选择。比如set_scan_configuration默认的clock mixing策略偏保守,会严格限制哪些时钟的寄存器可以进同一条chain;chain数量默认取值也不一定适合你的供电、面积和测试时间约束。工具的思路是“宁可少干也不犯错”,很多跨时钟域优化需要你自己明确告诉它“这里可以mix”“这里需要lockup”“这里允许edge混用”。
这里想强调一个观念:DFT工具不是自动化的终点,而是把设计者的测试策略落地的手段。多时钟域scan chain设计里,真正干活的是你提前规划好的时钟关系、测试时钟方案和链分配策略,工具只负责执行得更精细。把锅甩给工具之前,先检查自己有没有把规则喂清楚。
2. 动手之前先把时钟家族梳理清楚:从RTL时钟树到测试时钟定义
2.1 一张纸画出时钟关系:源时钟、衍生时钟、门控时钟
我拿到RTL后不会马上打开工具,而是先在代码或者示意图上把时钟家族画出来。要标清楚这几类:
- 主时钟:芯片输入端口进来的原始时钟,比如PLL之前的参考时钟、外部接口时钟。
- 衍生时钟:由主时钟分频、倍频、门控得到的时钟,比如div2寄存器输出的时钟、AND门控出来的模块时钟。
- 测试时钟与测试模式信号:scan_en、test_clk这些在DFT阶段要用的端口。
- 每个时钟对应的触发器集合:哪些模块的寄存器是吃这个时钟的。
画完这张图,你基本就知道这个设计的scan chain会有哪些天然的“分界”。时钟域边界越多,链分配越碎片化,就越需要决定:这些域允许不允许串在同一条chain里。如果允许,必须用lockup latch和测试时钟对齐来兜底;如果不允许,chain数量至少要覆盖这些域的分组需求。这张图也是后续和前端、后端对齐的公共语言,功能验证的人看功能时钟,后端的人看物理时钟树,DFT的人看的其实是“哪些触发器可以按测试逻辑组合在一起”,三者的信息流全部从这张图开始。
2.2 SDC里的时钟分组与DFT的测试时钟定义,必须对同一件事说同一种话
多时钟域DFT最隐蔽的问题之一,是SDC里功能模式的时钟分组和DFT脚本里的测试时钟定义彼此脱节。举个例子:SDC里你已经用set_clock_groups -asynchronous分好了A、B、C三个异步时钟域,但DFT脚本里做set_dft_signal时,只定义了顶层test_clk作为ScanClock,没有把功能时钟和测试时钟的关系明确建模,工具就会自己“脑补”功能时钟的沿关系。一旦脑补的方向和SDC不一致,dft_drc可能仍然能过,但后端STA时必然露馅。
我的做法是把时钟定义集中在一个公共的setup脚本里,SDC资源和DFT资源共用。至少包含三类信息:功能时钟的周期和端口、异步时钟分组、测试时钟和scan_en的定义。下面是一个简化骨架,工具版本不同命令格式有小差异,但思路通用。
# 功能时钟定义 create_clock -name clk_a -period 10 [get_ports clk_a] create_clock -name clk_b -period 20 [get_ports clk_b] create_clock -name clk_c -period 5 [get_ports clk_c] # 异步时钟分组:功能模式下跨这些域的路径不做时序收敛 set_clock_groups -asynchronous \ -group { clk_a } \ -group { clk_b } \ -group { clk_c }# DFT相关信号定义 set_dft_signal -view existing_dft -type ScanClock \ -port [get_ports test_clk] -timing { 30 60 } set_dft_signal -view existing_dft -type ScanEnable \ -port [get_ports scan_en] -active_state 1 set_dft_signal -view existing_dft -type Reset \ -port [get_ports scan_rst_n] -active_state 0这里面有一个细节:-timing { 30 60 }填的是测试时钟上升沿/下降沿时刻对应的占空比信息,具体数值要根据实际测试时钟频率来定义。不管填多少,核心目的都是让工具和后续STA以同一个时钟沿为基准去计算扫描路径的时序,而不是各拿各的时钟沿乱算一气。
2.3 一套可以直接复用的多时钟域DFT脚本骨架
把上面这些拼起来,我通常会在项目里维护一个DFT流程脚本,按顺序完成环境加载、时钟定义、dft信号定义、dft_drc和insert_dft。下面是一个简化但完整的参考骨架,命令和选项以常见版本为基准,使用前建议对照你手上的工具版本再查一遍help。
# 加载设计、库和约束 read_verilog rtl_top.v current_design rtl_top link source common_clocks.sdc source dft_signals.tcl # 扫描配置:8条链,允许跨时钟混合,开启lockup自动插入 set_scan_configuration -chain_count 8 \ -clock_mixing mix_clocks \ -add_lockup true # 先做DRC检查 dft_drc # 预览插入后的链结构(不改变设计) preview_dft # 正式插入 insert_dft # 报告检查 report_scan_path report_dft_signal注意一个容易被忽略的顺序:dft_drc要先跑。很多人图省事,配置完直接insert_dft,结果DRC问题被插入动作一次性带出来,报告里的错误信息混杂在一起,很难快速定位。DRC阶段就把时钟问题、单元问题、扫描配置问题暴露出来,远比插入后回头排查省时间。遇到大规模设计时,这一步的差别可能是半天和两天的区别。
3. DFT Compiler插入扫描链:那些决定跨时钟域命运的命令参数
3.1 set_scan_configuration中的clock mixing选项到底在管什么事
多时钟域scan chain的核心配置基本都落在set_scan_configuration里。其中-clock_mixing是决定“不同时钟域的寄存器能不能串进同一条chain”的关键开关。常见值有这几个:
| 选项 | 行为 | 多时钟域场景下的适用性 |
|---|---|---|
| no_mix | 不同时钟域的寄存器不允许进同一条chain | 最保守,链数量往往偏多,跨域边界少,但chain利用率可能低 |
| mix_clocks | 允许不同时钟域的寄存器混合到同一chain | 最常用,能提高chain利用率,链数量和IO压力降低,但必须配合lockup latch保证跨域时序 |
| mix_edges | 在mix_clocks基础上允许跨越时钟边沿 | 适用于存在下降沿采样寄存器的设计,跨边沿时对时序要求更细 |
当你选了mix_clocks,工具就有了“把不同域的触发器拼在同一条链里”的权限,但拼的时候它会自己判断哪些跨域连接是安全的,不安全的会尝试插入lockup latch。这里要划一个重点:clock mixing不是百分比越高越好。跨时钟域混用的链,对测试时钟的skew要求非常严格,skew稍微大一点,lockup latch也救不回来。所以我的习惯是:异步时钟域之间能用独立chain的就尽量独立,只有chain数量受IO限制时才允许mix_clocks;同属一个主时钟的衍生时钟域之间,则可以放心用它来提升利用率。
3.2 preview_dft:插入之前免费检查一次结构风险
preview_dft这个命令很多人不常用,我强烈建议在insert_dft之前养成跑一下的习惯。它不会真的改设计,只是按你当前的配置做一次预演,输出每条扫描链的预期结构、时钟域分布、链长度、可能的DRC问题。对于多时钟域项目,preview_dft报告里我最关注三个信息。
第一,每条chain上有没有跨异步时钟域的连接。如果出现了,而脚本里没有设置mix_clocks,那你需要回去重新审视配置,否则insert_dft要么报错,要么以某种保守方式把链拆开。第二,chain长度是否均衡。多时钟域设计里,某个小时钟域只有几十个触发器,如果单独成链,整条链短得离谱,不仅浪费扫描IO,还让pattern时间被这条短链拖累。看到这种失衡,就该考虑把相邻安全域合并。第三,lockup latch的预期插入点。preview_dft如果显示某些跨域连接没有插入lockup,你就要去追问为什么——是不需要,还是工具认为那里是安全的同沿连接?这个判断直接关系后续STA结果。
3.3 insert_dft之后的产物怎么看:chain报告、lockup数量与跨域连接
insert_dft跑完,别急着导出网表,先看report_scan_path。多时钟域设计里,这份报告的价值远高于单时钟域,它详细列出了每条链上的寄存器分组、时钟关系、插入的lockup latch。我一般会重点核对三件事。
一是lockup latch数量。只要用了mix_clocks,跨域连接处都应能看到lockup。如果报告里lockup为0,而设计里确实存在异步域相邻寄存器的连接,那就说明工具认为没有跨域,或者你的时钟定义有漏洞,这需要回到第2章去查。二是chain与时钟域的映射关系。每条chain里都有哪些时钟域的触发器,这个信息直接决定后端在时钟树综合时对测试时钟的约束结构。如果一条链里混了3个异步域,后端看到这样的物理连接,就知道测试时钟树必须覆盖这3个域,否则hold问题无处可藏。三是每个时钟域是否都有对应的scan output可观测。多时钟域设计容易出现某个小域的触发器被分散在几条链里,但每条链的观测点又落在别的域,出问题的时候定位困难。我会建议在小时钟域至少保留一个独立的scan output,方便测试向量生成和良率分析时单独看这个域的状态。
4. 多时钟域scan chain三个真实翻车现场:从现象到根因的完整排查链路
4.1 翻车现场一:dft_drc红灯一片,根因是分频时钟定义不完整
有一次处理一颗带内部div2时钟的设计,dft_drc跑出来一大片和时钟识别相关的错误,具体表现是大量寄存器被报告无法归入任何有效时钟组,扫描配置怎么调都插不满链。我当时第一反应是scan cell库没配好,换库、换流程折腾了半天都没用。
后来静下心来看RTL才意识到,那个div2寄存器输出的时钟,在SDC里压根没有create_generated_clock,工具只能靠网表结构猜测这个时钟的存在。猜出来的结果时好时坏,一部分触发器被识别为吃div2时钟,一部分又因为组合逻辑太深被当成“无时钟器件”,整个clock group七零八落,DRC自然红灯一片。把分频关系用create_generated_clock定义清楚之后,DRC瞬间干净,链也顺利插完。
这个案例让我养成了一个习惯:脚本报错之前,先把设计里所有时钟的定义完整程度检查一遍。DFT工具对时钟关系的推断能力远没有你想象中那么强,一旦它需要“猜”,结果往往就是DRC里那些莫名其妙的错误码。多时钟域设计尤其如此,衍生时钟多、门控链深,只要有一个时钟定义不完整,工具后续的所有推演都是建立在沙子上的。
4.2 翻车现场二:post-route大量跨时钟域hold violation,根因在shift mode的STA约束
另一个项目更隐蔽。dft_drc全绿,insert_dft顺利,ATPG coverage也达标,结果后端完成时钟树综合和绕线之后,post-route STA报告里跨时钟域边界的scan路径上冒出一堆hold violation。一开始大家都以为是lockup latch没插,结果我打开report_scan_path,lockup数量是正常的,跨域连接处确实有latch保护。
真正的问题出在STA的shift mode约束上。功能模式SDC里set_clock_groups -asynchronous把三个域划开了,shift mode做时序检查时,工程团队偷懒直接复用了这份SDC,只额外加了测试时钟的时钟定义,没有删除异步分组。结果就是:shift模式下跨域路径在STA眼里依然是异步路径,工具根本不对它们做launch-capture对齐检查;但测试pattern实际执行时,所有寄存器都被测试时钟统一驱动,这些路径上是有真实时序要求的。等到了post-route,测试时钟树skew稍大一点,hold violation就在这些“没人检查”的路径上集中爆发。
修复的办法分两层。DFT层面:跨域连接处保留lockup latch,这是对时钟skew的硬件兜底;STA层面:shift mode必须使用独立的constraint,把测试时钟当作唯一的有效时钟,所有扫描路径都按同步路径去做setup/hold检查,同时给测试时钟树设置合理的uncertainty。这两件事一件都不能少,少了任何一个,问题只是换一个阶段暴露而已。这次之后,我在项目里强制要求shift mode的SDC单独维护,禁止复用功能模式的异步分组。
4.3 翻车现场三:同步器链进链后X态污染观察点,覆盖率死活上不去
第三个案例发生在ATPG阶段。芯片里有一批双触发器同步器,属于典型的跨时钟域接口逻辑。insert_dft后这些触发器都正常进了扫描链,ATPG生成pattern时工具也从它们身上抓到了不少fault coverage,但就是有一组观察点始终测不到,覆盖率卡在一个尴尬位置怎么都提不上去。
查了几天才定位到是X态传播问题。功能模式下,同步器的作用是把异步输入端来的信号同步到本地时钟域,输出是稳定的;但scan shift模式下,同步器链两级触发器作为普通扫描寄存器被移位,异步输入端的X态会沿着同步器链一路传播,通过组合逻辑冲到某些观察点上,把本来应该清晰捕获的值全部污染了。工具不是没做处理,而是这类跨时钟域接口上的X源很难在ATPG阶段自动识别干净。
最终的解决方案是从DFT阶段就介入:对同步器链做分组约束,避免把容易产生X态的异步输入直接串入关键观察路径;同时在测试约束里把同步器链的输入阶段设置为可预测的值,对确实无法处理的X源通过测试模式信号把它们置成固定状态。这个案例给我的教训是:多时钟域DFT不是插完链就结束的,ATPG阶段的可控性和可观测性一定要在DFT阶段就同步考虑,尤其是跨时钟域接口这种X态高发区域。
5. 长期沉淀下来的多时钟域scan chain自查清单
5.1 RTL与DFT准备阶段要确认的事
- 所有主时钟、衍生时钟、门控时钟是否都已定义完整的时钟关系,分频时钟有没有对应的generated clock定义。
- SDC里的异步时钟分组和DFT脚本里的dft signal定义是否来自同一个公共文件,两者对时钟域的划分是否一致。
- 测试时钟和scan_en在RTL里是否已经规划好端口,有没有被综合工具优化掉。
5.2 工具运行与结果检查阶段要确认的事
- set_scan_configuration的chain_count和clock_mixing是否基于第2章的时钟分组图决定,而不是随手填的默认值。
- dft_drc是否在insert_dft之前单独跑过,错误清单里有没有时钟定义不完整的痕迹。
- preview_dft报告里每条chain的时钟域混合情况是否符合预期,lockup latch的插入点是否合理。
- insert_dft后report_scan_path的chain长度是否均衡,小时钟域有没有独立的观测出口。
5.3 与STA、ATPG和后端PR需要对齐的信息
- shift mode下必须使用独立的约束文件,测试时钟是唯一的有效时钟,扫描路径全部按同步路径检查,异步分组在shift mode下要谨慎使用。
- 测试时钟树在后端实现阶段的skew目标,要按scan chain跨域连接的实际物理范围来设定,不能拿着功能时钟树的指标套用。
- ATPG阶段的X态处理策略要反馈到DFT阶段,同步器链、跨域接口这些X态高发区域最好在设计阶段就预留测试模式下的可控信号。
最后再分享一个我自己的实操习惯:每颗多时钟域芯片的DFT,我都会在项目启动时先做一次“全芯片只用一个测试时钟”的假设推演,把所有时钟域的寄存器按这个假设映射一遍。如果映射结果能接受,说明设计本身的测试结构是健康的;如果某个域怎么都映射不过去,那这个域多半就是后期所有时序和覆盖率问题的源头,趁早单独设计方案,比等后端报错再返工划算得多。这个习惯帮我避开了不少坑,也推荐你试试。