4. 链路层协议:握手协议、流控机制、错误检测与重传

链路层,说白了就是两个模块之间直接对话的那一层。它不关心数据从哪来、要到哪去,只关心一件事:你能不能把数据稳稳当当地送过来。

我做了十几年跨层互连设计,踩过最多的坑,其实都出在这一层。很多人觉得链路层简单,不就是握手嘛,不就是发个ready信号嘛。嗯,等你遇到跨时钟域、跨电压域、甚至跨芯片的链路时,你就知道这里面的门道有多深了。

4.1 握手协议:最基本的“你一句我一句”

握手协议,你可以想象成两个人打电话:

  • 发送方说:“我有话要说”(valid信号拉高)
  • 接收方说:“我准备好了,你说吧”(ready信号拉高)
  • 然后数据就传过去了

就这么简单?对,但细节决定成败。

4.1.1 经典的两线握手:valid-ready

这是最常用的握手方式。发送方把数据放到总线上,同时拉高valid。接收方什么时候ready了,就拉高ready。当valid和ready同时为高时,数据被采样,完成一次传输。

// 简单的valid-ready握手
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_out <= 0;
        valid_out <= 0;
    end else begin
        if (ready_in) begin
            data_out <= next_data;
            valid_out <= 1;
        end else begin
            // 保持当前数据,直到对方ready
            valid_out <= valid_out;
        end
    end
end
我的小技巧:我个人习惯把valid和ready分开处理。valid由发送方控制,ready由接收方控制。千万别在同一个always块里同时修改这两个信号,否则很容易出现竞争。

4.1.2 握手协议的变种

实际项目中,我们很少只用最简单的valid-ready。为什么呢?因为性能不够。

  • 全握手(Full Handshake):发送方valid后,必须等接收方ack,才能发下一个。适合慢速、可靠场景。
  • 流水线握手(Pipelined Handshake):发送方可以连续发多个valid,接收方按顺序处理。适合高速场景。
  • 背靠背传输(Back-to-Back):当接收方一直ready时,发送方可以每个周期都发数据。这是最高效的模式。

我记得有一次做DDR控制器,就是因为没处理好背靠背传输,导致带宽利用率只有60%。后来改成流水线握手,直接飙到95%。

4.2 流控机制:别把对方撑死

握手协议解决了“能不能传”的问题,但没解决“传多少”的问题。流控机制就是干这个的。

你想想看,如果发送方拼命发数据,接收方处理不过来,会发生什么?数据丢失、重传、甚至系统崩溃。所以我们需要流控。

4.2.1 基于信用的流控(Credit-Based Flow Control)

这是我最喜欢的方式。接收方告诉发送方:“我有N个缓冲区,你可以发N个数据。”发送方每发一个,就消耗一个信用。接收方处理完一个,就归还一个信用。

