【CPU微架構設計】分散式多埠(4寫2讀)暫存器堆設計
暫存器堆(Register File)是微處理的關鍵部件之一。暫存器堆往往具有多個讀寫埠,其中寫埠往往與多個處理單元相對應。傳統的方法是使用集中式暫存器堆,即一個集中式暫存器堆匹配N個處理單元。隨著埠數量的增加,集中式暫存器堆的功耗、面積、時序均會呈冪增長,進而可能降低處理器總體效能。
下圖所示為傳統的集中式暫存器堆結構:
本文討論一種基於分佈儲存和麵積與時序互換原則的多埠暫存器堆設計,我們暫時稱之為“分散式暫存器堆”。該種暫存器從埠使用上,仍與集中式暫存器堆完全相容,但該暫存器堆使用多個暫存器簇和塊分散式地儲存運算元。當並行寫入時,各暫存器簇分散式地儲存寫入結果;當讀出時,由相應的仲裁演算法在多個暫存器簇的結果中選取確定的一個簇作為最終輸出。圖二顯示了這種分散式暫存器堆的邏輯結構。
該結構主要由區塊(Section)和簇(Cluster)兩個維度組織暫存器:
(1)、從Section層面看,Section 1和Section 2是兩個完全相同的組成結構(相當於邏輯的複製),同時兩個Section中暫存器所持有的運算元也完全相同。一個Section只能處理一個讀埠的操作。Section1負責處理讀埠#1,而Section2負責處理讀埠#2。
(2)、從Cluster層面看,Cluster A、B、C、D是幾個分別獨立的集中式暫存器堆,其具體結構可等價為一個雙口RAM。寫入時,Clusters分別獨立地寫入各自要求的地址。讀出時,各Cluster從相同的讀出地址讀出各自的資料,最後將4個數據送到MUX進行最後的仲裁。
綜上,單個Section負責單個讀埠操作,單個Cluster負責單個寫埠操作。
分散式暫存器堆的設計關鍵在於仲裁排程演算法。由於一個Section只負責處理一個讀埠的操作,我們首先從單個Section維度考慮。雖然單個週期內需要同時處理4個寫埠,但讀埠是唯一的。事先記錄每個寫埠的地址所對應的Cluster編號,在讀出時,通過讀出地址反向獲取對應的Cluster,進而從該Cluster取出最終結果。
其次再考慮多個讀埠並行操作。考慮複製兩個相同的Section邏輯,並將所有寫埠並聯起來,則可以保證兩個Section所持有的運算元完全相同。對兩個Section同時進行讀操作,這樣便實現了並行地讀出兩個不同地址的資料。
接下來設計兩個關鍵部件:Cluster暫存器堆和資料仲裁器。
一、Cluster暫存器的設計
上文已經提到過,Cluster暫存器可等價為雙埠RAM。關於DPRAM的具體結構可參考相關資料,本文不再贅述。DPRAM容量根據最為通用的配置,採用32x32bit設計,可通過例化引數設定地址匯流排和資料匯流排的寬度。利用Verilog描述一個同步時鐘DPRAM的原始碼如下:
1 module dpram_sclk 2 #( 3 parameter ADDR_WIDTH = 32, 4 parameter DATA_WIDTH = 32, 5 parameter CLEAR_ON_INIT = 1, // Whether to rest the RAM while initialization (for simulation only) 6 parameter ENABLE_BYPASS = 1 // Whether enable data bypass 7 ) 8 (/*AUTOARG*/ 9 // Outputs 10 dout, 11 // Inputs 12 clk, rst, raddr, re, waddr, we, din 13 ); 14 15 // Port List 16 input clk; 17 input rst; 18 input [ADDR_WIDTH-1:0] raddr; 19 input re; 20 input [ADDR_WIDTH-1:0] waddr; 21 input we; 22 input [DATA_WIDTH-1:0] din; 23 output [DATA_WIDTH-1:0] dout; 24 25 reg [DATA_WIDTH-1:0] mem[(1<<ADDR_WIDTH)-1:0]; 26 reg [DATA_WIDTH-1:0] rdata; 27 reg re_r; 28 wire [DATA_WIDTH-1:0] dout_w; 29 30 generate 31 if(CLEAR_ON_INIT) begin :clear_on_init 32 integer entry; 33 initial begin 34 for(entry=0; entry < (1<<ADDR_WIDTH); entry=entry+1) // reset 35 mem[entry] = {DATA_WIDTH{1'b0}}; 36 end 37 end 38 endgenerate 39 40 // bypass control 41 generate 42 if (ENABLE_BYPASS) begin : bypass_gen 43 reg [DATA_WIDTH-1:0] din_r; 44 reg bypass; 45 46 assign dout_w = bypass ? din_r : rdata; 47 48 always @(posedge clk) 49 if (re) din_r <= din; 50 51 always @(posedge clk) 52 if (waddr == raddr && we && re) 53 bypass <= 1; 54 else 55 bypass <= 0; 56 end else begin 57 assign dout_w = rdata; 58 end 59 endgenerate 60 61 // R/W logic 62 always @(posedge clk) 63 re_r <= rst ? 1'b0 : re; 64 65 assign dout = re_r ? dout_w : {DATA_WIDTH{1'b0}}; 66 67 always @(posedge clk) begin 68 if (we) 69 mem[waddr] <= din; 70 if (re) 71 rdata <= mem[raddr]; 72 end 73 74 endmodulemodule dpram_sclk
值得注意的是,實現中加入了資料旁通機制,保證當發生同時讀寫且地址相同時,寫入資料能在單個操作週期內送達讀埠。
二、資料仲裁器的設計
資料仲裁器的核心是維護一個寫入地址→Cluster編號的對映表。假設分散式暫存器堆總共有32個暫存器(以5bit地址匯流排定址),則我們需要一個表項數為32的列表,儲存每個地址對應的Cluster編號。Verilog描述如下:
reg [1:0] sel_map[(1<<ADDR_WIDTH)-1:0];
對於每個寫埠,在寫操作週期維護對映表,記錄下寫入地址對應的Cluster,實現如下:
1 // Maintain the selection map 2 always @(posedge clk) begin 3 if (we1) 4 sel_map[waddr1] <= 2'd0; 5 if (we2) 6 sel_map[waddr2] <= 2'd1; 7 if (we3) 8 sel_map[waddr3] <= 2'd2; 9 if (we4) 10 sel_map[waddr4] <= 2'd3; 11 end
對於讀埠,先從對映表獲取實際儲存目標運算元的Cluster,然後利用多路複用器選取其輸出,作為該Section的最終讀取結果。
1 // mux 2 assign dout = sel_map[raddr]==2'd0 ? dout0 : 3 sel_map[raddr]==2'd1 ? dout1 : 4 sel_map[raddr]==2'd2 ? dout2 : 5 sel_map[raddr]==2'd3 ? dout3 : 6 {DATA_WIDTH{1'b0}}; /* never got this */
由此,我們可以得出單個Section的完整設計:
1 module cpram_sclk_4w1r #( 2 parameter ADDR_WIDTH = 5, 3 parameter DATA_WIDTH = 32, 4 parameter CLEAR_ON_INIT = 1, // Whether to rest the RAM while initialization (for simulation only) 5 parameter ENABLE_BYPASS = 1 // Whether enable data bypass 6 ) 7 (/*AUTOARG*/ 8 // Outputs 9 rdata, 10 // Inputs 11 clk, rst, we1, waddr1, wdata1, we2, waddr2, wdata2, we3, waddr3, 12 wdata3, we4, waddr4, wdata4, re, raddr 13 ); 14 15 // Ports 16 input clk; 17 input rst; 18 input we1; 19 input [ADDR_WIDTH-1:0] waddr1; 20 input [DATA_WIDTH-1:0] wdata1; 21 input we2; 22 input [ADDR_WIDTH-1:0] waddr2; 23 input [DATA_WIDTH-1:0] wdata2; 24 input we3; 25 input [ADDR_WIDTH-1:0] waddr3; 26 input [DATA_WIDTH-1:0] wdata3; 27 input we4; 28 input [ADDR_WIDTH-1:0] waddr4; 29 input [DATA_WIDTH-1:0] wdata4; 30 input re; 31 input [ADDR_WIDTH-1:0] raddr; 32 output [DATA_WIDTH-1:0] rdata; 33 34 // Internals 35 wire [DATA_WIDTH-1:0] dout; 36 wire [DATA_WIDTH-1:0] dout0; 37 wire [DATA_WIDTH-1:0] dout1; 38 wire [DATA_WIDTH-1:0] dout2; 39 wire [DATA_WIDTH-1:0] dout3; 40 reg [1:0] sel_map[(1<<ADDR_WIDTH)-1:0]; 41 42 // instance of sync dpram #1 for Cluster A 43 dpram_sclk 44 #( 45 .ADDR_WIDTH (ADDR_WIDTH), 46 .DATA_WIDTH (DATA_WIDTH), 47 .CLEAR_ON_INIT (CLEAR_ON_INIT), 48 .ENABLE_BYPASS (ENABLE_BYPASS) 49 ) 50 mem0 51 ( 52 .clk (clk), 53 .rst (rst), 54 .dout (dout0), 55 .raddr (raddr), 56 .re (re), 57 .waddr (waddr1), 58 .we (we1), 59 .din (wdata1) 60 ); 61 // instance of sync dpram #2 for Cluster B 62 dpram_sclk 63 #( 64 .ADDR_WIDTH (ADDR_WIDTH), 65 .DATA_WIDTH (DATA_WIDTH), 66 .CLEAR_ON_INIT (CLEAR_ON_INIT), 67 .ENABLE_BYPASS (ENABLE_BYPASS) 68 ) 69 mem1 70 ( 71 .clk (clk), 72 .rst (rst), 73 .dout (dout2), 74 .raddr (raddr), 75 .re (re), 76 .waddr (waddr2), 77 .we (we2), 78 .din (wdata2) 79 ); 80 // instance of sync dpram #3 for Cluster C 81 dpram_sclk 82 #( 83 .ADDR_WIDTH (ADDR_WIDTH), 84 .DATA_WIDTH (DATA_WIDTH), 85 .CLEAR_ON_INIT (CLEAR_ON_INIT), 86 .ENABLE_BYPASS (ENABLE_BYPASS) 87 ) 88 mem2 89 ( 90 .clk (clk), 91 .rst (rst), 92 .dout (dout3), 93 .raddr (raddr), 94 .re (re), 95 .waddr (waddr3), 96 .we (we3), 97 .din (wdata3) 98 ); 99 // instance of sync dpram #4 for Cluster D 100 dpram_sclk 101 #( 102 .ADDR_WIDTH (ADDR_WIDTH), 103 .DATA_WIDTH (DATA_WIDTH), 104 .CLEAR_ON_INIT (CLEAR_ON_INIT), 105 .ENABLE_BYPASS (ENABLE_BYPASS) 106 ) 107 mem3 108 ( 109 .clk (clk), 110 .rst (rst), 111 .dout (dout3), 112 .raddr (raddr), 113 .re (re), 114 .waddr (waddr4), 115 .we (we4), 116 .din (wdata4) 117 ); 118 119 // mux 120 assign dout = sel_map[raddr]==2'd0 ? dout0 : 121 sel_map[raddr]==2'd1 ? dout1 : 122 sel_map[raddr]==2'd2 ? dout2 : 123 sel_map[raddr]==2'd3 ? dout3 : 124 {DATA_WIDTH{1'b0}}; /* never got this */ 125 126 // Read output with/without bypass controlling 127 generate 128 if (ENABLE_BYPASS) begin : bypass_gen 129 assign rdata = 130 (we1 && (raddr==waddr1)) ? wdata1 : 131 (we2 && (raddr==waddr2)) ? wdata2 : 132 (we3 && (raddr==waddr3)) ? wdata3 : 133 (we4 && (raddr==waddr4)) ? wdata4 : 134 dout; 135 end else begin 136 assign rdata = dout; 137 end 138 endgenerate 139 140 // Maintain the selection map 141 always @(posedge clk) begin 142 if (we1) 143 sel_map[waddr1] <= 2'd0; 144 if (we2) 145 sel_map[waddr2] <= 2'd1; 146 if (we3) 147 sel_map[waddr3] <= 2'd2; 148 if (we4) 149 sel_map[waddr4] <= 2'd3; 150 end 151 endmodule
值得說明的是:上述實現仍然需要考慮四個寫埠與一個讀埠的資料旁通路徑。通過例化引數ENABLE_BYPASS可以指定是否使用資料旁通邏輯。
將兩個Section寫埠並聯,並分別引出其讀埠,即構成了一個4w 2r暫存器堆。
總結
本文討論的資料的分佈儲存方法和基於面積換時序的邏輯複製方法,在集中儲存式暫存器堆的優化中取得了較好的效果。但該結構缺點也十分明顯:首先,對集中暫存器堆的複製無疑增加了面積和功耗,其次,隨著寫埠數的增加,仲裁邏輯的規模也隨之增長,這將導致資料路徑延遲增加,進而降低暫存器堆時鐘工作頻率。
相比於ASIC設計,本結構更適合於FPGA驗證。理由如下:在FPGA中,暫存器是非常有限的資源。若直接實現一定規模的RAM結構,則需要大量佔用暫存器資源。為此,FPGA在硬體上集成了RAM Block資源。這些RAM Blocks多可配置為雙埠模式,但對於更復雜的埠配置(如本例的4w2r),則只能間接實現。
本設計完全採用RAM Blocks實現多埠暫存器堆。因為FPGA綜合工具在分析verilog原始碼時,將自動識別出我們所採用的DPRAM,轉而使用RAM Block資源,避免佔用暫存器資源,為設計的其它部分留出更多可用的暫存器資源。同時,這將避免使用LUT實現複雜的RAM單元定址和控制邏輯,從而優化時序。
2018 10.20
===================================================
本博文僅供參考,難免有疏漏之處,歡迎提出寶貴意見。