前言
valid-ready握手协议相对来说较简单,sender给出valid表示数据有效,同时给出数据,receiver给出ready代表可以接收数据。当valid和ready同时为高,代表握手成功,receiver端即可接收数据,sender端可准备发送下一个数据。
在实际应用场景中,当valid-ready握手电路中组合逻辑较多,频率上不去,就需要插寄存器打拍来提高频率。(虽然本人没遇到过这种应用场景,但觉得比较有意思,所以想尝试解决一下)
打拍分三种情况:哪条路频率上不去,就对哪条路打拍
- 对valid和data打拍
- 对ready打拍
- 对valid, data和ready都进行打拍
经过打拍后,sender端和receiver端就都要经过握手,如下图所示。打拍之后,肯定会出问题,比如,valid和data没有打拍,而ready打了一拍。本来receiver在T cycle表示我可以接收数据,然而传到sender端却表示在T+1 cycle可以接收数据,那就会丢数据。或者,valid和data打了一拍,ready没有打拍,由于数据延迟了一个周期,可以理解为在sender端,ready提前一个周期就到了,那么sender端就会认为receiver已经接收了数据,就会把新的数据放出来,把旧的数顶掉,也可能会造成丢数。
所以,就要对三种打拍的情况在逻辑上进行修正,使其仍可以正常传输数据。
说来惭愧,由于我逻辑思维较差,经验不足,本人针对前两种打拍情况,试着去写了一下,都没有实现最大化的传输效率。去看了些别人的代码并试着跑了一下,最终得出了正确的处理方案。我在sender端和receiver端都随机化了valid和ready的请求,遵循了AXI协议中对valid和ready的规范,以下的代码可以作为参考。
1. 对valid和data打拍
问题:如前所述,会出现这种错误:“valid和data打了一拍,ready没有打拍,由于数据延迟了一个周期,可以理解为在sender端,ready提前一个周期就到了,那么sender端就会认为receiver已经接收了数据,就会把新的数据放出来,把旧的数顶掉,也可能会造成丢数。”如下面波形所示,“2”和"7"都丢掉了。
解决:
1. 既然担心数据被顶掉,那就等待每次传输成功后再发送下一次的数据。
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
data_dst <= 8'd0;
end else if(ready_dst && valid_src) begin
data_dst <= data_src;
end
end
2. valid与data同理,等待每次传输成功后再发送下一次的valid。也可以简单一点,sender端有请求就拉高valid_dst,没有请求的话,就等待ready_src拉高,即receiver端接收到数据后,将valid_dst拉低。
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
valid_dst <= 1'b0;
end else if(valid_src) begin
valid_dst <= 1'b1;
end else if(ready_src) begin
valid_dst <= 1'b0;
end
end
3. ready信号不打拍,通过组合逻辑给到sender。这里需要注意,由于data每次都要通过寄存器才能到达receiver,所以每当receiver拉高ready想要接收数据,其data先要存到寄存器,才能给到receiver,这会使传输产生气泡,无法达到最高效率。为了消除气泡,我们需要对sender端接收到的ready信号做一点改动:当data寄存器空的时候(即valid_dst == 1'b0),也要拉高sender端的ready,先把数据存到data寄存器中。这样的话,receiver想要数据的时候,只需要从data寄存器中取数就可以了,实现了类似流水线的操作,降低了latency。
always@(*) begin
ready_dst = ready_src || ~valid_dst;
end
总体代码如下:
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
valid_dst <= 1'b0;
end else if(valid_src) begin
valid_dst <= 1'b1;
end else if(ready_src) begin
valid_dst <= 1'b0;
end
end
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
data_dst <= 8'd0;
end else if(ready_dst && valid_src) begin
data_dst <= data_src;
end
end
always@(*) begin
ready_dst = ready_src || ~valid_dst;
end
仿真波形:
2. 对ready打拍
问题:如前所述,会出现这种错误:“valid和data没有打拍,而ready打了一拍。本来receiver在T cycle表示我可以接收数据,然而传到sender端却表示在T+1 cycle可以接收数据,那就会丢数据。”如下波形所示。
解决:
1. 与valid data打拍类似,也是等待传输成功后再给出下一笔数据。
2. 消除气泡:这里是个难点,valid data打拍时,可以使用valid_dst消除气泡,但是ready打拍时,由于ready信号延迟了一个周期,如果还是通过判断valid_dst是否为0,仿真发现是无法消除气泡的。我参考了网上的两个设计,发现加入passby后自然会将气泡消除(也就是说,在ready打了拍的前提下,valid和data也需要打一拍寄存,只不过有些情况可以将valid和data直通,此种方式传输效率最高,无气泡),但将passby功能去掉后,仍有气泡。由于时间关系,还未找到更好的方案。
3. passby(直通):由于只对ready要求了打拍,valid和data都可以组合逻辑直通,那么在一些情况下,无需将数据输入data寄存器,而是直接直通给receiver。在如下代码中,valid_buf(可以理解为valid_dst)为0时,表示数据寄存器中没有待传的数据了,就可以进行data直通。但做到当前这里,仍然会有气泡,问题出在valid_buf和data_buf的产生,比如,当ready_src == 1'b1并且valid_src == 1'b1时,valid_buf不应拉高,data不应更新,这是因为此时需要进行data直通,若将valid_buf拉高,receiver会选择data寄存器中的数据,而不会选择直通。
最终代码如下所示:
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
valid_buf <= 1'b0;
end else if(valid_src && ~ready_src) begin //bubble clamping for passby
valid_buf <= 1'b1;
end else if(ready_src) begin
valid_buf <= 1'b0;
end
end
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
data_buf <= 8'd0;
end else if(src_ocu && ~ready_src) begin //for data_buf debug
data_buf <= data_src;
end
end
assign src_ocu = valid_src && ready_dst;
assign valid_dst = (valid_src && ~valid_buf) ? 1'b1 : valid_buf; //passby, also bubble clamping
assign data_dst = (~valid_buf) ? data_src : data_buf; //passby
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
ready_dst_pre <= 1'b1;
end else begin
ready_dst_pre <= ready_src;
end
end
always@(*) begin
ready_dst = ready_dst_pre || (~valid_buf);
end
仿真波形如下:
3. 对valid, data和ready都进行打拍
问题:这个不用多说,肯定会出问题。
解决:其实,这种情况可以理解为“对ready打拍”的一个特例,因为在对ready打拍时,我们其实也对valid和data进行了打拍。但不同的是,在对ready打拍时,并不强制要求valid和data必须经过打拍,所以valid和data可以经过组合逻辑直通,来提高传输效率;现在要对所有信号都打拍,就不允许直通了。然而,我参考的方案,在去掉passby后,就无法消除气泡了,所以需要改变方法。(如果不需要消除气泡,仍可以采用之前的 “ 等待传输结束,再给出下一笔数据 ” 的方法)
经过了许久的尝试之后,我意识到,如果对valid, data和ready都进行打拍,不利用passby的情况下,只用一个寄存器进行寄存,应该是无法消除气泡的,应该是需要两个寄存器来寄存。包括网上有人说,这种情况可以采用ping-pong buffer,也印证了我这一猜想,所以我采用了深度为2的同步FIFO来解决(经典遇事不决用FIFO),代码如下,用FIFO的话还是比较简单的。
assign ready_dst = ~full;
assign valid_dst = ~empty;
assign wr_en = valid_src && ready_dst;
assign rd_en = ready_src && valid_dst;
fifo_reg #(
.DEPTH (2),
.WIDTH (8)
) u_fifo_reg (
.clk (clk),
.rst_n (rst_n),
.wr_en (wr_en),
.rd_en (rd_en),
.clr (1'b0),
.full (full),
.empty (empty),
.thres ('d0),
.almost_full (), //not use
.almost_empty (), //not use
.din (data_src),
.dout (data_dst)
);
仿真波形如下:
后话
ping-pong buffer和深度为2的FIFO道理差不多,我就没再写,另外,了解到有个IP是专门解决valid-ready打拍问题的,叫skid buffer,下一期可能会去分析一下这个IP