五級流水線衝突的解決以及算法在流水線上的運用

  • 實驗目的
    1理解五級流水線中衝突的產生條件以及類型模塊,分析衝突對於流水線正常操作的影響以及它們可能會帶來的錯誤執行結果,同時尋求解決方案,並改進原有代碼使得流水線可以正常運行。
    2通過理解各種算法的運用,即解析它們的流程需要的指令,在本實驗中包括最小公倍數、最大公因數、64位加法、冒泡排序等算法的解析,然後在五級流水線上進行跑指令運算,類似於上一次對於init_test的指令測試集,然後在板子上進行驗證。
    3、理解這3次項目來的整體思想,加深對於工程搭建以及五級流水線的理解,同時總結整一個項目下來的感想。
  • 原理分析
  1. Hazard衝突的理解

對於之前的五級流水線的工程構建,我們一直都沒有談到衝突這麼一回事,那麼爲什麼會突然冒出這個東西出來呢?或者說它的存在是累贅嗎?或者只是單單爲了增強流水線的某方面性能呢?顯然答案是衝突是個非常嚴重的問題,之前我們之所以沒有談到這個問題,同時它也彷彿沒有給我們帶來什麼困擾,是因爲我們的指令代碼幾乎每一行的後面都加3了個Nop指令,這是解決衝突的方法,但顯然太笨了我們不可能對於一般的指令都去刻意地添加Nop使得程序得以正常運行,同時如果我們在上一次寫代碼的時候有留意的話,也會發現其實一些指令的後面加了Nop與沒加是一樣,但有一些確實一定要加的,其實當時我們可能可以大致推測到只要後面的兩三條指令與前面指令的寄存器沒有關係,那麼就沒有必要添加Nop,都能想到這一步了,那麼問題也就開始接近我們的主題了,這其實就是我們遇到的一個常見的衝突,前面的指令對於某一個寄存器進行了賦值,但是下一條指令立即就要求使用該寄存器的值,那麼這時候就會使用到之前留給該寄存器的值,而不是我們想要的,因爲我們上一條指令的值還沒有等到它賦給該寄存器的時間週期,我們就已經對它進行取值了,這就是一種衝突,只是舉個例子,下面我們進行詳細的講解。

         流水線的衝突對於具體的流水線來說,其實就是由於某些相關的存在使得指令流的下一條或者幾條指令不能在指定的時鐘準確執行。流水線的衝突通常包括了兩種類型,第一是我們剛剛提到的數據衝突,我們可以理解爲在指令執行時,我們的指令需要取到前一條指令還沒有最終寫到的寄存器的數值,這時就會取到錯誤的數值,專業一點的說法就是我們在安排指令時,當有關的指令(這裏我們假定他們會使用到前面的指令處理的寄存器),靠得足夠近時,它們在流水線中的重疊執行或者重新排序會改變指令讀寫操作數的順序,使之不同於它們非流水實現時的順序。我們可以簡單借用一下老師上課時講過的例子來看,左邊是指令,右邊是它們的一個執行週期列表,NG表示該指令出現了錯誤,而OK則表示可以正常運行,如圖:

   

        可以看到在第一條指令在第一個時鐘週期執行之後,第二個時鐘週期到來時,我們開始執行第二條指令,因爲它的加法指令用到了3號寄存器,而3號寄存器的值需要剛好前一條指令加法執行後的結果,所以當在第二條指令譯碼時,取到的3號寄存器的值不是前一條指令執行後的值,因爲時間還沒到!,同樣對於第三條指令,我們對於3號寄存器的需求也剛好在第一條指令得到結果之前,以此類推,只有當第五條指令到來時我們才能準確讀到3號寄存器的值,(前提是在此之前,有有對3號寄存器做出什麼賦值操作,否則我們的值又會錯下去!!),當然對於第四條指令,可能有參考一些其他流水線的設計會發現,爲什麼也是錯誤的,答案是因爲我們無法保證讀到的剛好是在賦值之後,可能我們在設計的時候可以弄成前半週期寫完操作,後半週期讀完操作,這樣就可以完美解決問題了,也使得第四條指令可以成功執行,之前我們也就可以少寫一個Nop。

         那麼針對這個問題,我們的就解決方案其實大家肯定也大致瞭解,那就是重新定向數據流向,也就是說,我們在一些特定的操作上通過將存儲器訪問階段或寫回階段的結果重新定向或者經過旁路到執行階段來化解,可以說就是在產生出我們想要的計算結果之前,其他指令並不真正立即需要該計算結果,如果我們能夠將這個計算結果從它產生的地方直接送到其他指令需要它的地方就好了,繼續引用老師上課講過的圖片:

   

        可以看到我們對於第二條指令其實可以從第一條指令的執行週期的運算單元直接讀到我們想要的值,那麼對於第三條指令呢,我們是可以通過對第一條指令它的reg_C來直接讀到需要的值,爲什麼不是從運算單元呢?額,因爲時鐘週期一直在跑,那個值也在跟着前進到下一級的,所以我們可以得到的結論就是,我們在判斷需要的值對應於前面指令的哪一個地方取值時,是看指令週期的運算碼是否與當前指令運算碼一致的,一旦一樣,就意味着那個運算值已經到達那個執行週期,我們就可以從它的寄存器中讀到數值,這是我們取值的一個大重點!!!

          因此我們可以看到對於各種運算碼我們需要進行分類,例如,對於在r1=r1+{val2,val3}類型的運算碼:BN、BC等等會產生這種結果的情況下,我們根據判斷操作碼是與某個執行週期的操作碼一致,就取到那個階段的寄存器的值,判斷可以看到如下:

    其實就是對於mem、ex、wb的判斷操作碼然後進行賦值,當然我們會注意到其中還有一些處理可能這裏還沒講,因爲會涉及其他的衝突沒所以我們放在下面的那個詳細講一下。

         但是我們還會有例如r1=r2#r3 or r1=r2#val等等的類型,其實就是找到它們的操作碼可能會產生的下一條指令對於這條指令的需求。同時要注意下它們分別在譯碼階段給到reg_B以及reg_A的是哪個值,進而能夠判斷出是要檢查哪個位置相等才需要對數據進行定向轉移。下圖是對於reg_B的處理:

          大致講了數據衝突(大致講完,其實還有很多細節我們會去處理,例如我們的各種操作碼的分類等等的問題),但是我們如果足夠細心的話,會發現,其實還有一個問題,因爲我們一直是取到能夠提前在運算單元取到的值,如果沒有能夠在合適的執行週期取到合適的值呢?那麼問題就來了,例如我們的load指令,可以看到下圖:

      因爲load指令只有到達存儲器級後才能讀取到數據,因此結果不能重定向到下一條指令的執行階段,也就是說它有兩個週期延遲,只有過了兩個週期延遲才能使用到正確的結果,對於這個問題我們需要的方案解決是阻塞化解,也就是說我們先把操作掛起阿里,直到數據有效,其實理解起來就是相當於加了Nop指令了,當然我們知道通過禁止某一級的流水線寄存器實現阻塞流水線,此時寄存器的內容應該不發生改變,當某一級流水線阻塞是,前面的各級也應該都阻塞,這樣後續的指令纔不會丟失。可以看到我們對於它的解決方法:

   

      接下來呢,我們還要來看一下另外一種衝突,是什麼呢?當然是控制衝突了,其實對於它來說,我們主要就是在跳轉方面會遇到這個問題,因爲我們在此時會遇到流水線碰到分支指令或者說其他會改變PC值的指令,對於執行分支指令的結果我們可以分爲兩種情況,第一是分支成功,那麼此時PC的值發生改變,變爲分支轉移的目標地址,在條件判定和轉移地址計算都完成後,才改變PC值;但是一旦不成功或者說失敗的或,我們的PC值需要保持正常遞增,同時指向順序的下一條指令。關於處理分支指令最簡單的方法就是凍結或者排空流水線,可以看到我們對於它的處理在PC的值以及指令處理如下:

     

     

