Testbench事件激励
你对语法的疑问很典型,很多人一开始都把 @ 和 always 绑在一起理解。其实 @ 是一个独立的时序控制,可以用在任何过程块(initial、always)里,也可以单独成句。我们把它拆开看就清楚了。
1. @ 到底是什么意思?
在 Verilog 里,@ 是一个事件控制符,它会挂起当前进程,直到指定的事件发生,然后才继续往下执行。
1 | @(posedge clk); // 挂起,直到 clk 从 0 变 1 的那个瞬间,然后继续执行下一条语句 |
它就像一句“等一会儿”:
- 执行到这条语句时,仿真时间停止流动(进程挂起);
- 当
clk的上升沿到来,进程被唤醒,时间继续; - 紧接着执行
@(posedge clk);后面的语句。
你可以把它放在 initial 块或 always 块里面,当作普通语句使用。
2. always @(posedge clk) 又是什么?
这是两种语法的组合:
1 | always @(posedge clk) begin |
always是一个无限循环,它告诉自己:“我里面这段代码要反复执行。”@(posedge clk)是敏感列表的写法,表示:每当clk的上升沿事件发生时,就执行一次内部代码。
效果等价于:
1 | always begin |
只不过你把 @ 写在了 always 的括号里,就成了敏感列表,而不需要在里面写一条单独的 @ 语句。所以它们本质上是一个东西:都是事件等待,只是放置位置不同。
3. 在 testbench 里单独写 @(posedge clk); 的好处
你的激励进程是 initial 块(只执行一次),你不能用 always 去写测试序列,因为那样会无限循环。所以我们需要在 initial 块里一步一步控制时间。
如果用 # 延迟,就像你原来写的 #2;,会和时钟没有严格对齐,容易造成竞争。
改用 @(posedge clk); 后,你可以精确地在时钟上升沿之后立即执行操作,从而建立无竞争的同步激励。例如:
1 | initial begin |
这里的 @(posedge clk); 就像是一个“检查点”,程序会在那里停止,直到下一个时钟上升沿到来,然后马上执行 wr_en <= 1; 等操作。因为非阻塞赋值 <= 的更新在事件队列的后面,所以当前上升沿(刚刚结束的那个)已经采样完毕,不会用到新的 wr_en 和 wr_data,完美实现了一拍延迟的效果。
4. 总结语义
@(event)单独成句:在initial或always块中作为时序控制语句,表示“等待指定事件后继续”。always @(event):将事件等待与无限循环结合在一起,形成边缘触发的过程块。- 你还可以有更复杂的用法,比如
@(posedge clk or negedge rst_n),或者@(negedge clk),这些都是同一个语法结构的延伸。
现在再回头看,你会觉得 @ 其实很自然,它就是一个“等待某个条件发生”的指令,放在哪儿都行,只是我们习惯了把它嵌在 always 的括号里而已。
所以说我完全可以养成只用@和repeat写tb的更好习惯,完全不用#?对了,repeat又是什么,语法是什么?它是在重复哪一部分的代码
你的理解非常到位!是的,在 testbench 中大量使用 @(posedge clk) 和 repeat 来替代硬编码的 # 延迟,是业界推荐的稳健写法。它能彻底避免时间对齐引发的竞争,让激励的时序关系一目了然。
1. repeat 是什么?语法和重复对象
repeat 是一个过程性循环,它会将紧跟在它后面的单条语句或 begin-end 块重复执行指定的次数。
语法:
1 | repeat (次数表达式) 单条语句; |
或者
1 | repeat (次数表达式) begin |
次数表达式在循环开始时计算一次,通常是一个整数常量。repeat会重复执行“后面那个语句”或“后面那个 begin-end 块”恰好那么多次。
示例 1:重复单条语句
1 | repeat (5) @(posedge clk); // 等待 5 个时钟上升沿 |
这里 repeat 重复的是 @(posedge clk); 这一条语句,即“等待一个上升沿”这个动作被连续执行了 5 次。
示例 2:重复一个代码块
1 | repeat (3) begin |
这个循环将“等待一个时钟沿,然后 wr_data 加 1”这个组合执行 3 次。
注意:repeat 在仿真时是顺序执行的,不是并行展开。它就像 C 语言里的 for 循环,但是次数是固定的。
2. 用 @ + repeat 构建时序激励(几乎不用 #)
我们可以把之前的写序列完全用 @ 和 repeat 改写,没有任何 #:
1 | initial begin |
这样写的激励,所有信号变化都严格发生在时钟上升沿之后,绝无竞争。你还可以用 repeat 配合循环生成大量数据,比如:
1 | repeat (256) begin |
3. 是否可以完全不用 #?
大多数情况可以,而且应该这么做。但有几个特殊场景 # 仍然有用:
产生时钟:
1
always #1 clk = ~clk; // 这里用 #1 定义时钟半周期,无法用 @ 替代
产生异步复位或非同步延迟:
例如,你想模拟外部异步复位,不用时钟沿触发,而是用绝对时间:1
2
3rst_n = 0;
#150;
rst_n = 1; // 模拟上电后 150ns 释放复位,不依赖时钟模拟组合逻辑的传递延迟(如果需要的话)。
对于这些场景,# 依然不可或缺。但在同步激励和时序验证中,所有的数据、使能信号都应该由 @(posedge clk) 驱动,避免任何裸 # 延迟与时钟沿撞在一起。
4. 好习惯总结
- 用
@(posedge clk)来控制所有与时钟相关的信号变化,让激励成为真正的“同步激励”。 - 用
repeat (N) @(posedge clk);来等待 N 个时钟周期,意图清晰,不需要心算#的数值。 - 把
#限制在时钟生成、异步延迟等非同步场景,保持 testbench 结构的清晰与稳定。
