☰
Verilog时序检查系统任务详解:setup/hold/recrem
2026/10/5 8:45:09 网站建设 项目流程

做FPGA或ASIC验证的朋友,大概率都见过这么一幕:后仿真跑着跑着,仿真器突然刷出一屏以$setup、$hold、$recrem开头的时间检查错误,然后紧跟着一大串看不懂的路径信息。我第一次看到的时候也懵过,以为仿真器出了问题,后来才明白,这是库单元里的时序检查系统任务被触发了,本质上是设计里存在真实可以被激励覆盖到的时序违例。

Verilog仿真中的时序检查系统任务(timing check system task),就是用来给仿真过程加“时间规矩”的。它们能检查建立时间(setup)、保持时间(hold)、时钟偏斜(skew)、脉冲宽度(width)、恢复时间(recovery)和移除时间(removal),配合STA静态时序分析,能非常高效地暴露设计中的时序风险。这篇文章我就把这些任务的原理、参数、实操方法,以及和STA的配合关系一次讲清楚,适合正在做后仿真、被时序violation整得头疼的验证工程师,也适合刚接触门级仿真的同学提前避坑。

1. 时序检查系统任务在验证流程中的定位

1.1 为什么要有这么多时序检查任务

功能仿真默认是零延迟模型,信号变化在同一个仿真时间步里完成,根本不会出现“数据还没稳定就被采样”的情况。但真实芯片不是这样,触发器有建立时间、保持时间,时钟树有偏斜,异步复位有恢复/移除时间要求。纯粹做RTL功能仿真,这些物理限制完全体现不出来,所以需要一套专门检查时间关系的机制。

时序检查系统任务就是干这件事的。它们在仿真过程中持续监控指定事件对之间的时间间隔,一旦不满足约束,就按配置报告warning或error,甚至触发notifier事件来让输出变成X态,模拟真实电路里的亚稳态行为。简单说,这就是在仿真世界里给电路立规矩,谁坏了规矩,立刻报警。

1.2 系统任务与STA静态时序分析的分工

很多人会问:既然有PrimeTime这类STA工具做全路径时序分析,仿真里还有必要放这些检查吗?答案是需要,而且两者的分工完全不同。

STA静态时序分析是对整个设计的所有路径做穷举计算,它不关心你给了什么激励,只要约束写好了,它就把每条路径的最大延迟、最小延迟都算一遍,看setup/hold是否满足。它的强项是覆盖全、速度快、不依赖激励。但STA有一个前提:所有路径都要有合理的约束。异步信号、跨时钟域、门控时钟这类场景,约束稍微写得不好,STA就“看不见”风险了。

动态仿真里的时序检查任务则是随激励走的,每一个事件沿都实时比对检查。它的优势在于能覆盖STA约束不到的动态场景,尤其是异步交互。比如异步复位释放相对时钟沿太近,STA里如果没做特殊约束,根本不会报,但后仿真时$recrem任务会立刻报出来。所以我的习惯是:STA解决“全路径最差情况是否满足”,仿真时序检查解决“实际激励下会不会出问题”,两者互相兜底,谁都不能缺。

1.3 Specify块、SDF反标与系统任务的绑定关系

标准Verilog里,时序检查任务一般写在一个叫specify块的区域里,跟模块的路径延迟声明放一起。库厂商在写仿真模型时,会按工艺库里的时序参数,把$setup、$hold这些调用直接嵌入specify块。

到了门级仿真阶段,综合或布局布线工具会生成SDF(Standard Delay Format)文件,里面携带了实际的单元延迟和时序检查约束。仿真开始后,通过$sdf_annotate任务把SDF反标到门级网表上,specify块里的时序参数就会被SDF里的值覆盖。这样每次跑完布局布线,只要更新SDF,仿真模型里的时序检查参数就自动跟着工艺结果走,不用手改。

理解了这个机制,你就明白了:后仿真里那些violation不是仿真器随便报的,而是实实在在对应着后端给的时序约束。

2. 六大核心时序检查任务拆解与参数讲解

2.1 $setup:建立时间检查,数据必须先到

$setup用于检查数据事件相对于时钟事件是否满足建立时间要求。标准调用格式是:

$setup(data_event, reference_event, limit, [notifier]);

