你对语法的疑问很典型,很多人一开始都把 @always 绑在一起理解。其实 @ 是一个独立的时序控制,可以用在任何过程块(initialalways)里,也可以单独成句。我们把它拆开看就清楚了。


1. @ 到底是什么意思?

在 Verilog 里,@ 是一个事件控制符,它会挂起当前进程,直到指定的事件发生,然后才继续往下执行。

1
@(posedge clk);   // 挂起,直到 clk 从 0 变 1 的那个瞬间,然后继续执行下一条语句

它就像一句“等一会儿”:

  • 执行到这条语句时,仿真时间停止流动(进程挂起);
  • clk 的上升沿到来,进程被唤醒,时间继续;
  • 紧接着执行 @(posedge clk); 后面的语句。

你可以把它放在 initial 块或 always 块里面,当作普通语句使用。


2. always @(posedge clk) 又是什么?

这是两种语法的组合:

1
2
3
always @(posedge clk) begin
// 过程代码
end
  • always 是一个无限循环,它告诉自己:“我里面这段代码要反复执行。”
  • @(posedge clk)敏感列表的写法,表示:每当 clk 的上升沿事件发生时,就执行一次内部代码

效果等价于:

1
2
3
4
always begin
@(posedge clk); // 先等一次上升沿
// 内部代码
end

只不过你把 @ 写在了 always 的括号里,就成了敏感列表,而不需要在里面写一条单独的 @ 语句。所以它们本质上是一个东西:都是事件等待,只是放置位置不同。


3. 在 testbench 里单独写 @(posedge clk); 的好处

你的激励进程是 initial 块(只执行一次),你不能用 always 去写测试序列,因为那样会无限循环。所以我们需要在 initial 块里一步一步控制时间。

如果用 # 延迟,就像你原来写的 #2;,会和时钟没有严格对齐,容易造成竞争。

改用 @(posedge clk); 后,你可以精确地在时钟上升沿之后立即执行操作,从而建立无竞争的同步激励。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
initial begin
rst_n = 0;
repeat(5) @(posedge clk); // 空等 5 个时钟上升沿,每个上升沿唤醒一次,然后立即继续等下一个
rst_n <= 1;

@(posedge clk); // 等下一个上升沿
wr_en <= 1; // 在上升沿发生后的“非阻塞赋值区”更新,因此当前上升沿采样不到新值
wr_data <= 8'hAA; // 下一个上升沿才会看到

@(posedge clk);
wr_data <= 8'hBB;

// ...
end

这里的 @(posedge clk); 就像是一个“检查点”,程序会在那里停止,直到下一个时钟上升沿到来,然后马上执行 wr_en <= 1; 等操作。因为非阻塞赋值 <= 的更新在事件队列的后面,所以当前上升沿(刚刚结束的那个)已经采样完毕,不会用到新的 wr_enwr_data,完美实现了一拍延迟的效果。


4. 总结语义

  • @(event) 单独成句:在 initialalways 块中作为时序控制语句,表示“等待指定事件后继续”。
  • 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
2
3
4
5
repeat (次数表达式) begin
语句1;
语句2;
...
end
  • 次数表达式 在循环开始时计算一次,通常是一个整数常量。
  • repeat 会重复执行“后面那个语句”或“后面那个 begin-end 块”恰好那么多次。

示例 1:重复单条语句

1
repeat (5) @(posedge clk);   // 等待 5 个时钟上升沿

这里 repeat 重复的是 @(posedge clk); 这一条语句,即“等待一个上升沿”这个动作被连续执行了 5 次。

示例 2:重复一个代码块

1
2
3
4
repeat (3) begin
@(posedge clk);
wr_data <= wr_data + 1;
end

这个循环将“等待一个时钟沿,然后 wr_data 加 1”这个组合执行 3 次。

注意repeat 在仿真时是顺序执行的,不是并行展开。它就像 C 语言里的 for 循环,但是次数是固定的。


2. 用 @ + repeat 构建时序激励(几乎不用 #

我们可以把之前的写序列完全用 @repeat 改写,没有任何 #

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
initial begin
// 初始化
rst_n = 0;
wr_en = 0;
rd_en = 0;
wr_data = 8'h00;

// 释放复位:等待 5 个时钟上升沿
repeat (5) @(posedge clk);
rst_n <= 1;

// 开始写入 5 个数据
@(posedge clk); // 先对齐到一个时钟沿后
wr_en <= 1;
wr_data <= 8'hAA;

@(posedge clk);
wr_data <= 8'hBB;

@(posedge clk);
wr_data <= 8'hCC;

@(posedge clk);
wr_data <= 8'hDD;

@(posedge clk);
wr_data <= 8'hEE;

@(posedge clk);
wr_en <= 0;
wr_data <= 8'h00;

// 读操作也可以类似展开
repeat (2) @(posedge clk);
rd_en <= 1;
repeat (5) @(posedge clk);
rd_en <= 0;

// 等待 10 个周期后结束
repeat (10) @(posedge clk);
$finish;
end

这样写的激励,所有信号变化都严格发生在时钟上升沿之后,绝无竞争。你还可以用 repeat 配合循环生成大量数据,比如:

1
2
3
4
repeat (256) begin
@(posedge clk);
wr_data <= $random;
end

3. 是否可以完全不用 #

大多数情况可以,而且应该这么做。但有几个特殊场景 # 仍然有用:

  1. 产生时钟

    1
    always #1 clk = ~clk;   // 这里用 #1 定义时钟半周期,无法用 @ 替代
  2. 产生异步复位或非同步延迟
    例如,你想模拟外部异步复位,不用时钟沿触发,而是用绝对时间:

    1
    2
    3
    rst_n = 0;
    #150;
    rst_n = 1; // 模拟上电后 150ns 释放复位,不依赖时钟
  3. 模拟组合逻辑的传递延迟(如果需要的话)。

对于这些场景,# 依然不可或缺。但在同步激励和时序验证中,所有的数据、使能信号都应该由 @(posedge clk) 驱动,避免任何裸 # 延迟与时钟沿撞在一起。


4. 好习惯总结

  • @(posedge clk) 来控制所有与时钟相关的信号变化,让激励成为真正的“同步激励”。
  • repeat (N) @(posedge clk); 来等待 N 个时钟周期,意图清晰,不需要心算 # 的数值。
  • # 限制在时钟生成、异步延迟等非同步场景,保持 testbench 结构的清晰与稳定。