
👆如果您希望可以時常見面,歡迎標星🌟收藏哦~
最近些年。RISC-V引起了全球關注。這款革命性的 ISA 憑藉其持續的創新,以及無數的學習和工具資源以及來自工程界的貢獻,像潮水般席捲了市場。RISC-V 最大的魅力在於它是一款開源 ISA。
在本文中,我(指代本文作者Mitu Raj,下同)將介紹如何從零開始設計一款RISC-V CPU ,我們將講解定義規格、設計和改進架構、識別和解決挑戰、開發 RTL、實現 CPU 以及在模擬/FPGA 板上測試 CPU 的流程。
以下為文章正文:
從命名開始
為你的想法命名或打造品牌至關重要,這樣才能激勵你不斷前進,直至達成目標!我們打算構建一個非常簡單的處理器,所以我想出了一個花哨的名字“ Pequeno ”,在西班牙語中是“微小”的意思;完整名稱是:Pequeno RISC-V CPU,又名PQR5。
RISC-V 的 ISA 架構有多種風格和擴充套件。我們先從最簡單的RV32I開始,它又稱為 32 位基本整數 ISA。該 ISA 適用於構建支援整數運算的 32 位 CPU。因此,Pequeno 的第一個規格如下:
Pequeno 是一款 32 位 RISC-V CPU,支援 RV32I ISA。
RV32I 有 37 條 32 位基本指令,我們計劃在 Pequeno 中實現。因此,我們必須深入瞭解每條指令。我費了一番功夫才完全掌握了 ISA。在此過程中,我學習了完整的規範,並設計了自己的彙編程式pqr5asm,並與一些流行的 RISC-V 彙編程式進行了驗證。
“RISBUJ”
上面六個字母的單詞總結了 RV32I 中的指令型別。這 37 條指令屬於以下類別之一:
R型:所有暫存器上的整數計算指令。
I 型:所有基於暫存器和立即數的整數計算指令。還包括 JALR 和 Load 指令。
S型:全部儲存說明。
B型:所有分支指令。
U型:LUI、AUIPC等特殊指令。
J型:類似JAL的跳轉指令。
RISC-V 架構中有 32 個通用暫存器,x0-x31. 所有暫存器都是 32 位的。在這 32 個暫存器中,零又稱為x0暫存器,是一個很有用的特殊暫存器,它被硬連線為零,無法寫入,並且始終讀取為零。那麼它有什麼用呢?你可以使用x0作為虛擬目標來轉儲您不想讀取的結果,或用作運算元零,或生成 NOP 指令來閒置 CPU。
整數計算指令是針對暫存器和/或12位立即數執行的ALU指令。載入/儲存指令用於在暫存器和資料儲存器之間儲存/載入資料。跳轉/分支指令用於將程式控制轉移到不同的位置。
每條指令的詳細資訊可以在 RISC-V 規範中找到:RISC-V 使用者級 ISA v2.2。
要學習 ISA,RISC-V 規範文件就足夠了。不過,為了更清晰起見,您可以研究一下 RTL 中不同開放核心的實現。
除了 37 條基本指令外,我還為 pqr5asm 添加了 13 條偽/自定義指令,並將 ISA 擴充套件至 50 條指令。這些指令源自基本指令,旨在簡化彙編程式設計師的工作……例如:
NOP指令與ADDI x0, x0, 0這在CPU上當然什麼也不做!但它更簡單,更容易在程式碼中解釋。
在開始設計處理器架構之前,我們的期望是完全瞭解每條指令如何以 32 位二進位制進行編碼以及它的功能是什麼。

我用 Python 開發的 RISC-V RV32I 彙編器 PQR5ASM 可以在我的 GitHub 上找到。您可以參考《彙編器指令手冊》編寫示例彙編程式碼。編譯它,並檢視它如何轉換為 32 位二進位制檔案,以便在繼續下一步之前鞏固/驗證您的理解。
規格和架構
在本章中,我們定義了 Pequeno 的完整規格和架構。上次我們只是簡單地將其定義為 32 位 CPU。接下來,我們將對其進行更詳細的介紹,以大致瞭解即將設計的架構。
我們將設計一個簡單的單核 CPU,它能夠按照獲取指令的順序一次執行一條指令,但仍採用流水線方式。我們不支援 RISC-V 特權規範,因為我們目前不打算讓我們的核心作業系統支援該規範,也不打算讓它支援中斷。
該CPU規格如下:
-
32位CPU,單發射,單核。
-
經典的五級 RISC 流水線。嚴格有序流水線。
-
符合RV32I 使用者級 ISA v2.2。支援全部 37 條基本指令。
-
用於指令和資料儲存器訪問的獨立匯流排介面。(為什麼?以後再討論……)
-
適用於裸機應用程式,不支援作業系統和中斷。(更確切地說是限制!)
正如上文所述,我們將支援 RV32I ISA。因此,CPU 僅支援整數運算。
CPU 中的所有暫存器都是 32 位的。地址和資料匯流排也是 32 位的。CPU 採用經典的小端位元組定址記憶體空間。每個地址對應於 CPU 地址空間中的一個位元組。
0x00 – byte[7:0], 0x01 – byte[15:8] …
32 位字可以透過 32 位對齊的地址訪問,即 4 的倍數的地址:
0x00—— byte 0,0x04—— byte 1……