其中data_event通常是数据信号的变化沿,reference_event是时钟有效沿,limit就是建立时间值。检查引擎在每次事件到来时比较两个事件的时间差,如果数据沿距时钟沿的间隔小于limit,就报违例。

举个例子,库模型里典型的写法:

specify $setup(posedge D, posedge CLK, 1.0); endspecify

意思是D端的数据必须在CLK上升沿之前至少1ns到达并稳定。假设仿真中D在t=9ns时变化,CLK在t=10ns时上升,差值只有1ns,恰好压线,算满足;如果D在t=9.5ns才变化,差值0.5ns,就会报violation。

实际使用要注意的是数据事件可以写成posedge D或negedge D,取决于信号有效沿。有些低电平有效的控制信号,就应该检查negedge沿。

2.2 $hold:保持时间检查,数据不能跑太快

$hold检查的是参考事件(时钟沿)之后,数据事件必须保持稳定的最小时间。格式为:

$hold(reference_event, data_event, limit, [notifier]);

注意参数位置跟$setup不一样,参考事件在前,数据事件在后。原因是hold检查关心的是:时钟沿采样完成后,数据不能立刻变化。

库模型里常见写法:

specify $hold(posedge CLK, posedge D, 0.5); endspecify

表示CLK上升沿之后,D至少要保持0.5ns不变。如果D在CLK上升沿后0.2ns就跳变,就报hold违例。

这里有个容易混淆的点:$hold的limit在很多库里是负值。比如$hold(posedge CLK, posedge D, -0.2),表示允许D在CLK沿之前0.2ns内就已经变化。这是合理的设计需求,因为某些触发器的数据路径本身存在延迟,数据在时钟沿附近小幅抖动是允许的。分析负值时,心里只要记住一点:仿真器比较的是两个事件的实际时间差,limit为负,就是在放宽限制方向移动门槛。

2.3 $setuphold:把建立与保持打包检查

实际工程中更常用的是合并版本$setuphold,一次把setup和hold都查掉:

$setuphold(reference_event, data_event, setup_limit, hold_limit, [notifier]);

例如:

specify $setuphold(posedge CLK, posedge D, 1.2, 0.4, notifier); endspecify

相比分开写$setup和$hold,$setuphold的优点是,当setup窗口和hold窗口存在重叠时,报告行为更可控,不会出现两个任务同时告警、信息互相干扰的情况。现在的标准单元库仿真模型里,绝大多数时序检查都是用$setuphold实现的。

再补充一点:从SDF文件反标时序检查值时,SDF里是拆成SETUP和HOLD两个条目分别给出的,而Verilog模型里用一个$setuphold承接。工具在反标时会把SDF的两条数据映射到合并任务的setup_limit和hold_limit上,这个对应关系在排查“为什么反标后检查值不对”的时候很关键。

2.4 $recovery与$removal:异步复位的恢复与移除检查

$recovery和$removal专门用于异步信号的检查,最常见的目标就是异步复位、异步置位。

恢复时间(recovery time)的意义是:异步信号(比如复位)释放沿到下一个有效时钟沿之间必须留出足够时间,让触发器退出复位后能稳定工作。如果复位释放离时钟沿太近,触发器可能采集到不确定状态。

移除时间(removal time)的意义是:时钟有效沿到来之后,异步信号必须继续保持有效一段时间,防止触发器的复位状态刚被采样就立刻撤销,产生亚稳态。

标准接口:

$recovery(reference_event, data_event, limit, [notifier]); $removal(reference_event, data_event, limit, [notifier]);

库模型里常见的组合写法:

specify $recovery(posedge CLK, posedge RST_N, 1.0); $removal(posedge CLK, posedge RST_N, 0.8); endspecify

这里posedge RST_N是低有效复位信号的释放沿(从0变1),把它当作data_event。时钟沿是reference_event。$recovery检查释放沿相对时钟沿之前的时间间隔,$removal检查相对时钟沿之后的时间间隔。

同样,也有合并版本$recrem:

$recrem(posedge CLK, posedge RST_N, 1.0, 0.8, notifier);

这个任务在后仿真里出场率极高,尤其是验证异步复位释放策略的时候。

2.5 $skew:两个信号边沿之间的偏差检查

$skew用来检查两个事件之间的时间偏差,格式为:

$skew(reference_event, data_event, limit, [notifier]);

它检查的是reference_event和data_event之间的时间差是否小于limit。如果小于limit,表示两个信号边沿靠得太近,可能触发问题。

