资讯动态

后仿真中的race问题:三类典型场景与五步定位法

发布时间:2026/10/23 6:33:14 来源:尧图企业网站定制
干后仿仿真时间长了我最怕的不是功能跑挂而是那种“功能仿真全绿、门级仿真一开就莫名出现X”的情况。这类问题里race-condition占了相当大一部分。前两篇讲了一些常见场景这篇继续补充三个我实际遇到过、而且定位过程比较有代表性的案例。如果你也在跟后仿中的X态、毛刺、偶发失效做斗争这期内容应该能帮你省下不少排查时间。所谓后仿race本质上是多个信号在同一仿真时刻、同一轮或相邻几轮事件调度里产生竞争导致仿真器按照事件到达顺序不同而给出不稳定的结果。它不是时序违例却往往以“偶发X”“某一次回归失败”“换了随机种子就消失”之类的面貌出现。真正可怕的是这类问题在代码review里看不见在PR前仿真的波形里也不出现等门级仿真一上就变成悬在头顶的剑。1. 后仿真里race的底层机制先搞懂再排查在讲具体场景之前先把race的根源说透。后仿中race高发和门级仿真的仿真机制有直接关系不理解这个底层机制后面排查会很费劲。1.1 前后仿真的“同时”不是一回事RTL功能仿真里组合逻辑没有绝对延迟同一个时钟沿触发的信号会在同一仿真时刻的同一轮事件里更新。看起来“所有信号同时变化”实际上仿真器把它们排在一个有序的事件队列里只是在同一个时间戳内完成而已。这个阶段很少有race因为信号更新的先后顺序对最终结果基本没有影响。门级仿真完全不同。经过SDF反标之后每个标准单元、每条互连路径都有延迟参数而且上升延迟和下降延迟经常不一样。组合逻辑的输出不再是一个时间点的跳变而是散布在多个时间点上的小翻转。两个输入信号到达同一个cell的延迟差可能在前仿中完全不存在在后仿中却直接暴露出来于是“同时变化”变成了“不同时刻的连续变化”。这里有一个很常见的误区总觉得race是那种“两个信号在同一时刻打架”的问题。其实后仿中的race往往表现为“一个信号在事件队列里的先后顺序与另一个信号的计算节点产生交叉”。它不一定是同一时刻的关系而是同一时间戳内不同delta cycledelta轮之间的竞争关系。1.2 delta轮次为什么会造成不确定性delta cycle是仿真器在同一仿真时间内为了处理信号传播而引入的连续迭代机制。比如一个NOT门输入从0变成1输出并不是立刻变化而是在下一个delta轮里被更新成0。这个输出再驱动下一级又会在再下一个delta轮更新。也就是说在一个固定的仿真时间点内部可能存在几十上百个delta轮次。要理解race就得把每个delta轮次当成一个独立的“事件计算阶段”。问题来了如果A事件在第3个delta轮更新了信号B事件在第4个delta轮采样了另一个信号那结果取决于到底谁先被仿真器取走。不同仿真器、不同版本、甚至同一仿真器的不同优化选项都可能导致这两个事件在delta轮上的顺序不同。我用一个生活化的类比帮你理解你同时给两个人发了消息一个说“灯关掉”另一个说“同学马上要来”。如果收信人先看到前一条他会关灯先看到后一条他会留着灯。硬件电路里这两个消息对应的是同一时刻到达的翻转和采样窗口在仿真器里它们的先后顺序由delta调度决定而不是由真实物理时间决定。这就是race的源头。1.3 区分仿真假象与真实隐患不是所有race都是硬件bug。有一部分race纯粹是仿真器对门级模型的事件调度引入的假象放到真实芯片上根本不会发生。但也有一部分race恰恰反映了真实电路中的时序风险只是发生在仿真时间尺度上被我们用“那只是个仿真X”的借口放过去了。我的经验是遇到race别急着改代码先做一次定性判断。如果问题只在特定仿真选项下出现或者只影响X传播路径而对0/1结果完全没有影响那多半是仿真调度假象。如果问题影响了判决结果比如采样到错误值、状态机进错分支并且路径上存在真实的时序余量不足那就要当成硬件隐患处理。这个判断直接影响后续的修复策略做反了会浪费大量时间。2. 场景A异步复位释放撞上时钟沿一个X态把一屏波形带崩第一个场景来自一次典型的后仿回归失败。某模块在复位释放后的第一个时钟上升沿出现了一大批寄存器输出X紧接着X像多米诺骨牌一样扩散到了后续好几个周期。功能仿真完全正常门级仿真只要换一个随机种子X扩散的范围和路径就变化。2.1 现象复位释放后的首个时钟沿一片X当时我的第一反应是检查复位释放时序。打开波形后瑞斯释放信号在某个时刻拉高紧接着不到几纳秒就是时钟上升沿。问题是这个释放沿并没有整齐地送到所有寄存器的复位端而是经过了好几级缓冲单元和修复单元。由于SDF反标各条复位分支的延迟差异很大导致释放沿到达不同寄存器复位端的时间点散开。真正触发X的寄存器其复位端在时钟沿到来时并没有完全变成1而是处在由X到1的过渡过程中。门级lib里的复位端模型通常不会瞬间翻转它在仿真器内部可能产生一个亚稳态X状态。一旦寄存器的复位端是X输出自然是X而且这个X从此跟定了这个寄存器除非有明确的复位或置位事件把它拉回来。我在波形里追下去发现第一个X源来自一个很小的触发器它既不是关键路径上的寄存器也不是复位树的末端而是刚好处于时钟沿和复位释放沿之间一个“夹缝”位置的倒霉蛋。它自己变成X之后下游一大片组合逻辑跟着被污染。2.2 根因复位释放沿与时钟采样的delta竞争打开事件列表后我印象非常深刻复位释放信号的下升沿这里注意是异步复位的释放沿不是有效沿和系统时钟上升沿在同一个仿真时间戳内但落在不同的delta轮次上。复位释放信号先在一个delta轮里变化经过缓冲单元后传到寄存器复位端却已经是几个delta轮之后此时时钟有效沿已经在更早的delta轮里完成了采样事件调度。寄存器在这种状态下如何判决不同厂家单元库的门级模型处理方式不一样。有的库会直接输出X有的库会保持上一状态有的库会在X恢复为1后再次触发Q端更新。我遇到的这个库选择了最不友好的路径复位端在时钟有效沿时是X输出端直接变X且后续不自动恢复。从根因看这是异步复位释放路径与时钟采样路径之间的真实约束缺口。PR工具通常会在复位释放路径上做时序修复但如果复位释放被误设成了false path或者时钟门控单元所在的时钟分支延迟反而比复位释放缓冲链快这个缺口就会被掩盖到后仿阶段。2.3 处置同步器、约束与仿真豁免这一类问题我最终的处置分成三层。第一层RTL层面强制修复复位释放必须经过同步器通常用两级触发器同步后再分发到各模块确保释放沿和采样时钟之间建立足够宽松的时间窗口。这属于让人挑不出毛病的标准做法。如果设计里用的是异步复位、异步置位那释放路径的同步化尤其不能省。第二层时序约束层面在SDC里把复位释放路径设为false path避免PR工具在复位树上花过多力气同时对真正的复位释放做set_output_delay或者set_case_analysis让工具严格检查释放沿与时钟沿的timing。这一点很容易被忽略因为很多项目复位释放路径从来不查时序结果后仿就把账算回来了。第三层仿真豁免层如果确认某条复位路径只是仿真模型过激反应可以这么做——在testbench里给复位释放信号加一个稍微提前的$delay让释放沿避开时钟有效沿的delta窗口或者对目标路径加仿真的no_timing_check。但要清醒这是豁免仿真工具的行为不是修复硬件只能作为验证环境的特殊处理不能写进交付代码。还有一个小提示这类race的排查非常依赖波形对比。我建议事后把复位释放分支里延迟最大的那条路径单独拉出来和时钟沿做时间差统计记录下来作为下次回归的监控指标。3. 场景BSDF反标后的组合毛刺在采样窗口翻车第二个场景是一个关于组合逻辑毛刺的典型坑。某模块的MUX输出在RTL仿真里稳如老狗后仿里却偶发采样到错误数据。最初我以为是数据路径上的时序违例做了一堆setup/hold分析全部绿。后来才发现问题根本不在时序收敛而是毛刺被采样了。3.1 为什么前仿看不到这个毛刺这个模块里MUX的sel信号来自两级组合逻辑而MUX的两个输入data分别来自不同的上游寄存器和组合路径。前仿中sel的两个来源信号在同一delta轮里更新sel在同一时刻完成跳变MUX的输出在同一个delta轮里直接跟随新的sel和data组合干净利落没有任何中间状态。门级仿真就不一样了。两条路径经过SDF反标后到达sel端的时间差可能是几百皮秒。如果sel信号的A来源先到、B来源后到那么sel在中间这段窗口内并不是一个稳定的0或1而是可能出现一次甚至多次翻转。这个翻转窗口如果恰好对准了时钟沿那MUX输出会先按旧sel的值采样再按新sel的值采样甚至可能出现X。问题的关键点在于毛刺不是“有没有”的问题而是“采样点会不会正好落在毛刺窗口”的问题。RTL仿真没有时间概念它不知道什么叫毛刺窗口后仿有了延迟时间毛刺窗口就变成了真实存在的事件序列。3.2 毛刺从产生到被采样的完整链路为了把整个链路讲清楚我假设一个简单的场景一个AND门输入A和输入B分别到达该门的延迟差为500ps而这个门的输入到输出传播延迟是200ps。B先到达A后到达。当B从0变成1时由于A还是0输出稳定在0几皮秒后A也变成1这时B已经是1AND输出从0变成1。这中间其实没有毛刺因为两个输入变化方向相同。但如果B先到达且从1变成0A还没变、仍为1此时AND输出会从1变成0再过几百皮秒A从1变成0输出保持0这也没有毛刺。真正产生毛刺的情况是B先到达且从1变0A未变化之前维持在1输出瞬间为0随后A变化到某个中间状态或恢复为1输出再次变成1。这样的0脉冲就是毛刺。这个窄脉冲只要宽度小于寄存器的采样窗口要求就可能不被后一级寄存器当作有效数据也可能被当成有效数据采集到。更麻烦的是门级lib对窄脉冲有最小脉冲宽度检查如果毛刺太窄单元库模型会直接把它变成X传播出去。我遇到的情况就是这样毛刺宽度不到50ps采样寄存器的时钟沿正好落在其中输出先是采到一个错误值同时由于库模型对窄脉冲的X化处理输出直接变成X。3.3 不是消灭毛刺而是清理采样路径接手这个问题时我最开始的想法是给MUX输出加一个毛刺滤波器。但想了想不对毛刺是路径延迟差造成的滤波它不如拉开采样点和毛刺的距离。最终方案是重定时。把MUX输出经过的那一级组合逻辑拆开在毛刺源和采样寄存器之间插入一级寄存器让毛刺在传播到采样点之前就被隔断。这步操作既改了RTL也重新走了PR后仿立即稳定。不过我见过更省事的处理方式如果确认毛刺只会出现在特定配置组合下可以调整SDC中的max_delay约束让两条路径的延迟差收敛到安全范围。本质上和重定时一样都是让毛刺窗口远离采样沿。但如果路径本身已经被推满约束调整的空间不大重定时往往更可靠。这里有一个工程判断并不是所有后仿毛刺都需要修硬件。如果是仿真器对太窄脉冲的过激反应且脉冲宽度远小于芯片实际可能出现的脉宽完全可以通过库模型或仿真参数处理掉。可一旦毛刺宽度接近或超过工艺下的最小可分辨脉宽就要当真实风险处理。4. 场景Cforce/release和时钟进程的竞争验证环境把自己带沟里第三个场景不是设计本身的问题而是验证环境自己给自己挖的坑。我在一个后仿调试环境里为了隔离某个模块的失效点直接对内部信号做了force。结果force本身引入了一个新的race让原本只是想Debug的现象变得完全不可复现。4.1 现象隔离式force让下游接口“随机”出错当时的现象很怪只要对某个时钟门控信号做了force下游接口偶发出现错误的数据选通去掉force之后又完全正常加大force持续时间错误概率又会变。我一度怀疑是设计有问题但换一种force方式之后问题又没了。后来把waveform导出事件列表逐条看发现问题出在force/release和时钟进程的同步上。force是在一个仿真时间点内执行的但它具体落在哪个delta轮取决于testbench里force语句的触发条件。如果force和当前时钟沿落在同一个时间戳内下游组合逻辑有可能在下一个delta轮就按照force后的值计算而另一部分逻辑仍然按force前的值计算形成行为上的割裂。release也类似。release的一瞬间逻辑值从强制值跳回原始值。这个跳变如果发生在时钟沿附近采样寄存器看到的可能不是“最终的稳定值”而是“跳跃过程中的某个中间值”。尤其是release的信号本身还在活跃翻转状态这个中间值就直接变成了X或者错误值。4.2 根因force/release事件的调度位置根因在于force/release在仿真器的事件调度模型里属于“用户命令区”操作它和普通的事件传播、NBA更新不在同一个优先级层级。不同仿真器对这个区域的实现细节不同有的仿真器允许force在任意delta轮之间插入有的则只在特定轮次生效。于是同一个testbench在不同仿真器上或者同一仿真器的不同优化级别下force生效的delta位置可能完全不一样。这种差异带来的后果就是你可以复现同一个测试向量但force的race行为每次不完全一样。它不是典型的0/1竞争而是“谁先看到这条命令”的竞争。从那之后我养成了一个习惯在做任何后仿debug时先弄清楚force是在什么仿真时间、什么delta轮被执行的而不是只看波形值。4.3 几个把force用安全的方法要避免这类race我的建议是三条。第一条强制的时间点不要贴着时钟沿。在testbench里先等待一个明确的事件比如时钟下降沿再执行force或release确保force动作发生在时钟静默期。这样避免和采样窗口正面冲突。第二条release之后不要立即做断言。等两三个时钟周期让信号从强制值恢复并稳定后再检查结果。很多临时Debug条件失败都是因为release后还没等信号稳定就下了结论。第三条如果不得不在时钟沿附近force那就给force的目标信号加一个显式的小延迟例如#1ps把它推到下一个仿真delta轮。注意这个延迟必须是仿真层级的小量不要影响其他信号的时序关系。我还遇到过一个更狡猾的情况force一个时钟门控信号为常1结果内部时钟被强制无毛刺地切换导致某个计数器模块以为自己永远处于活动状态跑出来的结果和真实芯片行为完全不同。这个案例再次提醒我force只能用来隔离问题不能用来模拟真实行为race问题可能因此被掩盖或放大。5. 通用排查链路与工程化建议前面三个场景各有各的细节但背后的排查思路是一致的。我整理了一套五步定位法基本可以覆盖绝大多数后仿race。5.1 五步定位法第一步找首因。不要从最终的X态或者错误结果往前倒推那样会被中间扩散的X态带偏。直接在波形里定位“第一个非预期信号变化”发生在哪个仿真时间、哪个信号上。第二步拉事件列表。把该时刻前后所有相关信号按delta轮排列重点看同时间戳内哪几个事件在竞争。这一步在GUI里很难看清我习惯直接导出文本事件日志。第三步做对照实验。换随机种子、换仿真器选项、调整SDF反标精度、临时修改单元延迟模型看race是否变化。如果换一种配置问题就消失多半是调度层竞争如果无论怎么换都稳定出现就要怀疑真实硬件风险。第四步排除testbench因素。看有没有$random、$urandom、force/release、interface里的时钟采样边沿等不确定因素混进来。这个顺序要放在硬件分析之后因为testbench self-inflicted的race也很多但容易被人忽略。第五步归类修复。硬件问题回到RTL/约束/PR去修仿真模型问题回到仿真参数去修testbench问题回到testbench去修。不要用仿真豁免去掩盖硬件问题也不要在testbench问题上反复调整RTL。5.2 race现象、首查动作与修复方向速查现象特征可能根因首查动作推荐方向复位释放后首个时钟沿X扩散异步复位释放与时钟沿竞争检查释放沿到各寄存器复位端的delta分布复位同步器、SDC豁免修正组合逻辑路径偶发采样错误但setup/hold全绿SDF反标后毛刺窗口命中采样沿拉取毛刺源路径延迟差与采样时间点重定时、约束收紧、毛刺滤波换随机种子后X路径变化事件调度顺序不稳定导出事件列表对比delta轮修正仿真选项、稳定testbench同步force/release后结果不稳定force动作与时钟进程竞争确认force/release所在delta轮force打点避让时钟沿、释放后稳定再断言5.3 仿真器设置与回归策略多数仿真器对X传播都做了优化控制。在排查race时我通常会把X传播相关报告全部打开而不是默认让仿真器自动把一个X变成0或1。一旦发现X立即定位到具体cell和delta轮比事后猜测要高效太多。关于回归策略我的做法是每一个涉及复位释放、时钟门控、跨时钟域的testbench都在常规回归之外再跑一轮随机种子轮换。这个成本不高但能把调度层的race暴露出来。很多race问题第一次出现都是“某一次随机回归挂了但重跑就好了”如果不做这种轮换这类问题可能在交付前都不会再出现。还有一点经验race问题尽量用小波形、小范围去查。不要一上来就铺满整屏信号那样既看不清事件顺序也容易被后续X传播的大量信号干扰。一次只看三个对象源头信号、中间路径信号、采样点信号。定位后逐步扩大范围。后仿真race排查的一些个人体会跟race打了这么多年交道最大的体会是它看着神出鬼没其实套路固定。我遇到的后仿race八成以上落在复位释放、组合逻辑毛刺采样、testbench自身调度这三类框架里。按前面说的顺序排查基本不会空手而归。再补一个小技巧排查race时多利用文本事件日志别只靠波形。波形适合看趋势但delta轮级别的先后顺序必须靠事件列表来判断。每次定位完一个race我会顺手把事件摘要存档后续同类问题直接对照排查速度会快很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