0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

流水线中Half-Buffer与Skid-Buffer的使用

网络交换FPGA 来源:网络交换FPGA 2024-11-05 17:16 次阅读

1.问题描述

在介绍skid buffer之前,我们先来假设这样一种情况,在一个多级流水模型之中,比如最为经典的顺序五级流水的处理器模型中,各级之间通过仅通过valid-ready的握手信号进行数据传递,(需要注意的是,这里的输入侧和输出侧的握手信号是不建议直连的,这样不符合流水设计思想的同时,还会加中时序压力)当其中某级发生阻塞的时候,比如lsu的执行访存指令,但是cache未命中,需要从更下级的储存器去请求数据的时候,此时需要通过握手信号来需要阻塞流水线,理所应当的,我们拉低lsu的input_ready信号来阻塞来自上级流水的输入(比如EXU),可是问题是此时上上级(比如IDU)并未被阻塞,还在向上级(EXU)传输数据,同样的情况发生在所有的上游模块。这篇文章便是用来解决上述问题。

a2b84bf6-90b3-11ef-a511-92fbcf53809c.png

2.Half-Buffer

2.1 Half-Buffer是什么?

引发上述问题的原因是未能及时阻塞之前的流水线,再深究其原因,是因为其输入侧和输出侧的握手允许在相同时钟周期完成,所以阻塞的信息没有同步到上级。为了解决以上问题,我们现在为流水线每级做如下限定:

1.输入侧和输出侧不能同时完成握手操作。

2.在没有有效数据之前之前只能进行输入握手,在内部有有效数据后,只能做输出握手,在完成握手后才能重新开始输入。

而这种方法叫做Half-Buffer,他内部只有一个buffer来缓存数据,所以他不支持输入和输出侧同时完成握手。他的缺点是显而易见的,每次启动或停止的时候需要两个时钟周期的同时,还让最大带宽减半。但是,对于内部需要多个时钟周期来计算结果的模块而言,其影响并没有那么大。

2.2 Half-Buffer源码分析

这里我们选取fpgacpu网站上的源码进行讲解,网址会帖在文末。

首先是接口部分,需要注意的是,此处的CIRCULAR_BUFFER部分非0 时候,是允许内部有效数据在未完成输出侧握手的情况下接受新数据对原有数据进行覆盖的。因为这种模式我们使用不多,这里现不做介绍。

