3. valid-ready协议详解:valid信号、ready信号、数据传输条件、背压机制

各位同学,今天我们来聊聊接口协议里最基础、也最核心的一个握手方式——valid-ready协议。说实话,我做了十几年芯片验证,接触过的接口协议少说也有几十种,但无论多复杂的协议,底层几乎都离不开valid-ready这套机制。你把它吃透了,后面学AXI、AHB、APB这些总线协议,会轻松很多。

3.1 为什么需要握手?

先问大家一个问题:两个模块之间传数据,最简单的做法是什么?

你可能会说:直接把数据线连过去,发送方往数据线上放数据,接收方从数据线上取数据,不就行了?

嗯,理论上可以。但实际芯片里,发送方和接收方的工作节奏往往不一样。发送方可能准备好了数据,但接收方还在处理上一条数据,没空接收。反过来,接收方准备好了,发送方却还没把数据算出来。

所以我们需要一种机制,让双方能互相告诉对方:

  • 发送方说:「我这里有数据,你要不要?」
  • 接收方说:「我现在有空,你发过来吧。」

这就是valid-ready协议要解决的核心问题。

核心思想:valid-ready协议通过两个控制信号,让发送方和接收方能够协调数据传输的节奏,避免数据丢失或重复。

3.2 信号定义

valid-ready协议只有两个控制信号,加上数据信号,总共三组线:

信号名 方向 含义
valid 发送方 → 接收方 发送方通知接收方:数据线上有有效数据
ready 接收方 → 发送方 接收方通知发送方:我已经准备好接收数据
data 发送方 → 接收方 实际传输的数据(宽度可变)

我个人习惯把valid想象成「敲门」,ready想象成「开门」。发送方先敲门(拉高valid),接收方如果在家且有空,就开门(拉高ready),这时候数据才能送进去。

3.3 数据传输条件

数据什么时候才算真正传输成功?

只有一个条件:valid和ready同时为高,且持续至少一个时钟周期。

换句话说,在同一个时钟上升沿,如果valid=1且ready=1,那么这一拍的数据就被成功传输了。

小技巧:我在写验证代码时,经常用这个条件来计数传输了多少笔数据:

if (valid_i && ready_o) begin
    trans_cnt <= trans_cnt + 1'b1;
end

这里要注意:valid和ready的拉高时机,可以不同步。valid可以先拉高,等ready;ready也可以先拉高,等valid。但只有两者同时为高时,才算一次有效传输。

3.4 三种握手场景

实际项目中,valid和ready的时序关系有三种常见情况。我画了一张图,方便大家理解:

valid-ready三种握手场景时序图 clk T0 T1 T2 T3 T4 场景1:valid先有效 valid ready data 有效数据 传输 场景2:ready先有效 ready valid data 有效数据 传输 场景3:同时有效 valid ready data 有效数据 传输 图例 valid信号 ready信号 data信号

三种场景总结一下:

  • 场景1(valid先有效):发送方先准备好数据,等待接收方。接收方忙完后再拉高ready,数据在T2时刻传输。
  • 场景2(ready先有效):接收方先准备好,等待发送方。发送方准备好数据后拉高valid,数据在T2时刻传输。
  • 场景3(同时有效):双方一拍即合,数据在T1时刻直接传输,零等待。

3.5 背压机制

背压(backpressure)这个词,说白了就是接收方告诉发送方:「你慢点,我跟不上了。」

在valid-ready协议里,背压是通过ready信号来实现的。当接收方处理不过来时,它会把ready拉低,发送方看到ready为低,即使valid为高,数据也不会被接收,发送方必须保持数据不变,直到ready再次拉高。

重要提醒:当valid为高但ready为低时,发送方不能改变data线上的数据!这是很多新手容易犯的错误。我曾经在项目中遇到过一个bug,发送方在等待ready时把数据换掉了,结果接收方后来ready拉高时,收到的是错误数据,排查了好久才发现问题。

背压机制的好处是:接收方可以按自己的节奏处理数据,不会被发送方「灌爆」。这在处理速度不匹配的场景下特别有用。

3.6 代码示例:一个简单的valid-ready接口

下面是一个简单的发送模块代码,展示了valid-ready的基本用法:

module sender (
    input  clk,
    input  rst_n,
    input  ready_i,      // 接收方的ready信号
    output reg valid_o,  // 发送方的valid信号
    output reg [7:0] data_o  // 数据
);

    reg [7:0] data_buf;
    reg       data_ready;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            valid_o <= 1'b0;
            data_o  <= 8'h00;
            data_buf <= 8'h00;
            data_ready <= 1'b0;
        end else begin
            // 假设内部逻辑产生了一笔新数据
            if (new_data_available) begin
                data_buf <= internal_data;
                data_ready <= 1'b1;
            end

            // 握手成功,数据被接收
            if (valid_o && ready_i) begin
                valid_o <= 1'b0;
                data_ready <= 1'b0;
            end

            // 有数据待发送,且接收方准备好了
            if (data_ready && !valid_o) begin
                valid_o <= 1'b1;
                data_o <= data_buf;
            end
        end
    end

endmodule

这段代码的逻辑其实很简单:

  1. 内部产生新数据后,先把数据存到buffer里,标记data_ready。
  2. 如果当前没有正在发送的数据(valid_o为低),就把buffer里的数据放到data_o上,拉高valid_o。
  3. 如果握手成功(valid_o和ready_i同时为高),就清除valid_o,表示这笔数据已经发送完毕。

3.7 验证中的常见陷阱

做valid-ready协议的验证时,有几个地方特别容易踩坑:

陷阱1:valid不能依赖ready

valid信号应该由发送方内部逻辑产生,不能等到看到ready才拉高。换句话说,valid是「我准备好了」的信号,不是「我看到你准备好了」的信号。

陷阱2:ready可以组合逻辑产生

ready信号可以是组合逻辑,也可以时序逻辑。但如果是组合逻辑,要注意时序收敛问题。我个人习惯用时序逻辑产生ready,这样时序更干净。

陷阱3:数据要保持稳定

前面已经说过,valid为高且ready为低时,data不能变。验证时一定要用断言检查这个规则:

assert property (
    @(posedge clk) 
    (valid_o && !ready_i) |=> 
    ($stable(data_o))
);

3.8 背压深度与FIFO

当接收方的处理速度远慢于发送方时,单纯的背压机制可能不够。因为发送方在等待ready时,如果内部buffer满了,新数据就没地方放了。

这时候就需要引入FIFO来解耦。发送方只管往FIFO里写,接收方只管从FIFO里读,FIFO的深度决定了能容忍多大的背压。

我曾经做过一个项目,发送方每拍都能产生数据,但接收方需要4拍才能处理一笔。如果不加FIFO,发送方每4拍才能发一笔数据,效率极低。加了深度为8的FIFO后,发送方可以连续发8笔,然后等接收方慢慢处理,整体吞吐量提升了近4倍。

经验之谈:FIFO深度怎么选?一般建议至少能容纳接收方最坏情况下的处理延迟。如果接收方最长需要N拍才能处理一笔数据,那FIFO深度至少为N+1,防止写满。

3.9 总结

valid-ready协议虽然简单,但它是所有握手协议的基础。记住三个要点:

  • 传输条件:valid和ready同时为高
  • 背压机制:通过ready拉低来实现
  • 数据稳定性:等待期间数据不能变

把这些搞清楚了,后面学更复杂的协议时,你会发现万变不离其宗。

可可精品资料库