等等這些操作其實都是在沖掉因爲控制衝突刷新的指令,當我們處在取址階段,Branch指令及其標誌位爲true則Flush掉一條指令;跳轉指令則直接跳轉並處理pc值延後一個週期的情況,其餘情況直接讀取下一條指令,而對於譯碼階段,如果爲Branch指令且標誌位爲true則需要Flush掉當前內容;否則如果衝突出現,則需要根據不同的運算指令從不同的地方取出內容。

講到這裏大致對於我們的衝突也就講完了,接下來是對於整體代碼的改動,我覺得這一次的話是在上一次代碼的基礎上修改的,因此ip核都能用,只是到時不同算法需要改進一下指令代碼的初始文件就可以了,其他的就是我們對於PCPU的主文件修改,大體就是按着我們的思路來修改,首先考慮數據定向移動,然後在這樣一個整體的趨勢下去注意load的影響,注意進行補充、阻塞。然後是控制衝突的解決,主要是對於PC端的影響;改動不大,主要精力是放在譯碼階段的A、B寄存器的值的重定向賦值比較難而已。

  1. 實現程序的功能介紹

對於本次的指令集測試,我覺得在算法本身這裏就不用多言了,因爲要實現的代碼算法我們如果要說的話大家都是比較熟悉的,之前的程序結構課程也就是這幾種算法不斷地運用而已,所以在本次講解中我就不涉及算法的精妙之處,直接把它轉化爲二進制或者十六進制初始文件然後逐一進行讀入i_mem的初始文件裏面,然後運用我們上次項目的板極驗證方法同樣運行一次後查看最後各個地址的數值是否與提供的一致就行啦。注意因爲我們解決了hazard問題所以我們在使用指令時就不用使用Nop進行隔斷啦!

 

