3、Verilog语法精讲(下):过程块(always)、阻塞与非阻塞赋值、条件与循环语句
好,咱们接着聊。上一节我们把连线、寄存器、运算符这些基础过了一遍。这一节,我要带你进入Verilog最核心、也最容易踩坑的地方——过程块。
说白了,过程块就是描述硬件“行为”的地方。你写的组合逻辑、时序逻辑,最终都要靠 always 块来实现。我见过不少新手,代码能仿真通过,一上板就跑飞,十有八九是这里出了问题。
3.1 always块:硬件行为的“舞台”
always 块是Verilog里最常用的过程块。它不像 assign 那样是连续赋值,而是在特定事件触发时才执行内部的语句。
它的基本结构长这样:
always @ (敏感列表) begin
// 你的逻辑代码
end
这里的“敏感列表”就是触发条件。我习惯把它理解成“闹钟”——列表里的信号一变,闹钟就响,块里的代码就跑一遍。
3.1.1 组合逻辑的always块
如果你想让 always 块描述组合逻辑,敏感列表里就要把所有输入信号都列进去。漏掉一个,综合出来的就是锁存器,而不是你想要的组合电路。
举个例子,一个简单的加法器:
always @ (a or b) begin
sum = a + b;
end
嗯,这里要注意。老一点的写法喜欢用 or 连接信号。现在更推荐用 always @ (*),星号代表“所有信号”。我个人觉得这写法更省心,不容易漏。
always @ (*) begin
sum = a + b;
end
always @ (*)。省事,还不会因为漏信号而生成奇怪的锁存器。
3.1.2 时序逻辑的always块
时序逻辑就不同了。它需要时钟边沿作为触发条件。比如一个D触发器:
always @ (posedge clk) begin
q <= d;
end
posedge 是上升沿,negedge 是下降沿。我建议你统一用上升沿,除非有特殊需求。这样代码风格一致,调试时也少一些混乱。
如果还要带异步复位,写法是这样的:
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里 rst_n 是低电平有效的复位信号。注意敏感列表里同时有 posedge clk 和 negedge rst_n。我曾经见过有人把复位信号漏掉,结果仿真时复位功能正常,上板后完全不受控——因为综合工具把它当成同步复位了。
3.2 阻塞赋值与非阻塞赋值:一字之差,天壤之别
这是Verilog里最容易让人迷糊的地方。我当年也在这上面栽过跟头。
简单说:
- 阻塞赋值(=):顺序执行,前面的赋值完成,后面的才开始。
- 非阻塞赋值(<=):并行执行,所有赋值在同一时刻发生。
你想想看,这区别在硬件里意味着什么?
3.2.1 组合逻辑用阻塞赋值
组合逻辑里,信号是实时传递的。用阻塞赋值正好符合这个特性。
always @ (*) begin
c = a & b;
d = c | e;
end
这里 c 先被赋值,然后 d 才用 c 的新值去计算。这符合组合逻辑的传播顺序。
3.2.2 时序逻辑用非阻塞赋值
时序逻辑里,所有寄存器都是在时钟沿同时更新的。非阻塞赋值正好模拟这个行为。
always @ (posedge clk) begin
q1 <= d;
q2 <= q1;
end
这段代码描述的是一个两级移位寄存器。在时钟上升沿,q1 和 q2 同时更新。q2 拿到的是 q1 的旧值,而不是新值。如果你用阻塞赋值,结果就完全不一样了——q2 会拿到 q1 的新值,变成两级直通,而不是移位。
- 组合逻辑:用阻塞赋值(=)
- 时序逻辑:用非阻塞赋值(<=)
- 同一个always块里,不要混用两种赋值方式
这个法则我建议你刻在脑子里。我见过太多因为混用赋值方式导致的bug,仿真时看不出来,上板后逻辑完全乱套。
3.3 条件语句:if-else 与 case
条件语句是描述硬件决策逻辑的基本工具。Verilog里主要有 if-else 和 case 两种。
3.3.1 if-else 语句
if-else 适合描述优先级逻辑。比如一个多路选择器:
always @ (*) begin
if (sel == 2'b00)
out = a;
else if (sel == 2'b01)
out = b;
else if (sel == 2'b10)
out = c;
else
out = d;
end
这里要注意,if-else 是有优先级的——先判断的条件优先级高。综合出来的电路,优先级高的信号离输出近,路径延迟小。
如果你写 if 没有对应的 else,综合工具会认为“条件不满足时保持原值”,从而生成锁存器。这通常不是你想要的。
if 一定要配 else。否则综合出锁存器,你的电路行为就会变得诡异。
3.3.2 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
default 分支很重要。它相当于 if-else 里的最后一个 else,用来处理所有未列出的情况。漏掉 default,同样可能生成锁存器。
我个人习惯,写 case 时永远带上 default,哪怕你觉得所有情况都覆盖了。为什么?因为仿真时信号可能有不定态(X)或高阻态(Z),没有 default 的话,输出就会变成X,导致后续逻辑全乱。
3.4 循环语句:for 与 generate
循环语句在Verilog里用得不多,但在某些场景下非常有用。
3.4.1 for 循环
for 循环主要用于简化重复的赋值或计算。比如初始化一个寄存器数组:
integer i;
always @ (posedge clk) begin
for (i = 0; i < 8; i = i + 1) begin
reg_array[i] <= 8'b0;
end
end
注意,这里的 for 循环在综合时会被展开成并行电路,而不是像软件那样顺序执行。所以循环次数必须是常数,不能是变量。
3.4.2 generate 循环
generate 是另一种循环方式,常用于模块实例化或参数化设计。
genvar i;
generate
for (i = 0; i < 4; i = i + 1) begin : adder_gen
adder u_adder (
.a(a[i*8 +: 8]),
.b(b[i*8 +: 8]),
.sum(sum[i*8 +: 8])
);
end
endgenerate
这段代码生成了4个8位加法器。用 generate 的好处是代码简洁,而且每个实例都有独立的标签(adder_gen[0] 到 adder_gen[3]),方便调试。
generate。它比手写4遍代码更清晰,也更容易维护。
3.5 本章小结
这一节我们聊了:
- always块:组合逻辑用
@ (*),时序逻辑用@ (posedge clk) - 阻塞与非阻塞赋值:组合逻辑用
=,时序逻辑用<=,别混用 - 条件语句:
if-else有优先级,case无优先级,都要注意补全所有分支 - 循环语句:
for和generate用于简化重复逻辑,循环次数必须固定
这些语法点,每一个我都踩过坑。你如果能把这些规则内化成习惯,写出来的代码就会少很多bug。下一节,我们聊聊状态机的设计——那是数字系统的“大脑”。