流水线中的握手与反压

简介: 流水线中的握手与反压

在芯片设计中,尤其是流水线设计中。为了考虑数据安全性,避免数据被覆盖等情况。常常需要使用握手与反压机制。当握手成功的时候认为数据发生了交互。当进行反压的时候则认为没有能力接收数据,上级应该维持住相应的数据保持不变。

1、回顾AXI-stream协议

ARM在AXI协议中使用了这套机制,AXI中的AXI-stream就非常适用于流水线设计。

我们现在只考虑点对点的情况,Master发出Valid、Data信号,接收ready信号。Slave接收Valid、Data信号,发出Ready信号。当握手成功的时候,对于Master而言,意味着它可以更新此时的Data和Valid了。而对于Slave而言,意味着在时钟上升沿的时候,它会成功采样此时的数据。其实握手的本质就意味着双方都认为数据发生了真正的传输,因此可以变换成新的数据了。

有关AXI-stream协议本身大家可以回顾此篇文章,以下对于协议本身只做简单回顾。

https://zhuanlan.zhihu.com/p/642670753

  • Valid先于Ready拉高:

  • Ready先于Valid拉高:

  • Valid和Ready同时拉高:

2、非阻塞型流水线

我们想一下,如果没有握手与反压机制会怎么样

我们考虑下面这样的一个例子,输入DIN先通过寄存器打一拍,然后经过第一级组合逻辑运算以后再打一拍,然后再经过第二级组合逻辑运算再打一拍。然后输出到Dout。看上去能够很好的满足要求,但是如果:

  • Dout所连接的模块或其内部的某一级无法接收新的数据;(下游接收不了数)
  • 中间的组合逻辑一拍运算不完;(上游数据没有准备好)

就会出现相应的问题,该例子中的寄存器在任何情况下,都会在时钟上升沿采样新的数据,但很多时候我们还没有准备好数据,或者是下游还不能接收新的数据。这样的情况都会导致运算结果出错,因此我们需要考虑使用带enable的寄存器,在我们想要的时候,才进行数据的更新,至于这个enable信号如何规定,我们后面再讲,我们先看一下带Enable的DFF长什么样子。

3、阻塞型流水线

3.1、DFF Model

需要在想要的情况再赋值给DFF,我们就需要用到带有enable的DFF,其RTL代码大致如下所示:

reg [31:0] dout;
always@(posedge clk or negedge rst_n) begin
    if(!rst_n)
        dout<=32'h0;
    else if(en)
        dout<=din;
end

可以看到,在这种情况下,只有enable拉高使能的时候,才会把新的din输出给dout,否则dout维持不变。我们一般有两种方式,一种是保持数据不变,另外一种是关闭时钟,通常称为门控时钟。分别如下图所示,其中门控时钟的策略更加适用于高速低功耗的电路中。

3.2、阻塞流水线

正因为非阻塞流水线无法实现我们预期的功能。因此我们需要借助enable信号来实现阻塞流水线,从而引出我们的握手与反压。这里我们首先考虑不使用FIFO的反压机制。

如下图所示,为阻塞型流水线。其中的Enable有两种机制:

  • 全局使能,只要某一级发生阻塞了,整个流水线都会停住,这样会存在某几级流水线可以正常工作,但无法发数和接收数据。存在性能浪费。
  • 独立使能,逐级反压。只要我自己的后级可以接收数据,我就把数往后发,再往前或者再往后是怎样的情况和我没关系。

3.3、阻塞型流水线设计实例

我们考虑pipe1->pipe2的组合逻辑是一个加法器,为单周期指令。而pipe2->pipe3的组合逻辑是一个乘法器,为多周期运算,需要三个时钟周期。

此例子中存在后级反压前级的情况,因此我们就需要根据握手协议来选择合适的时候传递数据,核心思想就是保证中的每一级寄存器的数据安全,不要当前的值还没有使用,就被新的值给覆盖掉了。

这种情况我们没有使用FIFO,但实际上我们可以把每一级寄存器当成一个深度为1的FIFO,下一级有无数据可以看对应的valid信号。下一级无数据或者下一级已经准备好了,那么就可以向上一级取数据。

下面的代码实例是前向打拍,因此ready的时序会比较差,此外正常情况,我们多周期指令实际上会拆成多级流水线,用这样的一级流水线加内部状态记录使能ready的情况很少见。实际上这样性能会比较差,因为存在断流。大家感兴趣可以自己写细粒度流水线的例子。

接下来我们分析一下代码:

  • 首先我们分析什么情况下会反压,即存在ready拉低。

pipe1_ready拉高即代表pipe1可以接收新的数据,什么情况下pipe1可以接收新的数据呢?

第一种情况是pipe1_valid为0,这种情况下我这个寄存器没有实际有效的输出,处于空闲状态,那自然而然可以接收一笔数据,与后级是啥情况都没有任何关系。

另外一种情况是pipe2_ready&pipe1_done。对于单周期运算而言,pipe1_done默认为1,即认为只要输入valid,那么下一个时钟周期上升沿必然可以采样到已经运算好的数据,不会采样到一个还处于运算中途的数据。此时当pipe2_ready拉高,即使我pipe1_valid为1,也就是此时不处于空闲状态。但由于下一级可以接收我的数据,因此不用担心数据还没用就被覆盖掉了。

