3. 全握手协议(Full Handshake):四相握手协议(REQ/ACK)的详细状态机设计

各位同学,今天我们来聊聊全握手协议。说实话,这是接口设计中我最喜欢的一个部分。为什么?因为它既简单又可靠,你想想看,两个模块之间要传数据,最怕什么?最怕数据丢了或者重复了。四相握手协议,说白了就是给数据传输上了个「双保险」。

我在项目中遇到过不少因为握手协议没设计好导致的bug。有一次,一个同事设计的接口在高速下老是丢数据,查了三天,最后发现是ACK信号采样时机不对。嗯,从那以后我对握手状态机的每个细节都格外较真。

3.1 四相握手协议的基本概念

四相握手,也叫全握手。它用两根控制线——REQ(请求)和ACK(应答)——来完成一次数据传输。为什么叫「四相」?因为一次完整的数据传输需要经历四个状态变化。

我个人习惯把四相握手的流程总结成一句话:发请求,等应答;撤请求,等撤销。你想想看,是不是这个理?

四相握手的四个阶段:

  1. 请求阶段(Phase 1):发送方拉高REQ,表示「我有数据要给你」
  2. 应答阶段(Phase 2):接收方拉高ACK,表示「我收到数据了」
  3. 撤销请求阶段(Phase 3):发送方拉低REQ,表示「我知道你收到了」
  4. 撤销应答阶段(Phase 4):接收方拉低ACK,表示「我准备好接收下一个了」

这里有个关键点:每个阶段都必须等待对方的响应才能进入下一阶段。这就是全握手「全」字的含义——双方完全同步,一步一个脚印。

3.2 状态机设计详解

好,接下来我们看看状态机怎么设计。我一般用四个状态来描述四相握手:

状态名称 状态编码 描述
IDLE 00 空闲状态,REQ=0,ACK=0
REQ_ASSERT 01 请求已发出,等待ACK拉高
ACK_ASSERT 11 ACK已拉高,等待REQ拉低
REQ_DEASSERT 10 REQ已拉低,等待ACK拉低

你可能会问:为什么状态编码用格雷码?嗯,这个问题问得好。我在一个高速接口项目中吃过亏——状态跳变时多个bit同时变化,导致组合逻辑产生了毛刺。从那以后,我习惯用格雷码或者独热码来编码状态机。

3.3 状态转移图

下面我用SVG画了一张状态转移图。这张图我画了很多遍,每次给新人讲课时都会拿出来。你仔细看,每个转移条件都对应着REQ和ACK的电平变化。

四相握手协议状态转移图 IDLE REQ=0, ACK=0 REQ_ASSERT REQ=1, ACK=0 ACK_ASSERT REQ=1, ACK=1 REQ_DEASSERT REQ=0, ACK=1 有数据要发送 ACK=1 数据已接收 拉低REQ ACK=0 无数据时保持 等待ACK 等待REQ拉低 等待ACK拉低

这张图你看懂了吗?我解释一下关键路径:

  • IDLE → REQ_ASSERT:发送方有数据要传,拉高REQ
  • REQ_ASSERT → ACK_ASSERT:接收方看到REQ拉高,拉高ACK表示收到
  • ACK_ASSERT → REQ_DEASSERT:发送方看到ACK拉高,拉低REQ表示「我撤了」
  • REQ_DEASSERT → IDLE:接收方看到REQ拉低,拉低ACK,回到初始状态

小技巧: 我习惯在状态机里加一个超时保护。万一对方模块挂了,REQ一直拉高怎么办?加个计数器,超时后强制回到IDLE。这个设计救过我一次——某次仿真中,对方模块的时钟门控没打开,ACK死活不拉高,要不是超时保护,整个系统就卡死了。

3.4 Verilog代码实现

下面是我常用的四相握手状态机代码。注意看,我用了三段式状态机写法——这是我在多个项目中验证过的稳定结构。

module full_handshake_fsm (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       data_valid,    // 发送方有数据
    input  wire       ack_in,        // 接收方应答
    output reg        req_out,       // 发送方请求
    output reg        data_sent      // 数据发送完成标志
);

    // 状态编码(格雷码)
    localparam IDLE         = 2'b00;
    localparam REQ_ASSERT   = 2'b01;
    localparam ACK_ASSERT   = 2'b11;
    localparam REQ_DEASSERT = 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: begin
                if (data_valid)
                    next_state = REQ_ASSERT;
            end
            REQ_ASSERT: begin
                if (ack_in)
                    next_state = ACK_ASSERT;
            end
            ACK_ASSERT: begin
                // 数据已接收,准备撤销请求
                next_state = REQ_DEASSERT;
            end
            REQ_DEASSERT: begin
                if (!ack_in)
                    next_state = IDLE;
            end
        endcase
    end

    // 第三段:输出逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            req_out   <= 1'b0;
            data_sent <= 1'b0;
        end else begin
            case (next_state)
                IDLE: begin
                    req_out   <= 1'b0;
                    data_sent <= 1'b0;
                end
                REQ_ASSERT: begin
                    req_out   <= 1'b1;
                    data_sent <= 1'b0;
                end
                ACK_ASSERT: begin
                    req_out   <= 1'b1;
                    data_sent <= 1'b1;  // 数据发送完成
                end
                REQ_DEASSERT: begin
                    req_out   <= 1'b0;
                    data_sent <= 1'b0;
                end
            endcase
        end
    end

endmodule

注意: 这段代码里有个细节——我在输出逻辑里用的是 next_state 而不是 state。为什么?因为用 next_state 可以避免输出晚一个时钟周期。我曾经在这个问题上栽过跟头,仿真时数据总比预期晚一拍,查了半天才发现是输出逻辑用了 state。

3.5 时序约束分析

四相握手的时序约束,说白了就是确保REQ和ACK的采样不出错。我一般关注三个点:

约束类型 路径 说明
建立时间约束 REQ → ACK采样寄存器 确保ACK在时钟沿前稳定
保持时间约束 ACK → REQ撤销 确保REQ撤销不被误采样
跨时钟域约束 REQ/ACK跨时钟域 如果两端时钟不同,需要同步器

我个人习惯在SDC文件里这样写:

# 四相握手时序约束
# 假设REQ和ACK在同一时钟域
set_max_delay -from [get_ports req_out] -to [get_ports ack_in] 5.0
set_min_delay -from [get_ports req_out] -to [get_ports ack_in] 1.0

# 如果跨时钟域,需要false_path + 同步器
set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b]

避坑指南: 我曾经在一个项目中,发送端和接收端用了不同的时钟。REQ信号直接跨时钟域采样,结果出现了亚稳态。从那以后,我只要看到跨时钟域握手,一定会加两级同步器。记住:跨时钟域握手,同步器不是可选项,是必选项。

3.6 四相握手的优缺点

说了这么多,我们来总结一下四相握手的优缺点。你想想看,它为什么叫「全握手」?因为它把每个步骤都确认得死死的。

优点 缺点
可靠性极高,不会丢数据 速度慢,每拍需要4个状态跳变
对时序要求宽松 带宽利用率低
容易调试,波形一目了然 状态机逻辑相对复杂
天然支持背压 不适合高速流水线

嗯,说到背压,这是四相握手的一个天然优势。接收方如果忙不过来,可以不拉高ACK,发送方就会一直等着。这种机制在异步FIFO设计中用得很多。

好了,四相握手协议的状态机设计就讲到这里。记住核心思想:发请求,等应答;撤请求,等撤销。下次你设计接口时,不妨试试这个结构,它虽然慢一点,但可靠。

可可精品资料库