Pequeno 是一款單發射 CPU,即每次只從記憶體中獲取一條指令,併發出指令進行解碼和執行。採用單發射的流水線處理器的最大IPC = 1(或最小/最佳CPI = 1),即最終目標是以每時鐘週期 1 條指令的速率執行。這在理論上是可以實現的最高效能。
經典的五級 RISC 流水線是理解任何其他 RISC 架構的基礎架構。這對於我們的 CPU 來說是最理想且最簡單的選擇。Pequeno 的架構就是圍繞這種五級流水線構建的。讓我們深入探討一下其底層概念。
簡單起見,我們將不支援 CPU 流水線中的計時器、中斷和異常。因此,CSR 和特權級別也無需實現。因此, RISC-V 特權 ISA不包含在 Pequeno 的當前實現中。
設計 CPU 最簡單的方法是非流水線方式。讓我們看看非流水線 RISC CPU 的幾種設計方法,並瞭解其缺點。
讓我們假設 CPU 執行指令所遵循的經典步驟序列:獲取、解碼、執行、記憶體訪問和寫回。
第一種設計方法是:將 CPU 設計成一個具有四到五個狀態的有限狀態機 (FSM),並按順序執行所有操作。例如:

但這種架構會嚴重影響指令執行速度。因為執行一條指令需要多個時鐘週期。比如,寫入暫存器需要 3 個時鐘週期。如果是載入/儲存指令,記憶體延遲也會隨之增加。這是一種糟糕且原始的 CPU 設計方法。我們徹底拋棄它吧!
第二種方法是:指令可以從指令儲存器中取出,解碼,然後由完全組合邏輯執行。然後,ALU 的結果被寫回到暫存器檔案。直到寫回的整個過程可以在一個時鐘週期內完成。這樣的 CPU 稱為單週期 CPU。如果指令需要訪問資料儲存器,則應考慮讀/寫延遲。如果讀/寫延遲為一個時鐘週期,則儲存指令仍可能像所有其他指令一樣在一個時鐘週期內完成執行,但載入指令可能額外需要一個時鐘週期,因為必須將載入的資料寫回到暫存器檔案。PC 生成邏輯必須處理這種延遲的影響。如果資料儲存器讀取介面是組合的(非同步讀取),則 CPU 對於所有指令都將真正變為單週期。

該架構的主要缺點顯然是從取指到寫入儲存器/暫存器檔案的組合邏輯關鍵路徑較長,這限制了時序效能。然而,這種設計方法簡單,適用於低端微控制器中那些需要低時鐘速度、低功耗和低面積的CPU。
為了實現更高的時鐘速度和效能,我們可以將 CPU 的指令順序處理功能分離出來。每個子程序被分配給獨立的處理單元。這些處理單元按順序級聯,形成流水線。所有單元並行工作,並對指令執行的不同部分進行操作。透過這種方式,可以並行處理多條指令。這種實現指令級並行性的技術稱為指令流水線。該執行流水線構成了流水線 CPU 的核心。

經典的五級 RISC 流水線有五個處理單元,也稱為流水線階段。這些階段分別是:取指(IF)、解碼(ID)、執行(EX)、記憶體訪問(MEM)、寫回(WB)。流水線的工作原理可以直觀地表示為:

每個時鐘週期,一條指令的不同部分會被處理,並且每個階段都會處理不同的指令。如果仔細觀察,會發現只有第 5 個週期,指令 1 才完成執行。這段延遲被稱為流水線延遲。Δ此延遲與流水線級數相同。在此延遲之後,第 6 個週期:指令 2 執行完畢,第 7 個週期:指令 3 執行完畢,依此類推……理論上,我們可以計算吞吐量(每週期指令數,IPC),如下所示:

因此,流水線CPU保證每個時鐘週期執行一條指令。這是單發射處理器中可能的最大IPC。
透過劃分多個流水線階段的關鍵路徑,CPU 現在也可以以更高的時鐘速度執行。從數學上講,這使得流水線 CPU 的吞吐量比同等的非流水線 CPU 提高了一個倍數。

這被稱為流水線加速。簡單來說,一個具有s階段流水線 CPU 的時鐘速度是非流水線產品的S倍。
流水線通常會增加面積/功耗,但效能提升是值得的。
數學計算假設流水線永遠不會停滯,也就是說,資料在每個時鐘週期內都會從一個階段持續傳輸到另一個階段。但在實際的 CPU 中,流水線可能會由於多種原因而停滯,主要原因是結構/控制/資料依賴性。
舉個例子:暫存器X不能被Nth指令讀到,因為X並不是由(N-1)th指令修改了X讀回,這是流水線中資料風險的一個例子。
Pequeno 的架構採用了經典的五級 RISC 流水線。我們將實現嚴格的順序流水線。在順序處理器中,指令的獲取、解碼、執行和完成/提交都按照編譯器生成的順序進行。如果一條指令停滯,整個流水線都會停滯。
在亂序處理器中,指令按照編譯器生成的順序獲取和解碼,但執行可以按不同的順序進行。如果一條指令停頓,除非存在依賴關係,否則它不會停頓後續指令。獨立的指令可以向前傳遞。執行仍然可以按順序完成/提交(這就是當今大多數CPU的現狀)。這為實現各種架構技術打開了大門,透過減少停頓所浪費的時鐘週期並最大限度地減少氣泡的插入(什麼是“氣泡”?繼續閱讀……) ,顯著提高吞吐量和效能。
亂序處理器由於指令的動態排程而相當複雜,但現在已成為當今高效能 CPU 中事實上的流水線架構。

