Half-Buffer与Skid-Buffer介绍及其在流水线中的应用

原创 网络交换FPGA 2024-05-16 16:39

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

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

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

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

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

  3. 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);    endendmodule

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 emptywire 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;    endend

我们可以看到,只有在输出侧在下一拍为低的时候,才拉高输入侧的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;    endend

    首先还是先来分析输入侧的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技巧。

5.参考

Half-Buffer:http://fpgacpu.ca/fpga/Pipeline_Half_Buffer.html;

Skid Buffer:http://fpgacpu.ca/fpga/Pipeline_Skid_Buffer.html

网络交换FPGA 秉承“工匠”精神,专注网络与交换领域FPGA开发与芯片实现,记录、分享与交流技术上的点点滴滴,与大家共同进步成长。
评论 (0)
  • 文/郭楚妤编辑/cc孙聪颖‍越来越多的企业开始蚕食动力电池市场,行业“去宁王化”态势逐渐明显。随着这种趋势的加强,打开新的市场对于宁德时代而言至关重要。“我们不希望被定义为电池的制造者,而是希望把自己称作新能源产业的开拓者。”4月21日,在宁德时代举行的“超级科技日”发布会上,宁德时代掌门人曾毓群如是说。随着宁德时代核心新品骁遥双核电池的发布,其搭载的“电电增程”技术也走进业界视野。除此之外,经过近3年试水,宁德时代在换电业务上重资加码。曾毓群认为换电是一个重资产、高投入、长周期的产业,涉及的利
    华尔街科技眼 2025-04-28 21:55 79浏览
  • 贞光科技代理品牌紫光国芯的车规级LPDDR4内存正成为智能驾驶舱的核心选择。在汽车电子国产化浪潮中,其产品以宽温域稳定工作能力、优异电磁兼容性和超长使用寿命赢得市场认可。紫光国芯不仅确保供应链安全可控,还提供专业本地技术支持。面向未来,紫光国芯正研发LPDDR5车规级产品,将以更高带宽、更低功耗支持汽车智能化发展。随着智能网联汽车的迅猛发展,智能驾驶舱作为人机交互的核心载体,对处理器和存储器的性能与可靠性提出了更高要求。在汽车电子国产化浪潮中,贞光科技代理品牌紫光国芯的车规级LPDDR4内存凭借
    贞光科技 2025-04-28 16:52 169浏览
  • 在智能硬件设备趋向微型化的背景下,语音芯片方案厂商针对小体积设备开发了多款超小型语音芯片方案,其中WTV系列和WT2003H系列凭借其QFN封装设计、高性能与高集成度,成为微型设备语音方案的理想选择。以下从封装特性、功能优势及典型应用场景三个方面进行详细介绍。一、超小体积封装:QFN技术的核心优势WTV系列与WT2003H系列均提供QFN封装(如QFN32,尺寸为4×4mm),这种封装形式具有以下特点:体积紧凑:QFN封装通过减少引脚间距和优化内部结构,显著缩小芯片体积,适用于智能门铃、穿戴设备
    广州唯创电子 2025-04-30 09:02 112浏览
  • 你是不是也有在公共场合被偷看手机或笔电的经验呢?科技时代下,不少现代人的各式机密数据都在手机、平板或是笔电等可携式的3C产品上处理,若是经常性地需要在公共场合使用,不管是工作上的机密文件,或是重要的个人信息等,民众都有防窃防盗意识,为了避免他人窥探内容,都会选择使用「防窥保护贴片」,以防止数据外泄。现今市面上「防窥保护贴」、「防窥片」、「屏幕防窥膜」等产品就是这种目的下产物 (以下简称防窥片)!防窥片功能与常见问题解析首先,防窥片最主要的功能就是用来防止他人窥视屏幕上的隐私信息,它是利用百叶窗的
    百佳泰测试实验室 2025-04-30 13:28 88浏览
  • 在CAN总线分析软件领域,当CANoe不再是唯一选择时,虹科PCAN-Explorer 6软件成为了一个有竞争力的解决方案。在现代工业控制和汽车领域,CAN总线分析软件的重要性不言而喻。随着技术的进步和市场需求的多样化,单一的解决方案已无法满足所有用户的需求。正是在这样的背景下,虹科PCAN-Explorer 6软件以其独特的模块化设计和灵活的功能扩展,为CAN总线分析领域带来了新的选择和可能性。本文将深入探讨虹科PCAN-Explorer 6软件如何以其创新的模块化插件策略,提供定制化的功能选
    虹科汽车智能互联 2025-04-28 16:00 139浏览
  • 浪潮之上:智能时代的觉醒    近日参加了一场课题的答辩,这是医疗人工智能揭榜挂帅的国家项目的地区考场,参与者众多,围绕着医疗健康的主题,八仙过海各显神通,百花齐放。   中国大地正在发生着激动人心的场景:深圳前海深港人工智能算力中心高速运转的液冷服务器,武汉马路上自动驾驶出租车穿行的智慧道路,机器人参与北京的马拉松竞赛。从中央到地方,人工智能相关政策和消息如雨后春笋般不断出台,数字中国的建设图景正在智能浪潮中徐徐展开,战略布局如同围棋
    广州铁金刚 2025-04-30 15:24 82浏览
  • 文/Leon编辑/cc孙聪颖‍2023年,厨电行业在相对平稳的市场环境中迎来温和复苏,看似为行业增长积蓄势能。带着对市场向好的预期,2024 年初,老板电器副董事长兼总经理任富佳为企业定下双位数增长目标。然而现实与预期相悖,过去一年,这家老牌厨电企业不仅未能达成业绩目标,曾提出的“三年再造一个老板电器”愿景,也因市场下行压力面临落空风险。作为“企二代”管理者,任富佳在掌舵企业穿越市场周期的过程中,正面临着前所未有的挑战。4月29日,老板电器(002508.SZ)发布了2024年年度报告及2025
    华尔街科技眼 2025-04-30 12:40 98浏览
  • 一、gao效冷却与控温机制‌1、‌冷媒流动设计‌采用低压液氮(或液氦)通过毛细管路导入蒸发器,蒸汽喷射至样品腔实现快速冷却,冷却效率高(室温至80K约20分钟,至4.2K约30分钟)。通过控温仪动态调节蒸发器加热功率,结合温度传感器(如PT100铂电阻或Cernox磁场不敏感传感器),实现±0.01K的高精度温度稳定性。2、‌宽温区覆盖与扩展性‌标准温区为80K-325K,通过降压选件可将下限延伸至65K(液氮模式)或4K(液氦模式)。可选配475K高温模块,满足材料在ji端温度下的性能测试需求
    锦正茂科技 2025-04-30 13:08 82浏览
  • 随着电子元器件的快速发展,导致各种常见的贴片电阻元器件也越来越小,给我们分辨也就变得越来越难,下面就由smt贴片加工厂_安徽英特丽就来告诉大家如何分辨的SMT贴片元器件。先来看看贴片电感和贴片电容的区分:(1)看颜色(黑色)——一般黑色都是贴片电感。贴片电容只有勇于精密设备中的贴片钽电容才是黑色的,其他普通贴片电容基本都不是黑色的。(2)看型号标码——贴片电感以L开头,贴片电容以C开头。从外形是圆形初步判断应为电感,测量两端电阻为零点几欧,则为电感。(3)检测——贴片电感一般阻值小,更没有“充放
    贴片加工小安 2025-04-29 14:59 124浏览
  • 网约车,真的“饱和”了?近日,网约车市场的 “饱和” 话题再度引发热议。多地陆续发布网约车风险预警,提醒从业者谨慎入局,这背后究竟隐藏着怎样的市场现状呢?从数据来看,网约车市场的“过剩”现象已愈发明显。以东莞为例,截至2024年12月底,全市网约车数量超过5.77万辆,考取网约车驾驶员证的人数更是超过13.48万人。随着司机数量的不断攀升,订单量却未能同步增长,导致单车日均接单量和营收双双下降。2024年下半年,东莞网约出租车单车日均订单量约10.5单,而单车日均营收也不容乐
    用户1742991715177 2025-04-29 18:28 121浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