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) );
接下來是最為重要的控制部分,首先我們先來將系統劃分為以下幾個狀態:
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文章
19286瀏覽量
229843 -
流水線
+關注
關注
0文章
120瀏覽量
25739 -
模型
+關注
關注
1文章
3243瀏覽量
48840
原文標題:Half-Buffer與Skid-Buffer介紹及其在流水線中的應用
文章出處:【微信號:gh_cb8502189068,微信公眾號:網絡交換FPGA】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論