AXI握手时序优化——pipeline缓冲器

  • skid buffer(pipeline缓冲器)介绍
  • 背景
  • 需求与模块定义
  • 数据路径
  • 控制路径


skid buffer(pipeline缓冲器)介绍

  解决ready/valid两路握手的时序困难,使路径流水线化。
  只关心valid时序参考这篇写得很好的博客链接: 握手协议(pvld/prdy或者valid-ready或AXI)中Valid及data打拍技巧 ;只关心ready时序修复可以参考同作者这篇文章链接: (AXI)握手协议(pvld/prdy或者valid-ready)中ready打拍技巧   一个skid buffer是最小的Pipeline FIFO Buffer,只有两个入口。当您需要在发送者和接收者之间为并发和/或定时流水线化路径时,它很有用,但不能消除数据速率不匹配。它还只需要两个数据寄存器,在这个规模上比LUT RAM或Block RAM小(取决于实现),并且具有更大的布局和布线自由度。

背景

  片上网络 (NoC) 和pipeline具有作为基本构建块的握手机制,其中链路的每一端都可以发出信号,如果它们有数据要发送(“valid”),或者它们是否能够接收数据(“ready”)。当两端一致(有效且均为高电平)时,在该时钟周期发生数据传输。
   但是,流水线握手更复杂:1.如果直接将valid、ready和data流水线寄存是可以工作的,但是每次传输需要两个周期开始,两个周期停止。如果每次握手只传输一个数据块,这在带宽利用方面还不错。但是现在接收器必须知道它和发送器之间存在多少管道阶段,才能有足够的内部缓冲数据(在它发出不再准备好接收更多数据的信号后,吸收不断到达的数据,因为说了ready要过一拍才能传上去)。
  这是基于信用的连接的基础(不在这里讨论),它可以最大化带宽,但是如果只需要在两端之间添加单个pipe阶段,而无需修改master/slave,只是满足时序或允许每一端发送一项数据而不必等待打拍的延迟响应(因此重叠通信,这是可取的)。

需求与模块定义

  为了开始设计一个pipeline缓冲器,让我们想象一个可以执行valid/ready握手并接收输入数据的单元,输出执行相同的握手以输出数据。

python 寄存器 pipeline寄存器_数字通信


  理想情况下,输入和输出接口同时握手以获得最大带宽:即在相同的时钟周期内,输入接口接收到新的数据并将其放入内部寄存器中;此时该寄存器同时在输出接口被握手读出。但是,如果输出接口在给定周期内没有发送数据,则输入接口在该周期内也不能输入数据,否则旧数据将被覆盖。为避免此问题,上游输入边的ready接口应该在下游输出端ready未就绪的同一周期中声明自己未就绪。但这形成了它们之间的直接组合连接,而不是流水线连接。

  为了解决这个矛盾,我们需要一个额外的缓冲buffer来保存数据,此时输入接口正在获取数据但输出接口没有发送数据的情况,并且pipeline寄存器中已经有数据。然后,在下一个周期,输入接口可以发出信号它不再准备好,并且没有数据丢失。我们可以想象这个额外的缓冲buffer允许输入接口“延迟一拍”停止,而不是立即停止(与ready打一拍同步了),解决了之前的问题。

下面展示一些 内联代码片