三、實驗步驟

  1. 轉化指令集的操作碼,然後進行初始化文件的更新。

對於我們修改之後的hazard代碼的驗證,我們當然是通過不同的算法指令集來驗證,那麼在本次項目中,我們主要是對指令存儲器以及數據存儲器的IP核的初始化文件進行修改就行了,同時注意的是我們的指令集不再需要是加NOP來緩解衝突,所以我們只需要把操作直接轉化爲二進制或者十六進制進行初始化就行了,這類操作我們在上一次的實驗項目報告中也已經詳細說明了一下。所以可以看到我們對於五個複雜指令集的初始化文件(局部)如下圖,教給大家一個小技巧,輕鬆獲取我們的初始文件十六進制碼,可能之前助教提供了那個文件也可以直接複製那些有效的操作碼前面的數字,當然我們也可以把操作碼保留,然後如果你還記得上一次的那個數據存儲器的仿真文件輸出的話,就可以相似地用這種方法輸出指令代碼,這時輸出的都是十六進制的碼數,你可以直接複製用上啦!

下面是我們的初始文件(包括指令集存儲器的初始化以及數據存儲器的初始化),當然對應於每一個算法它們兩者的初始化都是需要同時進行改變的,具體看下面圖:

圖一:對應於64位加法的指令初始化(局部)

圖二:對應於64位加法的數據初始化

圖三:對應於冒泡排序的局部指令初始化

       因爲指令集翻譯其實還算是一種簡單暴力的操作,所以我們不多說啦,直接給幾張圖大致參考一下就可以啦,就是根據複雜指令進行數值轉化嘛,經過一系列的堆疊我們就把初始化都寫完啦,接下來就是我們的仿真嘍。

2、進行仿真,通過觀察運行過程中的各種執行碼的正確性以及寄存器的變化,同時讀出最終的數據寄存器的地址對應的值進行比對。

3、進行板極驗證,觀察結果。詳細見下面結果的分析。

 

三、實驗結果

• Lab3:

  1. 綜合的RTL電路圖

      

 

                                        圖一:流水線RTL圖

      如圖一所示,綜合出的RTL圖顯示出CPU的通信關係,沒毛病。

  1. 仿真結果

仿真這一次因爲要驗證的指令集較多,我就是按照之前驗證init_test的方法生成最終的數據寄存器的文件然後與實驗的結果文件進行對照,如果一致就說明仿真成功。

可以看到我們的init_test的結果:

