4. Verilog基础(二):过程块(always)、阻塞与非阻塞赋值、if-else与case语句

好,咱们接着聊Verilog。上一章我们把模块、端口、连续赋值这些基础概念捋了一遍。这一章,我们要进入一个更核心、也更容易踩坑的领域——过程块

说白了,连续赋值(assign)是描述组合逻辑的,而过程块(always)才是描述时序逻辑和复杂组合逻辑的主力。我刚开始学的时候,总觉得always块就是个“黑盒子”,往里塞代码就行。结果呢?仿真波形一出来,全是毛刺和不定态。嗯,后来才明白,这里面的门道深着呢。

4.1 过程块(always)—— 数字逻辑的“心脏”

always块,你可以把它想象成一个“永远在跑”的循环。只要敏感列表里的信号发生变化,它就会执行一次块内的代码。

它的基本语法长这样:

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

这个“敏感列表”是关键。它告诉always块,你该“监听”哪些信号。

4.1.1 敏感列表的两种写法

  • 电平敏感(组合逻辑):用 @(*)@(a or b or c)。只要列表里的信号电平变了,就触发。我建议你写组合逻辑时,永远用 @(*)。为什么?省事,而且不会漏信号。我曾经在项目里手写了一个很长的敏感列表,结果漏了一个信号,仿真和综合结果对不上,查了整整一天。从那以后,我再也不敢手动列组合逻辑的敏感列表了。
  • 边沿敏感(时序逻辑):用 @(posedge clk)@(negedge clk)。只在时钟的上升沿或下降沿触发。这是描述寄存器、计数器、状态机的基础。

核心原则: 一个always块里,要么全是电平敏感,要么全是边沿敏感。千万别混着写,否则综合工具会给你报一堆警告,甚至综合出你意想不到的锁存器。

4.1.2 组合逻辑 vs 时序逻辑

我们用一个简单的例子来区分:

// 组合逻辑:用 @(*)
always @(*) begin
    y = a & b;
end

// 时序逻辑:用 @(posedge clk)
always @(posedge clk) begin
    q <= d;
end

你看,组合逻辑的输出y,只跟输入a、b的当前值有关。而时序逻辑的输出q,是在时钟上升沿才把d的值“抓”进来。这就是数字电路里“记忆”和“延迟”的体现。

4.2 阻塞赋值与非阻塞赋值—— 一个让无数新手抓狂的话题

说实话,我刚接触这个知识点时,也觉得特别绕。为什么赋值还要分两种?直接等号不就行了?

答案在于:硬件是并行的,而软件是串行的。Verilog要模拟硬件的并行行为,就必须有这两种赋值方式。

4.2.1 阻塞赋值(=)

阻塞赋值,顾名思义,它会“阻塞”后面的语句。执行完当前赋值,才会执行下一条。它描述的是组合逻辑的行为。

always @(*) begin
    a = b;
    c = a;  // 这里c得到的是b的新值
end

在这个例子里,a先被赋值为b,然后c被赋值为a。因为a已经更新了,所以c得到的是b的值。这就像软件里的顺序执行。

4.2.2 非阻塞赋值(<=)

非阻塞赋值,它不会阻塞。所有非阻塞赋值都是在同一个时间点被“计划”好,然后在时间步的末尾统一更新。它描述的是时序逻辑的行为。

always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里c得到的是a的旧值
end

在这个例子里,a和c的赋值是“同时”被计划的。a计划更新为b,c计划更新为a的旧值。所以,c得到的是a在时钟上升沿之前的值,而不是b。

我的个人习惯: 写组合逻辑用阻塞赋值(=),写时序逻辑用非阻塞赋值(<=)。这个规则,我建议你当成“铁律”来遵守。我在项目中见过太多因为混用这两种赋值方式导致的仿真与综合不一致的bug了。

4.2.3 为什么不能混用?

我们来看一个反面教材:

// 错误示例:混用阻塞和非阻塞
always @(posedge clk) begin
    a = b;      // 阻塞
    c <= a;     // 非阻塞
end

这段代码,综合工具可能会报错,或者综合出你意想不到的电路。因为阻塞赋值会立即更新a,而非阻塞赋值又是在时间步末尾更新c。这个时间差,会导致仿真结果和实际硬件行为不一致。

警告: 永远不要在同一个always块里混用阻塞和非阻塞赋值。这是Verilog设计中的大忌。

4.3 if-else与case语句—— 条件分支的艺术

有了always块,我们还需要在块内做判断。if-else和case就是干这个的。

4.3.1 if-else语句

if-else的语法和C语言很像:

always @(*) begin
    if (sel) begin
        y = a;
    end else begin
        y = b;
    end
end

这里有个坑:if-else必须写全。如果你只写了if,没写else,综合工具会认为“当条件不满足时,输出保持不变”。对于组合逻辑来说,这就意味着会综合出锁存器(latch)。锁存器在大多数设计中是不受欢迎的,因为它对毛刺敏感,而且时序分析困难。

避坑指南: 写组合逻辑的if-else时,一定要有else分支。如果确实不需要else,那就给输出赋一个默认值。我曾经在一个状态机里漏了一个else分支,结果综合出了一堆锁存器,导致芯片功耗超标。教训深刻啊。

4.3.2 case语句

case语句适合多分支选择。它的语法是:

always @(*) begin
    case (sel)
        2'b00: y = a;
        2'b01: y = b;
        2'b10: y = c;
        2'b11: y = d;
        default: y = 1'b0;  // 建议加上default
    endcase
end

case语句也有一个类似的坑:分支必须完备。如果sel的取值没有覆盖所有可能,综合工具也会推断出锁存器。所以,永远加上default分支

另外,case语句还有两个变种:casexcasez。它们允许在比较时使用“无关位”(x或z)。我个人建议,除非你非常清楚自己在做什么,否则尽量用普通的case。casex和casez容易导致仿真与综合不一致。

4.4 知识体系图

为了让你更直观地理解本章的知识结构,我画了一张图:

Verilog基础(二):过程块与赋值 过程块 (always) 电平敏感 @(*) 边沿敏感 @(posedge) 赋值方式 阻塞 (=) 非阻塞 (<=) 条件语句 if-else case 核心原则:组合逻辑用阻塞赋值 + 电平敏感;时序逻辑用非阻塞赋值 + 边沿敏感 常见陷阱:漏写else/default → 综合出锁存器;混用阻塞/非阻塞 → 仿真与综合不一致 总结:always块是骨架,赋值方式是血肉,条件语句是灵魂 三者结合,才能写出可综合、可仿真的高质量Verilog代码

4.5 实战建议

理论说完了,咱们来点实际的。我给你几个写代码时的检查清单:

  1. 检查敏感列表:组合逻辑用 @(*),时序逻辑用 @(posedge clk)。别偷懒。
  2. 检查赋值方式:组合逻辑用 =,时序逻辑用 <=。别混用。
  3. 检查分支完备性:if-else要有else,case要有default。别让综合工具“猜”你的意图。
  4. 检查代码风格:一个always块只做一件事。要么全是组合逻辑,要么全是时序逻辑。别把两者混在一个块里。

嗯,这一章的内容就到这里。这些规则,你刚开始可能会觉得繁琐,但相信我,养成好习惯后,你会感谢自己的。毕竟,谁都不想因为一个锁存器或者一个仿真bug,而加班到深夜吧?