4. Verilog基础(二):阻塞赋值与非阻塞赋值、if-else与case语句、for循环与generate、任务与函数

各位同学,欢迎来到第四讲。上一章我们聊了Verilog的基本语法和数据类型,算是入了门。今天这节内容,是真正区分“写代码”和“写硬件”的分水岭。说白了,如果你搞不清阻塞赋值和非阻塞赋值的区别,写出来的代码大概率是没法用的。我当年刚入行时,就在这上面栽过跟头。

4.1 阻塞赋值与非阻塞赋值:硬件设计的“时间观”

这是Verilog里最核心、也最容易让人迷糊的概念。我们先看两个符号:= 和 <=。

  • 阻塞赋值(=):执行完当前赋值语句,再执行下一条。它是“串行”的,像C语言。
  • 非阻塞赋值(<=):所有赋值语句“同时”计算右值,然后“同时”更新左值。它是“并行”的,像硬件。

你想想看,硬件里所有触发器都是同时工作的,对吧?所以非阻塞赋值才是描述时序逻辑的正确方式。

核心原则(请牢记):

  • 写组合逻辑(always @(*))时,用阻塞赋值 =
  • 写时序逻辑(always @(posedge clk))时,用非阻塞赋值 <=

我在项目中遇到过一位同事,把时序逻辑里的赋值全写成了阻塞赋值。仿真时看着没问题,一上板子,数据总是错位。查了两天才发现是赋值方式搞错了。嗯,从那以后,我每次写always块都会先看一眼敏感列表。

来看个对比示例:

// 错误示例:时序逻辑用了阻塞赋值
always @(posedge clk) begin
  a = b;
  c = a;  // 这里c拿到的是a更新后的值,不是上一拍的值
end

// 正确示例:时序逻辑用非阻塞赋值
always @(posedge clk) begin
  a <= b;
  c <= a;  // 这里c拿到的是a上一拍的值,符合硬件行为
end

警告:同一个always块里,不要混用阻塞和非阻塞赋值。我曾经见过一个项目,因为混用导致综合结果和仿真结果不一致,最后不得不重写整个模块。

4.2 if-else与case语句:选择结构的两种写法

这两种语句都是用来做条件判断的。但它们的硬件实现方式不同,使用场景也有讲究。

4.2.1 if-else语句

if-else会生成优先级编码器。什么意思呢?就是条件判断是有先后顺序的,先判断的条件优先级高。这在组合逻辑里特别容易产生长路径,影响时序。

// 优先级编码器:a的优先级最高,c的优先级最低
always @(*) begin
  if (a)       y = 4'b0001;
  else if (b)  y = 4'b0010;
  else if (c)  y = 4'b0100;
  else         y = 4'b1000;
end

我个人习惯,在判断条件有优先级关系时用if-else。比如状态机里的状态跳转,或者中断优先级处理。

4.2.2 case语句

case语句生成的是多路选择器(MUX),所有分支是并行的,没有优先级。它的综合结果更均衡,时序也更好控制。

// 多路选择器:所有分支并行
always @(*) begin
  case (sel)
    2'b00: y = a;
    2'b01: y = b;
    2'b10: y = c;
    2'b11: y = d;
    default: y = 4'b0;
  endcase
end

小技巧:case语句一定要写default分支。如果不写,综合工具会生成锁存器(latch),这通常不是我们想要的。我刚开始写代码时,就因为漏了default,导致一个模块在特定输入下输出不定态,查了好久才发现。

什么时候用if-else,什么时候用case?我的经验是:

  • 条件有优先级 → if-else
  • 条件互斥、且分支较多 → case
  • 条件涉及范围判断(如大于、小于) → if-else

4.3 for循环与generate:批量操作的两种思路

for循环和generate都能实现重复操作,但它们的本质完全不同。

4.3.1 for循环

for循环是在仿真或综合时展开的。它本质上是代码的“复制粘贴”,不是真正的循环执行。所以for循环的循环次数必须是常数。

// 用for循环实现8位数据反转
always @(*) begin
  for (i = 0; i < 8; i = i + 1) begin
    y[i] = a[7 - i];
  end
end

这个代码综合出来就是8条赋值语句,没有循环逻辑。你想想看,硬件里哪有“循环”这个概念?

4.3.2 generate语句

generate是更“硬件”的批量生成方式。它可以在模块实例化时使用,也可以用来生成always块或assign语句。

// generate实现8位数据反转
generate
  genvar j;
  for (j = 0; j < 8; j = j + 1) begin : rev
    assign y[j] = a[7 - j];
  end
endgenerate

generate和for循环的区别在于:generate可以生成模块实例,而for循环不行。我在做多通道数据处理时,经常用generate来例化多个相同的处理模块,代码会简洁很多。

注意:for循环和generate都是在编译阶段展开的,不会生成真正的循环硬件。它们的目的是减少重复代码,提高可维护性。

4.4 任务与函数:代码复用的两种方式

任务(task)和函数(function)都能实现代码复用,但它们的用法和限制不同。

4.4.1 函数(function)

函数的特点是:

  • 必须返回一个值
  • 不能包含时序控制(如#10、@(posedge clk))
  • 至少有一个输入参数
  • 通常用于组合逻辑计算
// 函数示例:计算两个数的最大值
function [7:0] max;
  input [7:0] a, b;
  begin
    if (a > b)
      max = a;
    else
      max = b;
  end
endfunction

// 调用
wire [7:0] result = max(data1, data2);

4.4.2 任务(task)

任务的特点是:

  • 可以有多个输出,也可以没有返回值
  • 可以包含时序控制
  • 可以用于描述复杂的时序行为
// 任务示例:产生一个脉冲
task pulse;
  output reg sig;
  input [7:0] width;
  begin
    sig = 1;
    #width sig = 0;
  end
endtask

// 调用(只能在always块里)
always @(posedge clk) begin
  pulse(pulse_out, 8'd10);
end

我的建议:能用函数解决的问题,尽量别用任务。函数更简单,综合结果也更可控。任务因为可以包含时序控制,通常只用在仿真测试中。我在写testbench时特别喜欢用任务,可以封装复杂的激励序列。

4.5 本章知识体系

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考。

Verilog基础(二)知识体系 赋值方式 阻塞赋值 (=) 组合逻辑用 非阻塞赋值 (<=) 时序逻辑用 ⚠ 不要混用 条件语句 if-else 优先级编码器 case 多路选择器 ⚠ 写default 循环与生成 for循环 编译时展开 generate 可例化模块 减少重复代码 任务与函数 function 有返回值 task 可含时序 仿真常用 核心要点总结 1. 时序逻辑用非阻塞赋值 (<=),组合逻辑用阻塞赋值 (=) 2. if-else生成优先级编码器,case生成多路选择器 3. for和generate都是编译时展开,用于减少重复代码 4. 函数用于组合逻辑计算,任务可用于仿真中的时序行为

好了,这一章的内容就到这里。阻塞赋值和非阻塞赋值的区别,是Verilog学习的第一个大坎。跨过去,后面的路就好走了。if-else和case的选择,for和generate的用法,任务和函数的取舍,这些都是在实际项目中每天都会遇到的事情。多写、多练、多仿真,慢慢就会形成自己的风格。

可可精品资料库