五個流水線階段被設計為獨立單元:取指單元(FU)、譯碼單元(DU)、執行單元(EXU)、記憶體訪問單元(MACCU)和寫回單元(WBU)。
取指單元(FU):流水線的第一級,與指令儲存器介面。FU 從指令儲存器中取指並送至譯碼單元。FU 可能包含指令緩衝區、初始分支邏輯等。
解碼單元(DU):流水線的第二階段,負責解碼來自執行單元 (FU) 的指令。DU 還會啟動對暫存器檔案的讀取訪問。來自 DU 和暫存器檔案的資料包被重新定時同步,並一起傳送到執行單元 (Execution Unit)。
執行單元(EXU):流水線的第三階段,用於驗證並執行來自 DU 的所有解碼指令。無效/不支援的指令不允許在流水線中繼續執行,它們會成為“氣泡”。算術單元 (ALU)負責所有整數算術和邏輯指令。分支單元 (Branch Unit)負責處理跳轉/分支指令。載入/儲存單元 (Load-Store Unit)負責處理需要訪問記憶體的載入/儲存指令。
記憶體訪問單元(MACCU):流水線的第四級,用於與資料儲存器介面。MACCU 負責根據 EXU 的指令發起所有記憶體訪問。資料儲存器是定址空間,可能由資料 RAM、記憶體對映的 I/O 外設、橋接器、互連等組成。
寫回單元(WBU):流水線的第五級或最後一級。指令在此完成執行。WBU 負責將 EXU/MACCU 中的資料(載入資料)寫回暫存器檔案。
在流水線階段之間,實現了有效-就緒握手。乍一看這並不那麼明顯。每個階段都會註冊一個數據包並將其傳送到下一階段。該資料包可能是下一階段或後續階段要使用的指令/控制/資料資訊。該資料包透過有效訊號進行驗證。如果資料包無效,則在流水線中稱為氣泡(Bubble)。氣泡只不過是流水線中的“洞”(hole),它只是在流水線中向前移動,實際上不執行任何操作。這類似於 NOP 指令。但不要認為它們沒有用!在後續部分討論流水線風險時,我們將看到它們的一種用途。下表定義了 Pequeno 指令流水線中的氣泡。

每個階段還可以透過發出停頓訊號來停頓前一個階段。一旦停頓,該階段將保留其資料包,直到停頓狀態消失。此訊號與反轉的就緒訊號相同。在順序處理器中,任何階段產生的停頓都類似於全域性停頓,因為它最終會停頓整個流水線。

flush訊號用於重新整理管道。重新整理操作將一次性使之前階段註冊的所有資料包失效,因為它們被識別為不再有用。

舉個例子,當流水線在執行跳轉/分支指令後,從錯誤的分支獲取並解碼了指令,而該指令僅在執行階段被識別為錯誤時,流水線應該被重新整理,並從正確的分支獲取指令!
雖然流水線顯著提升了效能,但也增加了 CPU 架構的複雜性。CPU 的流水線技術總是伴隨著它的孿生兄弟——流水線風險!現在,我們假設我們對流水線風險一無所知。我們在設計架構時並沒有考慮風險。
處理流水線風險
在本章中,我們將探討流水線風險。我們上次成功設計了 CPU 的流水線架構,但卻沒有考慮到伴隨流水線而來的“邪惡雙胞胎”。流水線風險對架構可能造成哪些影響?需要進行哪些架構修改來緩解這些風險?讓我們繼續,揭開它們的神秘面紗!
CPU 指令流水線中的危險是指一些依賴關係,這些依賴關係會干擾流水線的正常執行。當危險發生時,指令無法在指定的時鐘週期內執行,因為這可能導致錯誤的計算結果或控制流。因此,流水線可能會被迫暫停,直到指令能夠成功執行。

在上面的例子中,CPU 按照編譯器生成的順序按序執行指令。假設指令 i2對i1有一定的依賴性,比如i2需要讀取某個暫存器,但該暫存器也正在被前一條指令i1修改。因此,i2必須等到i1將結果寫回暫存器檔案,否則舊資料將被解碼並從暫存器檔案讀取,供執行階段使用。為了避免這種資料不一致,i2被強制暫停三個時鐘週期。流水線中插入的氣泡表示暫停或等待狀態。只有當i1完成時,i2才會被解碼。最終,i2在第 10 個時鐘週期而不是第 7 個時鐘週期完成執行。由於資料依賴性導致的暫停,引入了三個時鐘週期的延遲。這種延遲如何影響 CPU 效能?
理想情況下,我們期望 CPU 以滿吞吐量執行,即 CPI = 1。但是,當流水線暫停時,由於 CPI 增加,CPU 的吞吐量/效能會降低。對於非理想 CPU:

管道中發生危險的方式多種多樣。管道危險可分為三類:
-
結構性危險
-
控制危害
-
資料危害
結構性風險是由於硬體資源衝突而發生的。例如,當流水線的兩個階段想要訪問同一資源時。例如:兩條指令需要在同一時鐘週期內訪問記憶體。