`default_nettype none
 
module Pipeline_Half_Buffer
#(
    parameter WORD_WIDTH            = 0,
    parameter CIRCULAR_BUFFER       = 0     // non-zero to enable
)
(
    input  wire                     clock,
    input  wire                     clear,
 
    input  wire                     input_valid,
    output reg                      input_ready,
    input  wire [WORD_WIDTH-1:0]    input_data,
 
    output reg                      output_valid,
    input  wire                     output_ready,
    output wire [WORD_WIDTH-1:0]    output_data
);
 
    localparam WORD_ZERO = {WORD_WIDTH{1'b0}};

这部分是half_buffer部分,可以看到其内部只有一个buffer用来储存数据:

    reg half_buffer_load = 1'b0;
 
    Register
    #(
        .WORD_WIDTH     (WORD_WIDTH),
        .RESET_VALUE    (WORD_ZERO)
    )
    half_buffer
    (
        .clock          (clock),
        .clock_enable   (half_buffer_load),
        .clear          (clear),
        .data_in        (input_data),
        .data_out       (output_data)
    );

空满信号的产生模块:

    reg  set_to_empty = 1'b0;
    reg  set_to_full  = 1'b0;
    wire buffer_full;
 
    Register
    #(
        .WORD_WIDTH     (1),
        .RESET_VALUE    (1'b0)
    )
    empty_full
    (
        .clock          (clock),
        .clock_enable   (set_to_full),
        .clear          (set_to_empty),
        .data_in        (1'b1),
        .data_out       (buffer_full)
    );

然后是最为重要的逻辑模块,我们可以发现,在非循环模式下,input_ready和output_valid是互斥的,这也就完成了我们之前所说的每次只能完成一边的握手。
在完成输入握手之后将full信号拉高,并将数据写入buffer,在完成输出握手之后,将empty信号拉高。同时我们看到,在初始情况下,内部为empty,所以必须先完成empty->full->empty这个流程,这与我们预期相符。

 always @(*) begin
        input_ready      = (buffer_full   == 1'b0) || (CIRCULAR_BUFFER != 0);
        output_valid     = (buffer_full   == 1'b1);
        set_to_full      = (input_valid   == 1'b1) && (input_ready  == 1'b1);
        set_to_empty     = (output_valid  == 1'b1) && (output_ready == 1'b1) && (set_to_full == 1'b0);
        set_to_empty     = (set_to_empty  == 1'b1) || (clear == 1'b1);
        half_buffer_load = (set_to_full   == 1'b1);
    end
 
endmodule

3.Skid Buffer

3.1 Skid Buffer是什么?

那么有没有其他方法能够解决问题的同时,避免到Half-Buffer带来的损耗呢?如果输入输出同时允许握手带来的后果是可能在阻塞的情况下冲刷掉内部的有效数据,那么如果我们让内部不止一个Buffer是不可以解决这个问题呢?

Skid Buffer就是这么来的,它其实是是一个最小的FIFO,深度为2,一个用于输出,一个用来缓存,同时在缓存的这个周期,就能将下一级的阻塞信号传递到上级,这样便可以在允许两次同时握手,消除Half-Buffer带来的两个周期和最大带宽的损耗的同时,拥有更好的布局布线空间。

3.2 Skid Buffer源码分析

这里我们同样选取fpgacpu网站上的源码进行讲解(ps:这个真的是最近发现的最宝藏的网站,之后如果有时间,可以会出一个专门介绍和解析这个网站源码的一个专栏)

首先是接口部分,需要注意的是,此处的CIRCULAR_BUFFER部分非0 时候,是指可以在内部数据已经满的情况下,进行覆盖,同理,我们对该模式不做解析。

`default_nettype none
 
module Pipeline_Skid_Buffer
#(
    parameter WORD_WIDTH                = 0,
    parameter CIRCULAR_BUFFER           = 0     // non-zero to enable
)
(
    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,data_buffer_out为缓存buffer,output_Data为输出的数据,通过2mux1来决定输出来源于缓存还是input_data。他这个地方还有个聪明之处在于他将数据通路和状态解耦,这样大大的便捷了整体的设计,是一个值得学习的地方。

 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)
    );

接下来是最为重要的控制部分,首先我们先来将系统划分为以下几个状态:

a2deb5a2-90b3-11ef-a511-92fbcf53809c.png

Empty:输出和缓存区都没有数据。

Busy :在输出寄存器有一个有效值待处理,缓存区为空。

Full : 输出寄存器和缓存区都有有效数据待处理 。

需要注意的是,在Empty下,只支持输入侧的握手,在Full模式下,只支持输出侧的握手,这样可以有效防止数据的覆盖和重复读取。

我们来看一下每个状态之间的转换条件:

load:缓存区和输出寄存器为空,数据直接载入输出寄存器。(输入握手,输出没握手)

fill:输出寄存器为空,将数据载入缓存区。(输入握手,输出没握手)

flow:输出寄存器的值被下级接收的同时,将输入的数据载入到输出寄存器。(输入输出同时握手)

flush:输出寄存器的值被下级接受,将缓存区的有效数据载入输出寄存器(输入没握手,输出握手)

unload:输出寄存器的被下级接受,现在输出和缓存区都为空。(输入没握手,输出握手)。

在得到所有的转化条件之后,我们还需要去决定输入的ready和输出valid信号。我们只需要在当前非满时拉高ready信号,在当前非空的时候拉高valid信号即可。

    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) || (CIRCULAR_BUFFER != 0)),
        .data_out       (input_ready)
    );
 
    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

