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
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
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比特错误。
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 链路层协议的综合设计
好了,我们把握手、流控、错误检测与重传结合起来,看看一个完整的链路层协议长什么样。
从这张图可以看出,链路层协议的核心就是三个环环相扣的机制:
- 握手:建立通信通道,确保数据能传过去
- 流控:管理数据流量,防止接收方过载
- 错误检测与重传:保证数据完整性,处理传输错误
这三个机制缺一不可。没有握手,数据可能丢失;没有流控,系统可能崩溃;没有错误检测与重传,数据可能出错。
嗯,最后说一句。链路层协议看起来简单,但真正做好很难。我建议你在设计时,先画清楚状态机,再写代码。别一上来就写RTL,否则很容易陷入细节出不来。
可可精品资料库