$skew的典型应用场景是检查差分时钟、门控时钟和原始时钟之间的偏斜。比如一个时钟门控单元,要确保门控后的时钟沿与原始时钟沿之间的偏差不能太大:

specify $skew(posedge CLK, posedge GCLK, 0.3); endspecify

意思是CLK上升沿和GCLK上升沿之间的间隔必须大于0.3ns,太近就报错。这跟$setup和$hold的检查逻辑不一样:$setup/$hold是与一个共同的时钟沿比较,$skew是直接衡量两个事件之间的间隔,更像一个“最小间距守卫”。

2.6 $width:脉冲宽度检查

$width专门检查脉冲宽度,调用格式:

$width(reference_event, limit, threshold, [notifier]);

它通常以脉冲的起始沿作为reference_event,比如:

specify $width(posedge CLK, 3.0, 0, notifier); endspecify

表示CLK从上升沿开始,高电平持续宽度必须至少3ns。如果CLK在2ns后就掉下去了,就报错。threshold参数用于设置检测阈值,一般取0,表示不做额外的电平滤波;对于某些模拟行为模型,可以设置非零阈值来抑制小幅毛刺。

$width用的地方相对少,但在检查时钟最小脉宽、复位脉冲最小宽度时很实用。芯片后端对最小脉宽是有严格要求的,时序报告里会有min pulse width检查项,仿真里用$width可以提前发现激励侧的问题。

2.7 notifier参数:让时序违例真正影响仿真行为

前面几个任务的最后一个参数都是可选的notifier,这个参数很多人忽略,但它恰恰是时序检查的精髓。

notifier必须是一个寄存器变量。当时序检查失败时,仿真器会让这个寄存器变量的值发生翻转。如果在specify块后面跟着时序建模逻辑,把notifier连接到输出端的X态赋值条件上,仿真结果就会出现真实的X态传播,模拟亚稳态导致的输出不确定。

没有接notifier的检查,违反时序时只打印一条告警,功能上电路照样按理想情况跑;接了notifier之后,输出会变成X,下游逻辑跟着受影响,这种“感染式”的传播更能暴露真实故障。标准单元库的仿真模型里,notifier通常被很好地利用起来了。

3. 实操过程与关键实现细节

3.1 在testbench的specify块中手动添加时序检查

很多时候我们并不想等到门级仿真才看时序,在RTL阶段就想给关键信号加上时序约束做冒烟验证。这时候可以在testbench里自己写一个时序检查模块,专门做这件事。

下面是一个完整的示例,覆盖了$setuphold、$recrem和$width三种检查:

`timescale 1ns/1ps module timing_checker( input clk, input d, input rst_n ); reg notifier; specify $setuphold(posedge clk, d, 2.0, 0.5, notifier); $recrem(posedge clk, posedge rst_n, 1.5, 0.8, notifier); $width(posedge clk, 4.0, 0, notifier); endspecify always @(notifier) begin if (notifier !== 1'bx) begin $display("%0t [TIMING] notifier toggled, timing violation detected", $time); end end endmodule

然后在主testbench里把需要监控的信号接到这个checker上:

`timescale 1ns/1ps module tb_top; reg clk = 0; reg d = 0; reg rst_n = 1; wire q; always #5 clk = ~clk; // 被测设计 dff_async u_dut( .clk(clk), .d(d), .rst_n(rst_n), .q(q) ); timing_checker u_checker( .clk(clk), .d(d), .rst_n(rst_n) ); initial begin rst_n = 0; #5 rst_n = 1; #3 d = 1'b1; // clk上升沿在t=10,d在t=8变化,setup间隔2ns,压线 #10 d = 1'b0; // clk上升沿在t=15? 不对,实际是t=10之后下一个沿t=20 #20 $finish; end endmodule

这里d在t=8变化,下一个clk上升沿是t=10,建立时间刚好2ns,满足$setuphold里2.0的约束。如果你想看violation,把d的驱动时间改成t=9,建立间隔只有1ns,仿真器就会报错。

有一个地方要提醒:specify块里的事件表达式所使用的信号,必须模块端口可见,这是语法要求。所以我上面用了一个独立模块来做检查器,而不是直接写在tb_top里,否则某些仿真器会报端口相关限制。

3.2 门级后仿与SDF反标流程

