3. Verilog语法核心(二):阻塞赋值与非阻塞赋值(深度理解),always块详解(组合逻辑与时序逻辑),条件语句(if-else, case, casez),循环语句(for, generate)
好,咱们接着聊。上一章我们把Verilog的基础数据类型和运算符过了一遍。这一章,才是真正决定你写的代码是“硬件”还是“软件”的关键。说白了,就是阻塞赋值、非阻塞赋值、always块这些概念。很多新手在这里栽跟头,我当年也不例外。
3.1 阻塞赋值与非阻塞赋值:一个决定生死的区别
先问大家一个问题:在Verilog里,=和<=有什么区别?
如果你回答“一个是阻塞赋值,一个是非阻塞赋值”,那只是背了概念。真正的区别在于:它们决定了硬件电路的行为是“串行”还是“并行”。
3.1.1 阻塞赋值(=)
阻塞赋值,顾名思义,它会“阻塞”后面的语句执行。在一个begin...end块里,如果用了阻塞赋值,那么语句会按顺序执行。前一句没算完,后一句就等着。
这听起来很像C语言,对吧?但你要小心,在硬件里,这种“顺序”往往对应的是组合逻辑的传播延迟。我见过有人用阻塞赋值写了一个简单的加法器,结果仿真波形一塌糊涂,因为赋值顺序导致了错误的中间结果。
// 阻塞赋值示例:组合逻辑
always @(*) begin
a = b & c; // 先算a
d = a | e; // 再算d,此时a已经更新
end
这段代码综合出来就是一个组合逻辑网络。a和d的更新是“顺序”的,但硬件上其实是同时计算的,只是仿真器模拟了这种顺序。嗯,这里要注意:阻塞赋值用于组合逻辑,这是行业惯例。
3.1.2 非阻塞赋值(<=)
非阻塞赋值就完全不同了。它不会阻塞后面的语句。在同一个always块里,所有非阻塞赋值都是“同时”发生的。它们会在块结束时统一更新。
我个人习惯,写时序逻辑(比如触发器、寄存器)时,一律用非阻塞赋值。为什么?因为这样才能模拟出硬件里寄存器同时采样的行为。
// 非阻塞赋值示例:时序逻辑
always @(posedge clk) begin
q1 <= d; // 在时钟上升沿,q1采样d
q2 <= q1; // 同时,q2采样q1的旧值
end
你看,这里q1和q2的赋值是并行的。如果换成阻塞赋值,q2就会拿到q1的新值,那就变成移位寄存器了?不,那会变成两个寄存器串联,但行为完全不一样。我曾经在项目里调试一个SPI接口,就是因为把非阻塞写成了阻塞,导致数据总是错位一位。查了整整两天,最后发现是赋值符号的问题。你说冤不冤?
核心原则:
- 组合逻辑:用阻塞赋值(=)
- 时序逻辑:用非阻塞赋值(<=)
- 同一个always块里,不要混用两种赋值方式
避坑指南:我曾经见过有人在一个always块里,既用阻塞赋值又用非阻塞赋值。仿真能过,但综合出来的电路行为完全不可预测。千万别这么干。
3.2 always块详解:组合逻辑与时序逻辑
always块是Verilog里最核心的语法结构。它描述的是“什么时候做什么事”。但很多人搞不清楚,什么时候该用组合逻辑,什么时候该用时序逻辑。
3.2.1 组合逻辑的always块
组合逻辑的always块,敏感列表里通常用@(*),表示所有输入信号变化都会触发。输出只取决于当前输入,没有记忆功能。
// 组合逻辑:多路选择器
always @(*) begin
if (sel)
out = a;
else
out = b;
end
这个代码综合出来就是一个MUX。没有时钟,没有寄存器。你想想看,如果这里用了非阻塞赋值,会怎样?仿真可能没问题,但综合工具会给你生成一堆锁存器(latch)。嗯,锁存器在ASIC里是禁忌,在FPGA里也尽量少用。
3.2.2 时序逻辑的always块
时序逻辑的always块,敏感列表里通常是时钟边沿(posedge clk)和异步复位(posedge rst)。输出不仅取决于当前输入,还取决于之前的状态。
// 时序逻辑:带异步复位的D触发器
always @(posedge clk or posedge rst) begin
if (rst)
q <= 1'b0;
else
q <= d;
end
这里我强调一下:时序逻辑的赋值一定要用非阻塞。这是铁律。我刚开始做FPGA时,有一次写了一个计数器,用了阻塞赋值,结果仿真波形里计数器跳变完全不对。后来发现,阻塞赋值导致同一个时钟周期内多次更新,完全违背了寄存器的行为。
个人经验:写时序逻辑时,我习惯把复位逻辑放在最前面。这样代码结构清晰,也方便后期维护。另外,异步复位虽然方便,但要注意复位信号的毛刺问题。我一般会加一个同步器。
3.3 条件语句:if-else, case, casez
条件语句是描述硬件行为的基础。但不同的条件语句,综合出来的电路结构完全不同。
3.3.1 if-else语句
if-else语句综合出来的是优先级编码器。也就是说,条件判断是有优先级的。第一个条件优先级最高,最后一个最低。
// if-else:优先级编码器
always @(*) begin
if (cond1)
out = 4'b0001;
else if (cond2)
out = 4'b0010;
else
out = 4'b1000;
end
这个代码综合出来,cond1的优先级最高。如果cond1和cond2同时为真,out只会等于4'b0001。这在某些场景下是需要的,但如果你想要并行判断,那就得用case。
3.3.2 case语句
case语句综合出来的是并行多路选择器。所有条件都是平等的,没有优先级。
// case:并行多路选择器
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 1'b0;
endcase
end
这里要注意:case语句必须写default分支。如果不写,综合工具会推断出锁存器。我见过有人写case语句忘了default,结果综合出来的电路多了一堆latch,功耗和面积都超标。
3.3.3 casez语句
casez是case的变种,它允许使用“?”作为通配符,表示“不关心”该位的值。这在描述地址译码器时特别有用。
// casez:地址译码器
always @(*) begin
casez (addr)
3'b1??: out = 4'b0001; // 最高位为1,其他位任意
3'b01?: out = 4'b0010; // 高两位为01,最低位任意
3'b001: out = 4'b0100; // 精确匹配
default: out = 4'b1000;
endcase
end
我个人习惯,在写casez时,一定要把精确匹配的放在前面,通配符的放在后面。否则,通配符可能会提前匹配,导致后面的分支永远无法执行。嗯,这有点像C语言里的switch-case,但硬件里更严格。
总结:
- if-else:优先级编码器,适合有优先级的判断
- case:并行多路选择器,适合无优先级的判断
- casez:带通配符的并行选择器,适合地址译码
3.4 循环语句:for和generate
循环语句在Verilog里主要用于批量生成硬件结构。但要注意,这里的循环不是软件里的“循环执行”,而是“循环展开”。
3.4.1 for循环
for循环在always块里使用,用于重复执行相同的逻辑。综合工具会把循环展开成多个硬件单元。
// for循环:批量赋值
reg [7:0] data_in;
reg [7:0] data_out;
integer i;
always @(*) begin
for (i = 0; i < 8; i = i + 1) begin
data_out[i] = data_in[7 - i]; // 位反转
end
end
这个代码综合出来就是8个并行的连线,把输入位反转后接到输出。循环在综合时就已经展开了,不会生成任何循环逻辑。
注意:for循环的循环次数必须是常数。如果循环次数是变量,综合工具会报错。我曾经试过用变量控制循环次数,结果综合工具直接罢工了。
3.4.2 generate循环
generate循环是更高级的批量生成方式。它可以在模块级别生成多个实例或连线。
// generate循环:生成多个加法器
module adder_array #(parameter N = 8) (
input [N-1:0] a, b,
output [N-1:0] sum
);
genvar i;
generate
for (i = 0; i < N; i = i + 1) begin : adder_gen
assign sum[i] = a[i] ^ b[i]; // 半加器
end
endgenerate
endmodule
generate循环的好处是,它可以生成带层次结构的硬件。每个循环迭代都会生成一个独立的硬件块,名字就是adder_gen[0]、adder_gen[1]这样。这在调试时特别有用,你可以直接看每个实例的波形。
我个人习惯,在需要生成大量重复结构时,优先用generate循环。比如生成一个N位的加法器、N位的移位寄存器等。for循环虽然也能做,但generate的代码可读性更好,也更容易维护。
小技巧:generate循环里可以嵌套if-else和case语句,实现更复杂的生成逻辑。比如根据参数生成不同位宽的模块。
3.5 本章知识体系
为了让大家更直观地理解本章的知识结构,我画了一张图。这张图展示了阻塞赋值与非阻塞赋值、always块、条件语句、循环语句之间的逻辑关系。
这张图把本章的核心内容串起来了。你想想看,赋值语句决定了always块的行为,条件语句和循环语句则是在always块里描述具体的硬件逻辑。理解了这些,你就能写出正确的、可综合的Verilog代码。