基本握手信号:Valid与Ready信号

各位同学,今天我们来聊聊接口握手协议里最基础、也最重要的两个信号——Valid和Ready。说实话,我做了十几年数字IC设计,见过太多因为握手没处理好导致的bug。有的芯片流片回来,接口就是死活不通,查到最后,往往是握手时序出了问题。

Valid和Ready,说白了就是一对「你准备好了吗」和「我准备好了」的信号。它们共同决定了数据什么时候能传输。我习惯把Valid看作是发送方的「我有数据给你」,Ready则是接收方的「我现在能收」。两个信号都有效,数据才算真正传过去。

信号定义

先看定义,这个很直接:

  • Valid:由发送方驱动。拉高时,表示发送方已经准备好了有效数据,并且数据在数据总线上是稳定的。
  • Ready:由接收方驱动。拉高时,表示接收方当前可以接收数据。

这里有个关键点——数据只在Valid和Ready同时为高的那个时钟上升沿被采样。嗯,这个规则一定要刻在脑子里。

核心规则:数据在 (Valid == 1) && (Ready == 1) 的时钟沿被传输。

握手时序图

我画了一张时序图,把四种基本握手行为都展示出来了。你仔细看看:

CLK T1 T2 T3 T4 T5 T6 Valid Ready Data D1 D1 D1 D2 D2 D3 Valid before Ready Ready before Valid 同时有效 无效 传输D1 传输D2 无传输

这张图里,我标注了四个关键时间点。你看T3周期,Valid先拉高,Ready后拉高——这就是Valid before Ready。T4周期反过来,Ready先有效。T5周期两个同时有效,数据一拍就过去了。T6周期...嗯,Ready拉低了,数据传不了。

四种基本握手行为

咱们一个一个说清楚。

1. Valid before Ready

发送方先准备好数据,Valid拉高。接收方可能还在忙,过几个周期才把Ready拉高。数据在Ready拉高的那个时钟沿被采样。

我在项目中遇到过这种情况:一个高速数据源,比如ADC,数据出来得很快,但下游的处理模块偶尔会卡住。这时候Valid必须保持住,不能因为Ready没来就把数据撤了。你想想看,如果发送方看到Ready没来就把Valid拉低,那数据就丢了。

经验之谈:Valid一旦拉高,必须保持到握手成功。这是协议的基本要求,也是很多新手容易犯的错。

2. Ready before Valid

接收方提前告诉发送方:「我准备好了,你有数据就发过来。」发送方随后把Valid拉高,数据一拍传走。

这种模式在总线仲裁里很常见。我曾经设计过一个DMA控制器,从设备提前把Ready拉高,主设备看到后开始搬数据。好处是延迟低,一拍就能传。坏处是如果发送方迟迟不来数据,接收方就得一直等着,白白浪费带宽。

3. 同时有效

Valid和Ready在同一时钟周期同时拉高。这是最理想的情况——数据一拍传完,零等待。说白了就是「天时地利人和」,发送方有数据,接收方正好能收。

实际设计中,我们当然希望尽可能多地出现这种情况。但现实往往不完美,所以才需要握手协议来兜底。

4. 无效(无传输)

Valid和Ready中至少有一个为低,数据不传输。这包括三种子情况:

  • Valid低,Ready高:发送方没数据
  • Valid高,Ready低:接收方忙
  • Valid低,Ready低:双方都没准备好

这里有个细节要注意:当Valid为低时,数据总线上的值是没有意义的。接收方不能采样。我曾经见过一个同事,在Valid为低时还去读数据总线,结果采到了上一拍的残留数据,整个系统跑飞了。

警告:接收方只能在Valid和Ready同时为高时采样数据。其他任何时候,数据总线上的值都是无效的。

握手行为的Verilog实现

我写了一个简单的握手状态机,你感受一下:

module handshake_fsm (
    input  clk,
    input  rst_n,
    input  data_valid,   // 发送方数据有效
    output reg ready,     // 接收方准备好
    input  [7:0] data_in,
    output reg [7:0] data_out,
    output reg data_ack   // 握手成功标志
);

    // 状态定义
    localparam IDLE     = 2'b00;
    localparam WAIT_VALID = 2'b01;
    localparam WAIT_READY = 2'b10;
    localparam DONE     = 2'b11;

    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 = WAIT_READY;  // Valid before Ready
                else if (ready)
                    next_state = WAIT_VALID;  // Ready before Valid
                else
                    next_state = IDLE;
            end
            WAIT_VALID: begin
                if (data_valid)
                    next_state = DONE;  // 同时有效
                else
                    next_state = WAIT_VALID;
            end
            WAIT_READY: begin
                if (ready)
                    next_state = DONE;  // 握手成功
                else
                    next_state = WAIT_READY;
            end
            DONE: begin
                next_state = IDLE;
            end
        endcase
    end

    // 输出逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            ready <= 1'b0;
            data_out <= 8'b0;
            data_ack <= 1'b0;
        end else begin
            case (next_state)
                IDLE: begin
                    ready <= 1'b1;  // 默认准备好接收
                    data_ack <= 1'b0;
                end
                WAIT_VALID: begin
                    ready <= 1'b1;  // 保持Ready
                    data_ack <= 1'b0;
                end
                WAIT_READY: begin
                    ready <= 1'b0;  // 暂时不Ready
                    data_ack <= 1'b0;
                end
                DONE: begin
                    data_out <= data_in;  // 采样数据
                    data_ack <= 1'b1;     // 握手成功
                    ready <= 1'b1;
                end
            endcase
        end
    end

endmodule

这段代码里,我用了四个状态来覆盖四种握手行为。IDLE状态下,接收方默认把Ready拉高,表示「我随时能收」。如果发送方先来数据,就进入WAIT_READY;如果接收方先准备好,就进入WAIT_VALID。两种路径最终都会汇聚到DONE状态,完成数据传输。

你可能会问:「为什么IDLE状态要把Ready拉高?」嗯,这是为了追求低延迟。如果接收方总是默认准备好,数据一来就能传,一拍搞定。当然,如果接收方确实忙不过来,可以在WAIT_READY状态把Ready拉低,让发送方等一等。

避坑指南

最后,我分享几个实际项目中踩过的坑:

  • Valid不能乱撤:我曾经设计过一个模块,Valid拉高后因为内部流水线冲突又拉低了,结果接收方只采到了一半数据。记住,Valid一旦拉高,必须保持到握手成功。
  • Ready不能乱变:接收方在拉高Ready后,如果又反悔拉低,发送方可能已经发出了数据。这会导致数据丢失。所以Ready的变化也要谨慎。
  • 组合逻辑环路:Valid和Ready如果互相依赖,容易形成组合逻辑环路。比如发送方用Ready决定是否拉高Valid,接收方用Valid决定是否拉高Ready——这就死锁了。我建议用寄存器打一拍,切断环路。

好了,基本握手信号就讲到这里。记住那四种行为,理解它们的时序关系,后面讲更复杂的协议时你就能跟上了。

可可精品资料库