一、寫在前面
-
首先,何為流水線CPU,流水線CPU和單周期CPU有什么差別?
單周期CPU上所有指令都在一個時鍾周期內完成,所以其時鍾周期一般較長(能夠完成最慢的指令),吞吐量不高。出於增大吞吐量的考慮,引入了流水線CPU,同一時刻有多條指令在其上運行,因此理論上五段流水CPU的吞吐量是單周期的五倍。——指導書
-
那么,如何實現多條指令同時運行?
五級流水線CPU按照指令運行的五個階段對應地分成了五個頂層模塊:
IF(instruction fetch)
、DEC(decode)
、EXE(execute)
、ME(memory)
、WB(write back)
,每個模塊執行一條指令的一個階段。
為了實現階段的分割與傳遞性,需要在單周期CPU里添加4個寄存器:
IF/DEC
、DEC/EXE
、EXE/MEM
、MEM/WB
,給予統一的時鍾與復位信號。每次時鍾上升沿到來,寄存器便會將前一個階段的信息傳遞給后一個階段,即將所有指令均向后傳遞一個階段,從而實現互不干擾,階段推進。寫回模塊后面不需要流水線寄存器的原因是:其一,它運行的是指令執行的最后一個階段,它的運行數據並不需要給對應指令的下一階段使用;其二,寫回部分是寄存器堆,本身可以看作“第五個流水線寄存器”。——指導書。
第五階段寄存器寫回寄存器不需要再等待一個時鍾上升沿,但讀出的rs與rt寄存器的值卻並不准確,這里考慮內部轉發,后文介紹,這里就不贅述了。
注意:①每條指令都要走完5個周期,盡管可能在某些階段沒有操作;②所有對寄存器的寫回,都在
WB
階段進行,這樣才能保證按照指令順序寫回寄存器,不會出現順序錯亂的情況。
-
實驗之前,需要注意什么?
因為我們是基於已搭建的單周期CPU,將其更改為流水線CPU,為了之后的操作更加順暢,這里建議:①確保單周期CPU搭建的正確性;②將盡可能多的部分模塊化;③合並多選器:將一位選擇指令合並為多位選擇指令。
二、單條指令的添加與測試
單周期CPU已經實現了單條指令單獨走完一個周期的正確性,那么在流水線CPU里,是不是不用額外更改就可實現單條指令的完成?當然不是的。
讀者可以注意以下幾點:
-
各條指令數據是否通過寄存器傳遞
-
各條指令的控制信號是否在正確階段產生
控制信號由控制模塊產生,但一個控制模塊只服務於一個指令,而D、E、M、W四個階段分別運行着4條不同的指令(F階段無需生成控制信號),很顯然一個control模塊是不夠用的。這里不妨采用分布式譯碼的方式(集中式譯碼讀者可自行探索),即放入4個control模塊,對每個階段的指令進行譯碼操作。
下圖為M階段的control模塊:
因為在進行該階段時,才需要判斷是否寫入存儲器,故我們只需要根據此階段的指令生成一個memw信號。
-
一些特殊指令:
nop
:什么都不用做j
、jal
與beq
:根據指令集,我們需要獲取PC高4位與指令生成跳轉地址,因為j
與jal
都在D階段判斷執行跳轉,故此二者都需從D階段取出(而不是F)。對於beq
指令,應注意此時IFU模塊里地址已為PC+4而不是PC。特需注意的是:jal
指令會將PC+4存入寄存器$31
中,而添加延時槽后實際存入的值為PC+8。為什么是PC+8:因為延時槽的存在,在跳轉或分支指令之后,還會執行一條延時槽指令,該指令在來不及地址跳轉的時候就已經被執行。
IFU模塊I/O數據:
-
關於調試:
關於mips中代碼的導出與導入機器碼,讀者可移步:在線教程 → ROM、RAM的使用和MARS導出機器碼的講解。
這個階段的測試非常重要!因為還不考慮轉發和阻塞,所以造數據的時候應確保不要沖突,觀察寄存器堆的時候直接雙擊寄存器堆上的放大鏡即可,點擊左側的模塊是無法查看實質內容的(哭,記得多加
nop
。
三、轉發
首先,介紹一些概念:
-
tnew和tuse:
對於指令的源寄存器s有時間tuse_D(/E/M),意思是在從D(/E/M)段開始,過幾個時鍾周期需要使用s里的數據。對於指令的目的寄存器des有數據tnew_D(/E/M),意思是從D(/E/M)段開始,過幾個時鍾周期將要寫入des的數據產生。——指導書
簡而言之,①tuse只在判斷阻塞的時候使用,指離源寄存器被使用的階段還有多少個時鍾周期。舉個例子:對於addu指令而言,
$rs
和$rt
寄存器的值在E階段被使用,故tuse_D_rs=1,因為D離E階段還有一個時鍾周期,而tuse_E_rs=0。②tnew判斷離該階段目的寄存器數據准備好還有多少個時鍾周期,當tnew=0,說明數據已經准備完畢,可以轉發。舉個例子,對於lw指令而言,其在M階段進行取出存儲器里數據的操作,故在W階段數據准備完畢,則對於lw指令,tnew_D=3,tnew_E=2,tnew_M=1,tnew_W=0。③對於每一個指令來說,其在每一個階段的tuse和tnew固定不變,那么,如何獲取每一個階段指令的tuse與tnew?在控制模塊用與或門的方式,添加控制信號即可:
-
內部轉發
對於寫回模塊到譯碼模塊(W到D)的轉發,由於寫回階段寫入和譯碼階段讀出都是對於寄存器堆,我們可以采用內部轉發:通過寄存器堆的結構使得,在同一個時鍾上升沿,若有數據寫入寄存器r,也有從寄存器r讀出數據的請求,把將寫入數據直接讀出。這樣一來,寫回模塊轉發到譯碼模塊的數據可以不通過外部轉發,當然外部轉發也可。
內部轉發邏輯:寄存器堆的RD1輸出:是0當且僅當A1輸入為0; 是將要寫入寄存器A3的數據當且僅當將要寫入的寄存器編號A3和讀取寄存器編號A1相等; 是寄存器A1的數據當且僅當將要寫入的寄存器編號A3和讀取寄存器編號A1不相等。 寄存器堆的RD2輸出:是0當且僅當A2輸入為0;是將要寫入寄存器A3的數據當且僅當將要寫入的寄存器編號A3和讀取寄存器編號A2相等; 是寄存器A2的數據當且僅當將要寫入的寄存器編號A3和讀取寄存器編號A2不相等。
——指導書
什么意思呢?我們需要在寄存器堆內部進行改造,看看圖就明白了:
那么,何時轉發?
-
轉發都是從流水線寄存器出發,傳遞給各個執行階段的
為什么會這樣?不如先來思考一下,如果想要轉發ALU運算結果,應該在什么位置轉發?在E階段,數據還在進行運算,只有到E階段末尾,數據才被運算完成。但我們無法判斷E階段末尾,而E/M寄存器中傳遞的值就是已經運算完成的ALU結果,所以應當轉發E/M寄存器右側數據,此時M階段tnew為0,數據轉發是有效的。
那么,從這個思路出發,我們就會發現:
-
一共有兩種轉發情況
- E/M流水線寄存器轉發: 轉發給E和D階段
- M/W流水線寄存器轉發:轉發給E,M,D階段。其中,轉發給D階段采用內部轉發方式。
可以發現,被轉發的數據只有E階段的運算結果和W階段的寫回數據,為什么沒有對M階段存入地址的判斷過程?因為存儲器訪問地址不會造成沖突!
-
注意進行第一種轉發情況時,需要對
jal
指令在W階段寫回$31
的數值進行轉發。
下面來看看具體模塊:
-
內部:
從此圖我們可以看見模塊對轉發操作的實現原理,當源寄存器等於目的寄存器且目的寄存器的值已經准備好的時候(tnew=0),進行轉發操作。當多個階段都滿足轉發條件,選擇數據最新的那個階段。
注意$0
始終為0,故不進行$0
的轉發。 -
外部
了解了內部邏輯,再來看模塊外觀,就比較好理解了。
說明:des_E不是指E階段ALU運算結果所在的目的寄存器,而是指M階段當前指令的目的寄存器。其含義雖是前者,但由於實際在M階段轉發,所以采用des_M來直觀表示。
四、阻塞
何時阻塞?
將要使用的數據來不及轉發過來,如果不阻塞就會使用錯誤的數據進行計算。由於分支指令最早在譯碼階段就需要使用寄存器數據計算,暫停階段放在譯碼階段,即DEC模塊。
結合開始介紹的概念,每條指令i在譯碼段時(DEC模塊運行指令i),要和它前面運行的指令j(前面模塊運行的指令j,一般是前面的所有模塊)對照判斷,看是否需要暫停:當i的源寄存器和j的目的寄存器相同時(設為寄存器k),i和j存在數據關聯,這時如果i的tuse小於j的tnew,代表k中數據還沒被j准備好(甚至不能轉發過來),這時需要暫停D段指令i,否則i會使用k中舊數據里錯誤運行。
——指導書
簡而言之:① 數據關聯;② 來不及轉發;③ 如何判斷:tuse < tnew
怎么阻塞?
將當前指令和其后面的指令全部“凍結”,而其前面的指令正常運行,以等待數據准備好。D段指令凍結也就是F/D寄存器仍然保持當前狀態,而此時F段中的pc已經取出下一條指令地址,為防止丟失,pc也保持當前狀態。再者,為了D段需要暫停的指令無法向后進行,需要在下一上升沿在E段插入氣泡。
故需要將D/E流水線寄存器同步復位(插入氣泡,使當前指令無法向下傳遞),且IF/DEC流水線寄存器和PC鎖定(維持當前狀態,即禁止使能),直到暫停的條件被打破。
——指導書
模塊外觀如下,記得添加非門:
五、整體調試
-
自造***鑽的數據,① 先全部執行看寄存器堆里的值是否符合預期、存儲器內的值是否正確,② 再單步調試看每一步每一階段狀態是否與mars一致(探針是個好東西。
-
看測評結果,測評結果中含有WA測試點的的當前測試指令和相鄰測試指令,將其轉為二進制,對照指令集可以看出是執行哪個指令的時候出了問題。不過,不一定是該指令本身出了問題,如果檢查后不是指令本身出錯,可能是以下兩種情況:
- 寫入的地址不對:跳轉或分支指令出錯
- 寫入的數據不對:涉及改變寄存器、存儲器數據的所有指令都有出錯可能。
-
這里給出一組測試數據,讀者可以試試
ori $2,$1,0x1234 lui $1,0x5678 addu $1,$1,$2 jal a nop beq $2,$1,a nop lw $2,0($0) a: sw $1,0($0) lw $2,0($0) sw $2,4($0)
v2.0 raw 34221234 3c015678 00220821 0c000c08 00000000 10410002 00000000 8c020000 ac010000 8c020000 ac020004
output_reg: $1: 0x56781234 $2: 0x56781234 $31: 0x00003014 output_mem: 0x56781234 0x56781234
(當然過了這組數據可能依然過不了...讀者可以根據自己的測評情況制造更***鑽的數據()