在上面的例子中,CPU 只有一個記憶體用於儲存指令和資料。取指階段每個時鐘週期都會訪問記憶體以獲取下一條指令。因此,如果記憶體訪問階段的上一條指令也需要訪問記憶體,則取指階段和記憶體訪問階段的指令可能會發生衝突。這將迫使 CPU 增加停頓週期,取指階段必須等待,直到記憶體訪問階段的指令釋放資源(記憶體)。
減輕結構性危險的一些方法包括:
-
暫停管道,直到資源可用。
-
複製資源,這樣就不會發生任何衝突。
-
流水線資源,使得兩條指令將處於流水線資源的不同階段。
-
讓我們分析一下可能導致 Pequeno 管道出現結構性危險的不同情況,以及如何解決。 我們無意使用停工作為緩解結構性危險的選項!

在 Pequeno 的架構中,我們實施了上述三種解決方案來減輕各種結構性危險。
控制風險是由跳轉/分支指令引起的。跳轉/分支指令是 CPU ISA 中的流程控制指令。當控制權到達跳轉/分支指令時,CPU 必須決定是否執行該分支指令。此時,CPU 應該採取以下操作之一。
在 PC+4 處獲取下一條指令(不執行分支)或獲取分支目標地址處的指令(分支已執行)。
只有在執行階段計算分支指令的結果時,才能判斷決策的正確與否。根據分支是否被執行,確定分支地址(CPU 應該分支到的地址)。如果之前做出的決策是錯誤的,那麼在該時鐘週期之前在流水線中獲取和解碼的所有指令都應該被丟棄。因為這些指令根本不應該被執行!這是透過重新整理流水線並在下一個時鐘週期獲取分支地址的指令來實現的。重新整理使指令無效並將其轉換為 NOP 或冒泡。這會花費大量的時鐘週期作為懲罰。這被稱為分支懲罰。因此,控制冒險對 CPU 效能的影響最嚴重。

在上面的例子中,i10在第 10 個時鐘週期完成了執行,但它應該在第 7 個時鐘週期完成執行。由於執行了錯誤的分支指令 (i5),因此損失了 3 個時鐘週期。當執行階段在第 4 個時鐘週期識別出錯誤分支指令時,必須在流水線中進行重新整理。這會如何影響 CPU 效能?
如果在上述 CPU 上執行的程式包含 30% 的分支指令,則 CPI 將變為:

CPU 效能降低50%!
為了減輕控制風險,我們可以在架構中採用一些策略……
如果指令被識別為分支指令,則只需暫停流水線即可。該解碼邏輯可以在提取階段本身實現。一旦執行了分支指令並解析了分支地址,就可以提取下一條指令並恢復流水線。
在 Fetch 階段新增類似分支預測的專用分支邏輯。
分支預測的本質是:我們在取指階段採用某種預測邏輯來猜測分支是否應該被執行。在下一個時鐘週期,我們獲取猜測的指令。這條指令要麼從 PC+4 處獲取(預測分支不被執行),要麼從分支目標地址處獲取(預測分支被執行)。現在有兩種可能性:
如果在執行階段發現預測正確,則不執行任何操作,管道可以繼續處理。
如果發現預測錯誤,則重新整理流水線,從執行階段解析的分支地址中獲取正確的指令。這會產生分支懲罰。
如您所見,分支預測如果預測錯誤,仍然會招致分支懲罰。設計目標應該是降低錯誤預測的機率。CPU 的效能很大程度上取決於預測演算法的“好壞”。像動態分支預測這樣的複雜技術會儲存指令歷史記錄,以便以 80% 到 90% 的機率進行正確預測。
為了減輕 Pequeno 中的控制風險,我們將實現一個簡單的分支預測邏輯。更多細節將在我們即將釋出的關於提取單元設計的部落格中揭曉。

當一條指令的執行對流水線中仍在處理的上一條指令的結果存在資料依賴時,就會發生資料風險。讓我們透過示例來了解三種類型的資料風險,以便更好地理解這個概念。
假設一條指令i1將結果寫入暫存器 x。下一條指令i2也將結果寫入同一暫存器。程式順序中的任何後續指令都應讀取 x 處i2的結果。否則,資料完整性將受損。這種資料依賴關係稱為輸出依賴關係,可能導致 WAW((Write-After-Write)) 資料風險。

假設一條指令i1讀取了暫存器 x。下一條指令i2將結果寫入同一暫存器。此時,i1應該讀取 暫存器X的舊值,而不是i2的結果。如果 i2在i1讀取結果之前將結果寫入 x,則會導致資料風險。這種資料依賴稱為反依賴,可能導致 WAR ((Write-After-Read))資料風險。

假設一條指令i1將結果寫入暫存器 x。下一條指令i2讀取同一個暫存器。此時,i2應該讀取 i1寫入暫存器 x 的值,而不是之前的那個值。這種資料依賴關係被稱為真依賴,可能導致 RAW (Read-After-Write)資料風險。

這是流水線 CPU 中最常見、最主要的資料危險型別。
為了減輕有序 CPU 中的資料危險,我們可以採用一些技術:
檢測到資料依賴性時,暫停流水線(參見第一張圖)。解碼階段可以等到上一條指令執行完成後再執行。
編譯重新排程:編譯器透過排程程式碼到稍後執行來重新安排程式碼,以避免資料風險。這樣做的目的是避免程式停頓,同時又不影響程式控制流的完整性,但這並非總是可行。編譯器也可以在兩個具有資料依賴性的指令之間插入 NOP 指令。但這會導致停頓,從而影響效能。