我们来分析一下。如果pipe2_ready拉高,并且我pipe1_valid为0,即此时实际上处于第一种情况。那自然而然可以接收数据。如果pipe1_valid为高,实际上这种情况真正对应的是pipe2_ready&pipe1_2_pipe2_valid,即下游握手成功。这个时候当前的数据会在下一个时钟上升沿的情况下就被pipe2采走,因此我pipe1_ready拉高自然没有关系。你即使新的数据进来了,也不可能覆盖掉我之前的数据。

大家如果理解不了可以思考一下深度为1的同步FIFO。第一种情况实际上就是FIFO为空,那当然可以拉高ready,你进来一个新的数据这个FIFO才没有浪费掉。第二种情况实际上就是FIFO里面有一个数据,但是我这个数据已经准备好给别人拿走了,并且别人也可以拿走,这样你进来一个新的数据没有关系。因为之前的那个数据被下一级拿走了,没有被浪费掉。(我们上述的基于valid和ready的pipe,实际上和深度为1的同步FIFO没有太大区别,不过相比于深度为1的FIFO不会断流,大家可以分析一下为什么)。

我们再来看第二级pipe,第二级的区别在于其要运算乘法指令,而乘法指令的运算需要3个时钟周期。因此当第二级pipe的valid拉高的时候,并且第三级的ready拉高的时候。并不意味着这个数据是运算完成的数据了,如果此时直接采样,会采到错误的数据,同时第二级的pipe也会被错误的数据覆盖。

我们看一下第二级pipe的pipe2_ready。什么情况下pipe2_ready拉高呢?

第一种情况是pipe2_valid为0,这种情况和上面的情况一样。即寄存器没有真正有效的输出,实际上处于空闲状态,那当然可以接收一笔数据。

另外一种情况是pipe2_done && pipe3_ready。pipe3_ready意味着下一级可以接收上一级的数据。但这并不意味着pipe2自己也可以接收上一级的数据。因为这是一个多周期运算,它后级可以接收数据了,不代表你的数据已经算完了。你只有算完的情况下,并且后级可以接收了。这种情况下你才可以接收来自前级新的数据。这里我们使用一个移位寄存器,当pipe2_valid拉高的时候,即代表数据有效了。我们相应的统计两拍,然后在第三拍的时候。可以把valid和data一起向后传过去。

  • 我们再看一下valid信号,即什么时候数据准备好了,并且可以传到下一级

首先对于pipe1_2_pipe2_valid信号而言,当pipe1_valid拉高的时候该信号也会拉高。也就是pipe1拿到数据以后,默认下一级下一拍就去采是没有问题的。那pipe1_valid什么时候拉高呢?我们认为pipe1_ready拉高的时候,相应的可以将valid_i赋值给pipe1_valid。(这里不是valid依赖于ready拉高,因为pipe1_valid不是和pipe1_ready握手)实际上我们考虑,valid_i为高,则说明握手成功,因此可以把pipe1_valid拉高,否则pipe1_valid仍然为0。

pipe2_2_pipe3_valid是类似的,其区别在于。pipe2_valid拉高的时候,它的数据还没有运算完,因此不能马上将pipe2_2_pipe3_valid拉高,否则下一级就采集到了错误的数据了。

至于data,则认为握手成功的时候可以更新为新的数据。

module BlockPipe(
    input           clk,
    input           rst_n,
    //upstream
    input           valid_i,
    input [31:0]    data_i,
    output          ready_o,
    //downstream
    input           ready_i,
    output          valid_o,
    output [31:0]   data_o
);
//pipeline signals
reg         pipe1_valid;
reg [31:0]  pipe1_data;
wire        pipe1_ready,pipe1_done;
reg         pipe2_valid;
reg [31:0]  pipe2_data;
wire        pipe2_ready,pipe2_done;
reg         pipe3_valid;
reg [31:0]  pipe3_data;
wire        pipe3_ready,pipe3_done;
wire        pipe1_2_pipe2_valid;
wire        pipe2_2_pipe3_valid;
reg [1:0]   mul_done;
wire [31:0] mul_result;
//pipeline 1 stage
assign pipe1_done = 1'b1;
assign pipe1_ready = !pipe1_valid || (pipe1_done && pipe2_ready);
assign pipe1_2_pipe2_valid = pipe1_valid & pipe1_done;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        pipe1_valid <= 1'b0;
    else if(pipe1_ready)
        pipe1_valid <= valid_i;
end
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        pipe1_data <= 32'h0;
    else if(valid_i && pipe1_ready)
        pipe1_data <= data_i;