最后便是状态的转化和数据通路的选择部分,在此不做赘述。

    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.
    reg dump    = 1'b0; // New inserted data into buffer register. Move data from buffer register into output register. Discard old output data. (CBM)
    reg pass    = 1'b0; // New inserted data into buffer register. Move data from buffer register into output register. Remove old output data.  (CBM)
    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);
        unload  = (state == BUSY)  && (insert == 1'b0) && (remove == 1'b1);
        flush   = (state == FULL)  && (insert == 1'b0) && (remove == 1'b1);
        dump    = (state == FULL)  && (insert == 1'b1) && (remove == 1'b0) && (CIRCULAR_BUFFER != 0);
        pass    = (state == FULL)  && (insert == 1'b1) && (remove == 1'b1) && (CIRCULAR_BUFFER != 0);
    end
    always @(*) begin
        data_out_wren     = (load  == 1'b1) || (flow == 1'b1) || (flush == 1'b1) || (dump == 1'b1) || (pass == 1'b1);
        data_buffer_wren  = (fill  == 1'b1)                                      || (dump == 1'b1) || (pass == 1'b1);
        use_buffered_data = (flush == 1'b1)                                      || (dump == 1'b1) || (pass == 1'b1);
    end
endmodule

4.刚玉中的流水代码分析

在开源代码刚玉中大量运用了流水线,我们以其为例子进行分析。我们以其axi_register_rd中对于ar port的流水处理进行分析。

刚玉采用了三种可选方式,bypass,Half-Buffer以及Skid-Buffer。我们针对其后两种进行分析。需要说明的是,其中s_axi为输入侧,m_axi为输出侧。ps:刚玉的作者Alex的代码水平真的十分高,他经常用一些互斥条件的组合来代替状态机的书写,所以对我来说想要理解往往需要花费一定的时间。

4.1 刚玉中的Half-Buffer

// enable ready input next cycle if output buffer will be empty
wire s_axi_arready_early = !m_axi_arvalid_next;
 
always @* begin
    // transfer sink ready state to source
    m_axi_arvalid_next = m_axi_arvalid_reg;
 
    store_axi_ar_input_to_output = 1'b0;
    if (s_axi_arready_reg) begin
        m_axi_arvalid_next = s_axi_arvalid;
        store_axi_ar_input_to_output = 1'b1;
    end else if (m_axi_arready) begin
        m_axi_arvalid_next = 1'b0;
    end
end

我们可以看到,只有在输出侧在下一拍为低的时候,才拉高输入侧的ready信号,保证每一拍只有一侧的握手是可以完成的。

然后在输入侧ready的情况下,将上一级的有效信号传递到输出寄存器,这里比较有意思的是,他没有等到输入握手成功再传递,而是直接传递,这是因为输入侧的ready和输出侧的valid是互斥的,即使没有握手就传递,也不会出现两边同时握手的情况。

如果输入侧的ready无效,但是输入侧的ready有效时,将下一拍的输出侧的有效信号拉低,我当初看到这里很疑惑,后来一想其实很简单,因为输入侧的ready无效就意味着当前拍的输出侧valid肯定是拉高的,这句话其实可以理解成完成输出侧握手后,将已经处理过的有效信号拉低的操作。

4.2 刚玉中的Skid-Buffer

wire s_axi_arready_early = m_axi_arready | (~temp_m_axi_arvalid_reg & (~m_axi_arvalid_reg | ~s_axi_arvalid));
 
always @* begin
    // transfer sink ready state to source
    m_axi_arvalid_next = m_axi_arvalid_reg;
    temp_m_axi_arvalid_next = temp_m_axi_arvalid_reg;
 
    store_axi_ar_input_to_output = 1'b0;
    store_axi_ar_input_to_temp = 1'b0;
    store_axi_ar_temp_to_output = 1'b0;
    if (s_axi_arready_reg) begin
        // input is ready
        if (m_axi_arready | ~m_axi_arvalid_reg) begin
            // output is ready or currently not valid, transfer data to output
            m_axi_arvalid_next = s_axi_arvalid;
            store_axi_ar_input_to_output = 1'b1;
        end else begin
            // output is not ready, store input in temp
            temp_m_axi_arvalid_next = s_axi_arvalid;
            store_axi_ar_input_to_temp = 1'b1;
        end
    end else if (m_axi_arready) begin
        // input is not ready, but output is ready
        m_axi_arvalid_next = temp_m_axi_arvalid_reg;
        temp_m_axi_arvalid_next = 1'b0;
        store_axi_ar_temp_to_output = 1'b1;
    end
end

首先还是先来分析输入侧的ready信号,可以看到,他拉高的条件有两个,首先是输入侧的ready为高,这是为什么?我们来简单分析一下,当输出侧的ready为高的时候,他的输出寄存器主要有效,那么一定会被读取,所以当前状态永远不会是full,所以可以拉高。

第二个条件:

(~temp_m_axi_arvalid_reg & (~m_axi_arvalid_reg | ~s_axi_arvalid))

我们来解析一下,首先他要求缓存寄存器为空的同时,输入侧和输出寄存器不能同时有待处理的请求,这个也很好理解,我们这个系统最大的待处理请求只能是两个,如果不满足以上条件,那么系统中可能会出现待处理请求,缓存区的请求有被覆盖的风险。

    if (s_axi_arready_reg) begin
        // input is ready
        if (m_axi_arready | ~m_axi_arvalid_reg) begin
            // output is ready or currently not valid, transfer data to output
            m_axi_arvalid_next = s_axi_arvalid;
            store_axi_ar_input_to_output = 1'b1;
        end else begin
            // output is not ready, store input in temp
            temp_m_axi_arvalid_next = s_axi_arvalid;
            store_axi_ar_input_to_temp = 1'b1;
        end

然后就是接下来的部分,我们看到,在输入侧ready的情况下,如果输出侧ready有效或者没有待处理的请求时,可以将新的请求从输入加载到输出寄存器。又是很奇怪是不是?这里真的感叹一句Alex的水平之高,好了,我们来认真分析一下,如果输出侧ready有效,那意味着当前状态不为full,那么任何被传递的请求都是可以被下级处理的,同理,如果下级已经没有待处理的请求,那么自然可以加载新的有效请求。然后,如果下级不能处理新的请求的时候,也就是对应我们之前的BUSY状态下,可以完成输入侧握手,不能完成输出侧握手的时候,我们就需要把输入侧的请求存入缓存区。

    end else if (m_axi_arready) begin
        // input is not ready, but output is ready
        m_axi_arvalid_next = temp_m_axi_arvalid_reg;
        temp_m_axi_arvalid_next = 1'b0;
        store_axi_ar_temp_to_output = 1'b1;
    end

最后,便是输出侧可以完成握手,但是输入侧不能完成的时候,对应之前的flush状态,输出寄存器被下级读取之后,我们把缓存区的数据载入到输出寄存器即可。

5.结语

文章主要分析了流水线中的Half-Buffer与Skid-Buffer的使用,之后如果有机会,将继续分享更多DE技巧。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    19283

    浏览量

    229796
  • 流水线
    +关注

    关注

    0

    文章

    120

    浏览量

    25737
  • 模型
    +关注

    关注

    1

    文章

    3243

    浏览量

    48835

原文标题:Half-Buffer与Skid-Buffer介绍及其在流水线中的应用

文章出处:【微信号:gh_cb8502189068,微信公众号:网络交换FPGA】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    FPGA流水线设计

    处理速度)。第二 什么时候用流水线设计使用流水线一般是时序比较紧张,对电路工作频率较高的时候。典型情况如下:1)功能模块之间的流水线,用乒乓 buffer 来交互数据。代价是增加了 m
    发表于 10-26 14:38

    现代RISC流水线技术

    流水线技术是提高系统吞吐率的一项强大的实现技术,并且不需要大量重复设置硬件。20世界60年代早期的一些高端机器第一次采用了流水线技术。第一个采用指令流水线的机器是IBM7030(又称
    发表于 03-01 17:52

    周期精确的流水线仿真模型

    使用软件仿真硬件流水线是很耗时又复杂的工作,仿真过程由于流水线的冲突而导致运行速度缓慢。本文通过对嵌入式处理器的流水线, 指令集, 设备控制器等内部结构的分析和
    发表于 12-31 11:30 9次下载

    什么是流水线技术

    什么是流水线技术 流水线技术
    发表于 02-04 10:21 3927次阅读

    流水线的相关培训教程[1]

    流水线的相关培训教程[1]  学习目标     理解流水线相关的分类及定义;
    发表于 04-13 15:56 1050次阅读

    流水线的相关培训教程[3]

    流水线的相关培训教程[3] (1) 写后读相关(RAW:Read After Write) (命名规则) :j 的执行要用到 i 的计算结果,当它们在流水线重叠执行时,j 可
    发表于 04-13 16:02 845次阅读

    流水线的相关培训教程[4]

    流水线的相关培训教程[4] 下面讨论如何利用编译器技术来减少这种必须的暂停,然后论述如何在流水线实现数据相关检测和定向。
    发表于 04-13 16:09 4804次阅读

    电镀流水线的PLC控制

    电镀流水线的PLC控制电镀流水线的PLC控制电镀流水线的PLC控制
    发表于 02-17 17:13 36次下载

    采用单通道通讯协议设计高速异步流水线控制器STFB电路的设计

    单元GasP电路,在文中提出的准延时无关QDI异步流水线控制单元WCHB(weak condition half buffer)、PCHB(precharged fullbuffer),以及在文中提出的基于单通道通讯协议的QDI
    的头像 发表于 08-30 08:04 2749次阅读
    采用单通道通讯协议设计高速异步<b class='flag-5'>流水线</b>控制器STFB电路的设计

    各种流水线特点及常见流水线设计方式

    按照流水线的输送方式大体可以分为:皮带流水装配线、板链线、倍速链、插件线、网带线、悬挂线及滚筒流水线这七类流水线
    的头像 发表于 07-05 11:12 7351次阅读
    各种<b class='flag-5'>流水线</b>特点及常见<b class='flag-5'>流水线</b>设计方式

    滚筒输流水线故障排除方法

    在工程建造,滚筒流水线演着重要的角色。在一些工程建造过程,经常看到滚筒流水线的身影。在工业不断发展下的今天,滚筒流水线日益增长,走向多元
    发表于 07-08 09:32 1907次阅读

    如何选择合适的LED生产流水线输送方式

    LED生产流水线输送形式分为平面直线传输流水线、各种角度平面转弯传输流水线、斜面上传流水线、斜面下传流水线这四种输送方式,企业也是可以根据L
    发表于 08-06 11:53 1028次阅读

    嵌入式_流水线

    ,每个子过程由专门的功能部件来实现。• 把多个处理过程在时间上错开,依次通过各功能段,这样,每个子过程就可以与其他的子过程并行进行。流水线的每个子过程及其功能部件称为流水线的级或段,段与段相互连接形成
    发表于 10-20 20:51 6次下载
    嵌入式_<b class='flag-5'>流水线</b>

    CPU流水线的问题

    1989 年推出的 i486 处理器引入了五级流水线。这时,在 CPU 不再仅运行一条指令,每一级流水线在同一时刻都运行着不同的指令。这个设计使得 i486 比同频率的 386 处理器性能提升了不止一倍。
    的头像 发表于 09-22 10:04 2007次阅读

    什么是流水线 Jenkins的流水线详解

    jenkins 有 2 种流水线分为声明式流水线与脚本化流水线,脚本化流水线是 jenkins 旧版本使用的流水线脚本,新版本 Jenkin
    发表于 05-17 16:57 1086次阅读