資料/運算元轉發:這是順序執行 CPU 中緩解 RAW 資料風險的突出架構解決方案。讓我們分析一下 CPU 流水線,以瞭解這項技術背後的原理。
假設兩個相鄰的指令i1和i2,它們之間存在 RAW 資料依賴性,因為它們都在訪問暫存器X。CPU 應該暫停指令i2,直到i1將結果寫回暫存器x。如果 CPU 沒有停頓機制,則i2會在第三個時鐘週期的解碼階段從 x 讀取較舊的值。在第四個時鐘週期,i2指令會執行錯誤的 x 值。

如果你仔細觀察管道,我們在第三個時鐘週期就已經得到了i1的結果。當然,它不會被寫回暫存器檔案,但結果仍然可以在執行階段的輸出端使用。因此,如果我們能夠以某種方式檢測資料依賴性,然後將該資料“forward”到執行階段的輸入,那麼下一條指令就可以使用轉發的資料,而不是來自解碼階段的資料。這樣一來,資料風險就得到了緩解!這個想法是這樣的:

這稱為資料/運算元轉發或資料/運算元旁路。我們將資料按時間向前轉發,以便流水線中後續的依賴指令可以訪問這些被旁路的資料,並在執行階段執行。
這個想法可以擴充套件到不同的階段。在一個按 i1、i2、..in順序執行指令的 5 級流水線中,資料依賴關係可能存在於:
-
i1和i2- 需要在執行階段和解碼階段的輸出之間旁路。
-
i1和i3- 需要在記憶體訪問階段和解碼階段的輸出之間旁路。
-
i1和i4- 需要在寫回階段和解碼階段的輸出之間旁路。
用於緩解源自流水線任何階段的 RAW 資料風險的架構解決方案如下所示:

請考慮以下情形:

兩條相鄰指令i1和i2之間存在資料依賴關係,其中第一條指令是 Load。這是資料風險的一種特殊情況。這裡,在資料載入到 x1 之前,我們無法執行i2。那麼,問題在於我們是否仍然可以透過資料轉發來緩解這種資料風險?載入資料僅在 i1的記憶體訪問階段可用,並且必須將其轉發到i2的解碼階段才能防止這種風險。該要求如下所示:

假設載入資料在第 4 個週期的記憶體訪問階段可用,您需要將此資料“轉發”到第 3 個週期,傳送到i2的解碼階段輸出(為什麼是第 3 個週期?因為在第 4 個週期,i 就已經在執行階段完成了執行!)。本質上,您是在嘗試將當前資料轉發到過去,除非您的 CPU 進行時間旅行,否則這是不可能的!這不是資料轉發,而是“資料回溯”。
資料轉發只能沿時間方向向前進行。
這種資料風險稱為流水線互鎖(Pipeline Interlock)。解決這個問題的唯一方法是,在檢測到資料依賴性時插入一個氣泡,使流水線暫停一個時鐘週期。

在 i1和i2之間插入了 NOP 指令(又稱 Bubble)。這會將i2延遲一個週期,因此資料轉發現在可以將載入資料從記憶體訪問階段轉發到解碼階段的輸出。
到目前為止,我們只討論瞭如何緩解 RAW 資料風險。那麼,WAW 和 WAR 風險又如何呢?RISC-V 架構本身就具備抵抗有序流水線實現的 WAW 和 WAR 風險的能力!
-
所有暫存器的寫回都按照指令發出的順序進行。寫回的資料總是會被後續寫入同一暫存器的指令覆蓋。因此,WAW 風險永遠不會發生!
-
寫回是流水線的最後一個階段。當寫回發生時,讀取指令已經成功完成了對較舊資料的執行。因此,WAR 風險永遠不會發生!
為了緩解 Pequeno 中的 RAW 資料風險,我們將使用流水線互鎖保護功能硬體實現資料轉發。更多細節將在後文揭曉,屆時我們將在其中設計資料轉發邏輯。

我們理解並分析了現有 CPU 架構中可能導致指令執行失敗的各種潛在流水線風險。我們還設計瞭解決方案和機制來緩解這些風險。讓我們整合必要的微架構,並最終設計出 Pequeno RISC-V CPU 的架構,使其完全杜絕所有型別的流水線風險!

在接下來的文章中,我們將深入探討每個流水線階段/功能單元的 RTL 設計。我們將討論設計階段中不同的微架構決策和挑戰。
獲取單元
從這裡開始,我們開始深入探討微架構和 RTL 設計了!在本章中,我們將構建和設計Pequeno 的Fetch Unit (FU) 。
取指單元 (FU) 是 CPU 流水線的第一階段,用於與指令儲存器互動。取指單元 (FU) 從指令儲存器中取指,並將取指的指令傳送到譯碼單元 (DU) 。正如前文中 Pequeno 的改進架構所討論的那樣,FU 包含分支預測邏輯和重新整理支援。
1
介面
讓我們定義 Fetch Unit 的介面:


