中央處理器(Central Processing Unit, CPU)
CPU的基本架構和工作原理其實百科上講得已經相當清楚了,不過我覺得有些事情呢還是給個例子出來比較方便學習。
本文會先從內存地址,計算機的一般架構之類的基礎知識出發,然后逐步為讀者"拼裝"出一個超級簡單的8-bit CPU。。。就像下圖這樣(大圖點開)
這就是本文的目標:拼裝這樣一個結構的CPU
-----------------------------------------------------------------------------------------------------------------------
上面那個大圖里有幾個梯形的符號
它們叫做數據選擇器(Multiplexer),也叫多路選擇器或多路開關
已經知道這個東西是干啥的童鞋直接跳過此樓吧。。
這圖是一個2選1選擇器,A,B,S為輸入,Z為輸出,它們可取的值當然都只有0和1
怎么工作的呢?
以該圖為例:
當S=1的時候,輸出值Z = 輸入值B
當S=0的時候,輸出值Z = 輸入值A
比如說A=1,B=0,S=0的時候輸出是多少?S=0就是說:選擇A的值輸出,也就是說輸出值Z=A=1
就這么簡單
同樣地,我們也可以有4選1選擇器
只不過控制輸入S變成了兩位(00,01,10,11,分別對應一二三四),道理還是一樣的
如果你對這個東東怎么做成的感興趣的話。。。下面就是4選1選擇器的其中一種電路 = =
哦,還有。。本文使用的邏輯門符號均是ANSI/IEEE Std 91-1984中的Distinctive shape,不是用方框符號。。。= =
你只要知道數據選擇器是干啥的就好,不用惦記上邊那電路。。
------------------------------------------------------------------------------------------------------------
1. 計算機架構(Computer Architecture)
CPU、內部存儲器(Internal Storage Device)和輸入/輸出設備(Input/Output Device, I/O)是電子計算機三大核心部件。內部儲存器可以是硬盤,內存,緩存等;輸入設備可以是鼠標,鍵盤;輸出設備可以有屏幕,音箱等等。。
當你打開電腦硬盤上安裝的某個程序時,你的操作系統會把硬盤上的相應內容放入內存中。至于怎么放,在內存的什么地方放那可是一門大學問,光這個就夠一般人喝一壺的。。相關知識可以在大學的操作系統課程里學到
比如說你放個音樂。要放音樂,先用鼠標點開一個mp3文件,于是你就使用了一個輸入設備。這個輸入設備會把一個中斷請求(Interrupt Request)送到CPU那邊,結合來自操作系統的信息后CPU就知道:哦,你用鼠標點開那個mp3文件了!于是:
1. CPU執行操作系統里關于文件關聯的代碼,于是你的電腦就知道要用WMP打開文件了
2. 你的操作系統開始把WMP這個程序里含有的指令和mp3文件的內容從硬盤上拉進內存里(還是CPU的工作)
3. 然后你的CPU開始一條一條地(雙核的話那你就當成兩條兩條地好了)執行內存里的WMP程序指令(也就是如何解碼mp3),并且把解碼后的PCM比特流傳到聲卡上,再由聲卡把數字信號轉換成模擬信號送到音箱/耳機(輸出設備)里。So now you have music!
又比如說你要編輯一個txt文件。還是先得用鼠標點開文件,又用了一次輸入設備。于是:
1. CPU執行操作系統里關于文件關聯的代碼,于是你的電腦就知道要用notepad打開文件了
2. 你的操作系統把notepad的程序指令和文件內容拉進內存
3. 然后你的CPU又開始執行Notepad程序的指令了
4. 每當你敲一次鍵盤(還是輸入設備!!),都會向CPU發送一個中斷請求好讓CPU知道你敲了某個鍵。比如說你敲個Y,那么CPU就會把Y這個字符寫進內存里。然后你要保存的時候操作系統就會把內存里改過的東東倒進硬盤里!~
。。。好吧我承認實際過程跟這兒說的不大一樣并且復雜得多,有些細節會在后面詳細講,but that's the basic idea.
這大約就是CPU,輸入/輸出設備和內存之間的互動方式了。
---------------------------------------------------------------------------------------------------
2.內存(Memory)
內存就是暫時存儲程序以及數據的地方,比如當我們在使用WPS處理文稿時,當你在鍵盤上敲入字符時,它就被存入內存中,當你選擇存盤時,內存中的數據才會被存入硬盤。一斷電內存上的東東就沒了
內存里的數據是根據內存地址(Memory Address)來組織的。每個地址都是獨特的,每個地址一般來說對應著一個字節(byte)=8 bit,我們管這叫Byte addressable memory.
在32 bit的系統上,內存地址的長度就是32 bit。那么一個32 bit長度的二進制數最大可以表示的數是多少呢?很簡單,2^32 = 4294967296。也就是說,32 bit的內存地址最大可以對應4294967296字節的內存!
這個數字換算一下就可以得出它相當于4GB。現在你知道為什么32位系統不支持4GB以上的內存了嗎?
------------------------------------------------------------------------------------------------------
3. 指令編碼(Instruction Encoding)
終于要說點正經的了。。前面說過CPU會執行內存/緩存中的程序指令,可是這些指令是以什么樣的形式儲存在內存里的呢?要知道所謂的指令其實就是一長串的0和1而已。那CPU如何從這些0和1里知道指令是什么呢?這就是指令編碼的內容了。
先說說CPU。您說,CPU能干啥?其實很簡單,無非就是加減乘除,讀寫內存,邏輯運算什么的。若是復雜些的CPU可能指令集要大些,不過基本的指令大概就這些。CPU內部也有自己的儲存單元,叫做寄存器(Register),也是暫時用來放數據的地方,速度特別快,容量特別小。
就拿我經常用的NIOS II來說,它內部有32個寄存器,它可以執行的指令包括(不好意思我要用匯編語言了=_=):
add rA,rB,rC #把寄存器rB,rC里的數加起來,結果放入寄存器rC
addi rB, rA, IMM16 #把rA里的數跟一個16位的數相加,結果放入rB
beq rB,rA,LABEL #若rA=rB,則跳到LABEL指定的內存地址開始執行指令,否則繼續按照內存地址順序執行指令
stwio rB, b_o(rA) #從內存地址rA+b_o處讀取一個字節,數據放入rB
ldwio rB, b_o(rA) #從內存地址rA+b_o處開始寫入一個字節,寫入的數據在rB里
。
。
等等
總結起來,CPU可以有以下幾個功能:
1.進行寄存器之間的運算和比較
2.由寄存器內指定的地址讀寫內存
3.分支指令,類似于C語言里的if語句。比如跳到某個寄存器里指定的內存地址開始讀取并執行指令
當然,更復雜的指令集是有可能的,不過這里就不說了
---------------------------------------------------------------------------------------------
我知道讀者可能好幾樓沒見著個圖有點煩躁了,不過請有些耐心,等開始拼裝CPU的時候圖片絕對多。。。
Anyway,繼續說指令編碼
大致來說,上面的指令可以分為三大類:I-type,R-type,J-type
P.S.這種分類適用于MIPS架構的處理器,其他我就不知道了
1. I-type
以32 bit為例。一條I-type指令包括四個元素:
兩個寄存器編號,一個16位數字和一個操作碼
31-27位代表指令里寄存器rA的編號
26-22位代表寄存器rB的編號
21-6位是一個16位的二進制數
5-0位是操作碼
例子:
NIOS II匯編指令 addi r6,r7,310表示把寄存器r7里的數加上310,結果放入寄存器r6。如果我們規定addi運算對應的六位操作碼是000011,那么請問整條指令的編碼是?
解答:
寄存器r6的編號是6,即00110
寄存器r7的編號是7,即00111
數字310對應的二進制數是0000000100110110
addi的操作碼是000011
所以整條指令的編碼就是00110 00111 0000000100110110 000011
-----------------------r6-----r7--------310---------addi------
共32位!
這就是I-type指令在內存里存在的形式!!~
----------------------------------------------------------------------------
2. R-type
還是以32 bit為例。一條R-type指令通常包括四個元素:
三個寄存器編號,一個操作碼
31-27位是寄存器rA的編號
26-22位是寄存器rB的編號
21-17位是寄存器rC(一般來說這個是目標寄存器)的編號
16-6位是OPX,是操作碼
5-0位。。你當它沒用吧,寫上000000就好 = =
例子:
匯編指令 add r10,r9,r8是典型的R-type指令。它表示把寄存器r9,r8里的數加起來,然后把結果寫入寄存器r10(目標寄存器)。若規定add運算的操作碼為00000011111,請問整條指令的編碼是?
解答:
寄存器r10的編號是10,即01010
寄存器r9的編號是9,即01001
寄存器r8的編號是8,即01000
add運算操作碼是00000011111
OP = 000000
所以整條指令的二進制編碼是01001 01000 01010 00000011111 000000
---------------------------r9-----r8---r10------add-------OP
共32位!
-----------------------------------------------------------------------------------------
3. J-Type
一條32bit的J-Type指令包含兩個元素:
一個26位的數字(通常是內存地址)和一個6位的操作碼
31-6位是數字
5-0位是操作碼
例子:
匯編指令 call ROUTINE_3是典型的J-Type指令,它表示該指令執行完畢后CPU將從ROUTINE_3開始的內存地址讀取并執行其他指令。若ROUTINE_3開頭指令的內存地址是0x00002b3c,call的操作碼是000000,請問整條指令的二進制編碼是?
解答:
16進制數0x00002b3c = 0000 0000 0000 0000 0010 1011 0011 1101
call操作碼是000000
所以整個指令的編碼是00000000000000000010101100111101 000000
------------------------------ROUTINE_3--------------call--
還是32位!
4.再說些CPU的事情
讓我試著用圖片總結一下前面的基礎知識。。
上圖概括了CPU和計算機其他部分的互動方式。該圖與實際的計算機有很大差距,but you get the idea...現在讓我們把注意力集中在CPU身上!
CPU只知道執行指令,而指令是在內存里的(實際上不一定,但是為了讓事情簡單些,我們假設指令都是在內存里的)。所以CPU需要從內存里取出指令,這一步叫做提取(Fetch)。
CPU還需要知道這條指令是干什么的,所以被編碼過的指令會被傳到CPU的控制電路那邊解碼以正確設置控制信號,這樣CPU才能正確執行指令,這一步叫解碼(Decode)。
上面兩步完成后CPU就可以執行該條指令了,也就是執行(Execute)
運算后的結果經常需要保存,用來進行下一個指令的運算。那保存在哪里呢?無非就是寄存器組和內存。這一步叫做寫回(Writeback)
CPU的結構簡圖(省略了到輸入/輸出設備的連接):
時鐘信號(Clock Signal)說白了其實就是一個頻率很高的方波,就像這樣:
它控制著CPU內核的工作節奏,每當時鐘信號由0變1(rising edge)的時候,CPU里面的元件就會做點什么。
數據通路(Datapath)是一個能夠執行任何指令集內的指令的電路,但是它需要控制電路告訴它應該在什么時候執行什么指令。數據通路包括了寄存器組,算術邏輯單元(Arithmetic logic unit, ALU)以及很多其他的元件。
控制電路負責解碼指令并且正確設置控制信號,于是數據通路就能根據這些控制信號知道應當執行哪一條指令。
----------------------------------------------------------------------------------------------------------
5. 好了,開始搭CPU吧 = =
如果讀者到目前為止都還能懂的話,那么恭喜!你終于有了足夠的基礎知識來搭建一個簡單的CPU了。
當然了,CPU這玩意不是說搭就搭的。我們的CPU能干些什么?能執行些什么指令?指令是怎么編碼的?它由哪些小模塊組成?都有哪些控制信號?這些問題都必須有明確的回答。
從現在開始,我強烈建議讀者拿幾張空白的紙出來記下這些問題的回答,因為我們即將面對的是眾多的指令,模塊以及控制信號。這可比拼裝家具復雜多了,如果不記下來的話到時大概會頭暈目眩。
當初LZ對這個CPU做一丁點兒小改動的時候,可得對著一張電路圖,大大的控制信號表格以及超長的Verilog HDL代碼,花了不少時間和草稿紙呢
I will be back tomorrow or after 3 hours...~
--------------------------------------------------------------------------------------------------------------
我們的CPU能做什么?
從現在開始將進入本文最復雜,最能繞暈人的部分,請做好準備。。
下面要開始說明這個CPU的規格,信息量略大,推薦寫在紙上記著。
現在我們對下面的行**幾個簡化約定(要是不簡化的話,讀者就會看到一大堆密密麻麻的描述文字),請務必記好。
TMP = MEM[R2]
這個語句表示從寄存器R2指定的內存地址讀取數據,然后把讀到的數據賦值給TMP。
舉個例子:如果寄存器R2里的數字是0001 0011
而內存地址0001 0011處所存的數據是1111 1111
那么這個語句就表示TMP被賦值1111 1111,TMP = 1111 1111
MEM[R2] = TMP
這個語句表示TMP的值被寫入內存,寫入的位置是內存地址R2。
舉個例子:如果TMP = 1111 1110,R2 = 0000 0001
那么這個語句就表示內存地址0000 0001處的數據變成了1111 1110
R1 = TMP
這個語句表示寄存器R1寫入TMP的值
舉個例子:如果TMP = 0000 1111
那么這個語句表示寄存器R1里的數字變成了0000 1111
PC = PC + 1
幾乎每個指令都會帶有這個語句,意思是PC寄存器里的數字加1
PC寄存器中有指令所在的內存地址。每執行完一條指令后,這個內存地址一般都會加1,好讓CPU調出下一條指令
-------------------------------------------------------------------------------------------------
前面說過,我們的CPU是8-bit的,也就是說它最多只能支持2^8=256個內存地址。我們的CPU內部將會有四個通用寄存器(General Purpose Register)R0~R3,一個PC寄存器(Program Counter Register),每個寄存器容量為8 bit。這個CPU不支持中斷,意味著它不接受鍵盤和鼠標的輸入,只會從內存里讀取并執行指令。另外,CPU內部還有兩個特殊的比特位,N和Z。如果某個運算的結果是負數,那么N就會被設定為1;如果某個運算結果為零,那么Z就被設定為1。我們將會在跳轉指令里用到這兩個比特位。
該CPU可以執行10種指令:
1. LOAD R1 (R2)
實現方法:
TMP = MEM[R2]
R1 = TMP
PC = PC + 1
這條指令是把內存地址R2處的數據讀出來,然后放進寄存器R1里。接著PC寄存器加一為下一條指令做準備。看出來了嗎?下面將不再有這種文字描述,全部使用簡寫。
2. STORE R1 (R2)
實現方法:
MEM[R2] = R1
PC = PC + 1
3. ADD R1 R2 [加法運算]
實現方法:?
TMP = R1 + R2
R1 = TMP
IF (TMP == 0) Z = 1; ELSE Z = 0;
IF (TMP < 0) N = 1; ELSE N = 0;
PC = PC + 1
4. SUB R1 R2 [減法運算]
實現方法:
TMP = R1 - R2
R1 = TMP
IF (TMP == 0) Z = 1; ELSE Z = 0;
IF (TMP < 0) N = 1; ELSE N = 0;
PC = PC + 1
5. NAND R1 R2 [NAND邏輯運算]
實現方法:
TMP = R1 NAND R2
R1 = TMP
IF (TMP == 0) Z = 1; ELSE Z = 0;
IF (TMP < 0) N = 1; ELSE N = 0;
PC = PC + 1
6. ORI IMM5 [OR邏輯運算]
實現方法:
TMP = R1 OR IMM5, IMM5是一個5-bit的二進制數
R1 = TMP
IF (TMP == 0) Z = 1; ELSE Z = 0;
IF (TMP < 0) N = 1; ELSE N = 0;
PC = PC + 1
7. SHIFT L/R R1 IMM2 [移位運算]
實現方法:
IF (L) THEN TMP = R1 << IMM2
ELSE TMP = R1 >> IMM2
R1 = TMP
IF (TMP == 0) Z = 1; ELSE Z = 0;
IF (TMP < 0) N = 1; ELSE N = 0;
PC = PC + 1
8. BZ IMM4 [如果Z=1,就跳過IMM4個指令]
實現方法:
IF (Z == 1) PC = PC + 1 + (SIGN-EXTEND8(IMM4))
ELSE PC = PC + 1
9. BNZ IMM4 [跟上一條指令相反]
實現方法:
IF (Z == 0) PC = PC + 1 + (SIGN-EXTEND8(IMM4))
ELSE PC = PC + 1
10. BPZ IMM4 [若N = 0,就跳過IMM4個指令]
實現方法:
IF (N == 0) PC = PC + 1 + (SIGN-EXTEND8(IMM4))
ELSE PC = PC + 1
這些描述都比較抽象,做拼裝的時候這些東西應該會表現得更具體些。
---------------------------------------------------------------------------------------------------
CPU的指令編碼
前5個指令的編碼方式都是:
7-6位是寄存器R1的編號
5-4位是寄存器R2的編號
3-0位是操作碼
ORI指令的編碼
SHIFT指令的編碼
三個跳轉指令的編碼
--------------------------------------------------------------------------------------------------------------------------
數據通路的設計
這個CPU的數據通路將由以下部件組成:
寄存器
data in ---------- 寫入寄存器數據,8條線,因為是8-bit的
data out ---------- 輸出寄存器的數據,8條線,因為是8-bit的
控制信號write ---------- 是否允許寫入數據。是的話write = 1, 否則 write = 0
clock ---------- 時鐘信號
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
數據選擇器
相信諸位都應該知道這玩意怎么工作的吧?當然輸入輸出都是8條線
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
通用寄存器組
通用寄存器R0-R3都在這里面。
reg1,reg2 ----------說明指令里涉及哪兩個寄存器
regw ---------- 指明要往哪個寄存器里寫數據(應該是2條線,沒畫出來)
data0 ---------- 從reg1指定的寄存器中輸出數據
data1 ---------- 從reg2指定的寄存器中輸出數據
dataw ---------- 實際寫入寄存器的數據從這里進去(應該是8條線,沒畫出來)
控制信號write ---------- 是否允許寫入數據?是的話write = 1,否則write = 0
clock ---------- 時鐘信號
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
算術邏輯單元(ALU)
這個模塊就是CPU做運算的地方了。它能實現加減法,NAND和OR運算,以及移位運算。
In0,In1 ---------- 輸入
控制信號ALUop ---------- 告訴ALU應該做哪個運算
Z,N ---------- 前面提過的特殊比特位,ALU要負責根據運算結果設置Z和N
OUT ---------- 運算結果輸出
-----------------------------------------------------------------------------------------------------------------
繼續說CPU組件。。。
指令內存
addr ---------- 指定從哪個內存地址讀取指令
Out ---------- 從內存里輸出的指令在這里去往CPU
控制信號Read ---------- 是否允許讀取指令?是的話Read = 1, 否則 Read = 0
我們假設指令內存是只讀(Read Only)的
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
數據內存
addr ---------- 指定從哪個內存地址讀取數據
Din ---------- 往內存里寫的數據從這里進去
Dout ---------- 從內存里讀取的數據從這里出去
Clock ---------- 時鐘信號
控制信號MemWrite ---------- 是否允許數據寫入內存?是的話MemWrite = 1, 否則為0
控制信號MemRead ---------- 是否允許讀取?是的話MemRead = 1,否則為0
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
現在我們要用上面的模塊去組裝CPU的數據通路。
首先,無論執行的是哪一條指令,CPU都必須先從內存里把指令調出來,前面說過這一步叫做提取。此步驟由下圖的電路執行。
這個電路由一個PC寄存器(Program Counter Register)和指令內存組成。PC寄存器里儲存的是該條指令所在的內存地址,然后指令內存會根據PC寄存器指定的地址向CPU輸出相應的指令。控制信號PCWrite決定是否允許更改PC寄存器里的數字,如果允許那么PCWrite = 1, 否則為0
-------------------------------------------------------------------------------------------------
能執行加法指令的電路
前面說過加法指令ADD R1 R2的編碼形式如下
該指令會把寄存器R1和R2里的數字加起來,然后把結果寫回R1。
INST線的3-0位是操作碼,會被送到控制電路那邊解碼。解碼后控制電路會設置好各個控制信號使得CPU的數據通路執行加法運算。我們以后再詳細說說控制電路的事情,現在讓我們來看看在數據通路里,INST線7-4位是如何使用的。
上圖是一個可以執行加法指令的電路(圖里的數字有點小錯誤,不要在意)
INST 7-6位代表R1的編號,作為通用寄存器組的reg1和regw輸入
INST 5-4位代表R2的編號,作為reg2的輸入
然后R1和R2里的數據從data0,data1輸出,送到ALU做加法運算(ALUop會告訴ALU做加法運算)
TMP = R1 + R2完成
然后加法運算的結果被送到寄存器組的dataw輸入。這時RFWrite = 1。由于此時regw指定的寄存器編號是R1,所以加法運算的結果就被寫回了寄存器R1
R1 = TMP完成
此外,ALU還會把N和Z這兩個特殊的比特位根據運算結果設置好
IF (TMP == 0) Z = 1; ELSE Z = 0;
IF (TMP < 0) N = 1; ELSE N = 0;完成
這條指令還沒完,我們需要把PC寄存器里的數字加一,這樣CPU才能取得下一條指令。因此還得加點東西。
有了上面那個電路之后
PC = PC + 1就可以完成了
同樣的電路也可以用作執行SUB和NAND指令。唯一不同的是控制信號ALUop會讓ALU做減法或者NAND運算。
-----------------------------------------------------------------------------------------
執行ORI指令的電路
ORI IMM5的編碼
這個指令會把寄存器R1里的數字與一個5-bit的二進制數做OR運算,然后把結果寫回R1
要實現這個指令只需把上面的電路稍作更改即可
可以看到這個電路增加了兩個數據選擇器(圖中的reg0,reg1應分別為reg1,reg2)
首先,因為ORI指令總是在寄存器R1上進行操作,不像ADD,SUB,NAND等其他指令需要指定在哪些寄存器上進行操作,所以我們加入一個控制信號為R1Sel的選擇器。
當執行ORI指令時,R1Sel = 1,這樣reg1的輸入在執行ORI指令時總會是01
所以data0輸出也總是會輸出R1的數據到ALU
這時,ALU的另外一個輸入應當是指令里的IMM5,而不是從寄存器組那邊過來的輸入
于是我們加入另一個控制信號為ALU2的選擇器,這樣我們就可以選擇是從寄存器組還是從INST線那邊輸入ALU數據。
當執行ORI指令時,ALU2 = 1,這樣ALU就會把INST 7-3位的5-bit二進制數作為輸入
然后控制信號ALUop告訴ALU進行OR運算
TMP = R1 OR IMM5完成
結果寫回R1
R1 = TMP完成
ALU根據運算結果設置N和Z
IF (TMP == 0) Z = 1; ELSE Z = 0;
IF (TMP < 0) N = 1; ELSE N = 0;完成
PC寄存器加一,CPU為下一條指令做好準備
PC = PC + 1完成
-----------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------
能執行內存讀取以及SHIFT指令的電路
LOAD R1 (R2)
STORE R1 (R2)
由于這兩條指令里的R2部分總是作為地址使用,所以寄存器R2的輸出要連到數據內存的addr輸入;而R1在STORE指令中是作為數據源的寄存器使用的,所以連接到Datain輸入。R1在LOAD指令中是作為放內存讀出數據的寄存器使用的,所以連回到寄存器組的dataw輸入。中間加了一個RFin選擇器,這樣寄存器組就可以選擇是從ALU還是從數據內存那邊寫入數據。
現在我們要把數據內存加入我們的電路里,如下圖:
最后我們還把ALU2選擇器擴展了一下,使得執行SHIFT指令時ALU能夠選擇從INST線讀到運算需要的數據(圖中的INST 5-2應當為INST 4-3,因為SHIFT指令的IMM2在指令編碼的4-3位)。
然后這個CPU的數據通路就基本完成了!!