3. Verilog基础(二):过程块、阻塞与非阻塞赋值、条件语句与有限状态机
各位同学,欢迎来到第三讲。上一讲我们聊了Verilog的数据类型和基本运算符,算是把“名词”和“动词”认全了。今天我们要进入真正的“造句”阶段——过程块、赋值方式、条件判断,以及数字设计里最核心的套路:有限状态机。
这部分内容,说白了就是RTL设计的骨架。你写出来的代码能不能综合成硬件,综合出来的电路是不是你想要的,全看这几样东西用得对不对。我见过太多新手,甚至工作一两年的工程师,在阻塞与非阻塞赋值上栽跟头。嗯,咱们今天就把这些坑一个个填平。
3.1 过程块:always
Verilog里描述硬件行为,主要靠always块。它不像assign那样是连续赋值,而是“当某个条件触发时,执行块内的语句”。
我个人习惯把always块分成三类,这样写代码时思路特别清晰:
| 类型 | 敏感列表写法 | 典型用途 | 综合出的硬件 |
|---|---|---|---|
| 组合逻辑 | always @(*) 或 always @(a or b) |
译码器、多路选择器、加法器 | 纯组合门电路 |
| 电平敏感锁存器 | always @(clk or rst_n) 但缺少else分支 |
(应避免,除非刻意设计) | 透明锁存器 |
| 边沿触发时序逻辑 | always @(posedge clk or negedge rst_n) |
寄存器、计数器、状态机 | D触发器 + 组合逻辑 |
always @(a or b) 但漏掉了信号c。仿真时c变化了,块不执行,结果死活不对。后来我统一改用 always @(*),再也没犯过这种低级错误。你想想看,综合工具会自动推导敏感列表,何必自己手动列呢?
3.2 阻塞赋值与非阻塞赋值
这是Verilog里最容易让人懵圈的地方,也是面试必考题。我直接说结论:
- 阻塞赋值
=:顺序执行,上一条没完,下一条等着。用于组合逻辑。 - 非阻塞赋值
<=:并行执行,所有右值在同一时刻采样,然后同时更新左值。用于时序逻辑。
为什么会有这种区别?因为硬件本身就是并行的。你写 a <= b; c <= a; 在时序逻辑里,c拿到的是a的旧值,不是b。这恰好对应了D触发器的行为——输出是上一个时钟沿采到的值。
• 写组合逻辑(always @(*))用阻塞赋值
=• 写时序逻辑(always @(posedge clk))用非阻塞赋值
<=• 同一个always块里,不要混用两种赋值方式
我有个血泪教训。刚入行时写一个移位寄存器,图省事在时序块里用了阻塞赋值:
// 错误示范:时序逻辑里用了阻塞赋值
always @(posedge clk) begin
shift_reg[0] = data_in;
shift_reg[1] = shift_reg[0]; // 这里拿到的是刚更新的值!
shift_reg[2] = shift_reg[1];
end
仿真波形看起来是对的,但综合出来的电路根本不是移位寄存器,而是一堆组合逻辑环。流片回来,功能全乱。从那以后,我写时序逻辑时,手指会自动敲出 <=。
3.3 if-else 与 case 语句
这两种语句都只能在过程块(always/initial)里使用。它们描述的是“条件判断”行为。
3.3.1 if-else
if-else 有优先级。综合出来是级联的MUX树,或者带优先级的编码器。
// 优先级编码器:a的优先级最高
always @(*) begin
if (a) y = 4'b0001;
else if (b) y = 4'b0010;
else if (c) y = 4'b0100;
else y = 4'b1000;
end
3.3.2 case 语句
case语句是并行的,综合出来是MUX。它有三种变体:
| 类型 | 行为 | 综合结果 | 典型用途 |
|---|---|---|---|
case |
精确匹配,包含x/z | MUX | 状态机、指令译码 |
casez |
将z视为无关位 | MUX(可能更小) | 地址译码、中断向量 |
casex |
将x和z都视为无关位 | MUX(可能更小) | (慎用,仿真与综合可能不一致) |
写case语句时,一定要覆盖所有分支。漏掉分支,综合工具会给你生成锁存器。我建议养成好习惯:
// 完整覆盖的case
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = d; // 覆盖2'b11和其他情况
endcase
end
unique case 或 priority case(SystemVerilog)告诉工具“其他情况不会出现”。但Verilog-2001里,老老实实写default吧。
3.4 有限状态机基础
有限状态机(FSM)是数字设计的核心技能。说白了,就是“根据当前状态和输入,决定下一个状态和输出”。
我习惯把FSM分成三类,写代码前先想清楚用哪种:
我个人强烈推荐三段式写法。为什么?因为可读性强,容易维护,而且综合工具能优化得更好。一段式(把所有逻辑塞进一个always块)虽然代码短,但调试起来想哭。
来看一个经典的三段式状态机模板——检测序列“101”:
// 三段式Moore型状态机:检测序列"101"
module seq_detector (
input wire clk,
input wire rst_n,
input wire data_in,
output reg found
);
// 状态编码
localparam IDLE = 2'b00,
S1 = 2'b01,
S10 = 2'b10,
S101 = 2'b11;
reg [1:0] current_state, next_state;
// 第一段:状态寄存器(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
next_state = current_state; // 默认保持
case (current_state)
IDLE: if (data_in) next_state = S1;
S1: if (!data_in) next_state = S10;
S10: if (data_in) next_state = S101;
S101: next_state = data_in ? S1 : IDLE; // 重叠检测
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑)
always @(*) begin
found = (current_state == S101);
end
endmodule
• 状态数少(<10个):用独热码(one-hot),综合面积小,速度快
• 状态数多(>20个):用二进制码或格雷码,节省寄存器
• 不确定时:用独热码。现代综合工具对独热码优化得很好
写状态机时,有几个容易忽略的细节:
- 复位状态:一定要有,而且必须是有效状态。我见过有人把复位状态设成非法值,结果状态机跑飞了回不来。
- default分支:case语句里必须写,把未定义状态引导到安全状态(通常是IDLE)。
- 输出不要有毛刺:如果输出要驱动关键路径,考虑把输出寄存器化(在第三段输出逻辑后加一级寄存器)。