真实项目里,时序检查任务大多是库模型自带并在门级仿真时被自动激活的。跑门级后仿的标准流程大概是这样的。

第一步,准备好门级网表和SDF文件。综合或布局布线后,工具会输出top_netlist.v和top.sdf,同时你需要一套和工艺匹配的仿真库模型,通常是sim_models.v。

第二步,编译库模型和网表。以常见的VCS/ModelSim流程为例:

vlog -work work lib/sim_models.v vlog -work work netlist/top_netlist.v vlog -work work tb/tb_top.v

第三步,在testbench里调用$sdf_annotate反标SDF:

initial begin $sdf_annotate("../output/top.sdf", tb_top.u_dut, , "sdf_annotate.log"); end

$sdf_annotate的第一个参数是SDF文件路径,第二个参数是被反标的设计实例路径。反标完成后,库模型specify块里的时序参数会被SDF中的值覆盖,时序检查任务开始按真实时序约束工作。

第四步,跑仿真并收集violation。这个阶段报出的所有timing violation,每一行都值得认真对待,因为它们背后大概率对应真实的时序风险。

我在实际项目中见过不少团队跳过SDF反标,只跑功能后仿。那样做的话,门级网表的单元延迟出来了,但时序检查参数还是库默认值,根本反映不出后端最终时序收敛状态。所以,反标这一步不能省。

3.3 异步复位释放场景的recovery/removal检查实操

下面用一个非常典型的异步复位释放场景,演示recovery/removal检查的实际表现。

假设被测模块是一个带异步复位的D触发器,时钟周期10ns,SDF反标后,库模型要求recovery时间为1.5ns,removal时间为0.8ns。testbench里让复位信号在时钟上升沿附近释放:

`timescale 1ns/1ps module tb_async_reset; reg clk = 0; reg rst_n = 0; reg d = 1; wire q; always #5 clk = ~clk; dff_async u_dut( .clk(clk), .d(d), .rst_n(rst_n), .q(q) ); initial begin // 复位保持3个时钟周期 repeat(6) @(posedge clk); // 在时钟下降沿后1ns释放复位,也就是离下一个上升沿4ns @(negedge clk); #1 rst_n = 1; repeat(2) @(posedge clk); $display("%0t [TB] reset release test done", $time); $finish; end endmodule

复位释放发生在negedge clk之后1ns,到下一个posedge clk的间隔是4ns,远大于1.5ns recovery,所以这轮不会有问题。

如果把释放时刻改成negedge clk之后4.5ns:

@(negedge clk); #4.5 rst_n = 1;

下一个posedge clk在5ns之后,中间间隔只有0.5ns,小于1.5ns的recovery要求。此时仿真器会给出类似下面的报告:

** Error: $recrem: (tb_async_reset.u_dut) Order: posedge rst_n, posedge clk (rst_n changed at time 24500 ps, clk at 25000 ps) Violation: recovery time 1500 ps not met

这个报错直接告诉我们,复位释放离时钟沿太近了。看到这种错误,第一反应不是去改仿真激励把报告压掉,而是应该反思复位释放策略本身:是不是需要加同步器?是不是复位树延迟没有评估准确?这才是时序检查的价值所在。

3.4 实例:一个不满足时序的复位释放如何被排查定位

有一次我在做异步FIFO模块的后仿,STAR里时序全部收敛,但后仿一直报$recrem违例。刚开始我以为是SDF反标出问题,排查了半天,发现反标本身没问题,真正原因是复位释放路径上的约束在STA里被误设成了false path,导致STA根本没检查异步复位释放的时序。

这个案例特别典型。STA工具全路径检查的前提是约束完备,异步路径一旦被错误地set_false_path,它就认为“这里不用查”,但实际上异步复位的释放相对时钟沿,是需要真正的时序保证的,哪怕它不在STA默认的同步约束框架里。

排查步骤我归纳下来是这样的:

第一步,从后仿log里定位violation的具体路径和信号名,确认是哪个实例报的。

第二步,打开该实例对应的库模型,查specify块里对应的时序检查参数值,和SDF反标结果比较。如果SDF里的值明显不合理,优先检查反标路径是否写错,或SDF文件本身是否包含该单元。

第三步,回到STA工具里,用同样的路径做一轮report_timing。如果STA报告全部meet,就要检查约束里是不是有false path、case analysis这类例外覆盖了路径。

第四步,如果确认是约束问题,修正约束后重新做STA,并把新SDF拿回来后仿再跑一遍。

这套流程走下来,基本能解决大部分“STA过了但后仿报时序错”的诡异问题。

4. 常见问题与排查技巧实录

4.1 后仿violation问题速查表

工作中我整理过一张时序检查violation排查表,放在这里给各位参考。

现象可能原因排查方向
大量$setup/$hold同时报错SDF未正确反标,或库模型和网表不匹配检查sdf_annotate日志,单独反标一条路径做验证
只有$setup报错,$hold正常数据路径延迟过大,或时钟树偏斜导致建立时间裕量不足STA中检查max_delay路径,分析时钟偏斜
复位释放附近报$recrem复位释放沿离时钟沿太近,或异步路径被设成false path调整复位释放策略,检查STA例外约束
$width频繁报错激励侧时钟/复位脉宽不满足最小要求查看时钟产生逻辑,确认分频/门控电路无误
报错位置在testbench内部信号检查器事件沿选错(posedge/negedge反了)核对信号极性,确认数据有效沿
后仿有violation但功能正常未接notifier,violation没有传播X态检查库模型notifier连接,尽量保留X态传播

这张表不只是排查用,在写仿真环境的时候也可以反过来当设计checklist:你要提前想清楚哪些检查必须开,哪些信号需要被监控,别等violation刷屏了再手忙脚乱。

4.2 仿真报错信息的解读与定位方法

刚接触时序检查的人,经常被仿真器里一大段报错信息搞晕。其实把关键字段拆开看,结构很清楚。

比如VCS里常见的这种输出:

** Error: $setuphold: (tb_top.u_dut.u_reg1) Order: posedge clk, data (data changed at time 13750 ps, clk at 15000 ps) Violation: setup time 1500 ps not met

第一行说明了任务类型和实例路径,$setuphold告诉你这个错误来自哪个检查,tb_top.u_dut.u_reg1告诉你发生在哪个实例。

第二行Order说明了参数排列。这里posedge clk对应reference_event,data对应data_event,顺序信息决定了哪个是时钟、哪个是数据。

第三行给出了实际时间,data在13750ps变化,clk在15000ps上升,间隔1250ps,小于要求的setup时间1500ps,所以报错。看到这种信息,先算差值,再跟库里的时序约束值对比,很快就能判断违例的严重程度。

还有一个实操技巧:很多仿真器会把所有timing violation统一汇总到仿真日志开头或结尾,但中间也会穿插打印。建议在仿真命令里加上参数,把violation单独输出到一个文件,例如VCS里加上-assert report相关选项,这样不会在海量日志里捞错误。

4.3 与STA报告不一致时的思考路径

同事最常问的问题是:“为什么PT报告setup都meet了,后仿还在报$setup violation?”这跟前面异步复位案例类似,通常可以从三个层面找原因。

第一层,激励本身不合理。仿真里给的数据变化沿比真实场景严苛得多,比如testbench里让数据在一个时钟周期内连续翻转,导致动态仿真触发setup违例,但实际场景根本不会这样驱动。STA做的是静态最差路径分析,关注的是路径延迟,不是激励波形。这种情况属于激励问题,修正激励时机即可。

第二层,约束和STA不一致。STA里做了set_multicycle_path、set_false_path、或时钟分组,这些例外不会自动同步到仿真环境。仿真里的时序检查任务只认specify块和SDF里的值,不知道你设了多少条多周期路径。这是最常见的不一致来源。

第三层,异步路径与仿真检查的覆盖范围不同。STA默认只查同步路径,异步信号交互属于仿真检查的领地。如果PT没对异步路径建模,那后仿报$recrem反而说明仿真环境比STA更敏感,这时候应该把问题带回前端设计去讨论,而不是忽略告警。

我个人在实际操作中的一个体会是:不要急着让后仿“闭嘴”,把所有violation都手动waive掉。每一条时序violation都是信息,尤其是那些和STA结论不一致的项,往往是设计约束、复位策略或跨时钟域方案真正需要优化的地方。把这些violation当回事,项目越往后越省心。最后再分享一个小技巧,后仿真log里凡是出现timing check相关报错,我都会顺手在STA工具里对同一实例重新查一遍路径,两边对照着看,很多隐藏问题一眼就能揪出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询