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
小技巧: 如果你需要“无优先级”的并行判断,用case语句。if-else天生带优先级,综合面积可能更大。但有些场景(比如中断仲裁)恰恰需要优先级,这时候if-else就是对的。

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分成三类,写代码前先想清楚用哪种:

有限状态机分类 Moore 型 Mealy 型 Medly 混合型 输出只与当前状态有关 与输入无关 安全、稳定、延迟大 输出与当前状态和输入都有关 响应快、组合逻辑多 可能有毛刺 部分输出与状态有关 部分输出与输入有关 折中方案 推荐:三段式写法(状态跳转 + 次态逻辑 + 输出逻辑) 第一段:状态寄存器 时序逻辑 current_state <= next_state 第二段:次态逻辑 组合逻辑 case(current_state) ... 第三段:输出逻辑 组合逻辑 assign out = ...

我个人强烈推荐三段式写法。为什么?因为可读性强,容易维护,而且综合工具能优化得更好。一段式(把所有逻辑塞进一个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)。
  • 输出不要有毛刺:如果输出要驱动关键路径,考虑把输出寄存器化(在第三段输出逻辑后加一级寄存器)。