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的选择,for和generate的用法,任务和函数的取舍,这些都是在实际项目中每天都会遇到的事情。多写、多练、多仿真,慢慢就会形成自己的风格。
可可精品资料库