end
//pipeline 2 stage
assign pipe2_done = mul_done[1];
assign pipe2_ready = !pipe2_valid || (pipe2_done && pipe3_ready);
assign pipe2_2_pipe3_valid = pipe2_valid & pipe2_done;
assign mul_result = pipe2_data * 4'h5;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        mul_done <= 2'b00;
    else if(pipe2_ready && pipe1_2_pipe2_valid)
        mul_done <= 2'b00;
    else if(pipe2_valid)
        mul_done <= {mul_done[1:0],1'b1};
end
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        pipe2_valid <= 1'b0;
    else if(pipe2_ready)
        pipe2_valid <= pipe1_2_pipe2_valid;
end
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        pipe2_data <= 32'h0;
    else if(pipe1_2_pipe2_valid && pipe2_ready)
        pipe2_data <= pipe1_data + 4'h4;
end
//pipeline 3 stage
assign pipe3_done = 1'b1;
assign pipe3_ready = !pipe3_valid || (pipe3_done && ready_i);
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        pipe3_valid <= 1'b0;
    else if(pipe3_ready)
        pipe3_valid <= pipe2_2_pipe3_valid;
end
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        pipe3_data <= 32'h0;
    else if(pipe2_2_pipe3_valid && pipe3_ready)
        pipe3_data <= mul_result;
end
assign valid_o = pipe3_valid;
assign data_o =pipe3_data;
assign ready_o = pipe1_ready;
endmodule

该例子中ready时序较差。大家可以在两端使用fw_bw双向的pipe打拍,来让时序变好。或者我们也可以使用带存储体的反压。

4、带存储体的反压

最典型的例子就是借助于同步FIFO。我们看下图这样的一个例子,中间运算的数据会持续的进入到同步FIFO当中,后级模块再从同步FIFO里面去取数据。这种情况下,什么时候应该反压前级呢?

很多人会觉得当同步FIFO满了以后反压前级,但是我们的10级流水线内部是非阻塞型的,也就是每一个时钟周期上升沿都会更新数据。如果你同步FIFO已经满了再进行反压。此时R0确实不会再出数据了,但是10级流水线内部的数据怎么办呢?每一个周期上级数据都会覆盖掉下一级的数据。最后一级的数据又不能进入到同步FIFO当中,因此数据都被冲刷掉了。

这个时候就应该用Almost Full去反压。当FIFO Depth=Max Depth-pipe_length的时候,就拉低ready。这样你的10笔数据,还是可以进入到同步FIFO当中,不至于数据被冲刷掉。当FIFO非空的时候就给下一级的valid_o拉高,即valid_o=~full,而下一级的ready_i可以作为FIFO的读使能信号。

这种方式的缺点就是开销比较大,此外还要确定好FIFO深度,almost_full情况。避免数据被冲刷掉。优点是时序较好,设计简单,不用考虑复杂的握手条件。

上述的例子如果有问题,欢迎大家指正。


目录
相关文章
|
6月前
|
资源调度 监控 关系型数据库
在Flink CDC作业提交过程中,出现超时问题可能与多种因素有关
【2月更文挑战第8天】在Flink CDC作业提交过程中,出现超时问题可能与多种因素有关
297 11
|
3月前
|
运维 测试技术 持续交付
部署流水线解析
部署流水线解析
37 1
|
3月前
|
存储 监控 Serverless
Serverless 应用的监控与调试问题之Flink对于Checkpoint Barrier流动缓慢的问题要如何解决
Serverless 应用的监控与调试问题之Flink对于Checkpoint Barrier流动缓慢的问题要如何解决
|
6月前
|
弹性计算 运维 Devops
云效产品使用报错问题之想用流水线A执行结束,触发流水线B,配置失败如何解决
本合集将整理呈现用户在使用过程中遇到的报错及其对应的解决办法,包括但不限于账户权限设置错误、项目配置不正确、代码提交冲突、构建任务执行失败、测试环境异常、需求流转阻塞等问题。阿里云云效是一站式企业级研发协同和DevOps平台,为企业提供从需求规划、开发、测试、发布到运维、运营的全流程端到端服务和工具支撑,致力于提升企业的研发效能和创新能力。
|
NoSQL Redis
pipline(流水线、管道)
一、什么是 pipline 1. 一次网络命令的通信模型 1次网络命令时间 = 1次网络传输时间(往返) + 1次命令执行时间
 pipline(流水线、管道)
|
存储 SQL 分布式计算
数据流水线架构
数据流水线架构
394 0
数据流水线架构
|
存储 内存技术
CPU设计(单周期和流水线)
CPU设计(单周期和流水线)
249 0
CPU设计(单周期和流水线)
|
开发者
E906的流水线|学习笔记
快速学习 E906的流水线
196 0
E906的流水线|学习笔记
|
消息中间件 分布式计算 Kafka
【Flink】(九)状态一致性、端到端的精确一次(ecactly-once)保证1
【Flink】(九)状态一致性、端到端的精确一次(ecactly-once)保证1
309 0
【Flink】(九)状态一致性、端到端的精确一次(ecactly-once)保证1
|
消息中间件 存储 缓存
【Flink】(九)状态一致性、端到端的精确一次(ecactly-once)保证2
【Flink】(九)状态一致性、端到端的精确一次(ecactly-once)保证2
446 0
【Flink】(九)状态一致性、端到端的精确一次(ecactly-once)保证2