// 信用流控示例
module credit_flow_ctrl (
    input  clk, rst_n,
    input  [7:0] credit_return,  // 接收方归还的信用
    output reg [7:0] credit_count, // 当前可用信用
    output reg can_send           // 是否可以发送
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        credit_count <= 8'd16;  // 初始16个信用
    end else begin
        // 发送消耗信用
        if (send_valid & send_ready) begin
            credit_count <= credit_count - 1;
        end
        // 接收方归还信用
        if (credit_return != 0) begin
            credit_count <= credit_count + credit_return;
        end
    end
end

assign can_send = (credit_count > 0);

endmodule
关键点:信用流控的优点是无阻塞。发送方不需要等接收方ready,只要还有信用就可以发。这在长延迟链路(比如跨芯片)中特别有用。

4.2.2 基于反压的流控(Backpressure Flow Control)

另一种常见方式是反压。接收方通过拉低ready信号,告诉发送方“我满了,别发了”。

嗯,这里要注意:反压信号本身也有延迟。如果接收方刚拉低ready,发送方已经发出了数据,那就出问题了。所以反压流控通常需要配合缓冲区深度来设计。

我曾经在一个项目中,反压信号要跨三个时钟域才能到达发送方。结果接收方已经满了,发送方还在疯狂发数据。最后我们加了一个深度为8的FIFO才解决。

4.2.3 混合流控

实际项目中,我建议把信用流控和反压流控结合起来。信用流控用于长距离、高延迟链路,反压流控用于短距离、低延迟链路。两者互补,效果最好。

4.3 错误检测与重传:出了问题怎么办

链路层再可靠,也架不住物理层的干扰。信号反射、串扰、电源噪声,都可能导致数据出错。所以我们需要错误检测和重传机制。

4.3.1 错误检测:CRC和ECC

最常用的错误检测方式是CRC(循环冗余校验)。发送方计算数据的CRC值,附在数据后面。接收方收到后重新计算,如果一致,说明数据没出错。

// CRC-32 计算示例(简化版)
function [31:0] crc32;
    input [7:0] data;
    input [31:0] crc_in;
    reg [31:0] crc_out;
    integer i;
    begin
        crc_out = crc_in;
        for (i = 0; i < 8; i = i + 1) begin
            if ((data[i] ^ crc_out[31]) == 1) begin
                crc_out = {crc_out[30:0], 1'b0} ^ 32'h04C11DB7;
            end else begin
                crc_out = {crc_out[30:0], 1'b0};
            end
        end
        crc32 = crc_out;
    end
endfunction

CRC只能检测错误,不能纠正。如果需要纠错,就要用ECC(纠错码)。ECC可以检测并纠正一定数量的比特错误。比如汉明码可以纠正1比特错误,检测2比特错误。

注意:ECC的纠错能力是有限的。如果错误比特数超过纠错能力,ECC会误判。所以对于高可靠性场景,我建议CRC+ECC一起用。CRC负责检测,ECC负责纠正。

4.3.2 重传机制:ARQ协议

检测到错误后怎么办?重传。ARQ(自动重传请求)就是干这个的。

常见的ARQ有三种:

  • 停等ARQ(Stop-and-Wait):发送方发一个数据,等接收方确认。确认了再发下一个。简单,但效率低。
  • 回退N步ARQ(Go-Back-N):发送方可以连续发多个数据。如果某个数据出错,接收方要求重传从那个数据开始的所有后续数据。效率中等。
  • 选择重传ARQ(Selective Repeat):发送方可以连续发多个数据。如果某个数据出错,只重传那个数据。效率最高,但实现复杂。

我个人习惯在高速链路中使用选择重传ARQ。虽然实现复杂一点,但带宽利用率高。我曾经在一个PCIe项目中,从停等ARQ改成选择重传ARQ,吞吐量提升了40%。

4.3.3 超时机制

重传还需要超时机制。如果发送方发了数据,但接收方一直没确认,怎么办?不能无限等下去。所以我们需要一个超时计数器。

超时时间怎么设?太短会导致不必要的重传,太长会降低响应速度。我一般建议设为链路往返延迟的2-3倍。这个值需要通过仿真和实测来调整。

避坑指南:我曾经把超时时间设得太短,结果链路稍微有点抖动就疯狂重传,导致网络拥塞。后来改成动态超时,根据实际延迟自动调整,问题就解决了。

4.4 链路层协议的综合设计

好了,我们把握手、流控、错误检测与重传结合起来,看看一个完整的链路层协议长什么样。

链路层协议核心逻辑 发送方 数据生成 CRC计算 信用管理 接收方 数据接收 CRC校验 信用归还 数据 + CRC valid / ready 信用归还 / 反压 NACK / 重传请求 重传缓冲区 存储已发送但未确认的数据 接收缓冲区 存储乱序到达的数据 超时定时器 触发重传 重传控制 选择重传策略 图例 数据流 控制流

从这张图可以看出,链路层协议的核心就是三个环环相扣的机制:

  1. 握手:建立通信通道,确保数据能传过去
  2. 流控:管理数据流量,防止接收方过载
  3. 错误检测与重传:保证数据完整性,处理传输错误

这三个机制缺一不可。没有握手,数据可能丢失;没有流控,系统可能崩溃;没有错误检测与重传,数据可能出错。

总结一下:链路层协议的设计,本质上是在性能和可靠性之间找平衡。握手越复杂,性能越低,但可靠性越高。流控越激进,性能越高,但风险越大。错误检测越强,开销越大,但数据越安全。

嗯,最后说一句。链路层协议看起来简单,但真正做好很难。我建议你在设计时,先画清楚状态机,再写代码。别一上来就写RTL,否则很容易陷入细节出不来。

可可精品资料库