# Linux 核心專題: 將 Linux 執行於 FPGA 為基礎 RISC-V 處理器 > 執行人: Chiwawachiwawa > [專題解說錄影](https://youtu.be/sDlCRbQSr4k) :::success :question: 提問清單 * ? ::: ## 任務描述 以 [vivado-risc-v](https://github.com/eugene-tarassov/vivado-risc-v) 為出發點,理解多核 RISC-V 處理器的運作,並在其上運作 Linux 核心 (v5.19)。預計產出: * 熟悉 RTL 合成和相關模擬器 * 確保多核 RISC-V 處理器運作符合預期,並運用 Linux 核心來驗證 ### 自訂義 OOOE multicore soc(以 NCKUJSARCHOOoE_v1 以及 CWWPPBV1 為例) 如何定義:我設定了四個大顆的 BOOM,以及兩個小顆的 Rocket64,並且拓寬了 tilebus (可添加選項有:NLP,ROCC...) ![](https://hackmd.io/_uploads/r1y7qp7S3.png) ## Chipyard 講解與自行定義(RocketTile) 想要了解如何自訂一個 RISC-V 的 SoC ,我們需要修改相關的設定檔案。 這邊由於我傾向於著眼於 rocket-chip 這個專案進行客製化設定,目前我的想法是, 我們可以修改一個本專案不需要的 CONFIG 組合 e.g., 4 核 rocketcore,也就是說, 當 make 這個 CONFIG 的時候,實際上在 workspace 生成出的東西我們只需要他的自定義 CPU 而已, 這也是 CWWPPB & JSARCHOOOEV1 產出的方法, 目前有問題的是 JSARCHOOOEV1 ,原因出在我將四個 mageboom 與兩個 rocket64 塞入其中,導致了在同一個時鐘因為電路(net_list長度不同而導致的巨大偏斜) 最後有問題的兩個地方都被 vivado 在優化時給處理掉,導致最後的 gem-bitstream不成功。 最終的妥協為我減少了四顆 CPU,剩下的兩顆便可以進行正常運作。 這邊附上我的代碼 單核 ```scala class NCKUJSARCHOOoEv1 extends Config( new WithInclusiveCache ++ new WithNBreakpoints(8) ++ new boom.common.WithNNCKUCSOoOEv1(1) ++ new RocketWideBusConfig) ``` 多核 ```scala class CWWPPBV1 extends Config( new WithInclusiveCache ++ new WithNBreakpoints(8) ++ new boom.common.WithNLargeBooms(2) ++ new WithNBigCores(4) ++ new RocketWideBusConfig) ``` 自定義的代碼 ```scala class WithNNCKUCSOoOEv1(n: Int = 1, overrideIdOffset: Option[Int] = None) extends Config( new WithTAGELBPD ++ // Default to TAGE-L BPD new Config((site, here, up) => { case TilesLocated(InSubsystem) => { val prev = up(TilesLocated(InSubsystem), site) val idOffset = overrideIdOffset.getOrElse(prev.size) (0 until n).map { i => BoomTileAttachParams( tileParams = BoomTileParams( core = BoomCoreParams( fetchWidth = 4, decodeWidth = 2, numRobEntries = 64, NLP = True, debug = False, issueParams = Seq( IssueParams(issueWidth=1, numEntries=12, iqType=IQT_MEM.litValue, dispatchWidth=2), IssueParams(issueWidth=2, numEntries=20, iqType=IQT_INT.litValue, dispatchWidth=2), IssueParams(issueWidth=1, numEntries=16, iqType=IQT_FP.litValue , dispatchWidth=2)), numIntPhysRegisters = 96,//diff 80 numFpPhysRegisters = 96,//diff 64 numLdqEntries = 32,//16 numStqEntries = 32,//16 maxBrCount = 60,//12 enableBranchPrediction = true,//diff numFetchBufferEntries = 16, ftq = FtqParameters(nEntries=32), nPerfCounters = 6, fpu = Some(freechips.rocketchip.tile.FPUParams(sfmaLatency=4, dfmaLatency=4, divSqrt=true)) ), dcache = Some( DCacheParams(rowBits = site(SystemBusKey).beatBits, nSets=64, nWays=4, nMSHRs=2, nTLBWays=8) ), icache = Some( ICacheParams(rowBits = site(SystemBusKey).beatBits, nSets=64, nWays=4, fetchBytes=2*4) ), hartId = i + idOffset ), crossingParams = RocketCrossingParams() ) } ++ prev } case SystemBusKey => up(SystemBusKey, site).copy(beatBytes = 8) case XLen => 64 }) ) ``` ![](https://hackmd.io/_uploads/Syn0pvXu3.png) CWWPPBV1 ![](https://hackmd.io/_uploads/HyMPGTcPh.png) config內容: ```scala class CWWPPBV1 extends Config( new WithInclusiveCache ++ new WithNBreakpoints(8) ++ new boom.common.WithNLargeBooms(2) ++ new WithNBigCores(4) ++ new RocketWideBusConfig) ``` ![](https://hackmd.io/_uploads/Sya-o8nwn.png) ## block designs ### Arty A7-100T ![](https://hackmd.io/_uploads/r1PhxS9vh.png) 布局 ![](https://hackmd.io/_uploads/HynLZrqwn.png) 我這邊採用了 SD card 來進行我們的資料儲存, 考量到這塊板子的 LUT 有限, 我們直接在整個 SOC 內部處理好網路相關的作業, 採取了一個 sd-controller 搭配乙太網路的 IP 進行 MAC 層級溝通, 時鐘方面採用 Arty A7 裡面自帶的一個時鐘源。 最後利用 uart 來進行控制以及 debug。 ### VCU1525 請參考以下文件 [VCU1525](https://docs.xilinx.com/v/u/en-US/ug1268-vcu1525-reconfig-accel-platform) ![](https://hackmd.io/_uploads/BkqkuEivh.png) 由於他自帶 DDR4(一共四條) ,因此我需要利用 PCIE-DMA 來控制他, 我選擇將資料放在其中自帶的記憶體之中 布局 ![](https://hackmd.io/_uploads/ry-xO4jPn.png) ### C1100 布局(重新設計中) 由於牽涉到 HBM 內容,原本的 AXI 介面變得更加麻煩,因此重新設計中... 使用的 RISC-V CPU ### 使用Boom成功點亮機器 mobax觀察開機成果: ![](https://hackmd.io/_uploads/Sk0cGkjwn.png) 可以看到我們的版本是正確的(rocket64b1 & rocket64z1 & CWWPPBV1 偷偷的...桑卑鄙) ![](https://hackmd.io/_uploads/ByJOGeoDn.png) 由於一些原因,我目前的 C1100 只能點亮無乙太網路的版本(主要原因是因為窮) ### 虛擬機器的問題: 燒錄SD卡片的問題: 格式化問題,請不要先將 SD卡 掛在虛擬機器上面,因為一個IO設備一次只能給一個機器使用,因此我們需要先在 windows 本地端先將 SD-card 轉成可用的格式。 這邊我發現,專案中的 make sd-card 有可能會導致 sd 卡片損毀(別懷疑,我搞壞了兩片) FPGA flash 燒錄遇到的問題(請不要直接用 GUI 來進行燒錄): HW_server 對於虛擬機器產生的問題,由於 xilinx 的設計有要求我們的虛擬機用到的空間不得為位於 C 槽的地址(因為我的 vivado 裝在 D槽),因此我的解決方法為再 windows 端建立一個相同的環境(使用一些工具讓我的 windows 可以用 make 等等簡單的 linux 指令), 這樣就解決了驗證燒錄 flash 的時候,HW_server被重複占用的情形(我的理解是,由於 HW_server 的實體位置在電腦的 D 槽內,但是我的虛擬機器使用的是C槽,並且開啟他的時候需要再被掛載到 virtual box 的IO上面,造成我們對於燒錄地址的驗證會發生錯誤?) 這個問題有點像是我在處理 vivado licence 的時候遇到的問題。 也是因為虛擬機器的網路遮罩以及 mac 地址對不上才發生的錯誤,遇到這樣的情況我都是傾向於直接兩地都試看看 custom 對於資源的取捨: 我為了將一顆 mega-boom 塞入 Arty A7 之中,我關閉了在其中的 NLP config & debug 選項,其中有兩個原因: 1. 關閉 NLP 的原因很簡單,因為 LUT 的單位不夠了 2. 關閉 debug 的原因,因為我是直接使用 risc-v 64 boom 來 boot Linux,我不需要為了先去驗證他的正確性而花大筆的資源再 jtag-debug 上面,更何況,我再 vcu1525 上面進行調適的時候,出問題的地方就是用來 debug 的 jtag-intrer-clock,在我關掉它設定的這個功能之後,問題也隨之消失! 遇到多項問題 1.ip問題: 由於我使用的是VCU系列為原型模板,因此我需要考量到 IP 版本問題, 不過我目前皆已解決。 資源不足問題( IO 未正確封裝) 這個比較麻煩,由於 C1100 的 IO 數量比較少,並且我拿到的不是公版,因此我目前沒版法把 ethernet 相關的 IP 加入進去 重新封裝(開蓋重新裝回去,讓合法IP update,會這樣用因為她兩個版本的IP通用,因此可以這樣搞,不曉得為啥會一開始無法使用) 移植 vivado_riscv 為 C1100 專用的高速乙太網路介面(目前先捨棄) 因為沒有多的 ports 給乙太網路,因此我想了一兩個替代方案(UART 方案放棄,因為我只有一個 UART介面,我需要留下來進行驗證)。 重新定義腳位,修改為符合的~~DDR4協定內容~~(更改為HBM),我寫到後面發現... C1100 根本不是用 DDR4,這兩塊板子根本不一樣!!!! 事實證明不能一味的相信那個專案 移植遇到的問題 ### IP介紹篇: 重要的 IPs&介面 #### AXI & AXIlite 我參考的是這份文件 [Vivado AXI Referenc-xilinx](https://docs.xilinx.com/v/u/en-US/ug1037-vivado-axi-reference-guide) >AXI is part of ARM AMBA, a family of micro controller buses first introduced in 1996. The first version of AXI was first included in AMBA 3.0, released in 2003. AMBA 4.0, released in 2010, includes the second major version of AXI, AXI4. There are three types of AXI4 interfaces: • AXI4: For high-performance memory-mapped requirements. • AXI4-Lite: For simple, low-throughput memory-mapped communication (for example, to and from control and status registers). • AXI4-Stream: For high-speed streaming data. 可以看到 AXI 的主要用處,是讓我們可以不必跟負責且多樣的介面訊號去做一根一根的對接 舉個例子 由我的出發點開始,也就是我透過 vivado risc-v 獲得的 BOOM 原始程式碼, 他其實是附上了40多個介面的 CPU 但是我們可以透過封裝的方式為他留出幾個介面出來。 這其中就包含了我們的 AXILite 介面 (負責與我們的 DMA 對接,還有透過 smartconnector 來控制我們的 IO)。 ![](https://hackmd.io/_uploads/B1FEPiaw3.png) ![](https://hackmd.io/_uploads/Sya-o8nwn.png) 接下來利用 jtag-serise7 以及 syn_rest 就可以得到一個容易操作的 BOOM CPU 啦。 ![](https://hackmd.io/_uploads/HyMPGTcPh.png) 這邊有一個問題,由於每一塊板子的 jtag-serise7 的版本都不同(有的要用XX.1有的要用XX.2),我是將被鎖住的,已經封裝過的 rocket ip 直接拿去綜合,這樣系統就會自行找到需要升級或是替換的地方,很幸運的是,我們只需要進行升級就可以把 vcu1525 的 ip 升級為 C1100 可以用的版本。 講了這麼多,我們來看看 AXI的原理吧!!(以上資訊接來自 UG 1037) ![](https://hackmd.io/_uploads/BkoVbw3Ph.png) 這是 AXI4-Lite & AXI4 共有的介面部分, 其中有一些規則: 讀地址信號都以AR開頭(A:address;R:read)。 寫地址信號都以AW開頭(A:address;W:write)。 讀數據信號都以R開頭(R:read)。 寫數據信號都以W開頭(W:write)。 應答型號都以B開頭(B:back(answer back))。 AXI4-Stream 的主要 interface 有: (1)ACLK信號:總線時鐘,上升沿有效; (2)ARESETN信號:總線復位,低電平有效(這也是為啥有些 IP 要設定為 active low 的原因)。 (3)TREADY信號:從機告訴主機準備好傳輸; (4)TDATA信號:數據,可選寬度32、64、128、256位。 (5)TSTRB信號:每一位對應TDATA的一個有效字節,寬度為TDATA/8。 (6)TLAST信號:主機告訴從機該次傳輸為突發傳輸的結尾; (7)TVALID信號:主機告訴從機本次傳輸有效; (8)TUSER信號:用戶定義信號,寬度為128位。 AXI在我這個專案之中起到的一個很重要的作用是: 他可以自訂我們的主從數量以及之間的關係(但是要嚴格的限制我們的 clock 來源,不同來源的 master 要控制適合他們的 slaves),若不然,很容易造成一些很嚴重的問題,要達成這個目的,我使用了 smartconnector這個 IP, 以下提供一個實際案例 ![](https://hackmd.io/_uploads/HkbdTPnPh.png) 可以看到其中兩個 smart connector 他們接有來自不同時域的 master,並且控制了一些相同的 blocks 這樣衍生出一個問題,如果我們的時鐘來源沒有設計好,會造成資料讀取上,以及綜合上的失敗,因此,我們要添加各種不同的執中來源,可以在 IP 的客製化界面選擇(這很重要阿,沒綁對有的時候綜合依然可以完成,但是後續會出大問題...) 這張來自CSDN的圖片我覺得統整得很好 ![](https://hackmd.io/_uploads/rkh7HuhPh.png) ##### hand-shake 機制 這個機制主要是,當我們的發送端需要發送東西給他的接收端時, 他會產生一個 valid 訊號, 來證明他的控制 or 數據是合法的。 而在接收端會發出 ready 訊號, 表示他已經準備好去接收來自發送端的訊號, 當兩者皆為 active_high 的時候,就可以進行傳輸了。 那這邊有一個問題, 如果都沒有同時 active_high 呢? 會產生幾種情況, 但是最終會回到某一端先 active_high 等待他的另外一方接受他的訊號(跟高中時單戀的我一樣可憐QQ) ##### 另外一個類似 smart connecter 的 IP ![](https://hackmd.io/_uploads/B1ccDsTwh.png) ##### 資料修飾 AXI4-Stream 主要傳遞三種不一樣的資訊, data type、position type、null type。 其中分別是 data type 表示我們要用的資料, position type 表示我們的訊號位移或是相對地址, null type 則代表我們無用的訊號。 接下來回到我們常用的 AXI 的幾種介面,這邊我提供一個案例。 DDR: 這邊我們給個案例,可以看到 AXI 的命名方式符合我上述提到的規則!! ![](https://hackmd.io/_uploads/ryWyEuhD3.png) #### HBM [來自 AMD 的參考資料](https://www.amd.com/zh-hant/technologies/hbm) 尚在研究 #### PCIe XDMA [參考的 IP 文檔](https://docs.xilinx.com/r/en-US/pg195-pcie-dma/Introduction) ![](https://hackmd.io/_uploads/rJCxjspw3.png) 以 Xilinx 家的 DMA 控制器(AXI Direct Memory Access)的讀取功能(Read Channel)為例,它能夠通過 AXI 讀取某個地址範圍的數據,並將這些數據以數據流的形式傳輸給處理單元。這樣可以在無需 CPU 干預的情況下進行數據傳輸和搬運。 緩衝區長度的最大位寬為26,對應的單次傳輸大小最大為 64MByte。 對於簡單的數據傳輸,普通模式的 DMA 完全可以滿足要求。 但是在處理大規模數據(單次傳輸大於 64MByte )或非連續地址的情況下,普通模式的 DMA 無法滿足要求。 即使可以將大規模數據分割為多個小於 64MByte 的片段,但這個過程需要額外的處理器(例如 ARM/NIOS/MicroBlaze/RISC-V)進行查詢和監測,或者需要啟用中斷函數,這樣會額外消耗處理器性能(我們希望一次配置,永久使用)。 針對這樣的問題,通常啟用該 IP 的高級功能—— Scatter Gather (SG) Engine 模式。 雖然單個傳輸片段仍然有 64MByte 的限制(取決於 Buffer Length Width),但我們可以將大於 64MByte 的數據劃分為多個小於64MByte的區域來解決。而且在這種模式下,我們可以傳輸多個地址不連續的片段,同樣能夠完美解決這個問題。 在啟用 Scatter Gather Engine 後,IP 核的介面如下圖所示。與普通模式的 DMA 相比,多了一根 M_AXI_SG 總線。 ![](https://hackmd.io/_uploads/B1A_TopP3.png) 這個 IP 有三個功能可以使用, 1. 7 Series Integrated Block for PCI Express > 這個很複雜,故我沒有採納(並且 C1100 也不是 7 系列的)。 3. AXI Memory Mapped To PCI Express ![](https://hackmd.io/_uploads/HJyOZhpw3.png) 5. DMA/Bridge Subsystem for PCI Express (PCIe) > 我最後採用的是它,因為它的介面相對簡單,並且需要的 LUT 格數較少,很契合我在 C1100 上面的需求(詳細原理與運作請看上面的文獻)。 這個 IP 可以分為兩個部分, 第一個部分為 AXI MM/S bridge 第二個部分為 AXI-S Enhanced pcie 他們分別為:用戶與 AXI4 溝通的介面,另外一端為與 PCIE 傳輸的介面, 由於我們的 rocket64 core 封裝時,我使用了 AXI4-Lite 的介面,因此我需要用到這個 IP 其中的 AXI MM/S brige。 而末端的 AXI-S 模組則是負責與 PCI-e 交流,它可以將 PCI-e 的訊號轉換為 AXI 使用的資訊,詳細的演算法請見我上面提供的文件以及可以在 vivado 上面的 ip-src/ 目錄內獲得!! #### UART & UART_LITE [UART](https://docs.xilinx.com/v/u/en-US/axi_uartlite_ds741) [UART_LITE](https://docs.xilinx.com/v/u/en-US/axi_uartlite_ds741) 看一下 uartlite 我主要用到的是它 ![](https://hackmd.io/_uploads/H1MDGmCw2.jpg) 可以看到它內部有四組暫存器, 1. 用來確保接收: active high 為保留 low 為有效 2. 用來確保發送: 與 RX 類似 3. 紀錄狀態: 最低位(第0位),只要是1就會清空 FIFO ,若0則保留 FIFO,切記,任何操作之後最終都會歸0(免得顯示為發送或接收但是並無資料可用),另外一提,第四位表示中斷是否有效,若為1,表示有效中斷訊號,其餘保留。 4. 控制:狀態暫存器,控制每一個單位的狀態 >第01位表示 RX 的 FIFO 是否有數據,以及是否為滿的(預設為0) >第23位表示 TX 的 FIFO 是否有數據,以及是否為滿的(同樣預設為0) >第4位如果表示 1 則代表需要進行中斷, >第5位為1表示有 overrun(FIFO滿了還在收數據) >第6位為1表示 frame 錯誤(stop bit 為 0) >第7位表示奇偶驗證時發生錯誤,此時表示為1。 接下來就是基本的收發介面。 ### 約束與時間篇: #### 如何定義好時間約束 教學1 [xilinx 官方文件](https://docs.xilinx.com/r/2021.2-English/ug899-vivado-io-clock-planning/Using-the-Memory-Bank/Byte-Planner) 教學2[同樣來自官方文件](https://xilinx.eetrend.com/files-eetrend-xilinx/forum/201508/9045-19845-fpgashi_xu_yue_shu_fang_fa_.pdf) 我前期吃到的大虧 1.**時鐘偏斜**[相關文章](https://blog.csdn.net/sinat_41774721/article/details/123430089?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168694526416800225517796%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=168694526416800225517796&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-20-123430089-null-null.142^v88^insert_down1,239^v2^insert_chatgpt&utm_term=clock%20tree%20synthesis&spm=1018.2226.3001.4187)我的參考資料 ![](https://hackmd.io/_uploads/HykFR45wh.png) #### 如何合理的安排你的管腳 遇到的問題 1. 電壓 相同 bank 的電壓要相同,但是請觀察規格書,像是在 C1100 跟 VCU1525 ,同樣的 IP 但是要求的電壓卻不同,這也影響到了我們的管腳規劃策略(像是 VCU1525 把 DDR4 跟 XDMA 綁在相同的 BANK 之中,但是在 C1100 裡面,HBM跟XDMA卻不可以綁一起,因為他們一個要求要18,一個要15) 2. 管腳種類 有的管腳可以替代其他類型的管腳,但是極度不建議這樣做。 這邊需要提及的是,有些 IO 的腳位規定要綁訂在同一個 bank 之中,但是因為需要的電壓是不同的,我們需要調整 IP 進行重新設計,不然就是利用 byte planner 進行調整。 ### 奇怪的通靈手法 我發現當我們把 uart 以及 BOOM直接共用一個clock會導致在opt_design時會造成BOOM裡面 Jtag的 latch,並且把uart_tx 的電路給優化掉,但是 uart_tx 用的是一個intar_clock 我沒辦法比照內部時序的處理方法來解決這個問題(將有問題的netlist設定成偽路徑( fault_path )) 這個問題在 arty & C1100 上面好解決,降頻就好...(值得一說,佈局上的距離長短並不能完全說明兩個腳位實際相差距離,有可能明明我配置得很近,但是我時間約束,針腳特性約束出問題,導致優化線路時時序爆炸導致被系統分配到一個過遠的距離! ) C1100 結構 ![](https://hackmd.io/_uploads/B1xOxN5v2.png) 我拿到的是沒有QSFP28介面的(有的話我也沒法用,因為我家沒有光纖網路...) 我目前是嘗試將 MII 綁在PCIE上面,透過 DMA brige IP進行 tx,rx的轉換, 到時候用 pciex16 to USB 再接一條乙太網路轉 USB 的轉接線進行測試(縫合怪) 目前佈局成功(但沒有將乙太網路模組裝上去) ## 香山篇 研究 [XiangShan](https://github.com/OpenXiangShan/XiangShan) (香山) 實作,嘗試補足其 RTL。 預計產出: * 熟悉 RTL 合成和相關模擬器 ### 在模擬器上模擬香山 我們採取 NEMU 以及 verilator 來進行驗證。 (完成:香山可以模擬,vivado risc-v 完成) NEMU 以及 verilator 皆已完成 ![](https://hackmd.io/_uploads/Hy_ARonP3.jpg) ![](https://hackmd.io/_uploads/H1_ACinw2.jpg) 香山生成,模擬成功(可透過兩種方式呈現) 未來會於 C1100 上面進行驗證 獲得的香山 core ![](https://hackmd.io/_uploads/ryyjfQx_2.png) ### 香山可以探討的問題: 當我要在 FPGA 上面進行香山的驗證時,我發現香山的 IO,MEM 等等的介面並非使用 AXI4 的協議, * 確保 RISC-V 處理器運作符合預期,並運用 Linux 核心來驗證 請見下面附錄,我在 C1100 上面目前需要把 device tree 上面的部分內容修掉(主要是我加入乙太網路的話會出問題,因此我拿了無乙太網路的版本,見 vivado risc-v 的專案早期版本) Arty A7-100T的話是可以全部運行的 ## TODO: 在 Xilinx FPGA 驗證 [vivado-risc-v](https://github.com/eugene-tarassov/vivado-risc-v) 完成,請見實體 ### 安裝步驟,FPGA,Vivado等等雜事 一秒鐘 3 mb 的下載速度,我下載了兩次... ## 安裝妥善我們的專案 在我們獲得 [vivado-risc-v](https://github.com/eugene-tarassov/vivado-risc-v) 專案 workspeace/ 底下的 bitstream ,我需要以下是一個雙核的 rocketCpu SOC (dual Boom core 我會自己畫), 目前的了解為 tile 可以替換為 BOOM core (而非整個 tile 替換成) 這邊發現不管是幾個核的 SOC ,皆需要一個 TileBus 來跟他的 systembus 做溝通,我目前只看懂 `config.scala` 的初步概念,但是要了解整個系統是如何做到溝通的,還是需要細讀下去, 這邊進入編譯好的專案 scala/system 底下 我們主要看到兩個有關自行定義的部分 `testharness.scala` & `CONFIG.scala` 修復他 sd-card 的準備腳本 修復一些 makefile 的小問題以及註解 ## rocket vs BOOM 硬體部分介紹 ![](https://hackmd.io/_uploads/r1y7qp7S3.png) 我們需要了解多核如何相互溝通,我先閱讀 Config.scala 這檔案 (用來自訂的),最後我需要去看生成器對於 Bus 的設計 2.PTW 3.L1 cache 4.TileBus 暫存器堆(register file)用於 register renaming 何為 N-wide CPU? 依據我目前的理解,代表一次可以執行 N 道指令的 CPU (wide 並非越大越好,需考慮 branch 的因素,若是我們的pipeline stage 不夠,會造成當我們浪費過量的 pipeline stages) 這邊我們來探討 MegaBoom vs rocket MegaBoom rocket 在我的實驗中,產出 Boom 所需花費的成本是 rocket 的很多倍(vivado的模擬時間,產出的 vivado 專案大小), ### 如何實作異質多核 ```scala class DualLargeBoomAndSingleRocketConfig extends Config( new boom.common.WithNLargeBooms(2) ++ // add 2 boom cores new freechips.rocketchip.subsystem.WithNBigCores(1) ++ // add 1 rocket core new chipyard.config.WithSystemBusWidth(128) ++ new chipyard.config.AbstractConfig) ``` 進度,模擬器可以正常工作,但是加入 C1100 的時候會遇到大問題。 ### 自訂一顆屬於你的 CPU CORE 以CWWPPBv1來做案例 [](https://hackmd.io/_uploads/SJH7_69D3.png) #### 透過 Config 可以決定的生成內容為: These guides will walk you through customization of your system-on-chip: 首先我們看到這個 exampleRocketsystem.scala 整顆 core 的個階段細節可以在 /scala/subsystem/Configs.scala 可以看見 #### 遇到的障礙 ## cores rocket&BOOM 行為機制介紹 ### BOOM 如何運作 #### 我們要探討為何 Boom 會比 rocket 大得多? [官方參考文章](https://docs.boom-core.org/en/latest/) 主要講解一下各階段相對重要的 Chisel codes rocket 為 in-order 核,BOOM 則為 out-of-order ![](https://hackmd.io/_uploads/BJLGpKiB3.png) Boom 一共分為 10 級管線 stage ,但是實際只實作 7 級 ### (1)Fetch(4 cycles) > Instructions are fetched from instruction memory and pushed into a FIFO queue, known as the Fetch Buffer. > Branch prediction also occurs in this stage, redirecting the fetched instructions as necessary. 這邊進行補充,因為 Boom 有對全分支的預測,推測進行支援。 C extention in Boom 特色一: 32 位元指令沒有對齊要求,可以從 half-word 邊界開始 特色二: 所有 16 位元指令直接映射到更長的 32 位元指令。 data from Sifive [17 “C” Standard Extension for Compressed Instructions, Version 2.0](https://five-embeddev.com/riscv-isa-manual/latest/c.html) 而要如何預測呢?將要透過 fetch 之中的各個小階段來完成(F0.F1...) 在前端階段,BOOM 從 i-cache 中檢索一個 Fetch Packet,快速解碼分支預測指令,並將 Fetch Packet 推入 Fetch Buffer。 但是!!! 我們會遇到四個問題 1. 因為微指令的加入,會增加我們 Decode 的複雜度。 2. Finding where the instruction begins.(這句話我並不是很理解)。 3. 在整個指令庫中要刪除+4 prediction,尤其在處理我們的 branch 時。 4. 有些指令會對不齊,尤其是從 cacheline 以及 virtual pages 上面運行的指令。 ![](https://hackmd.io/_uploads/By6_r8hSh.png) 首先我們看到 i-cache 這邊沿用了 rocket-core 的檔案 我們來看一下 i-cache 的結構(這邊我們只看結構,關於程式碼他有很多是採取了 rocket-core 的 scale 代碼)。 ![](https://hackmd.io/_uploads/SJanQbCBn.png) 這個模組的主要功能為進行指令的一級緩衝、取值,其中也使用了 btb.scala 中定義的分支預測技術(我們在 F1 的時候細講) 目前 i-cache 不支持跨 cache-lines 的指令抽取動作(也就是說該按照順序去進行包裹) #### 一個指令在 Boom fetch-stage 之中的處理流程 從 Front-end 返還 16 位元的資料, Front-end 保證必定會是 16 位元, 接下來我們會將資料傳輸進入 F3 階段, 這個階段主要是對我們的指令包由 i-cache response queue 釋出,並且放入 Fetch Buffer 。 這樣還沒結束,F3 階段需要將 fetch-packet 的最後16 bits,PC,以及 fetch-packet 的邊界(instruction boundaries)加以結合,成為一個 32 位元的資料,將之放入 Fetch Buffer 之中。 然後我們關注一下 Predecode 這個東西,他保證了每個 Fetch-packet 的起點。 並且它可以將一些用不到的雜訊利用 mask 的原理進行遮擋 `val in_mask = Wire(Vec(fetchWidth, Bool()))`, 使得我們的 Fetch-Buffer 不會儲存到無效或是非法格式的指令(微指令)。 #### 其他的小細節 主要有兩個問題, 其一: 我們需要判定我們的微指令是 16 位元,抑或 32 位元,用以將我們的 `PC + 2` 或是 `PC + 4` 其二: 不同的 Fetch-Packet 有可能有不同道指令數量,但我們一次是抓取 n wide 條指令,因此會遇到跨 packet 的但是需要一起抓取的情形,也就是抓取的範圍會超過 Fetch-packet 的 instruction boundaries,這邊需要額外判斷。 #### Fetch-Buffer 我們來看看他的 chisel code 如何運作的, 首先注意這段註釋 Bundle that is made up of converted MicroOps from the Fetch Bundle input to the Fetch Buffer. This is handed to the Decode stage. **何為 Micro-Op (UOP):** 在 Fetch-buffer 中的宣告代碼。 `val in_uops = Wire(Vec(fetchWidth, new MicroOp()))` "Element sent throughout the pipeline holding information about the type of Micro-Op, its PC, pointers to the FTQ, ROB, LDQ, STQs, and more." 以上來自官方的文件敘述。 接下來看這個模組的整個 IO 結構。 ```scala val numEntries = numFetchBufferEntries //可以自訂義 val io = IO(new BoomBundle { val enq = Flipped(Decoupled(new FetchBundle())) val deq = new DecoupledIO(new FetchBufferResp()) // Was the pipeline redirected? Clear/reset the fetchbuffer. val clear = Input(Bool()) }) ``` Step 1: Convert FetchPacket into a vector of MicroOps. ```scala for (b <- 0 until nBanks) { for (w <- 0 until bankWidth) { val i = (b * bankWidth) + w //這邊的 W 是依據我們使用的 Wide 寬度(1~4) val pc = (bankAlign(io.enq.bits.pc) + (i << 1).U) //這邊我們宣告 inputs in_uops(i) := DontCare in_mask(i) := io.enq.valid && io.enq.bits.mask(i) in_uops(i).edge_inst := false.B in_uops(i).debug_pc := pc in_uops(i).pc_lob := pc in_uops(i).is_sfb := io.enq.bits.sfbs(i) || io.enq.bits.shadowed_mask(i) // //接下來進行 Fetch-Buffer covert into a vector of MicroOps // if (w == 0) { when (io.enq.bits.edge_inst(b)) { in_uops(i).debug_pc := bankAlign(io.enq.bits.pc) + (b * bankBytes).U - 2.U in_uops(i).pc_lob := bankAlign(io.enq.bits.pc) + (b * bankBytes).U in_uops(i).edge_inst := true.B } } in_uops(i).ftq_idx := io.enq.bits.ftq_idx in_uops(i).inst := io.enq.bits.exp_insts(i) in_uops(i).debug_inst := io.enq.bits.insts(i) in_uops(i).is_rvc := io.enq.bits.insts(i)(1,0) =/= 3.U in_uops(i).taken := io.enq.bits.cfi_idx.bits === i.U && io.enq.bits.cfi_idx.valid in_uops(i).xcpt_pf_if := io.enq.bits.xcpt_pf_if in_uops(i).xcpt_ae_if := io.enq.bits.xcpt_ae_if in_uops(i).bp_debug_if := io.enq.bits.bp_debug_if_oh(i) in_uops(i).bp_xcpt_if := io.enq.bits.bp_xcpt_if_oh(i) in_uops(i).debug_fsrc := io.enq.bits.fsrc } } // 這邊我們可以看到,實際轉換的 in_uops(i).XXX 實際上是先指派 pc (program counter address)為特殊的"位址 or bits" 我們來看指定值的類型:io.enq.bits. io 代表我們的 in/out puts ,而由於下一階段需要 enqueue 我們的 outputs,因此將 enq 定義為下一個特性,最後由於我們需要配合 bit mask 進行一些 bitwise ops,因此給予最後一個特性為 bits。 // ``` Step 2: Generate one-hot write indices. ```scala val enq_idxs = Wire(Vec(fetchWidth, UInt(numEntries.W))) def inc(ptr: UInt) = { val n = ptr.getWidth Cat(ptr(n-2,0), ptr(n-1)) } var enq_idx = tail for (i <- 0 until fetchWidth) { enq_idxs(i) := enq_idx enq_idx = Mux(in_mask(i), inc(enq_idx), enq_idx) } ``` Step 3: Write MicroOps into the RAM. ```scala for (i <- 0 until fetchWidth) { for (j <- 0 until numEntries) { when (do_enq && in_mask(i) && enq_idxs(i)(j)) { ram(j) := in_uops(i) } } } ``` 進行完 enqueue 的三個步驟以後,進行 Dequeue Uops ```scala val tail_collisions = VecInit((0 until numEntries).map(i =>head(i/coreWidth) && (!maybe_full || (i % coreWidth != 0).B))).asUInt & tail val slot_will_hit_tail = (0 until numRows).map(i => tail_collisions((i+1)*coreWidth-1, i*coreWidth)).reduce(_|_) //這邊是用來判斷微指令的地址是否合法,主要判斷方式為,地址是否為 coreWidth 的倍數(確保大部分情況下皆有獲取到正確的 fetch-packet) val will_hit_tail = slot_will_hit_tail.orR //這邊宣告一個 tail(合法的地址界線)。 val do_deq = io.deq.ready && !will_hit_tail val deq_valids = (~MaskUpper(slot_will_hit_tail)).asBools //用 mask 來進行判斷是否 hit-the-tail ``` 產出可以 dequeue 到 read port 的向量 ```scala for (i <- 0 until numEntries) { deq_vec(i/coreWidth)(i%coreWidth) := ram(i) } io.deq.bits.uops zip deq_valids map {case (d,v) => d.valid := v} io.deq.bits.uops zip Mux1H(head, deq_vec) map {case (d,q) => d.bits := q} io.deq.valid := deq_valids.reduce(_||_) ``` 最後更新 I-Cache 的狀態 ```scala when (do_enq) { tail := enq_idx when (in_mask.reduce(_||_)) { maybe_full := true.B } } when (do_deq) { head := inc(head) maybe_full := false.B } when (io.clear) { head := 1.U tail := 1.U maybe_full := false.B } // TODO Is this necessary? when (reset.asBool) { io.deq.bits.uops map { u => u.valid := false.B } } } ``` 接下來,經過以上步驟之後,我們需要來討論 Fetch Target Queue 的程式碼。 並且探討 F1 的一些關鍵模組的程式碼。 ### NLP VS BPD [predictors](https://github.com/riscv-boom/riscv-boom/blob/master/src/main/scala/ifu/bpd/predictor.scala) ### NLP logic components(相對快速) 接下來看到這個單元。 它是由三個子模塊所組成的。 一個 Branch Target Buffer(BTB),BIM,以及 RAS 可以見到它的結構 ![](https://hackmd.io/_uploads/rJ9ur1l8n.png) #### BTB ![](https://hackmd.io/_uploads/rJl8Na182.png) 它是用來儲存 branch address ,以及用來存下預測後的結果。 在我們 fetch pc 時,需要跟 BTB 裡面的 TAG 進行比對,這樣才可找到"唯一的"對應欄位,進而讀取內容。 每一個 BTB 之中的欄位涵蓋一個預測過後的地址,當預測命中時,我們便可以將其中儲存的地址用做下一個 PC 地址。 #### BIM 全名:Branch Instruction Map 是用於儲存分支指令歷史記錄的資料結構。BIM 用於確定之前的預測是否正確,即分支是否被成功預測為「taken」(分支發生)或「not taken」(分支未發生)。 這邊我想討論 BIM 關於模型部分的程式碼。 BPD只做分支是否發生的預測,因此它需要依賴其他機制來提供有關哪些指令是分支指令以及它們的目標的信息。BPD可以使用分支目標緩衝器(BTB)來獲取這些信息,也可以等待指令從指令緩存中取出後自行解碼。這樣可以避免在BPD內部需要儲存PC標籤和分支目標 BPD在取指階段中以及與指令緩存和 BTB 並行存取,這允許 BPD 使用順序儲存器(例如 SRAM)來儲存。通過巧妙的設計,BPD 可以使用單口 SRAM 來實作所需的儲存密度。 ```scala package boom.ifu import chisel3._ import chisel3.util._ import org.chipsalliance.cde.config.{Field, Parameters} import freechips.rocketchip.diplomacy._ import freechips.rocketchip.tilelink._ import boom.common._ import boom.util.{BoomCoreStringPrefix, WrapInc} import scala.math.min class BIMMeta(implicit p: Parameters) extends BoomBundle()(p) //為一個 bundle ,它的特性來自 BoomBundle with HasBoomFrontendParameters { val bims = Vec(bankWidth, UInt(2.W)) } //定義參數大小,依據我們的 wide case class BoomBIMParams( nSets: Int = 2048 ) //定義sets class BIMBranchPredictorBank(params: BoomBIMParams = BoomBIMParams())(implicit p: Parameters) extends BranchPredictorBank()(p) { override val nSets = params.nSets require(isPow2(nSets)) //需要為 2 的冪次方 val nWrBypassEntries = 2 def bimWrite(v: UInt, taken: Bool): UInt = { val old_bim_sat_taken = v === 3.U val old_bim_sat_ntaken = v === 0.U Mux(old_bim_sat_taken && taken, 3.U, Mux(old_bim_sat_ntaken && !taken, 0.U, Mux(taken, v + 1.U, v - 1.U))) } ``` #### RAS 全名為 Return Address Stack, 用來儲存函數調用到的指令所需返回的地址, 也就是說,當我們的指令進行跳躍時,我們可以將原本的地址儲存進入 stack 之中, 而為何要進行存储呢? 因為我們可以將存下的地址進行判斷是否有跳躍,包含了跳躍,分支,或是返回的指令, 這有助於我們判斷哪一些指令是需要進行跳躍。 他是跟 BTB 一起工作的,因為我們要從 BTB 獲得 Fetch-Packet 進行對地址的判斷。 #### Backing Predictor (BPD )耗時較多 因為 NLP 雖然可以快速且良好的處理單週期的指令預測,但是它的面積以及耗能代價十分昂貴,並且 BIM 的模型無法學習很複雜或長的歷史模式, 為了捕捉更多的分支指令和更複雜的分支行為,BOOM 提供對 Backing Predictor (BPD) 的支持。 BPD的目標是在佔用面積較小的情況下提供非常高的準確性。 ### (2)Decode/Rename > pulls instructions out of the Fetch Buffer and generates the appropriate Micro-Op(s) (UOPs) to place into the pipeline. > 解碼階段(Decode stage)從取指緩衝區(Fetch Buffer)中獲取指令,將其進行解碼,並根據每個指令的需求分配必要的資源。如果不是所有的資源都可用,解碼階段將根據需要進行暫停(stall)。 ### (3)Rename/Dispatch 這個階段主要在重命名階段將每個指令的ISA(或邏輯)註冊器指定符映射到物理註冊器指定符。 重命名的目標是打破指令之間的輸出相依性(Write-After-Write)和反相依性(Write-After-Read),僅保留真正的相依性(Read-After-Write)。 ![](https://hackmd.io/_uploads/Syyo5DsP3.png) 可以看到`A Physical Register File, containing many more registers than the ISA dictates` BOOM 用的是超出 ISA 定義的 register 數量以及名字,未命名的 registers 他可以用來將需要重複使用的特定 registers 進行重新命名並且獲得指令所需的資源, > Rename Map Table s contain the information needed to recover the committed state. As instructions are renamed, their register specifiers are explicitly updated to point to physical registers located in the Physical Register File. ![](https://hackmd.io/_uploads/ByMJoDiD3.png) 可以看到 rename table 可以監控,觀察 rename 的每一個步驟以及資源。 他設計來保證我們的資料以及 register 的正確性以及資料的一致性。 而這個 table 之餘 CPU 該如何使用呢? 我們可以看到這一段: > As the RV64G ISA uses fixed locations of the register specifiers (and no implicit register specifiers), the Map Table can be read before the instruction is decoded! And hence the Decode and Rename stages can be combined. 可以看到 RV64G 有特定的暫存器可以用來讀取 table 裡面特定的 tags,這樣可以將 decode&rename 結合一起, 這也是明明我們有十個步驟卻只有七個 stage 的原因之一。 **另外在 IC 的設計上面,他可以減少電路的路徑深度(因為他將功能合併在一起) 這可以減少上述提到的時鐘偏斜問題!!!** 進而提高效能(我認為之所以異質多核的 CPU 會產生問題,是因為 rocket64使用的指令及雖然相同,但是他們在標記 rename-register 的位置會因為他經過了沒有 rename 功能的核心,導致了這個標誌是錯誤的,接下來進入 boom 讀取 table 時會有一些問題) #### Resets on Exceptions and Flushes 這邊有一個專門處理這些問題的 commit table (可以在 config 之中選擇是否需要,不選擇也沒關係) 主要的功能是讓我們的 CPU 可以提早獲知是否下面的指令有無被 flush 掉。 進而提高我們的 rename 的準確以及命中率! ### (4)Issue/RegisterRead Reorder Buffer (ROB) 他用來追蹤所有正在 pipeline 之中 instructions 的狀態,他可以讓使用者直觀上認為所有的指令都是在(in order)的,但其實不然,在我們添加指令進入之後,他會記錄其中的狀態以及是否忙碌,當指令完成時,會發送一個訊號令我們的 ROB 將指令標記為完成,當 ROB 之中 header 被標記為 wide 裡面的指令被全部方忙碌時, ROB 會顯示所有其中的指令(inorder 狀態),但是如果發生例外的情況(flush jump 等等)。 會將 ROB 裡面相關的指令清除乾淨並且不會顯示 不論有沒有完成所有的指令, ROB 在現階段指令(帳面上,其實裡面還是亂序)結束時會將 PC 指向正確的位置。 #### ROB的架構 ![](https://hackmd.io/_uploads/SJh7t_iP2.png) 可以看到在 ROB 裡面待得最久的指令是在 ROB的頭部,而最新的(或是最近添加的)會在 ROB 的尾部, 他的本質就是一個 BUFFER,他可以確保雖然我們的指令是亂序處理,出來時卻是順序的結果, 而當我們要加入的指令在不同的 instruction_bank 之中時, ROB 會將整個 BANK紀錄在其中。 這樣的好處是,我們每次添加指令時,成本會變得很便宜。 因為ROB紀錄的是添加指令的地址以及他所在的 instruction_bank 之中,因此我們不必將所有的指令重新沖刷,並重新加入前面的幾個步驟,或是需要大幅度的調整我們的 ROB_BUFFER(將所有的 BANK 加入所需要的代價是非常大的,等於我們需要重新設定一個新的 bank,只添加特定訊息可以很好的分擔整個系統的壓力與所需成本)。 #### ROB State #### Exception State ROB 會關注最前面的 exception(透過每個 queue裡面紀錄得一個 tag ,當有編著異常的 tag 到達頭部時,會進行 flush ,後面沒意義關注了,會被沖刷掉) #### PC Storage ROB 需要記錄下每個包含在其中的指令所擁有的 PC 位置, 其中包含以下資訊: EPC(紀錄是否發生異常)。 分支指令需要知道自己的 base 在哪裡。 **跳躍寄存器指令必須知道它們自己的 PC 和程序中下一條指令的 PC,以驗證前端是否正確預測了 JR 目標。** 但是記錄下他們所需的成本非常的高,因此,這個專案有進行一些優化, PC File以兩個 BANK 的形式儲存,允許單個讀取端口同時讀取兩個連續的項目(用於JR指令,自己的 BASE 跟他在 QUEUE 裡面的下一個指令的目標地址,這樣就可以拿來判斷預測是否正確,是否需要跳躍)。 #### The Commit Stage 當我們最前端的指令不再顯是他正在處理,也沒有意外時, 我們的 ROB 會盡可能地將指令推出去(推去記憶體或是完結這項指令),並且盡可能得釋放資源,目前,我們不提供跨行 ROB 指令查找 #### Exceptions and Flushes >產生的例外有那些呢? >這邊有一些RV64定義到的例外: 1. load/Store Unit: page faults 2. Branch Unit: misaligned fetches 3. Decode Stage: all other exceptions and interrupts can be handled before the instruction is dispatched to the ROB 當產生例外時,我們會清空 ROB & pipeline, Rename Map Tables必須被重置以表示真實的、非推測性的已提交狀態, 接下來有兩個請況需要處理, 第一個是產生例外得是微指令,我們會將取址失敗的指令重新取址並且放回我們的 ROB 當中, 第二個是整個指令,我們會將產生例外指令的 PC 存入 CSR 當中, #### Parameterization - Rollback versus Single-cycle Reset 重置Rename Map Tables的行為是可參數化的。第一種選項是每個時脈逐行回滾ROB,以解開重命名狀態(這是MIPS R10k的行為)。對於每個指令,將過時的物理目的地寄存器寫回到Map Table的邏輯目的地指定器中。 另一種更快的單時脈重置方法是使用另一個重命名快照來跟踪重命名表的已提交狀態。這個已提交的Map Table在指令提交時更新。 #### Point of No Return (PNR) 在ROB提交頭之前運行,標記下一個可能出現錯誤預測或引發異常的指令。這些包括未解析的分支指令和未翻譯的記憶體操作。因此,提交頭之前和PNR頭之後的指令保證是非預測的,即使它們尚未寫回。 目前,PNR僅用於RoCC指令。RoCC協處理器通常要求按順序執行其指令,並且不容忍錯誤預測。因此,只有當指令越過PNR頭且不再是預測的時候,我們才能將指令發送給協處理器。 ### (5)The Issue Unit 他分為各種類型,用來將已經分配完但是還沒有被執行的微指令進行列隊等待執行(利用 queue 結構), 類型有:整數、浮點、記憶體... ![](https://hackmd.io/_uploads/SkDaBsjP3.png) 來自發射隊列的單個發射槽。指令被派發到發射隊列後,等待其所有操作數就緒(“p”是存在位,標記了操作數是否適合出現在註冊文件中)。一旦就緒,發射槽將斷言其“請求”信號,並等待被發射。 每個發射選擇邏輯端口都是一個靜態優先級編碼器,用於獲取發射隊列中第一個可用的UOP。每個端口只調度該端口能夠處理的UOP(例如,浮點UOP只調度到管理浮點單元的端口上)。這將為可以相互調用相同UOP的端口創建級聯的優先級編碼器。 如果某個功能單元不可用,將取消其可用信號的斷言,並且不會向其發射指令(例如非管線的除法器)。 #### BOOM的兩種微指令發射&喚醒策略 亂序發射隊列 >類似MIPS R10K的亂序發射隊列。派發的指令被放置在第一個可用的發射隊列槽中,並一直保持在該位置,直到被發射。這可能會導致嚴重的性能下降,特別是當不可預測的分支被放置在優先級較低的槽中時,直到ROB文件被填滿且發射窗口耗盡才能被發射。由於分支後的指令僅隱式依賴於該分支,因此無法通過其他強制功能使分支儘早發射。 有序發射隊列 >派發的指令被放置在發射隊列底部(最低優先級)。每個時期,每條指令都向上移動一個位置。因此,最早的指令處於最高的發射優先級。儘管這可以通過儘早調度早期的分支和載入指令來提高性能,但由於每個時期每個發射隊列槽都可能被讀寫,可能會產生潛在的能量損失。 快速喚醒(fast wakeup):可以使用橫向的線路 慢速喚醒(slow wakeup,也稱為長延遲喚醒):不可以使用橫向的線路 ### The Register Files and Bypass Network ![](https://hackmd.io/_uploads/rJjnPfhwn.png) 整數暫存器需要六個讀取以及三個寫入端口,並且浮點數暫存器需要三個讀取以及兩個寫入的端口。 ALU可以在任何階段存取任意的暫存器。 ### The Execute Pipeline ![](https://hackmd.io/_uploads/Sy9KaGnP3.png) 可以看到這邊有兩個 issue 分別負責了各自的功能。 而當暫存器的數值需要跨 issue 讀取時,我們可以透過 bypass 的方法將數值傳遞出去(記住,這裡不是pipeline 形式進行的,這個設計是用來充分的發揮 ex units 裡面所有的功能部件,讓他在一個 cycle 之中可以充分使用。 #### Execution Units ![](https://hackmd.io/_uploads/rJIk-QnD2.png) 個例子展示了一個整數ALU(可以將結果旁路到相依的指令)和一個非管線的除法器,在操作期間變為忙碌狀態。兩個功能單元共用一個寫端口。執行單元接受kill信號和分支解析信號,並根據需要將它們傳遞給內部的功能單元。 執行單元是一個模塊,單一發射端口將UOP(微操作)調度到其中,並包含一些功能單元的組合。換句話說,Issue Queue中的每個發射端口只與一個執行單元進行通信。執行單元可以只包含一個簡單的整數ALU,或者可以包含一組完整的浮點單元、一個整數ALU和一個整數乘法器。 執行單元的目的是提供一個靈活的抽象層,讓架構師能夠對他們的管線添加不同類型的執行單元,從而對其進行更多的控制。 而這些 Execution Units 是可以讓我們自行定義以及添加的,只要在特定的 config 之中填入是否開啟某些功能,甚至你可以自己寫一些小模組添加進去,這可以大大的提高設計者的自由度以及應付各式不同的場景!! #### Functional Unit ![](https://hackmd.io/_uploads/SkcdVmhP2.png) 他就是 CPU 用來實現功能最基礎的單位,根據指令的需求計算所需的操作。實現功能單元通常需要具有相應領域知識的專家(我猜是 git 上面那個 Al 開頭的美國人,他有夠強),以確保其正確高效。 在 BOOM 之中,採用的是 rocket64 的原始碼作為基礎架構。 採用 low-level Functional Unit 作者將多個 low-level Functional Unit 包裝起來,但是由於他們本來的用途是執行順序指令的單元,顯然地與 BOOM 的需求不同,這使得作者需要額外添加一個類似 top 的功能(畢竟指令拆成再多微指令,還是需要有人順序去執行他), “包裝”低層級功能單元並提供所需的參數化自動生成支援代碼,以使它們能夠在BOOM中正常工作。請求和回應端口被抽象化,使功能單元能夠提供統一的、可互換的介面。 以上這張圖就是其中一種的抽象 Functional Unit ,如果預測失誤,那我們整個 unit 將會被沖刷掉!! ![](https://hackmd.io/_uploads/rycCTm2vn.png) 虛線的部分就是 boom 額外添加的。 方塊代表實例化低層級功能單元的具體類別,八邊形則代表提供通用預測支援和與BOOM管線進行介面的抽象類別。浮點除法和平方根單元既不適合於Pipelined抽象類別,也不適合於Unpipelined抽象類別,因此直接繼承自FunctionalUnit超級類別。 ### Load/Store Unit ![](https://hackmd.io/_uploads/BJUt1V2v3.png) Load/Store Unit(LSU)負責決定何時向記憶體系統發送存取記憶體的操作。 它包含兩個隊列:Load Queue(LDQ)和Store Queue(STQ)。 Load指令生成一個名為“uopLD”(Load Micro-Op)的微操作(UOP)。 當發出該“uopLD”時,它會計算讀取的記憶體位址並將結果放入LDQ中。Store指令(可能)生成兩個UOP,即“uopSTA”(Store Address Generation)和“uopSTD”(Store Data Generation)。 STA UOP用於計算儲存的位址並更新STQ條目中的地址。STD UOP將儲存的資料移入STQ條目。只要它們的操作數準備就緒,這些UOP中的每個都會從Issue Window中發出。 #### Store Micro-Ops Store指令以單一指令的形式插入到Issue Window中(而不是分解為單獨的地址生成和數據生成UOP)。 這樣可以防止佔用昂貴的Issue Window項目和對LSU的issue端口造成額外的爭用。 當儲存指令的兩個操作數都準備就緒時,可以將其作為單一UOP發出到LSU,該UOP同時提供地址和數據給LSU。 雖然這需要儲存指令具有兩個註冊檔讀取端口的訪問,但這是為了不會在儲存密集的代碼上降低性能。 但是,儲存的地址往往在儲存數據之前就已經得知。為了使後續的讀取避免任何記憶體順序失敗,儲存地址應盡早移入STQ。因此,Issue Window將根據需要發出uopSTA或uopSTD UOP,但保留儲存的剩餘部分,直到第二個操作數就緒。 #### Load Instructions 在Decode階段分配Load Queue(LDQ)中的項目(ldq(i).valid)。在Decode階段,每個load項目還被賦予一個store mask(ldq(i).bits.st_dep_mask),該mask標記了該load所依賴的Store Queue中的哪些儲存操作。當一個store被發射到記憶體並離開Store Queue時,store mask中相應的位將被清除。 一旦load地址被計算並放置在LDQ中,相應的valid位被設定(ldq(i).addr.valid)。 在到達LSU時,load被樂觀地發送到記憶體(早期發送load對於out-of-order管線來說是一個巨大的好處)。同時,load指令將其地址與其所依賴的所有store地址進行比較。如果存在匹配,則記憶體請求被取消。如果相應的store數據存在,則將store數據轉發給load,load將自己標記為成功。如果store數據不存在,則load進入睡眠狀態。被置於睡眠狀態的load將在稍後重新嘗試。 #### The BOOM Memory Model BOOM遵循RVWMO(RISC-V Weak Memory Order)記憶一致性模型。 目前,BOOM展現了以下行為: Write -> Read限制被放寬(新的load指令可能在老的store指令之前執行)。 Read -> Read限制被保持(對同一地址的load指令按順序執行)。 一個線程可以提前讀取自己的寫入。 相同地址的load指令的順序 RISC-V WMO記憶模型要求對同一地址的load指令進行排序。這要求load指令與其他load指令進行地址衝突的檢查。如果一個年輕的load指令在一個具有匹配地址的老的load指令之前執行,則必須重新執行年輕的load指令並刷新管線中它後面的指令。然而,只有在緩存一致性探測事件(cache coherence probe)窺視了核心的記憶體,將重排序暴露給其他線程時,才需要進行這種場景的處理。如果沒有發生探測事件,則load指令的重排序可以安全地進行。 ## TODO: 搭配 [vivado-risc-v](https://github.com/eugene-tarassov/vivado-risc-v),升級 Linux 核心和相關套件 完成 linux kernel 以達到要求 ### 如何準備你的 linux kernel 我們的 risc-v machien 需要提供三種模式 1. Machine Mode:負責啟動 CPU ,執行 Bootloader,主要用來初始化我們的電腦,接下來便切入到 Supervisor mode。 2. Supervisor Mode:操控 CPU 執行 privileged instructions,像是中斷指令( csr 等等指令), 3. User Mode:負責執行一些應用程式指令(像是改變變數,執行 util function),當 CPU 想要切換模式時(Supervisor mode),會執行特有的 ecall指令(這個指令很重要) ![](https://hackmd.io/_uploads/HJv_29e_2.png) 案例(來自我上學期的[作業](https://hackmd.io/w1VYMkfMTK-Xhc1XC7rwYg)): ``` print: addi a0 s5 0 li a7 1 ecall li a7 10 ecall // turn to Supervisor mode ``` 想要在 risc-v 上面 boot linux,我們需要幾樣東西。 首先我們要先將 toolchain 處理好,這邊我們用的是 Sifive 提供的版本(請不要用鏡像檔案,後期會出錯,親身踩的坑), 我們參照 qemu 的流程來做啟動,並且生成 img 檔案 開機流程: 1.MSEL->選擇開機方式(請在你的 FPGA 上面選擇)。 2.zero stage bootloader->從 ROM 抓取核心的程式碼,請務必要綁定對地址,我這邊吃了大虧。 >重新清空我們開機時需要的記憶體跟暫存器, 安裝基本驅動 : UART: Early console ,SD Card/QSPI , Device Tree (需要客製化), 並且將 FSBL 從 QSPI 或是 SD 卡載入, 這個階段只有一個 CORE 在執行以上步驟。 我們來看到 bootrom 這個程式, 這是我們的 head.s 檔案: ```c #include "common.h" #define MIP_MSIP (1 << 3) .section .text.start, "ax", @progbits .globl _start _start: csrr a0, mhartid la a1, _dtb la s0, _ram jr s0 _start_bootrom: # a0 - hart ID # a1 - Device Tree # s0 - start of RAM and trap vector sw s2, 0(s0) # clear trap vector li sp, BOOTROM_MEM_END call main j _hang .section .text.hang, "ax", @progbits .globl _hang _hang: csrr a0, mhartid la a1, _dtb la s0, _ram beqz a0, _start_bootrom _hartx_loop: # write mtvec csrw mtvec, s0 # enable software interrupt csrwi mie, MIP_MSIP wfi # only start if interrupt request is set csrr a2, mip andi a2, a2, MIP_MSIP beqz a2, _hartx_loop # jump to code in RAM ecall j _hartx_loop .section .rodata.dtb, "a", @progbits .globl _dtb .align 5, 0 _dtb: .incbin DEVICE_TREE ``` 導入的一個 common.h, 這是因為我們需要上述的起始地址,以及一些其他資訊。 ```c #ifndef _SDBOOT_COMMON_H #define _SDBOOT_COMMON_H #define BOOTROM_DTB_ADDR 0x00010080 // Addresses must be aligned by 0x100 #define BOOTROM_MEM_ADDR 0x80000000 #define BOOTROM_MEM_END 0x80002000 #ifndef BOOTROM_MEM_ALT // If the application is linked to be loaded at BOOTROM_MEM_ADDR: // BOOTROM_MEM_ALT is used as temporary storage // system RAM size must be >= 128MB // the application size must be <= (128MB - 8KB) #define BOOTROM_MEM_ALT 0x87ffe000 #endif #endif ``` 3.FSBL(U-Boot SPL) 我們這邊採取的是 U-Boot(Universal Boot Loader) 來開機,它會將我們的記憶體初始化,並且載入 runtime,並且引導系統執行 bootloader。 講解一下其中的細節, U-Boot SPL 是一個微型的 bootloader,主要是將第二部分的大型 bootloader 加載進來, 而在進行 compile U-boot 之前, 我們需要執行 OpenSBI: >Runtime:Risc-v 通常會採用 OpenSBI(Supervisor Binary Interface) 這是一個 M Mode 模式下的程式, 它主要是提供 OS 擁有一個介面讓其可以存取 M mode 下的硬體資源。 OpenSBI 包含了以下幾種驅動 1.FW_PAYLOAD:下一引導階段被作為 payload 打包進來,通常是 U-Boot 或 Linux。這是兼容 Linux 的 RISC-V 硬體所使用的預設 firmware。 2.FW_JUMP:跳躍到一個固定地址,該地址上需存有下一個加載器。 3.FW_DYNAMIC:根據前一個階段傳入的信息加載下一個階段。通常是 U-Boot SPL 使用它。現在 QEMU 預設使用 FW_DYNAMIC。 我們來觀察以下的程式碼:`/firmware/fw_base.S` ```c _start: /* Find preferred boot HART id */ # 判斷 HART id MOV_3R s0, a0, s1, a1, s2, a2d call fw_boot_hart add a6, a0, zero MOV_3R a0, s0, a1, s1, a2, s2 li a7, -1 beq a6, a7, _try_lottery /* Jump to relocation wait loop if we are not boot hart */ bne a0, a6, _wait_relocate_copy_done _try_lottery: /* Reset all registers for boot HART */ # 清除 registers li ra, 0 call _reset_regs /* Zero-out BSS */ # 清除 BSS lla s4, _bss_start lla s5, _bss_end _bss_zero: REG_S zero, (s4) add s4, s4, __SIZEOF_POINTER__ blt s4, s5, _bss_zero /* Setup temporary stack */ # 初始化 temp stack lla s4, _fw_end li s5, (SBI_SCRATCH_SIZE * 2) add sp, s4, s5 /* * Initialize platform * Note: The a0 to a4 registers passed to the * firmware are parameters to this function. */ MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4 call fw_platform_init #讀取 device tree # FDT 重新定位 /* * Relocate Flatened Device Tree (FDT) * source FDT address = previous arg1 * destination FDT address = next arg1 * * Note: We will preserve a0 and a1 passed by * previous booting stage. */ ... /* Initialize SBI runtime */ csrr a0, CSR_MSCRATCH call sbi_init # 全部的初始化結束後,進行跳躍 ... 跳躍到 sbi_init 會判斷目前是 S 模式還是 M 模式啟動 OpenSBI IO初始化 載入 sbi_init,執行 init_coldboot, init_coldboot 具體程式碼在 /lib/sbi/sbi_init.c,大致的初始化如下: static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid) { ... /* Note: This has to be second thing in coldboot init sequence */ rc = sbi_domain_init(scratch, hartid); ... rc = sbi_platform_early_init(plat, TRUE); rc = sbi_console_init(scratch); rc = sbi_ipi_init(scratch, TRUE); rc = sbi_tlb_init(scratch, TRUE); # Timer INIT rc = sbi_timer_init(scratch, TRUE); sbi_printf("%s: PMP configure failed (error %d)\n", __func__, rc); sbi_hart_hang(); } # 準備下一階段的 Bootloader sbi_hsm_prepare_next_jump(scratch, hartid); ``` Bootloader: 這邊來稍微閱讀一下 U-boot 的相關程式碼。 閱讀的程式碼在 `arch/riscv/cpu/start.S` 可以具體見到 當我們透過U-Boot SPL 啟動第二部分的大型 U-Boot 時,會初始化以下部分。 SRAM: 需要初始化,因為它是揮發性記憶體。 SDRAM: 需要重新設定記憶體控制器。 SPL :U-Boot 被分為 uboot-spl 和 uboot 兩個部分。SPL 是 Secondary Program Loader 簡稱,第二段 bootloader 的加載程式。 ![](https://hackmd.io/_uploads/SytJ_gMun.png) (1)是 SPL 在 U-Boot 第一階段的裝載程序,初始化最基本的硬體,比如中斷,記憶體初始化,設定 temp stack 等最基本的操作 (2)它會加載 U-Boot 主程式,然後初始化其他 IO,如乙太網路介面、NAND Flash 等,並設定 U-Boot 本身的命令和環境變數 (3)是加載 Kernel 到 RAM,然後啟動 kernel。 總之,順序為 ROM code –> SPL –> u-boot –> kernel。 4.OS:Linux 我們這邊採用的是 [這個版本](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux) 我們使用的是 buildroot 工具來客製化我們的 linux kernel。 接下來演示一下我大致如何準備 SPL,U-boot,Kernel(請自行準備好 risc-v toolchain) 準備 QEMU ``` $ wget https://download.qemu.org/qemu-7.0.0.tar.xz $ tar xvJf qemu-7.0.0.tar.xz $ cd qemu-7.0.0 $ ./configure --target-list="riscv32-softmmu riscv64-softmmu" $ make -j $(nproc) $ make install ``` build RISC-V Open Source Supervisor Binary Interface (OpenSBI) ``` git clone https://github.com/riscv-software-src/opensbi bootloader: workspace/boot.elf workspace/boot.elf: opensbi/build/platform/vivado-risc-v/firmware/fw_payload.elf mkdir -p workspace cp $< $@ opensbi/build/platform/vivado-risc-v/firmware/fw_payload.elf: $(wildcard patches/opensbi/*) u-boot/u-boot-nodtb.bin mkdir -p opensbi/platform/vivado-risc-v cp -p -r patches/opensbi/* opensbi/platform/vivado-risc-v make -C opensbi CROSS_COMPILE=$(CROSS_COMPILE_LINUX) PLATFORM=vivado-risc-v \ FW_PAYLOAD_PATH=`realpath u-boot/u-boot-nodtb.bin` opensbi-qemu: cd qemu && if [ ! -d opensbi ]; then git clone ../opensbi; fi cd qemu && make -C opensbi clean && make -C opensbi PLATFORM=generic CROSS_COMPILE=$(CROSS_COMPILE_LINUX) FW_PAYLOAD_PATH=../u-boot/u-boot.bin ``` 準備 SBT 我們採用的是 rocketchip 專門提供的 SBT 來對 QEMU 進行模擬,成功之後產出對應的 IMG 檔案。 ## TODO 進一步完善 C1100 的 bolck designs 提高 BOOM 的頻率,解決拉高頻率時會出現 latch 的現象。 由於我們的程式碼是由 simulator 的 behavior verilog ,它所搭配的 IP 並非我們目前使用的各類版本, 這樣很容易在 implement 時出現隱藏的錯誤,需要進行降頻來解決。 總共需要讓他們都能動起來: 1. CWWPPBV2:自訂的 CPU 配合 chipyard 提供的樣板CPU + ROCC,使得整個 SOC 可以運行 AI 加速器,或是加密模組 2. JSARCHOOoEv2:純自訂的 CPU,需要進行更多的驗證並且修復一些內容加以提高它的可承受頻率。 我發現當我們進行乙太網路透過轉接頭轉換成 PCIE 的話,網速會變得非常差...頭痛 ## ROCC 初步探索 我目前正在研究 ROCC 與 AI 加速器相關的應用,目前由於 NV 的支援已經停更了。因此在尋找替代方案。 ROCC 目前可以使用 small 以及 large 的選項,尷尬的是 LUT 貌似會爆炸,因此我需要進行重新的客製化。 期望的目標是可以在上述的三款 FPGA 跑一些矩陣驗證,雖然 xilinx 目前不支援了(因為他們推出了自己的電腦視覺加速器,~~不過我看了規格沒有很喜歡~~,我錯了,它那顆 ZYNQ 超級豪華,還附帶了一顆 camera 處理器,收下我的錢錢吧,~~澤茂~~ 茂澤!!), ROCC 對於 chahe 的控制 ![](https://hackmd.io/_uploads/ByvRsvTOn.png) configs ```scala class CWWPPBv3 extends Config( new WithGemmini(16, 64) ++ new WithInclusiveCache() ++ new WithNBreakpoints(8) ++ new WithNBigCores(1) ++ new RocketBaseConfig) ``` 等待合成中...這東西有點不妙阿,大小有點可怕,不過我需要先在其他的板子上實驗一下,才可以進行移植,我這邊沒有使用 BOOM ,需要將一些 LUT 留給它。 心態繃了,連續試了兩塊都不能用,看來要請 C1100 出場了 :cry: 研讀了差不多了,驗證完來更新一下整個 Gemmini 的行為 ### Gemmini 佈局 成功佈局 ![](https://hackmd.io/_uploads/HJfPcU-Kn.png) ### Gemmini & 驗證 採用 Vitis AI 提供的模型 ### Vitis AI with Risc-V ACC SoC 在KV260上面的表現非常好(24/fps)。 ![S__30130178](https://hackmd.io/_uploads/BySClBQn6.jpg) # Arty A7-200T & 使用極低資源的特製 SoC (尚未 boot linux) [開發紀錄(台式英語撰寫?)](https://hackmd.io/@CWWPPB/Risc-V_MM_SoC_NCKUES_MMNLAB): 目標在 20000 LUT 的資源下 boot linux,所有用的的IP皆採用輕量化設計,希望將頻率提升至175/mhz 左右,不用任何商業 IP,寫好約束文件便可以直接在其他平台上面部屬,目前進度已經完成 CPU,可以透過 UART 來進行控制 ##