同時我們的add64位加法:

而我們的gcm:

還有冒泡排序:

以及sort:

經過對照仿真全部正確,可以燒板啦!

4、開發板的顯示效果圖
Init_test:
正如之前項目報告那個功能簡介已經說了,我們把一號波動開關設爲地址輸入的有效信號,具體的
地址信號在右邊那一行的撥動開關(注意是那一排小的),然後二號撥動開關是 reset 信號,三號撥
動開關是 enable 信號,而按鈕鍵是 start。所以實驗開始時,我們先讓 reset 置零,看到如下:
- 9 -
因爲我們一開始的 pc 和地址都是 0,可以看到數據是 fffd。
接下來我們點擊 start,然後通過撥動 enable 讓程序停下來,記錄數據後撥動它變成有效,再點擊
start 使它繼續執行,可以與上面的仿真數據相比較是對應着的。接下來我們驗證執行後的數據存
儲器的數據正確性,首先是將第一位撥動開關調至有效,然後點擊 start,運行後然後撥動開關選
擇地址,查看數據。(此時 pc 一直爲 00)
通過查看數據我們可以看到當調到 00000010 時,即是選擇 2 號地址輸出 0005;而調到 5(00000101)時
輸出 0041 合理!
當我們調到 1f 時顯示 00c3,同時調到 2f 時顯示 0069,都與上次實驗最後的輸出結果一致,得到驗證。
對於我們的 64 位加法來說,我們同樣經過運行 start 之後,通過查詢地址,驗證數值是否正確來評判板
極驗證的正確性,這一點在下面幾個指令集都是一樣(也就是說,下面我們也是直接通過改 IP 核的數據
以及指令寄存器的值然後運用到上次寫板極驗證的代碼燒寫到板子上,然後通過在板子運行然後檢驗地
址對應的數據是否正確即可):
- 10 -
64 位加法:
對應於地址 00 的數據是 fffe 對應於地址 04 的數據是 ffff
對應於地址 08 的數據是 fffd 對應於地址 07 的數據是 0000
對於 gcm 的算法:
GCM:
對應於地址 03 的數據是 0008 對應於地址 04 的數據是 0060
- 11 -
對於 bubble sort:
對應於 05 地址的數據是 2369 對應於地址 0a 的數據是 0004
對於 sort:
對應於 02 地址的數據是 0001 對應於地址 09 的數據是 0011
經過驗證板極驗證時正確的。奈斯!

四、附錄報告

對於功率報告我們由於沒有改動所以遇上一次一樣如下:

通過功率報告我們可以看到該實驗項目中總共的功率0.273w以及它的一個分配,整體我覺得不算高耗能,還行。主要消耗在時鐘管理上。同時IO口的輸入輸出也佔據了比較大的部分,但是我們可以大概猜到時鐘管理相比那些其他的邏輯門(它們甚至數量更多)耗能更大,它佔到的比例更高,可能是影響實驗運行的一個重要因素,不管是速度還是頻率接受。

對於利用指數報告:

我們也可以看到佔用較多的是MMCM時鐘管理這一部分,包括前面的那個功率消耗也是它相對較高。應該說對於實驗來說這個時鐘管理顯得極爲佔空間和功率。

而此次對於頻率沒有過分要求,也就按正常的100Mhz來運作,得到正常的時序報告,如下:

 

五、實驗感想

對於本次實驗我其實也想把它寫成一篇博客,可供其他人蔘考,所以對於各個細節都寫了一下,但可能還是有一些不足,只能不斷努力去補足,其實把源代碼改成hazard,最重要還是對於衝突的深入理解,而後纔是對於指令集的驗證,個人覺得只要結構寫出來了,什麼指令集不都是輕易代上去就可以解決了嗎?所以本次項目就是爲了讓我們深入理解衝突的產生並思考怎麼解決衝突而設計的,我覺得這個項目對於自己對於流水線的理解確確實實從表面達到了精華部分,但還是要不斷學習,才能汲取更多有用的知識。

發佈了12 篇原創文章 · 獲贊 9 · 訪問量 2721
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章