4. Verilog基础(二):过程块(always)、阻塞与非阻塞赋值、时序逻辑与组合逻辑

好,咱们接着往下聊。上一章我们把Verilog的基本结构、模块和端口捋了一遍。这一章,我要跟你聊聊Verilog里最核心、也最容易踩坑的几个概念——过程块(always)、阻塞赋值与非阻塞赋值,以及它们背后的时序逻辑与组合逻辑

说实话,我当年刚学Verilog的时候,被这几个概念绕得晕头转向。尤其是阻塞和非阻塞赋值,写出来的代码仿真结果跟实际电路对不上,查了半天才发现是赋值方式用错了。嗯,今天咱们就把这些坑一个个填平。

核心要点:过程块是Verilog描述硬件行为的基础,而赋值方式的选择直接决定了你描述的是组合逻辑还是时序逻辑。理解这两者的区别,是写出正确RTL代码的关键。

4.1 过程块(always)——硬件行为的“剧本”

always块,说白了就是描述硬件行为的“剧本”。它告诉仿真器和综合工具:什么时候执行,执行什么操作

它的基本语法长这样:

always @ (敏感列表) begin
    // 要执行的语句
end

这里的“敏感列表”就是触发条件。我习惯把它理解成“什么时候这个块里的代码会被激活”。

4.1.1 敏感列表的两种写法

  • 电平敏感(组合逻辑):用 @(*)@(a or b or c)。只要输入信号变化,就执行。
  • 边沿敏感(时序逻辑):用 @(posedge clk)@(negedge clk)。只在时钟上升沿或下降沿触发。

我个人建议,写组合逻辑时统一用 @(*),省得漏写信号。我在项目中就见过同事因为漏了敏感列表里的一个信号,仿真结果跟综合后对不上,查了两天才发现是这里的问题。

小技巧:如果你不确定敏感列表该写什么,直接用 @(*) 准没错。综合工具会自动推导出所有输入信号。

4.2 阻塞赋值与非阻塞赋值——一字之差,天壤之别

这是Verilog里最容易让人迷糊的地方。阻塞赋值用 =,非阻塞赋值用 <=。别看就差一个等号,背后的硬件行为完全不同。

4.2.1 阻塞赋值(=)

阻塞赋值的意思是:这条语句执行完,下一条才能执行。它是“串行”的,像C语言那样。

举个例子:

always @(*) begin
    a = b;
    c = a;
end

这段代码执行完后,a和c的值都等于b。因为先执行 a = b,然后 c = a 用的是更新后的a。

阻塞赋值通常用来描述组合逻辑。为什么?因为组合逻辑的输出只取决于当前输入,没有记忆性,赋值顺序就是电路连接顺序。

4.2.2 非阻塞赋值(<=)

非阻塞赋值的意思是:所有赋值同时进行。它是“并行”的。

看这个例子:

always @(posedge clk) begin
    a <= b;
    c <= a;
end

这段代码执行完后,a等于b的旧值,c等于a的旧值。因为所有 <= 右边的表达式都是在进入always块时“快照”下来的,然后统一赋值。

非阻塞赋值用来描述时序逻辑。为什么?因为寄存器在时钟沿同时采样输入,然后同时更新输出。这正好对应了硬件中D触发器的行为。

警告:千万不要在同一个always块里混用阻塞和非阻塞赋值!我曾经在一个项目里看到有人这么写,结果综合出来的电路跟仿真完全不一样,流片回来直接废了。记住:组合逻辑用 =,时序逻辑用 <=,泾渭分明。

4.3 时序逻辑与组合逻辑——硬件的“记忆”与“计算”

这两个概念是数字电路的基础。我简单给你捋一下:

  • 组合逻辑:输出只取决于当前输入。没有记忆,没有时钟。比如加法器、多路选择器。
  • 时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。有记忆,有时钟。比如计数器、状态机。

在Verilog里,我们通过always块的写法来区分它们:

逻辑类型 敏感列表 赋值方式 典型应用
组合逻辑 @(*) 或电平敏感 阻塞赋值 = 译码器、数据通路
时序逻辑 @(posedge clk) 非阻塞赋值 <= 寄存器、计数器、状态机

4.3.1 组合逻辑示例:一个简单的加法器

module adder (
    input  [3:0] a, b,
    output [3:0] sum
);
    always @(*) begin
        sum = a + b;  // 阻塞赋值,组合逻辑
    end
endmodule

这个加法器没有时钟,输入一变,输出立刻变。你想想看,这就是组合逻辑的本质。

4.3.2 时序逻辑示例:一个D触发器

module dff (
    input       clk,
    input       d,
    output reg  q
);
    always @(posedge clk) begin
        q <= d;  // 非阻塞赋值,时序逻辑
    end
endmodule

这个D触发器只在时钟上升沿采样d的值,然后更新q。这就是时序逻辑——有记忆,有时钟。

记住这个黄金法则:

  • 写组合逻辑 → always @(*) + =
  • 写时序逻辑 → always @(posedge clk) + <=

这个法则我用了十几年,从来没出过问题。

4.4 知识体系图

下面我用一张SVG图来展示本章的核心知识结构,帮你理清思路:

Verilog基础(二):过程块与赋值 always 过程块 敏感列表 电平敏感 @(*) 边沿敏感 @(posedge) 赋值方式 阻塞赋值 = 非阻塞赋值 <= 逻辑类型 组合逻辑 时序逻辑 黄金法则:组合逻辑用 @(*) + = ,时序逻辑用 @(posedge clk) + <= 两者不可混用,否则综合结果与仿真不一致

4.5 避坑指南——我踩过的那些坑

最后,分享几个我亲身经历过的教训:

  • 坑1:敏感列表不完整——我曾经写组合逻辑时只写了 @(a or b),漏了c。结果c变化时always块不执行,仿真结果完全错误。后来我统一用 @(*),再也没出过这个问题。
  • 坑2:时序逻辑里用了阻塞赋值——有一次我写计数器,在 @(posedge clk) 里用了 =,结果仿真波形看起来对,但综合出来的电路多了一级不必要的组合逻辑。嗯,从那以后我写时序逻辑只用 <=
  • 坑3:同一个always块里混用两种赋值——这个我前面提过,后果很严重。记住:要么全用 =,要么全用 <=,别混着来。

我的个人习惯:写代码前先想清楚这个模块是组合逻辑还是时序逻辑。想清楚了再动笔,赋值方式自然就选对了。别急着写代码,先画个草图,理清思路。

好了,这一章的内容就到这里。过程块、阻塞与非阻塞赋值、时序与组合逻辑,这三个概念是Verilog的基石。你只要把今天讲的黄金法则记住,写RTL代码就不会出大问题。


公众号:蓝海资料掘金营,微信deep3321