Chisel實戰之單周期RISC-V處理器實作(上)——需求分析和初步設計
需求分析
首先明确我們要做的是什麼,這個在标題裡面已經說明了,我們要做的是一個單周期RISC-V處理器。
但光是個短語不足以支撐我們開展項目,我們需要對項目目标做進一步的明确,也就是需求分析。
關于指令集架構(ISA)
設計一個處理器的依據是指令系統規範,也就是ISA的規範,不嚴謹地來說就是該指令集架構的機器語言的規範,即計算機軟體和硬體的接口。而設計處理器是在ISA規範的基礎上,對微體系結構進行設計,是以經典的教材《計算機體系結構:量化研究方法》中就将計算機體系結構描述為指令集架構(ISA)和微體系結構的結合。
是以,我們第一步就是要明确,我們這個項目支援的指令系統規範是什麼!
既然要做一個RISC-V處理器,那必然是要支援RISC-V指令集,我們可以在官網找到該指令集的規範檔案:
Specifications - RISC-V International (riscv.org)
指令集規範中包含了非特權指令集(目前版本規範為Unprivileged Spec v. 20191213)、特權指令集(目前版本規範為Privileged Spec v. 20211203),以及一些仍然處于其他階段的擴充規範。
我們這個項目的目标很簡單,不需要支援特權指令,更不需要支援拓展指令(比如向量拓展、位操作拓展等),僅需要支援非特權指令集就行了,而且是它的一個子集!
非特權指令集有以下基本子集:
基本集 | 說明 | 版本 | 狀态 |
---|---|---|---|
RVWMO | WMO即Weak Memory Ordering,是RISC-V的記憶體一緻性模型 | 2.0 | 正式準許 |
RV32I | 最基本的RISC-V 32位整數指令集 | 2.1 | 正式準許 |
RV64I | 最基本的RISC-V 64位整數指令集,可以看作是RV32I到64位的拓展 | 2.1 | 正式準許 |
RV32E | 面向嵌入式微控制器的基本的RISC-V 32位整數指令集,可以看作是RV32I的精簡 | 1.9 | 草案 |
RV128I | 最基本的RISC-V 128位整數指令集,可以看作是RV32I、RV64I到128位的拓展 | 1.7 | 草案 |
既然是基本子集,那就必須得有一個,我們的單周期處理器是單線程的、順序執行的,是以不需要考慮記憶體一緻性模型。我們作為例子也隻需要實作最簡單的32位版本,是以考慮将RV32I作為我們項目的基本指令集。而RV32E雖然是RV32I的精簡,但仍然處于草案階段,就也不考慮了。
注意:這裡的xx位指的是位址空間的位數。
還有一些拓展指令子集:
拓展集 | 說明 | 版本 | 狀态 |
---|---|---|---|
M | 整數乘法、除法拓展 | 2.0 | 正式準許 |
A | 原子指令拓展 | 2.1 | 正式準許 |
F | 單精度浮點數拓展 | 2.2 | 正式準許 |
D | 雙精度浮點數拓展 | 2.2 | 正式準許 |
Q | 四精度浮點數拓展 | 2.2 | 正式準許 |
C | 壓縮指令拓展 | 2.0 | 正式準許 |
Counters | 計數器、定時器、性能計數器拓展 | 2.0 | 草案 |
L | 十進制浮點數拓展 | 0.0 | 草案 |
B | 位操作拓展 | 0.0 | 草案 |
J | 對動态轉譯語言的支援拓展,動态轉譯也叫JIT,此拓展用于支援動态檢查和垃圾回收 | 0.0 | 草案 |
T | 事務性記憶體操作拓展 | 0.0 | 草案 |
P | Packed-SIMD指令拓展 | 0.2 | 草案 |
V | 向量拓展 | 0.7 | 草案 |
Zicsr | 控制和狀态寄存器拓展 | 2.0 | 正式準許 |
Zifencei | 指令抓取栅欄拓展 | 2.0 | 正式準許 |
Zam | 非對齊原子記憶體操作拓展 | 0.1 | 草案 |
Ztso | RVTSO(Total Store Ordering)記憶體一緻性模型拓展,是RVWMO的變體 | 0.1 | 草案 |
RV32I加上A拓展就可以支援作業系統了,但我們不需要,其他拓展更是不需要了。
我們可以根據需求選取需要的基本子集和拓展集,實作符合應用場景的處理器。由于我們的需求很簡單,那麼自然我們經過上面的分析,就可以得到結論:
我們隻需要支援RV32I基本指令集!不需要其他任何拓展!
當然了RV32I中的指令我們也并非都需要,在後續的實作中我們還會進行少許的取舍。
關于微體系結構(Microarchitecture)
CPU的設計無非隻有兩部分,一個是資料通路,另一個就是邏輯控制,不管如何我們先确定我們在微體系結構上的需求。
如果你學習過《計算機體系結構:量化研究方法》或其他類似的教材,那肯定知道很多現代處理器中常見的技術,比如Cache、流水線、分支預測、SIMD等等,想想就讓人害怕。不過好消息是我們這個項目暫時不涉及這些,不信我們先捋一捋:
- 記憶體層級方面:現代處理器都有Cache之類的東西,用來構成記憶體層級,然後用一些複雜的政策來保證Cache的命中率,而我們的項目中不需要記憶體層級,直接從記憶體通路指令、資料啥的就行;
- 指令集并行(ILP)方面:這裡引入了流水線技術,緊接着為了解決資料冒險,引入了轉發技術和動态排程技術(比如記分牌算法和Tomasulo算法),跟着一起出現的還有分支預測那些,另一方面引入了多發射技術,似乎越來越超綱了,但是我們不需要,我們是單周期CPU,沒有流水線,而且我們一次隻執行一條指令,也沒有多發射,根本就沒有指令集并行;
- 資料級并行(DLP)方面:顯然我們不需要,因為我們不支援向量拓展,跳過;
- 線程級并行(TLP)方面:我們是單核CPU,不支援多線程,更不存線上程之間共享記憶體,也沒有Cache啥的,是以我們同樣也不需要這個,複雜的記憶體一緻性模型完全不用考慮;
還有什麼位址轉換啥的,我們也不需要!
捋完了可以發現,我們什麼優化技術都不需要用上,簡簡單單就實作一個樸素的單周期RISC-V處理器就行了!
初步設計
需求分析結束之後,就可以開始我們的初步設計了!
RV32I指令集分析
通過上面的分析,我們隻需要把RV32I中我們需要的指令給支援了就行了,那麼我們從分析RV32I中的指令開始。
RV32I指令集指令有6種類型:
其中:
- R類型即寄存器(Register)類型,有三個操作數,兩個源操作數均來自寄存器(rs1和rs2),目的操作數為寄存器(rd);
- I類型即立即數(Immediate)類型,有三個操作數,兩個源操作數分别來自立即數(imm)和寄存器(rs1),目的操作數為寄存器(rd);
- S類型即存儲(Store)類型,有三個操作數,均為源操作數,其中寄存器rs1和立即數imm運算得到存儲的位址,rs2寄存器的值為被存儲的數;
- B類型即分支(Branch)類型,有三個操作數,均為源操作數,其中兩個寄存器(rs1和rs2)的值用于比較,立即數imm的值為分支目的位址的偏移量;
- U類型即無符号立即數(Unsigned immediate)類型,有兩個操作數,立即數imm為源操作數,rd為目的操作數,此指令用于将立即數加載到指定寄存器rd;
- J類型即跳轉(Jump)類型,有兩個操作數,立即數imm為源操作數,用于計算跳轉目的位址,rd為目的操作數,用于記錄跳轉前指令的下一條指令的位址;
進一步地,我們分析RV32I中的所有指令,共計四十條,如下表所示:
指令格式是比較規整的,除了
ECALL
和
EBREAK
以外,均顯然符合上面的六種指令格式類型。
可以按照指令的功能對指令進行分類:
- 直接跳轉類:包括
和JAL
;JALR
- 條件分支類:包括
、BEQ
、BNE
、BLT
、BGE
、BLTU
;BGEU
- 加載/存儲類:包括
、LB
、LH
、LW
、LBU
、LHU
、SB
、SH
;SW
- 算術邏輯運算和位運算類:包括所有加法、減法,按位與、或、異或,邏輯左移、邏輯右移、算術右移相關指令;
- 比較指令類:包括
、SLTI
、SLTIU
、SLT
;SLTU
- 其他指令類:
、FENCE
、ECALL
;EBREAK
再依次對這幾個類指令的行為進行分析:
- 直接跳轉類需要對PC寄存器的值進行直接修改,同時寫一個寄存器;
- 條件分支類首先需要進行比較,然後根據比較結果選擇是否修改PC寄存器;
- 加載/存儲類需要通路資料存儲,加載隻讀取資料,存儲隻寫入資料;
- 算術邏輯運算和位運算類會對操作數進行運算,然後将結果寫入目的寄存器;
- 比較指令類與算術邏輯運算和位運算類一緻,但操作變成了比較;
- 其他指令中,由于不需要維護記憶體一緻性和連續性,是以我們不需要實作
,同樣,由于不涉及環境調用中斷和調試調試中斷,是以我們暫時也不需要實作FENCE
和ECALL
;EBREAK
資料通路和控制邏輯的初步設計
根據上面的分析,我們設計的CPU中應該至少需要包含以下元件:
- 指令記憶體(
):接收一個32位的指令位址,讀取出指令;MemInst
- PC寄存器(
):為指令記憶體提供指令位址,每個時鐘周期位址都會+4,目前指令為跳轉時,下一條指令為跳轉目的位址,目前指令為分支指令且分支成功時,下一條指令為分支目标位址;PCReg
- 通用寄存器堆(
):可讀可寫的寄存器,接收寄存器号,為運算單元提供操作數,接收運算結果或從資料記憶體讀取到的值;Registers
- 資料記憶體(
):根據加載/存儲位址,加載或存儲資料,加載或存儲依賴于譯碼器的譯碼;MemData
- 指令譯碼器(
):對指令進行譯碼,解析得到立即數、操作碼、寄存器号等資訊;Decoder
- 運算單元(
):根據操作數和操作碼進行運算,運算結果寫到寄存器,分支指令時将比較結果發送給PC,加載存儲指令時計算位址;ALU
這些元件隻描述了資料通路,要使得CPU能正常運作,還需要良好的邏輯控制。
控制邏輯需要根據譯碼結果對資料通路進行控制,可能需要以下幾個方面:
-
:指令是否為跳轉指令?如果是,需要給控制信号到PC,要求在下一時鐘周期修改為跳轉目的位址;ctrlJump
-
:指令是否為分支指令?如果是,根據運算單元的比較結果(分支與否),決定是否讓PC在下一個時鐘周期跳轉的分支目标位址;ctrlBranch
-
:指令是否需要寫寄存器?如果是,将運算單元的結果或從資料記憶體中讀取的值寫入寄存器;ctrlRegWrite
-
:指令是否為加載指令?如果是,寫入寄存器的值來源應該是資料記憶體;ctrlLoad
-
:指令是否為存儲指令?如果是,将寄存器中的值寫入資料記憶體;ctrlStore
-
:指令的操作數2是立即數還是寄存器值?根據此選擇操作數2的值;ctrlALUSrc
-
:指令是否為JAL指令?如果是,操作數1的值應當為PC寄存器的值;ctrlJAL
-
:為ALU指定具體的操作,加?減?或者其他啥?ctrlOP
這些控制信号的生成和傳輸我們統一由控制器(Controller)完成。
上面的說明并不詳盡,隻作為初步設計,但我們也很難在開始的時候就考慮到所有細節,更多的細枝末節需要在設計、調試、修改的疊代中完善,但至少上面的内容足夠我們開始實作了。
最後放上初步設計的草圖:
再次說明,上面的設計是不完備的,比如目前還未考慮到加載/存儲時是位元組、半字還是字,雖然隻是一個信号的問題,但足以展現還有很多不完善的地方,在實作中疊代設計是很有必要的。接下來,我們就将基于這個不完備的設計開始我們的項目開發!