4.2 延迟模型与毛刺 本节摘要:Verilog 用 给事件排班,但仿真延迟与真实物理延迟是两套刻度;惯性延迟会吞掉窄脉冲,让门级仿真并非物理世界的完整投影。毛刺是组合逻辑的天然分泌物,应对之道不是消灭它,而是设计上不让任何人依赖它。 触发资格与调度次序清楚了,这一节看时间刻度本身:延迟怎么写、仿真器怎么解释、以及组合逻辑为什么天然产毛刺。这一节的内容在 RTL 设计里用得少、在仿真与调试里用得多——它是看懂门级波形的前置课。 延迟的三种挂法 延迟控制有三处挂点,语义各不相同: 语句前延迟是「先等再算」,assign 延迟是「先算再晚点送达」——一个推迟动作,一个推迟结果,方向相反。测试台里大量使用前者制造激励节拍;
本节摘要:Verilog 用
#延迟给事件排班,但仿真延迟与真实物理延迟是两套刻度;惯性延迟会吞掉窄脉冲,让门级仿真并非物理世界的完整投影。毛刺是组合逻辑的天然分泌物,应对之道不是消灭它,而是设计上不让任何人依赖它。
触发资格与调度次序清楚了,这一节看时间刻度本身:延迟怎么写、仿真器怎么解释、以及组合逻辑为什么天然产毛刺。这一节的内容在 RTL 设计里用得少、在仿真与调试里用得多——它是看懂门级波形的前置课。
延迟控制有三处挂点,语义各不相同:
// 挂在语句前:块内该语句暂停指定时间再执行(过程块内) initial begin rst_n = 1'b0; #23 rst_n = 1'b1; // 23 个时间单位后释放复位 end #10 x = a & b; // 等待 10 再求值赋值 // 挂在 assign 上:右侧变化后,延迟一段时间再更新左侧 assign #2 y = a & b; // a 或 b 变化,y 在 2 个单位后更新 // 挂在门原语上:门延迟,门级仿真的标配 and #3 g1(t, a, b);
语句前延迟是「先等再算」,assign 延迟是「先算再晚点送达」——一个推迟动作,一个推迟结果,方向相反。测试台里大量使用前者制造激励节拍;RTL 里写成综合是无效的(综合器忽略所有延迟),它的合法舞台是行为建模与门级网表反标。
延迟还可以细分为上升、下降、关断三段:and #(2,3) g1(t,a,b) 表示上升沿路径 2 个单位、下降沿路径 3 个单位。这套精细控制在库单元建模里常见,RTL 层面知道即可。
assign 与门的默认延迟是惯性延迟:脉宽短于延迟量的脉冲会被吞掉。物理直觉是——真实的门有响应时间,输入变化太快、来不及完成一次完整响应时,输出根本追不上。传输延迟则像理想传输线:来什么传什么,多窄都照传。语言里默认前者,要后者需要特殊建模手法,日常遇到的几乎都是惯性延迟。
module pulse_eater(input wire a, output wire y); assign #5 y = a; // 惯性延迟:宽度不足 5 的脉冲到不了 y endmodule
给 y 送一个宽度为 3 的脉冲,波形上 y 纹丝不动——脉冲在半路被吞了。这在调试时会制造假象:源端明明发了请求,目的端毫无反应,各波形窗口来回比对才发现请求窄于路径延迟。记住这条特性,看到「窄脉冲消失」先查惯性延迟,再看握手逻辑。
毛刺(glitch)指组合逻辑输出在稳定前的短暂错误翻转。成因是「汇聚路径不等长」:同一个信号经过不同延迟的多条路径汇合,先到的变化与后到的变化短暂共存,输出闪一下。仿真里这些翻转表现为同一时刻内多拍 delta 的来回震荡;真实电路里表现为纳秒级的窄脉冲。
关键认知:毛刺不是 bug,是组合逻辑的物理属性,消灭它既不必要也不可能。真正的纪律是下游设计「不依赖无毛刺假设」——组合输出直接被当作另一个模块的时钟、直接被当作异步复位,才是事故:
// 危险:组合信号当时钟 assign tick = (cnt == 8'd99); always @(posedge tick) pulse_cnt <= pulse_cnt + 1'b1; // tick 由比较器产生,比较器输出翻转瞬间可能毛刺, // 毛刺被 posedge 捕获就是假时钟沿——计数器多发脉冲 // 正确:统一时钟 + 使能 always @(posedge clk) begin tick_d <= tick; if (tick & ~tick_d) // 上升沿检测,与 clk 同步 pulse_cnt <= pulse_cnt + 1'b1; end
两段代码功能描述相同,电路命运相反:前者把组合毛刺喂进时钟端,后者把组合条件寄存一拍、用同步使能表达同样的逻辑。规矩由此而来:组合信号永远只进数据端,不进时钟端、不进异步复位端——这是硬件设计里少数没有例外可讲的铁律之一。
案例展开:寄存器输出为什么是美德。 背景:某模块的「完成」信号由组合译码产生,直连下游的状态机,联调时下游偶发提前一拍动作。操作:在完成信号后插入一级寄存器(输出打一拍),下游以寄存后的信号为准;重跑联调,异常消失。结果:模块对外接口全部改为寄存器输出,成为团队接口规范。解读:寄存器输出把「组合求值 + 传输延迟」的不确定窗口锁进一拍,下游看到的是沿上稳定的值;这不仅消除毛刺传播,还把两级模块间的时序路径切成两段独立路径,综合工具各自优化、互不牵连——工程上叫「时序弧切断」,是模块化设计的核心手法。变式:输出打拍会引入一拍延迟,对延迟敏感的路径(如存储器写使能)要核算协议时序余量再决定打拍位置,不能一刀切。
行为级 RTL 仿真用零延迟或统一延迟建模,组合逻辑在同一个 delta 内完成求值,毛刺被「抽象」掉了。门级仿真带真实单元延迟,毛刺才会显形。这解释了仿真策略的分工:RTL 仿真验功能,门级仿真验时序相关的物理现象,两者验证的目标不同、谁也替代不了谁。第九章调试技巧里会给出「什么时候值得花时间跑门级仿真」的判断标准。
对 RTL 设计者的实操结论:既然 RTL 仿真看不到毛刺,就更要靠设计纪律预防它——不把组合信号当时钟与复位、模块出口寄存器化、跨模块握手全部同步化。这些纪律的本质,是让设计正确性不依赖「组合输出干净」这种仿真里无法验证的假设。
可综合代码里不写——写了也被忽略,徒增误导。有两处例外:测试台里的激励延迟(那是仿真的节拍器)与行为级参考模型的粗略延迟(验证系统级时序假设时用)。区分「被综合的代码」与「只做仿真的代码」,延迟的使用边界就清楚了。
因为 RTL 模型根本不含物理延迟信息。真实延迟由门单元速度与布线长度决定,两者都要等综合布局后才存在。流程上的衔接是:布局布线完成后工具导出延迟文件,反标进门级仿真——这就是门级仿真排在流程后段的原因:它依赖物理实现的产物。
会掩盖时间类 bug(毛刺、竞争、建立保持),但不会掩盖功能类 bug(逻辑错误、协议错误)。这正是 RTL 仿真的定位:先保证功能正确,再由带延迟的验证层保证时间正确。两层验证的目标清单不同,跳过任何一层都是在另一层的盲区里赌博。
延迟与毛刺的规律清楚了,下一节落地到每个设计的节拍源:时钟与复位怎么建模才可靠。