2
指令訪問介面
CPU 中 FU 的核心功能是指令訪問。指令訪問介面 (Instruction Access:I/F)即用於此目的。指令在執行期間儲存在指令儲存器 (RAM) 中。現代 CPU 從快取記憶體 (Cache) 中獲取指令,而不是直接從指令儲存器中獲取。指令快取(在計算機架構術語中稱為主快取或L1 快取)更靠近 CPU,透過快取/儲存頻繁訪問的指令並在附近預取較大塊的指令,實現更快的指令訪問。因此,無需持續訪問速度較慢的主儲存器 (RAM)。因此,大多數指令都可以直接從快取中快速訪問。
CPU 不會直接訪問帶有指令快取/記憶體的介面。它們之間會有一個快取/記憶體控制器來控制它們之間的記憶體訪問。

定義一個標準介面是一個好主意,這樣任何標準指令儲存器/快取 (IMEM) 都可以輕鬆地插入到我們的 CPU 中,並且只需極少的膠合邏輯甚至無需膠合邏輯。讓我們定義兩個用於指令訪問的介面。請求介面 (I/F )處理從指令儲存器 (FU) 到指令儲存器的請求。響應介面 (I/F)處理從指令儲存器到指令儲存器 (FU) 的響應。我們將為指令儲存器 (FU) 定義一個簡單的基於有效就緒的請求和響應介面 (I/F),因為如果需要,這很容易轉換為 APB、AXI 等匯流排協議。

指令訪問需要知道指令在記憶體中的地址。透過請求介面 (Request I/F) 請求的地址實際上就是 FU 生成的 PC。在 FU 介面中,我們將使用暫停訊號 (stall signal) 來代替就緒訊號,其行為與就緒訊號相反。快取控制器通常有一個暫停訊號來暫停來自處理器的請求。該訊號由cpu_stall表示。來自記憶體的響應是透過響應介面 (Response I/F) 接收到的已取指令。除了已取指令之外,響應還應包含相應的 PC。PC 用作 ID,用於識別已收到響應的請求。換句話說,它指示已取指令的地址。這是 CPU 流水線下一階段所需的重要資訊(如何實現?我們很快就會看到! )。因此,已取指令及其 PC 構成了對 FU 的響應資料包。當內部流水線暫停時,CPU 可能還需要暫停來自指令記憶體的響應。該訊號由mem_stall表示。
此時,讓我們定義CPU 管道中的 instruction packet= {instruction, PC}。
3
PC 生成邏輯
FU 的核心是控制請求介面 (I/F) 的 PC 生成邏輯。由於我們設計的是 32 位 CPU,因此 PC 的生成應該以 4 為增量。該邏輯復位後,每個時鐘週期都會生成 PC。PC 的復位值可以硬編碼。這是 CPU 復位後從中獲取並執行指令的地址,即記憶體中第一條指令的地址。PC 生成是自由執行的邏輯,僅由 c pu_stall暫停。
自由執行的PC可以透過重新整理I/F和內部分支預測邏輯來繞過。PC生成演算法實現如下:

4
指令緩衝器
FU 內部有兩個背靠背的指令緩衝區。緩衝區 1緩衝從指令儲存器中獲取的指令。緩衝區 1 可以直接訪問響應介面 (Response I/F)。緩衝區 2緩衝來自緩衝區 1 的指令,然後透過 DU I/F 將其傳送到 DU。這兩個緩衝區構成了 FU 內部的指令流水線。

5
分支預測邏輯
正如上文所討論的,我們必須在 FU 中新增分支預測邏輯來緩解控制風險。我們將實現一個簡單且靜態的分支預測演算法。該演算法的主要內容如下:
總是會進行無條件跳轉。
如果分支指令是向後跳轉,則執行分支。因為可能性如下:
1、這條指令可能是某些do-while 迴圈的迴圈退出檢查的一部分。在這種情況下,如果我們執行分支指令,則正確的機率更高。
如果分支指令是向前跳轉,則不要執行它。因為可能性如下:
2、這條指令可能是某些for 迴圈或while 迴圈的迴圈入口檢查的一部分。如果我們不執行分支並繼續執行下一條指令,則正確的機率更高。
3、這條指令可能是某個if-else語句的一部分。在這種情況下,我們總是假設if條件為真,並繼續執行下一條指令。理論上,這筆交易(bargain)有50%是正確的。

緩衝區 1 的指令包由分支預測邏輯監控和分析,並生成分支預測訊號:branch_taken。該分支預測訊號隨後被註冊,並與傳送給 DU 的指令包同步傳輸。分支預測訊號透過 DU 介面傳送給 DU。
6
DU
這是獲取單元和解碼單元之間用於傳送有效載荷的主要介面。有效載荷包含獲取的指令和分支預測資訊。

由於這是CPU兩個流水線階段之間的介面,因此實現了有效就緒I/F。以下訊號構成了DU I/F:

在之前的博文中,我們討論了 CPU 流水線中停頓和重新整理的概念及其重要性。我們還討論了 Pequeno 架構中需要停頓或重新整理的各種場景。因此,必須在 CPU 的每個流水線階段中整合適當的停頓和重新整理邏輯。確定在哪個階段需要停頓或重新整理至關重要,以及該階段中哪些邏輯部分需要停頓和重新整理。
在實施停頓和重新整理邏輯之前的一些初步想法:
-
流水線階段可能會因外部或內部產生的條件而停止。
-
管道階段可以透過外部或內部生成的條件進行重新整理。
-
Pequeno 中沒有集中式的停頓或重新整理生成邏輯。每個階段可能都有自己的停頓和重新整理生成邏輯。
-
流水線中一個階段只能被下一個階段所阻塞。任何階段的阻塞最終都會影響流水線的上游,並導致整個流水線阻塞。
下游流水線中的任何一個階段都可以重新整理某個階段。這被稱為流水線重新整理,因為上游的整個流水線都需要同時重新整理。在 Pequeno 中,只有執行單元 (EXU)中的分支未命中才需要進行流水線重新整理。

停頓邏輯包含產生本地和外部停頓的邏輯。重新整理邏輯包含產生本地和流水線重新整理的邏輯。
本地停頓在內部產生,並在本地用於停止當前階段的執行。外部停頓在內部產生,並透過外部發送到上游流水線的下一級。本地和外部停頓均基於內部條件以及下游流水線下一級的外部停頓而產生。
本地重新整理 (Local flush)是指在內部生成並用於本地重新整理階段的重新整理。外部重新整理或管道重新整理 (Pipeline flush)是指在內部生成併發送到外部上游管道的重新整理。這會同時重新整理上游的所有階段。本地重新整理和外部重新整理均基於內部條件生成。

只有 DU 可以從外部停止 FU 的執行。當 DU 置位停頓時,FU 的內部指令流水線(緩衝區 1 –> 緩衝區 2)應立即停止,並且由於 FU 無法再接收來自 IMEM 的資料包,它還應向 IMEM 置位mem_stall 。根據 IMEM 中的流水線/緩衝深度,PC 生成邏輯最終也可能被來自 IMEM 的cpu_stall停止,因為 IMEM 無法再接收任何請求。FU 中不存在導致本地停頓的內部條件。
只有 EXU 可以外部重新整理 FU。EXU 會在 CPU 指令流水線中啟動branch_flush 函式,並傳入重新整理流水線後要獲取的下一條指令的地址 ( branch_pc )。FU 提供了重新整理介面 (Flush I/F),以便接受外部重新整理。
FU 中的緩衝區 1、緩衝區 2 和 PC 生成邏輯透過branch_flush重新整理。來自分支預測邏輯的訊號branch_taken也充當了對緩衝區 1 和 PC 生成邏輯的本地重新整理。如果分支被採用:
-
下一條指令應從分支預測的 PC 中獲取。因此,PC 生成邏輯應被重新整理,並且下一條 PC 應 = branch_pc。
-
緩衝區 1 中的下一條指令應被重新整理並使其無效,即插入 NOP/bubble。

奇怪為什麼 Buffer-2 沒有被branch_taken重新整理?因為來自 Buffer-1 的分支指令(負責重新整理生成)應該在下一個時鐘週期緩衝到 Buffer-2,並允許其在流水線中繼續執行。這條指令不應該被重新整理!
指令記憶體流水線也應該進行適當的重新整理。IMEM 重新整理mem_flush由branch_flush和branch_taken生成。
讓我們整合目前為止設計的所有微架構,以完成 Fetch Unit 的架構。

好了,各位!我們已經成功設計出Pequeno的Fetch Unit了。在接下來的部分中,我們將設計Pequeno 的解碼單元(DU:Decode Unit)。
解碼單元
解碼單元(DU)是 CPU 流水線的第二階段,負責將來自取指單元(FU)的指令譯碼,並送至執行單元(EXU)。此外,它還負責將暫存器地址譯碼,並送至暫存器檔案進行暫存器讀操作。
讓我們定義解碼單元的介面。

其中,FU介面是獲取單元和解碼單元之間接收有效載荷的主要介面。有效載荷包含獲取的指令和分支預測資訊。此介面已在上一部分討論過。
EXU介面是解碼單元和執行單元之間傳送有效載荷的主要介面。有效載荷包括解碼後的指令、分支預測資訊和解碼資料。
以下是構成 EXU I/F 的指令和分支預測訊號:

解碼資料是 DU 從獲取的指令中解碼併發送到 EXU 的重要資訊。讓我們來了解一下 EXU 執行一條指令需要哪些資訊。
-
Opcode、funct3、funct7:標識 EXU 對運算元要執行的操作。
-
運算元:根據操作碼,運算元可以是暫存器資料(rs0,rs1),用於寫回的暫存器地址(rdt),或 12 位/20 位立即數。
-
指令型別:標識必須處理哪些運算元/立即值。
-
解碼過程可能比較棘手。如果您正確理解了 ISA 和指令結構,就可以識別出不同型別的指令模式。識別模式有助於設計 DU 中的解碼邏輯。
以下資訊被解碼並透過 EXU I/F 傳送到 EXU。


EXU 將使用此資訊將資料解複用到適當的執行子單元並執行指令。
對於 R 型指令,必須解碼並讀取源暫存器rs1和rs2 。從暫存器讀取的資料即為運算元。所有通用使用者暫存器都位於 DU 外部的暫存器堆中。DU 使用暫存器堆介面將rs0和rs1 的地址傳送到暫存器堆進行暫存器訪問。從暫存器堆讀取的資料也應與有效載荷一起在同一時鐘週期內傳送到 EXU。

