4. 单路握手状态机设计:从发送端到接收端的可靠传输

Valid-ready握手协议,说白了就是数字世界里最朴素的“你准备好了吗?我准备好了!”的对话机制。我刚开始接触这个协议时,觉得它简单得有点无聊——不就是两个信号来回拉高拉低嘛。但后来在项目中吃过亏才明白,越是简单的东西,越容易在细节上翻车。

今天我们就来设计一个单路valid-ready握手状态机。别小看这个“单路”,它是所有复杂总线协议(比如AXI、Avalon)的基础模块。你把这个吃透了,后面学什么协议都轻松。

4.1 握手协议的基本规则

Valid-ready握手的核心规则其实就三条:

  • 发送端:数据准备好后,拉高valid信号
  • 接收端:准备好接收时,拉高ready信号
  • 传输发生:valid和ready同时为高,数据被成功接收

嗯,这里要注意一个关键点——valid信号一旦拉高,必须保持到握手成功为止。不能中途撤回去。我在项目中见过有人把valid当成脉冲信号用,结果接收端只采到半个数据,整个系统乱套了。

核心原则:valid不能依赖ready,ready可以依赖valid。这是设计时的黄金法则。

4.2 状态机设计思路

单路握手状态机,我习惯把它分成三个状态:

状态 含义 valid输出 ready输出
IDLE 空闲,等待发送 0 1(如果接收端空闲)
SEND 数据已发出,等待握手 1 0
DONE 握手成功,传输完成 0 1

你想想看,为什么需要三个状态?两个状态不行吗?

我曾经也这么想过,后来发现两个状态会出问题——如果发送端和接收端同时准备好,数据瞬间就传过去了,但状态机还没来得及记录这次传输。三个状态的好处是:每个传输周期都有明确的开始、等待和结束,不会丢数据。

4.3 状态转移图

下面我用SVG画了一张状态转移图,把整个流程可视化出来。你看一眼就明白了。

IDLE 空闲状态 SEND 等待握手 DONE 传输完成 有数据要发 valid & ready ready=0 下一笔数据

这张图其实已经把整个逻辑说清楚了。IDLE状态下,一旦有数据要发送,就跳转到SEND。在SEND状态,如果接收端ready为低,就原地等待;如果ready为高,握手成功,进入DONE。DONE状态表示这次传输结束,可以回到IDLE准备下一笔。

个人经验:我建议在SEND状态加一个超时保护。虽然标准握手协议里没有这个要求,但实际项目中如果接收端一直不拉高ready,发送端就会死等。加一个计数器,超时后报错或者重试,系统会更健壮。

4.4 Verilog代码实现

光说不练假把式。下面是我写的一个单路握手状态机代码,你直接拿去用都没问题。

module handshake_fsm (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       data_valid,   // 发送端数据有效
    input  wire       ready_in,     // 接收端准备好
    output reg        valid_out,    // 发送端valid
    output reg        ready_out,    // 发送端ready(接收端视角)
    output reg        handshake_done // 握手成功标志
);

    // 状态编码
    localparam IDLE = 2'b00;
    localparam SEND = 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: begin
                if (data_valid)
                    next_state = SEND;
            end
            SEND: begin
                if (ready_in)
                    next_state = DONE;
                // 否则保持SEND
            end
            DONE: begin
                next_state = IDLE;  // 自动回到空闲
            end
            default: next_state = IDLE;
        endcase
    end

    // 输出逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            valid_out      <= 1'b0;
            ready_out      <= 1'b1;
            handshake_done <= 1'b0;
        end else begin
            case (next_state)
                IDLE: begin
                    valid_out      <= 1'b0;
                    ready_out      <= 1'b1;
                    handshake_done <= 1'b0;
                end
                SEND: begin
                    valid_out      <= 1'b1;
                    ready_out      <= 1'b0;
                    handshake_done <= 1'b0;
                end
                DONE: begin
                    valid_out      <= 1'b0;
                    ready_out      <= 1'b1;
                    handshake_done <= 1'b1;
                end
                default: begin
                    valid_out      <= 1'b0;
                    ready_out      <= 1'b1;
                    handshake_done <= 1'b0;
                end
            endcase
        end
    end

endmodule
避坑指南:我曾经在输出逻辑里直接用组合逻辑赋值,结果valid信号在握手成功前就提前拉低了。记住,valid必须保持到握手成功那一刻。用时序逻辑(always @(posedge clk))来驱动输出,可以避免毛刺和误动作。

4.5 仿真验证要点

写完代码怎么验证?我一般会关注这几个场景:

  1. 正常握手:valid和ready同时为高,数据一拍内传完
  2. 接收端等待:ready一直为低,valid保持,直到ready拉高
  3. 发送端等待:valid为低,ready为高,状态机不动作
  4. 背靠背传输:上一笔刚传完,下一笔立即开始
  5. 复位行为:复位后状态机回到IDLE,所有输出清零

你想想看,如果接收端在握手成功的那一拍同时拉低ready,会发生什么?嗯,这个问题我当年面试时被问过。答案是:握手已经发生了,ready的变化不影响当前传输。所以代码里用next_state来判断输出,而不是用state,就是为了避免这种边界情况。

4.6 实际项目中的扩展

单路握手状态机虽然简单,但它是很多复杂模块的基石。我在做DMA控制器时,就用这个状态机作为每个通道的传输控制核心。只需要加一个数据计数器,就能实现指定长度的突发传输。

另外,如果你需要处理多个数据源,可以把valid和ready信号扩展成总线形式。每个数据源有自己的valid,接收端用仲裁逻辑决定先处理谁。但底层每个通道的握手逻辑,还是我们今天讲的这个三状态机。

总结一下:单路valid-ready握手状态机,三个状态搞定一切。IDLE等数据,SEND等握手,DONE收工。代码写起来也就几十行,但背后的时序逻辑一定要想清楚。尤其是valid的保持和ready的依赖关系,搞反了就是灾难。

好了,这一章的内容就到这里。代码你可以直接拿去仿真跑一跑,看看波形是不是符合预期。有什么问题,欢迎随时交流。

可可精品资料库