3. Verilog基础与RTL设计:组合逻辑与时序逻辑、阻塞与非阻塞赋值、状态机设计(FSM)、同步与异步复位

好,咱们进入第三讲。这一章是RTL设计的基石,说白了就是FPGA工程师的“内功心法”。很多新手写代码,仿真能过,一上板子就挂,十有八九是这里的基本功没打牢。我自己带团队时,面试必问这几个概念,能讲清楚的人,代码质量通常不会差。

3.1 组合逻辑 vs 时序逻辑

这两个概念,我习惯用一个比喻来理解:组合逻辑就像一根导线,输入一变,输出立刻跟着变,没有记忆能力。时序逻辑就像一个小盒子,只在时钟沿到来时才把输入“抓”进去存起来,输出不会乱跳。

在Verilog里,组合逻辑用 assign 或者 always @(*) 来描述。时序逻辑则用 always @(posedge clk) 来描述。记住这个区分,能避免很多低级错误。

核心区别一句话:组合逻辑的输出只取决于当前输入;时序逻辑的输出还取决于之前的状态(靠时钟驱动)。

我在项目中遇到过一位同事,把计数器写成了组合逻辑,结果仿真波形看着对,综合后却出现了一大堆latch。嗯,这里要提醒大家:组合逻辑中如果条件分支不完整,综合工具会推断出锁存器(latch),这通常不是我们想要的。

3.1.1 组合逻辑示例

// 组合逻辑:全加器
module full_adder (
    input  a, b, cin,
    output sum, cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (a & cin) | (b & cin);
endmodule

3.1.2 时序逻辑示例

// 时序逻辑:带同步使能的8位计数器
module counter (
    input        clk,
    input        rst_n,
    input        en,
    output reg [7:0] cnt
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 8'd0;
        else if (en)
            cnt <= cnt + 1'b1;
    end
endmodule

3.2 阻塞赋值与非阻塞赋值

这是新手最容易踩的坑,没有之一。我当年刚学Verilog时,也在这个问题上吃过亏。仿真跑得欢,综合出来逻辑全乱套。

规则其实很简单,但需要刻在脑子里:

  • 组合逻辑用阻塞赋值 =:因为它是“立即生效”的,符合组合逻辑的直通特性。
  • 时序逻辑用非阻塞赋值 <=:因为它是“并行赋值”的,所有赋值在时钟沿同时发生,符合寄存器行为。

警告:千万不要在同一个 always 块里混用 =<=。我曾经见过一个案例,工程师在时序逻辑里用了阻塞赋值,导致仿真和综合结果不一致,查了两天才找到原因。

3.2.1 为什么时序逻辑必须用非阻塞赋值?

你想想看,一个寄存器在时钟沿到来时,它的输入和输出是同时更新的。如果用阻塞赋值,就会变成“先赋值给第一个寄存器,再用更新后的值去赋值给第二个寄存器”,这就变成了串行行为,完全违背了硬件并行工作的本质。

// 错误示例:时序逻辑中使用阻塞赋值
always @(posedge clk) begin
    a = b;  // 先执行
    c = a;  // 再执行,此时a已经更新,c得到的是新a的值
end
// 综合后:a和c变成了同一个寄存器,逻辑错误!

// 正确示例:时序逻辑中使用非阻塞赋值
always @(posedge clk) begin
    a <= b;  // 同时采样
    c <= a;  // 同时采样,c得到的是a的旧值
end
// 综合后:两个独立的寄存器,行为正确

3.3 状态机设计(FSM)

状态机是数字设计的灵魂。以太网MAC层里,从数据包接收、CRC校验到帧间隙控制,几乎全是状态机在驱动。我个人习惯把状态机分成三段式来写,清晰又好维护。

3.3.1 三段式状态机模板

功能 使用赋值
第一段 状态转移(时序逻辑) 非阻塞赋值 <=
第二段 次态逻辑(组合逻辑) 阻塞赋值 =
第三段 输出逻辑(组合逻辑或时序逻辑) 根据需求选择
// 三段式状态机示例:简单数据包接收
module fsm_example (
    input        clk,
    input        rst_n,
    input        sop,      // 包起始
    input        eop,      // 包结束
    output reg   data_valid
);

    // 状态编码
    localparam IDLE    = 2'b00;
    localparam RECV    = 2'b01;
    localparam DONE    = 2'b10;

    reg [1:0] state, next_state;

    // 第一段:状态转移
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= IDLE;
        else
            state <= next_state;
    end

    // 第二段:次态逻辑(组合逻辑)
    always @(*) begin
        next_state = state;  // 默认保持
        case (state)
            IDLE:   if (sop) next_state = RECV;
            RECV:   if (eop) next_state = DONE;
            DONE:   next_state = IDLE;
            default: next_state = IDLE;
        endcase
    end

    // 第三段:输出逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            data_valid <= 1'b0;
        else if (state == RECV)
            data_valid <= 1'b1;
        else
            data_valid <= 1'b0;
    end

endmodule

小技巧:状态编码尽量用 localparam 定义,别用魔法数字。另外,我习惯在 case 里加 default 分支,防止综合出latch。

3.4 同步复位 vs 异步复位

复位策略的选择,直接影响芯片的可靠性和面积。说白了,就是“什么时候复位”和“怎么复位”的问题。

  • 同步复位:复位信号只在时钟沿有效。优点是抗毛刺能力强,综合后占用资源少。缺点是复位路径上需要组合逻辑,可能影响时序。
  • 异步复位:复位信号立即生效,不依赖时钟。优点是复位速度快,不需要时钟就能把电路拉回初始态。缺点是对毛刺敏感,且存在“恢复时间”和“移除时间”的时序约束问题。

3.4.1 我推荐的做法:异步复位、同步释放

这是业界最常用的方案。既保留了异步复位“立即生效”的优点,又解决了异步复位容易引起亚稳态的问题。说白了,就是复位信号先经过两级同步器,再送给内部逻辑。

// 异步复位、同步释放电路
module rst_sync (
    input  clk,
    input  rst_async_n,
    output rst_sync_n
);
    reg rst_meta, rst_sync;

    always @(posedge clk or negedge rst_async_n) begin
        if (!rst_async_n) begin
            rst_meta <= 1'b0;
            rst_sync <= 1'b0;
        end
        else begin
            rst_meta <= 1'b1;
            rst_sync <= rst_meta;
        end
    end

    assign rst_sync_n = rst_sync;
endmodule

注意:always @(posedge clk or negedge rst_n) 这种写法中,如果复位信号是异步的,一定要保证它满足恢复/移除时间约束。否则,寄存器可能进入亚稳态,导致整个系统行为不可预测。

3.5 本章知识体系图

下面这张图,是我自己总结的RTL设计核心脉络。你可以把它当作一张“思维导图”来用。

RTL设计核心 逻辑类型 组合逻辑 时序逻辑 assign / always @(*) always @(posedge clk) 赋值方式 阻塞 = 非阻塞 <= 组合逻辑用 时序逻辑用 状态机 FSM 一段式 三段式 不推荐 推荐使用 复位策略 同步复位 异步复位 异步复位同步释放

这张图把本章的四个核心知识点串在了一起。你写代码时,可以对照着检查:我这段逻辑是组合还是时序?赋值用对了吗?状态机结构清晰吗?复位可靠吗?

好了,这一章的内容就到这里。记住,RTL设计没有捷径,多写、多仿真、多上板验证,才是正道。


专注资料整理