`default_nettype none

module Pipeline_Skid_Buffer
#(
    parameter WORD_WIDTH = 0
)
(
    input   wire                        clock,
    input   wire                        clear,

    input   wire                        input_valid,
    output  wire                        input_ready,
    input   wire    [WORD_WIDTH-1:0]    input_data,

    output  wire                        output_valid,
    input   wire                        output_ready,
    output  wire    [WORD_WIDTH-1:0]    output_data
);

    localparam WORD_ZERO = {WORD_WIDTH{1'b0}};

数据路径

  数据路径的作用:在Buffer有数据时从buffer送数据给pipeline寄存器(输出数据寄存器),没有数据时从输入数据寄存。
  注意到,我们选择了将不同输入数据到单个输出寄存器(pipeline寄存器)的方案,而不是在两个相同的输出寄存器后加入MUX选通,从而避免新的单元和路径延迟。单个输出寄存器还能在下游继续重定时优化时序。
  选通的初始默认值为选通输入数据寄存,即认为buffer的“空”状态。因此默认情况下,第一个到达的数据最终一定会直接给到pipeline寄存器。我们不必担心此时选通信号的状态。

reg                     data_buffer_wren = 1'b0; // EMPTY at start, so don't load.
    wire [WORD_WIDTH-1:0]   data_buffer_out;

    Register
    #(
        .WORD_WIDTH     (WORD_WIDTH),
        .RESET_VALUE    (WORD_ZERO)
    )
    data_buffer_reg
    (
        .clock          (clock),
        .clock_enable   (data_buffer_wren),
        .clear          (clear),
        .data_in        (input_data),
        .data_out       (data_buffer_out)
    );

    reg                     data_out_wren       = 1'b1; // EMPTY at start, so accept data.
    reg                     use_buffered_data   = 1'b0;
    reg [WORD_WIDTH-1:0]    selected_data       = WORD_ZERO;

    always @(*) begin
        selected_data = (use_buffered_data == 1'b1) ? data_buffer_out : input_data;
    end

    Register
    #(
        .WORD_WIDTH     (WORD_WIDTH),
        .RESET_VALUE    (WORD_ZERO)
    )
    data_out_reg
    (
        .clock          (clock),
        .clock_enable   (data_out_wren),
        .clear          (clear),
        .data_in        (selected_data),
        .data_out       (output_data)
    );

控制路径

  我们将控制路径模块单独出来,因此数据路径模块不必知道有关当前状态或其编码的任何信息。

  控制路径为一个状态机,此FSM假定valid/ready握手信号的通常含义和行为——当两者都为高电平时,数据在时钟周期结束时传输。当无法接受数据时拉高ready或在无法发送数据时拉高valid都是错误的。这些协议层面的错误并不会被处理。

  要将使用我们的控制路径模块来操控数据路径用作缓冲区,我们需要了解我们希望允许它处于哪些状态,以及状态怎么转换。该pipeline缓冲器具有三种状态:

     1. Empty: Buffer和pipeline寄存器是空的。

     2. Busy: Pipeline寄存器满,此时等待新的数据或完成数据传送。

     3. Full: buffer和pipeline寄存器全满,必须等待pipeline寄存器及buffer都清空(将buffer加载进pipeline寄存器),否则“延迟一拍”将不再有效。

  在这些状态转换时的操作是:

    a. 将数据握手后送入数据路径的输入接口(+)

    b. 从数据路径中删除数据项的输出接口 ( -)

    c. 两个接口同时插入和移除 ( ±)

  我们还描述性地命名了状态之间的每个转换。这些名称稍后会出现在代码中。

python 寄存器 pipeline寄存器_fpga开发_02


  从状态图转移中我们可以看出,当datapath为空时,只能支持写入,而当datapath满时,只能支持读出。这些限制将在以后变得非常重要。 如果接口尝试在 Empty 时读出,或在 Full 时写入,则数据将分别重复或丢失。

  这个简单的 FSM 描述帮助我们澄清了问题,但它也掩盖了实现时的潜在复杂性:3 个状态中,总共两对握手信号,每个状态共有16个可能的组合转换,即总共 48 种可能的状态转换。

  没人会想手动枚举所有转换来合并等效的转换并排除所有不可能的情况。相反,如果我们用逻辑表达我们从状态图中确定的删除和插入的约束,以及数据路径上可能的转换,那么我们几乎可以轻松获得状态转换逻辑和数据路径控制信号逻辑。

  让我们描述数据路径的可能状态,并对其进行初始化。此代码描述了二进制状态编码,但CAD工具可以对状态编码进行重新编码和重新编号。

localparam STATE_BITS = 2;

    localparam [STATE_BITS-1:0] EMPTY = 'd0; // Output and buffer registers empty
    localparam [STATE_BITS-1:0] BUSY  = 'd1; // Output register holds data
    localparam [STATE_BITS-1:0] FULL  = 'd2; // Both output and buffer registers hold data
    // There is no case where only the buffer register would hold data.

    // No handling of erroneous and unreachable state 3.
    // We could check and raise an error flag.

    wire [STATE_BITS-1:0] state;
    reg  [STATE_BITS-1:0] state_next = EMPTY;

  现在,让我们表达我们从状态图中得出的约束:
     1. 输入接口只能在数据路径不处于FULL时接受数据。
     2. 只有当数据路径不为Empty时,输出接口才能读出数据。
  我们通过根据数据路径FSM的状态计算允许的读出时的valid/ready握手信号。我们使用state_next所以我们可以有很好的寄存器输出。这段代码删掉了大量无效的状态转换。
  这段代码很关键,因为它还暗示了pipeline缓冲区的基本操作假设:上游接口的当前状态不能依赖于下游接口的当前状态,否则会有组合路径也就起不到时序优化。
  计算上游的ready信号(非满不写)。

Register
    #(
        .WORD_WIDTH     (1),
        .RESET_VALUE    (1'b1) // EMPTY at start, so accept data
    )
    input_ready_reg
    (
        .clock          (clock),
        .clock_enable   (1'b1),
        .clear          (clear),
        .data_in        (state_next != FULL),
        .data_out       (input_ready)
    );

  计算下游的valid信号(非空可读)。

Register
    #(
        .WORD_WIDTH     (1),
        .RESET_VALUE    (1'b0)
    )
    output_valid_reg
    (
        .clock          (clock),
        .clock_enable   (1'b1),
        .clear          (clear),
        .data_in        (state_next != EMPTY),
        .data_out       (output_valid)
    );

  之后,让我们描述实现我们在数据路径模块上的两个基本操作的接口信号条件:读入和读出握手。这消除了许多可能的状态转换。

reg insert = 1'b0;
    reg remove = 1'b0;

    always @(*) begin
        insert = (input_valid  == 1'b1) && (input_ready  == 1'b1);
        remove = (output_valid == 1'b1) && (output_ready == 1'b1);
    end

  现在我们有了数据路径的状态和操作,让我们用它们来描述对数据路径的可能转换,以及它们可能发生的状态。你会看到这些准确地描述了状态图中的5条边,并且由于我们修剪了不必要的逻辑,使用了最少的逻辑来描述它们。

reg load    = 1'b0; // Empty datapath inserts data into output register.
    reg flow    = 1'b0; // New inserted data into output register as the old data is removed.
    reg fill    = 1'b0; // New inserted data into buffer register. Data not removed from output register.
    reg flush   = 1'b0; // Move data from buffer register into output register. Remove old data. No new data inserted.
    reg unload  = 1'b0; // Remove data from output register, leaving the datapath empty.

    always @(*) begin
        load    = (state == EMPTY) && (insert == 1'b1) && (remove == 1'b0);
        flow    = (state == BUSY)  && (insert == 1'b1) && (remove == 1'b1);
        fill    = (state == BUSY)  && (insert == 1'b1) && (remove == 1'b0);
        flush   = (state == FULL)  && (insert == 1'b0) && (remove == 1'b1);
        unload  = (state == BUSY)  && (insert == 1'b0) && (remove == 1'b1);
    end

  现在我们只需要计算每次数据路径转换后的下一个状态:

always @(*) begin
        state_next = (load   == 1'b1) ? BUSY  : state;
        state_next = (flow   == 1'b1) ? BUSY  : state_next;
        state_next = (fill   == 1'b1) ? FULL  : state_next;
        state_next = (flush  == 1'b1) ? BUSY  : state_next;
        state_next = (unload == 1'b1) ? EMPTY : state_next;
    end

    Register
    #(
        .WORD_WIDTH     (STATE_BITS),
        .RESET_VALUE    (EMPTY)         // Initial state
    )
    state_reg
    (
        .clock          (clock),
        .clock_enable   (1'b1),
        .clear          (clear),
        .data_in        (state_next),
        .data_out       (state)
    );

  同样,从控制FSM转换中,我们可以计算数据路径所需的所有控制信号。

always @(*) begin
        data_out_wren     = (load  == 1'b1) || (flow == 1'b1) || (flush == 1'b1);
        data_buffer_wren  = (fill  == 1'b1);
        use_buffered_data = (flush == 1'b1);
    end

endmodule

  对于64bit连接,生成的pipeline缓冲器使用128个寄存器,4到9个寄存器用于FSM和接口输出,具体取决于CAD工具选择的特定状态编码,并且很容易达到高运行速度。此致,欢迎学习和讨论,翻译自http://fpgacpu.ca/fpga/Pipeline_Skid_Buffer.html