一、前言
最近看了不少關于CPU設計的一些開源專案和文章,看了之后感覺作者真的太厲害了,反觀自己實在太菜了!不過正經來說,這些專案看多了自然就會對CPU的設計和運行有一個更清晰的理解!
在平時的課程和競賽中會經常接觸到SOC設計:比如上學期超大規模集成電路設計這門課的一個大作業使用到了ARM cortex-m0核;這學期參加的集創賽用到了ARM cortex-m1核;由于之前學習過RISC-V的一些知識,所以最近也熟悉了一下tinyriscv和蜂鳥E203這兩個使用RISC-V來設計CPU的開源專案,
所以,下面就簡單寫一下自己對CPU設計和運行的一些理解,不會涉及到細節,因為具體的細節我也不咋能說上來(或者以后再慢慢說),僅僅是對其做一個整體上的思路整理,我在平時學習的時候一直都喜歡先對一個東西有一個比較清晰的整體理解,否則在之后的具體學習中會覺得非常迷,不利于后面高效的學習!!!
二、關于CPU設計
下面就以tinyriscv為例,簡單的啰嗦幾句,下圖是tinyriscv soc的整體框架:

1、CPU核
CPU核是整個CPU最核心的部分,它主要負責處理指令,
我們都知道,CPU在處理一條指令的時候,一般包括以下幾個步驟:取指、譯碼、執行、訪存、寫回,而且一般采用流水線設計,tinyriscv將訪存和寫回操作放到了執行這一步,所以只有三級,采用了三級流水線,
在處理指令時,比如譯碼階段,我們就需要根據自己所使用的的指令集架構來對指令進行決議以保證正確獲取指令想要完成的操作,比如一條加法指令,我們就需要正確決議出它要進行的操作是加法,還要決議出運算中的立即數,如果需要訪存或者讀暫存器來獲得運算元,那么還要決議出資料地址是多少等等,只有按照所使用的的指令集架構來決議,才能保證后面的執行能夠進行正確的操作,關于指令集架構你可能使用的是RISC-V,也可能是使用的ARM等等,
CPU核里還會有很多的reg,包括指令計數器(pc_reg)和一些通用reg,這些通用暫存器可以存放一些比如計算操作涉及到的運算元,也可以存放一些狀態等等,
2、總線
設想一下一個沒有總線的SOC,處理器核與外設之間的連接是怎樣的,可能會如下圖所示:

可見,處理器核core直接與每個外設進行互動,假設一個外設有一條地址總線和一條資料總線,總共有N個外設,那么處理器核就有N條地址總線和N條資料總線,而且每增加一個外設就要修改(改動還不小)core的代碼,有了總線之后,處理器核只需要一條地址總線和一條資料總線,大大簡化了處理器核與外設之間的連接,
目前已經有不少成熟、標準的總線,比如AMBA、wishbone、AXI等,設計CPU時大可以直接使用其中某一種,以節省開發時間,
當從一條指令中決議到需要讀寫ROM、RAM或者其他掛在總線上的外設時,CPU核就會發出讀寫地址以及讀寫資料,總線模塊可以根據讀寫地址的某幾位來判斷需要操作哪一個外設,又會根據另外幾位判斷需要讀寫外設中的哪一個暫存器,這些外設和總線都有連接,但是總線模塊可以屏蔽掉其他的外設,只選通和某一個外設的連接(包括與該外設的輸入與輸出的連接),
3、外設
外設其實在我看來是一個比較重要的模塊,因為大多時候我們不會自己去設計CPU核,更多的是往CPU核上掛載自己設計的功能模塊以滿足專案需求,所以我們就需要弄清楚這些外設是怎么掛載到總線上的,通常我們需要在代碼中觀察!!!
上面也說了,我們對外設進行操作,其實就是操作的其中的暫存器,這很重要, 這些暫存器可以簡單的理解為是用來配置這些外設的,一般這些外設都不止一個暫存器,不同的暫存器功能不一樣,它們對應著不同的偏移地址,而每個外設又有自己的基地址(有時不同的外設還掛在不同的總線上,所以又會有總線基地址),所以通過基地址+偏移地址我們就可以唯一的確定某個具體外設的具體暫存器,然后就可以對其進行讀寫操作以完成一些任務,
下面就舉個簡單的例子—tinyriscv中SPI模塊的代碼:
// spi控制暫存器
// addr: 0x00
// [0]: 1: enable, 0: disable
// [1]: CPOL
// [2]: CPHA
// [3]: select slave, 1: select, 0: deselect
// [15:8]: clk div
reg[31:0] spi_ctrl;
// spi資料暫存器
// addr: 0x04
// [7:0] cmd or inout data
reg[31:0] spi_data;
// spi狀態暫存器
// addr: 0x08
// [0]: 1: busy, 0: idle
reg[31:0] spi_status;
當我們需要進行SPI通信時,我們就可以設定以上幾個暫存器來配置SPI模塊,如上所示:我們可以設定讀寫spi_status暫存器以獲取或者設定SPI此時的狀態;我們還可以讀寫spi_data以獲取接收到的資料或者設定將要發送出去的資料,至于為什么能夠發送資料,又為什么能夠接收資料,這就是由SPI模塊中的verilog代碼實作的,它就和我們平時使用FPGA進行SPI通信時寫的代碼差不多(我說的是使用狀態機實作SPI協議的核心代碼其實是差不多的,當然這是兩個不同的應用場景,你還需要根據實際情況組織自己的代碼),按照SPI協議完成資料的傳輸即可,
可以看出,底層寫好verilog代碼之后,我們在上層寫應用程式的時候就只需要操作暫存器和發出一些必要的如啟動接識訓者發送的控制信號即可,它為我們屏蔽了復雜的協議實作程序,大大簡化了應用開發!!!下面就剛好接著簡單說一下關于CPU運行的一些東西!!!
三、關于CPU運行
CPU的運行其實可以簡單的理解為,我們使用C/C++等高級語言撰寫好了應用程式,然后通過編譯器將應用程式按照CPU對應的指令集架構轉換成機器碼,也就是一條一條的指令,假如我們的指令存盤到了ROM中,那么CPU就可以讀取這些指令,然后對每條指令進行決議、執行以完成我們在應用程式中想要實作的功能,
如果應用程式不牽扯到外設,比如通信模塊,那么CPU核最多也就通過總線讀取ROM中的指令,讀寫RAM和內部通用暫存器,如果牽扯到外設,那么還會讀寫外設中的暫存器!!!
那我們在寫應用程式的時候是怎么和這些外設模塊聯系起來的呢?
其實如果你做過嵌入式開發,比如大家非常熟知的stm32,那么你一定知道那些使用起來非常方便的封裝好的外設驅動函式,內部其實就是在進行各種暫存器操作,也就是對指定地址進行讀寫操作,而且在系統的頭檔案中,肯定會事先定義好總線的基地址、各個外設相對總線的偏移地址以及各個外設內部的暫存器相對于外設的偏移地址(當然還會有其他比如中斷向量表等,這里就不說了),如下面兩張圖所示:


有了這些定義,我們就可以在應用程式中對指定地址處進行讀寫操作,其實到這里你就已經知道我們怎么操作指定地址處的模塊了,
但是為什么能呢?那是因為我們在應用程式中對該地址進行讀寫,編譯器就會根據指令集架構編譯出對該地址進行讀寫的指令,
(以下是個人理解)比如編譯器通過應用程式獲取地址、操作型別以及運算元,然后在生成的指令中,把地址、操作型別以及運算元放入32位或者64位的指令中(至于放到哪些位,怎么放,那就和你使用的指令集架構有關了),至于更加具體的細節,其實都是編譯器的作業,我不太了解,感興趣的小伙伴可以去了解一下!!!
下圖就是在uart驅動函式中,對uart外設進行初始化的函式,可以看到里面其實就是在利用事先定義好的地址資訊,對指定地址處的暫存器進行各種操作以完成uart的初始化!!!

四、小結
以上敘述無論是對實際流片的硬核CPU還是跑在FPGA上的軟核CPU都是適用的!!!
啰嗦了很多,肯定有些地方說的不是太對,如果有還希望指出來!!!本篇文章只是試圖幫助讀者從整體上弄清楚CPU的設計,以及明白我們寫的應用程式是如何最終被CPU執行的!!!溜了,溜了!!!
參考:從零開始寫RISC-V處理器
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/281231.html
標籤:其他