暫存器檔案讀取暫存器需要一個週期。DU 也需要一個週期來寄存要傳送到 EXU 的有效載荷。因此,源暫存器地址由組合邏輯直接從 FU 指令包解碼。這確保了 1) 從 DU 到 EXU 的有效載荷和 2) 從暫存器檔案到 EXU 的資料的時序同步。
只有 EXU 可以從外部停止 DU 的執行。當 EXU 置位停止時,DU 的內部指令流水線應立即停止,並且由於無法再接收來自 FU 的資料包,它還應向 FU 置位停止。為了實現同步操作,暫存器檔案應與 DU 一起停止,因為它們都位於 CPU 五級流水線的同一級。因此,DU 將外部停止從 EXU 反饋到暫存器檔案。DU 內部不存在導致本地停止的情況。
只有 EXU 可以外部重新整理 FU。EXU 會在 CPU 指令流水線中啟動branch_flush 函式,並傳入重新整理流水線後要獲取的下一條指令的地址 ( branch_pc )。DU 提供了重新整理介面 (Flush I/F),以便接受外部重新整理。
內部流水線由branch_flush重新整理。來自 EXU 的branch_flush應該立即使指向 EXU 的 DU 指令無效,且延遲時間為 0 個時鐘週期。這是為了避免在下一個時鐘週期 EXU 中出現潛在的控制風險。
在取指單元 (Fetch Unit) 的設計中,我們沒有在收到branch_flush 指令後,以 0 週期延遲使 FU 指令失效。這是因為 DU 在下一個時鐘週期也會被重新整理,因此 DU 中不會發生控制冒險 (control hazard)。所以,沒有必要使 FU 指令失效。同樣的思路也適用於從 IMEM 到 FU 的指令。

上述流程圖展示了來自 FU 的指令包和分支預測資料如何在指令流水線的 DU 中進行緩衝。DU 中僅使用單級緩衝。
讓我們整合迄今為止設計的所有微架構,以完成解碼單元的架構。

目前我們已經完成了:取指單元(FU)、譯碼單元(DU)。在接下來的部分中,我們將設計Pequeno的暫存器檔案。
暫存器檔案
在 RISC-V CPU 中,暫存器檔案是一個關鍵元件,它由一組通用暫存器組成,用於在執行期間儲存資料。Pequeno CPU 有 32 個 32 位通用暫存器 ( x0 – x31 )。
暫存器x0稱為零暫存器 (zero register)。它被硬連線到一個常量值 0,提供一個有用的預設值,可與其他指令一起使用。假設您想將另一個暫存器初始化為 0,只需執行mv x1, x0即可。
x1-x31是通用暫存器,用於儲存中間資料、地址和算術或邏輯運算的結果。
在前文設計的 CPU 架構中,暫存器檔案需要兩個訪問介面。


當中,讀訪問介面用於讀取 DU 傳送地址處的暫存器。某些指令(例如ADD)需要兩個源暫存器運算元rs1和rs2。因此,讀取訪問介面 (I/F) 需要兩個讀取埠,以便同時讀取兩個暫存器。讀取訪問應為單週期訪問,以便讀取資料與 DU 的有效載荷在同一時鐘週期內傳送到 EXU。這樣,讀取資料和 DU 的有效載荷在流水線中保持同步。
寫訪問介面用於將執行結果寫回到 WBU 傳送地址處的暫存器。執行結束時僅寫入一個目標暫存器rdt 。因此,一個寫入埠就足夠了。寫入訪問應為單週期訪問。
由於 DU 和暫存器檔案需要在流水線的同一階段保持同步,因此它們應該始終一起停止(為什麼?請檢視上一部分的框圖!)。例如,如果 DU 停止,暫存器檔案不應將讀取資料輸出到 EXU,因為這會損壞流水線。在這種情況下,暫存器檔案也應該停止。這可以透過將 DU 的停止訊號反轉生成暫存器檔案的read_enable來確保。當停止有效時,read_enable被驅動為低電平,先前的資料將保留在讀取資料輸出端,從而有效地停止暫存器檔案操作。
由於暫存器檔案不向EXU傳送任何指令包,因此它不需要任何重新整理邏輯。重新整理邏輯只需在DU內部處理。
總而言之,暫存器檔案設計有兩個獨立的讀取埠和一個寫入埠。讀寫訪問均為單週期。讀取的資料會被寄存。最終架構如下:

目前我們已經完成了:取指單元(FU)、譯碼單元(DU)、暫存器檔案。
後續部分,敬請期待。
參考連結
https://chipmunklogic.com/digital-logic-design/designing-pequeno-risc-v-cpu-from-scratch-part-1-getting-hold-of-the-isa/
END
👇半導體精品公眾號推薦👇
▲點選上方名片即可關注
專注半導體領域更多原創內容
▲點選上方名片即可關注
關注全球半導體產業動向與趨勢
*免責宣告:本文由作者原創。文章內容系作者個人觀點,半導體行業觀察轉載僅為了傳達一種不同的觀點,不代表半導體行業觀察對該觀點贊同或支援,如果有任何異議,歡迎聯絡半導體行業觀察。

今天是《半導體行業觀察》為您分享的第4030期內容,歡迎關注。
推薦閱讀



『半導體第一垂直媒體』
即時 專業 原創 深度
公眾號ID:icbank
喜歡我們的內容就點“在看”分享給小夥伴哦

