CPU的工作原理1——為什么芯片只能識別二進制

我們可能都聽說過這樣的話:“CPU是計算機的大腦,所有運算都在這里完成”,“CPU是由數十億個晶體管組成的”,“CPU只能識別二進制,類似于1001101100這樣的數據”,“所有的編程語言最終都會被轉換為二進制進行處理,因為CPU只識別二進制”……。時間久了,我們會把這些話當做約定俗成的公理來看待,很少思考這是為什么。

我有著20年編程經驗,雖然日常工作中使用編程解決各類問題在我看來都是信手拈來,但有幾個問題一直困擾我,CPU為什么能識別二進制?它的工作原理是什么?數十億個晶體管起到什么作用?這些問題不明白雖然不會影響我的生活、工作,但會讓我無法全面認知計算機運行的原理,總感覺缺少點什么,接下來我就帶著大家一起看看CPU是如何工作的。

先來看CPU的構成,其實不只是CPU,也包括GPU、NPU……等等所有的芯片都是一樣的,他們都是由很多個“開關”組成的,對,就是開關,你沒看錯!

來源|1688.com

就好像家里的水龍頭,打開,水流出來,關上,水沒了。當然芯片里不是真的有水龍頭,只是個比喻。早期的計算機使用的是繼電器開關,在一些工業電氣設備中還能看到這種器件。

來源|1688.com

典型的外觀就是一大卷銅線,旁邊有幾個彈簧片,彈簧片上有金屬觸點,當銅線圈通電時會產生磁場,吸引彈簧片產生位移,從而使金屬觸點連接或斷開,本質上說就是一個開關,只不過它控制的是電流,而不是水流。

后來人們發現這種繼電器開關故障率太高,哪怕是一只蟲子爬進去,也可能造成開關短路,軟件編程中的“bug”一詞就來源于此。所以人們又研究出了電子管開關,這種開關摒棄了機械運動部件,故障率大大降低。

來源|zhe2.com

現在逛一些舊家電市場,可能還會看到使用老式電子管的收音機。但電子管由于體積大,功耗高,后來基本被晶體管替代了,除了一些專業領域,比如音響功放,因為電子管制成的功率放大器可以保證高品質的原聲,所以音響發燒友,都會以擁有一臺優質的電子管功率放大器為榮。

最后我們看看晶體管,它也是一個電流開關,相較于電子管,它可以做得非常小,功耗非常低,所以現在的芯片都是基于晶體管開關的。

來源|電子通

當然這張圖片是普通的晶體管,并不是芯片中微型晶體管的樣子,晶體管可以比較大,在一些家用電器的電路板上經常能看到這樣的東西,外觀樣式很豐富。同時也可以做的非常小,在一個指甲蓋大小的區域里能集成上百億個晶體管。

我們說過晶體管就是一個開關,類似于水龍頭,水龍頭控制的是水流,之所以打開開關,有水流出,是因為有水壓的存在。

同樣晶體管開關控制的是電流,之所以打開開關,有電流通過,是因為有電壓的存在。我們用這個符號來表示一個晶體管開關。

上面是控制端,相當于水龍頭的開關,當控制端施加一個高電位(可以簡單理解為有電),輸入端和輸出端之間的通道打開,電流由輸入端流向輸出端。如果控制端是低電位(相當于沒電),通道關閉,輸出端沒有電流流出。為了描述方便,我們使用0代表沒電,1代表有電,一個晶體管開關會形成下面二種狀態:

也就是控制端有電(1),輸出端就有電(1),控制端沒電(0),輸出端沒電(0)。所以一個晶體管開關只能有二種狀態,要么是1要么是0,而1和0就是二進制,這也就是CPU只能識別二進制的道理。

本節我們介紹了芯片為什么只能識別二進制,下節開始我們會介紹由多個晶體管開關組合形成的基本門電路,若干門電路組合又能形成更復雜的存儲電路、運算電路,然后是半加器、全加器……直到形成整個CPU!

CPU的工作原理2——基本門電路

上節我們介紹了CPU的基本單元是晶體管開關,接下來我們就看看這一堆開關組合在一起能做什么?首先我們需要用晶體管開關制作幾種最基本的建筑材料——門電路。

這有一個晶體管開關,我們在輸入端添加一個通道(細線),這條線比其他線更細,留意這點。

然后我們人為規定控制端為輸入端,新加的細線為輸出端。

請注意,雖然晶體管本身有固定的輸出、輸出、控制端,但作為使用者我們完全可以根據需要調整輸入端、輸出端的定義,只要輸入端的變化能引發輸出端變化即可。

接下來我們分析輸入和輸出端之間的關系,當輸入端是0(0代表沒電,1代表有電),主通道關閉,從左邊過來的電流無法到達右邊,但可以經這條細線流出,所以輸出是1。即輸入是0,輸出是1。

接下來輸入是1的話,主通道打開,電流從左邊流到右邊,因為主通道通了,輸出端(細線)就沒有電流了。

我們可以做個類比,某天早晨打開水龍頭,發現沒水,一看新聞,原來市政主管道被挖斷了,路面上都能游泳了,所以家里就沒水了,這是同樣的道理,因為水都從主管道流走了,家里的小管道自然就沒水了。所以輸入是1,輸出是0。

我們用一個表格記錄輸入、輸出之間的關系,會發現它們之間存在相反的關系,所以這個簡單的電路稱為非門電路,非就是相反的意思,門是一個形象的解釋,就好像一個開關,門打開,人能進出,門關上,誰也不能進出。這個非門電路以后我們經常用到,為了簡單起見,我用一個符號代表這個電路。其中的NOT就是非的意思。

然后我們看第二個門電路,這有二個晶體管開關,把他們首尾相連,然后將兩個晶體管的控制端當做輸入,分別叫輸入A、輸入B,最后一個晶體管的輸出端當做輸出。

然后分析下輸入和輸出的關系,當輸入A=0,輸入B=1時,第一個開關主管道關閉,第二個開關主管道打開,電流無法從左邊流到右邊,輸出=0;當輸入A=1,輸入B=0時,也是類似,輸出=0;當輸入A=0,輸入B=0時,輸出當然=0;而當輸入A=1,輸入B=1時,二個開關都打開,輸出=1。

用表格整理一下,會發現一個規律,只有當A、B都是1的時候輸出才是1,可以用一句話描述“當輸入A=1并且輸入B=1時,輸出才=1”,其他的情況輸出都是0。這個電路稱為與門電路,與就是并且的意思。這個門電路以后會經常用到,也用一個符號代表,其中AND就是與的意思。

接下來再用二個晶體管彼此相連,但不是串聯,而是并聯,也就是兩個輸入端連在一起,兩個輸出端連在一起。

注意,圖中那個圓弧代表二條線不是相連,因為在平面上無法畫出立體圖,大家可以想象一下,二條線一個在上一個在下,沒有任何接觸。現在我們將兩個晶體管的控制端分別當做輸入A、輸入B,輸出端不變,接下來分析輸入輸出之間的關系。

當輸入A=0,輸入B=1時,下面這個晶體管打開,電流可以走下面通道從輸出端流出,輸出=1;當輸入A=1,輸入B=0時,上面這個晶體管打開,電流可以走上面通道從輸出端流出,輸出=1;當輸入A=0,輸入B=0時,二個晶體管都關閉,輸出=0;當輸入A=1,輸入B=1時,兩個通道都打開,輸出=1。

使用表格總結一下,會發現這樣一個規律:“只要輸入A或者輸入B有一個為1,輸出就=1”,這個門電路稱為或門電路,為了方便描述,使用這個符號代替或門電路,其中or就是或者的意思。

有了上面三種最基本的門電路,我們就可以組合出更復雜的門電路(姑且稱為二階門電路,與基本門電路區分),種類很多,比如與非門、或非門、異或門等等,我們舉其中一個例子——異或門。電路是這樣的,由二個與門、一個非門、一個或門組成。

電路分析用下面四張圖來表示,大家可以自己推理一下,看結果是否正確。

最后用表格統計出來:

它的規律是:當輸入A和輸入B不同時,輸出=1。我們用這個符號來描述異或門。

其他二階門電路我們不一一介紹了,有興趣的讀者可以自己查閱資料,下節我們來看看由這些門電路如何制作加法器,實現加法運算。

CPU的工作原理3——如何加減乘除

我們知道CPU的主要功能就是運算,加減乘除運算的基礎是加法運算,所以我們先來看如何制作一個加法器。

我們所說的加法器當然是指二進制的加法,請看下圖:

左邊表格是二個一位二進制數相加的情況分析(二進制沒有2,只能通過進位用10描述)。目標有了,怎么實現呢?之前介紹過的異或門的邏輯與此很像(右邊的表格),我們看到前三種情況的加法,異或門可以直接實現,而最后一種情況1+1的結果是0而不是10,這里面其實缺少了一個進位,我們可以增加一個與門,解決進位的問題。

電路圖是這樣的,其中異或門的輸出作為“和的個位數”,與門的輸出作為進位。最終實現的結果如下表:

可能有人會想,1+1=10沒問題,但0+0=00,0+1=01很是奇怪,能否把前面的0去掉呢?其實我們不用擔心,這只是與我們的日常思維習慣有些沖突,并不影響結果的正確性,畢竟在一個數字前面加0和不加0的結果都一樣。

這樣我們就實現了一位二進制數的加法電路,使用這個符號表示。

為什么叫“半加器”呢?因為還不完善,因為一個完整的加法必然要考慮多位數相加,剛剛分析的只是一位數相加,如果有多位數,必然要考慮前一位數字相加后可能有進位,這個進位也要加進來,所以還可以對這個電路繼續完善,直至滿足多位數相加,這個完善的加法電路就稱為全加器,用這個符號表示。

有了半加器、全加器,就可以實現多位二進制相加了,例如二個八位二進制數相加的電路如下:

可簡化為:

減法器的過程稍顯復雜,因為減法不考慮進位,但需要考慮借位,而借的這一位如何記錄,如何歸還都是問題,所以我們要想辦法將借位化解掉。我們先從熟悉的十進制減法入手,例如35-16,用借位法很容易得出結果19,但現在我們要避免借位,怎么做呢?我們可以把式子變換一下:

35-16=35-16+100-100=35-16+99+1-100=35+(99-16)+1-100

這樣99-16就不涉及到借位,因為對于二位數來說99是最大的。我們繼續變換式子:

35+(99-16)+1-100=35+83+1-100=118+1-100=119-100

這時又遇到減法,但是不涉及借位,只需把百位的1去掉即可,結果是19。雖然看起來繞了很大一個彎路,但我們成功地避免了借位。

接下來我們來看二個八位二進制數相減的例子:

參考前面十進制減法的例子,把這個式子轉換為:

這其中涉及到二次減法,其中:

觀察一下結果會發現,差和減數是按位取反的,也就是每位0、1正好相反,這個邏輯可以使用如下電路實現:

但問題是,該電路只會對輸入取反,而我們要做的是既能做加法也能做減法的電路,所以應該在減法時實現反轉,改造電路如下:

當做減法時,取反端=1,才對輸入取反,如果是加法,取反端=0,輸入不取反。我們將這個電路簡化一下,稱為求補器:

有了求補器,我們就可以將第一個減法變為加上取反后的結果:

這樣就只剩下最后一個減法,減去100000000,這個邏輯很簡單,只需將首位變為0即可,當然是在做減法的時候。終于我們將所有的減法都化解掉了,現在我們使用加法器,再配合異或門就可以實現加減法運算。

圖中有三個SUB端,這就是加減法的切換開關,當SUB=0時,進行加法運算,當SUB=1時,進行減法運算。在減法中,輸入B的數據會先通過求補器進行取反,然后再和輸入A相加。另外通過加法器的CI(進位輸入)可以實現結果+1(因為SUB=1),最后加法器的CO進位輸出(也就是結果的首位數據)通過一個異或門處理,減去1,最終得到最后的結果。

至此我們的電路已經能做加減法了,而乘法就是多次加法,除法就是多次減法,所以我們也就能實現乘除的運算了。

CPU的工作原理4——邏輯運算及ALU

日常生活中有很多涉及到邏輯運算的地方,例如“如果這個周末天氣好,我們也有空,就會去郊游”,這句話里就有邏輯運算,“周末天氣是否好”、“我們是否有空”這是二個條件,只有當二個條件都滿足時,“是否去郊游”這個結果才是肯定的,如果有一個條件不滿足,郊游就泡湯了。很明顯,這個邏輯與之前介紹的與門電路非常像,所以用門電路非常適合做邏輯運算,換句話說CPU不僅能做算術運算,邏輯運算更是它的拿手好戲。例如“判斷一個數字是否是負數”、“判斷所有輸入是否為0”等等。

下面這個電路就可以判斷所有輸入A1-A8是否都是0,只有全部是0,最終輸出才是1,否則輸出就是0。

目前為止,我們將晶體管開關進行巧妙的組合,已經能夠實現算術運算、邏輯運算了,按照慣例,我們將這樣一堆電路進行封裝,簡化為一個符號,它被稱為算術邏輯單元(或運算單元),簡稱ALU。

它具有二個8位二進制作為輸入信號,同時還要告訴它做什么運算(加、減……),所以我們用一個4位二進制表示操作運算符(例如1000代表加法,1100代表減法……),輸出結果也是8位二進制。與此同時,ALU還要輸出一些標記,這些標記只有1位二進制,代表某種狀態。例如如果輸出結果為0,是否為0標志位就是1,如果輸出結果為負數,是否負數標志位就是1,如果運算出現溢出(進位產生的),溢出標志位就是1。

至此我們已經創建了CPU的核心之一ALU,接下來我們還會了解計算機是如何存儲數據的,最后我們會完整搭建一顆CPU!

CPU的工作原理5——如何存儲數據

上一節我們創建了算術邏輯單元ALU,它可以進行算術、邏輯運算,但計算出來的結果如何保存呢?這就需要用到存儲單元,接下來我們看看用什么電路能夠保存數據。

由淺入深,我們先來存儲1位二進制數。先看這個改造過的或門電路。

它的特殊之處在于其輸出信號會作為輸入信號之一,看起來很奇怪是吧。一開始A、B都為0,輸出=0,當A=1時或門輸出=1,那么B=1,這時A、B都是1,輸出還是1,保持不變,接下來即使將A設為0,輸出依然是1。于是這個電路可以保存信號1。但我們也會發現一個問題,即這個保存是永久性的,不論A的值是什么,輸出都是1,這顯然不符合實際需求,保存的數據應該可以修改才對,這個問題暫時放一放,后面再解決。

接下來我們再看這個改造過的與門電路,同樣它的輸出信號也作為輸入信號之一。

一開始將A、B都設為1,輸出=1,當A設為0時,輸出=0,進而B=0,輸出還是0,保持不變。接下來不論A如何變化,輸出始終保持為0,所以這個電路可以保存0。

然后我們開始著手解決如何修改保存內容的問題,設計如下電路:

其中“輸入”端的信號就是這個電路要保存的值,“允許寫入”代表是否可以修改保存值,如果是0,代表不能修改,如果是1,代表可以修改,“輸出”端的信號就是保存的值。這個電路大家可以自行分析,我將四種不同的情況列在下面供大家參考。

允許寫入情況,輸入端信號1會保存起來

允許寫入情況,輸入端信號0會保存起來

禁止寫入情況,電路原本保存1,不論輸入端是什么都沒有影響

禁止寫入情況,電路原本保存0,不論輸入端是什么都沒有影響

我們用一個簡化的符號代替這個電路,這個電路可以保存1位二進制數,并且可以隨意修改,我們把它叫做鎖存器。

當我們將8個鎖存器放在一起,就可以保存8位二進制數了,這樣一組鎖存器被稱為寄存器。早期電腦使用8位寄存器,即由8個鎖存器組成,能保存8位二進制數,后來出現了16位寄存器、32位寄存器,到現在的電腦基本都是64位寄存器了。

接下來我們設計一個能存儲256位二進制數的電路,如下圖:

我們共需要16*16=256個鎖存器,將他們設計成16行16列的矩陣,為了能準確存取數據,我們需要知道每個鎖存器的位置,所以需要一個額外的電路確定要操作的鎖存器的地址。如果要確定地址,必須知道行數、列數,一共16行、16列,所以行和列各用4位二進制數表示就行(0000代表第1行,0001代表第2行……,1111代表第16行,列也是一樣的),這樣就需要8位的地址輸入信號。受篇幅所限,細節的電路不再畫了,最終我們有了一個能存取256位二進制數的電路,用這個圖形表示。

“8位地址”用于定位鎖存器的位置,“允許寫入”=1時,“數據”被存入某個鎖存器,“允許讀取”=1時,某個鎖存器的數據可以被讀取出來。

一個這樣的電路還是沒什么卵用的,還需要繼續擴大規模,我們將8個相同的電路如圖所示連接在一起。

這樣我們一次就可以讀寫8位二進制數了,8位也叫做一個字節(Byte,簡稱B)。同樣,我們簡化一下,使用下圖表示。

這個電路有256個地址,每個地址可以讀寫一個8位二進制數,一共可以保存256個字節(256B)的數據。這個容量的存儲空間能夠保存什么呢?很遺憾,現在的文檔、圖片動輒以KB、MB為單位,似乎什么也干不了。

補充一下小知識,1GB=1024MB,1MB=1024KB,1KB=1024B。

至此我們使用多個鎖存器構建出了可以存取數據的內存,雖然只有256B,更大的內存也是同樣的原理,下節我們就開始打造完整的CPU了!

CPU的工作原理6——組建CPU

前面我們介紹了所有的基礎零件,接下來就可以構建CPU了,這節的內容會比較多。

既然開始構建CPU,就少不了程序,因為CPU就是用來執行程序的。我們知道任何編程語言編寫的程序最終都會轉換為二進制,所以這里我們直接使用二進制的編程語言(機器語言)。比如我們讓CPU計算一個加法3+14,這個加法運算用機器語言來描述的話就類似于下面這段二進制:

看著挺亂是吧,沒關系,后面我們會講解。大家只要清楚這段二進制的程序需要保存在內存里,這樣CPU才能讀取并執行。

不僅是程序,3和14作為運算的數據也是保存在內存里的,CPU要做的動作是從內存中某個地址讀取數據3和14,然后根據程序要求(加、減……)對二個數字進行運算,運算的結果保存在內存中某個地址處。

要實現這樣的功能,必須有一個約定,要讓CPU能夠識別相應的動作,即讀取、運算、保存,所以我們建立如下約定:(稱為指令表)。

指令表可以理解為是程序的解釋器,當CPU拿到一段程序,例如00101110時,它必須知道這意味著什么,而指令表就能起到答疑解惑的作用。

具體來說每段程序(指令)的前四位對應著指令表中的操作碼,后四位對應著指令表中的地址。比如00101110,前四位0010是操作碼,它的含義是LOAD_A(見指令表),代表讀取數據放入寄存器A。再看后四位1110,指令表中描述的內容是“4位內存地址”,這其實就是要讀取的數據的內存地址。連在一起的含義就是“在1110這個地址處讀取數據保存到寄存器A”。

接下來上電路!首先我們需要一塊內存,可以直接使用上節提到的256B內存,但為了方便起見,我們假設它只有16個地址,可以保存16個8位二進制數。另外需要六個寄存器,每個可以保存一個8位二進制數,其中寄存器A-D用于臨時存儲和操作數據,指令地址寄存器用于記錄程序運行到哪里了(程序指令的地址),指令寄存器用于存放指令內容。

接下來分析工作過程,當計算機啟動時,所有寄存器初始值都是00000000,CPU開始進入第一個階段:取指令,也就是從內存中獲取指令。指令地址寄存器會連接到內存,讀取地址為00000000的數據,即00101110,這個數據會保存在指令寄存器,第一個階段結束。

第二個階段:解碼,即弄清楚指令要做什么。其實前面我們已經做了鋪墊,指令內容是00101110,其中前四位0010是操作碼,在指令表中對應的就是LOAD_A,指令后四位是1110,對應的是4位內存地址,整體意思就是從1110的位置讀取數據保存在寄存器A。但現在還沒有現成的電路能夠做解碼工作,所以我們需要添加一部分電路,如圖。

這部分新添加的電路我們暫且稱為解碼電路,指令寄存器的前四位數據0010作為解碼電路的輸入,經過這些門電路的處理,最終會輸出1。換句話說,解碼電路的作用就是識別指令是否是0010(LOAD_A),只有指令是0010時,輸出才是1,否則就是0。

第三個階段:執行。解碼電路的輸出會連接到內存的允許讀取端口,而指令寄存器的后四位1110會連接到內存的地址端口,這樣就相當于允許讀取內存地址為1110的數據,這個數據是00000011(十進制3)。

接下來這個數據要如何保存到寄存器A呢?我們需要讓解碼電路的輸出同時連接到寄存器A的允許寫入端口,而四個寄存器的數據輸入端口要連接到內存的數據端口。當數據00000011被讀取出來時,會同時發給四個寄存器,但只有寄存器A是允許寫入的,所以數據就被保存在寄存器A中了。

接下來將指令地址寄存器+1,變成00000001,以便取下一條指令,后面的步驟與前面類似。需要注意的是,前面的解碼電路只能識別第一條指令LOAD_A,后面每一條指令都需要單獨的解碼電路支持。我們把所有指令對應的解碼電路和指令寄存器、指令地址寄存器等部分統一叫做控制單元

接下來我們快速分析剩余的指令,現在指令地址是00000001,所以從內存中取出00011111存入指令寄存器。前四位0001對應的指令就是LOAD_B,后四位1111是要讀取的內存地址,對應的數據是00001110(十進制14),這個數據會被存放到寄存器B中。然后指令地址寄存器+1(00000010),繼續取下一條指令。指令內容是10000100,前四位1000在指令表中對應的是ADD,后四位0100分別是二個寄存器的地址01和00(因為寄存器A-D只有四個,用二位二進制數就可以描述),其中00是寄存器A,01是寄存器B,所以這個指令的作用就是將寄存器A、寄存器B的值相加。涉及到加法,就要用到之前講過的算術邏輯單元,也稱運算單元(ALU)了,我們簡化一下電路如下:

寄存器A、B會通過控制單元連接到運算單元的二個輸入端,同時控制單元會將操作符也傳遞給運算單元,這樣就可以計算了。計算的結果必須保存起來才行,但指令本身并未明確保存在哪里,所以這里面有個約定,運算結果會保存在指令地址中最后一個寄存器里,二個寄存器地址分別是01和00,后面就是00,也就是寄存器A,所以最終結果會通過控制單元保存到寄存器A中,這個結果是00010001(十進制17)。

指令地址寄存器+1(00000011),繼續取下一條指令。指令內容是01000111,前四位0100表明指令是STORE_A,即將寄存器A的數據寫入內存,內存地址就是指令的后四位0111,控制單元會發送給寄存器A允許讀取信號,發送給內存允許寫入信號,將寄存器A的值保存在內存中對應的位置。

終于我們完成了一句簡單的程序任務,兩數相加,并成功保存結果。我們會發現每個指令的讀取、解碼、執行,相當于一個周期,CPU就是不斷的重復這個周期,進而完成各類任務。在每個周期中算術邏輯單元、控制單元、存儲單元(內存)都需要密切配合,節奏不能亂,才能保證最后的結果正確。但如何確保這個節奏是恰當的呢?既不能太快,因為即使是電信號的處理也需要時間,也不能太慢,造成計算效率太低。所以有一個單獨的電路在控制這個節奏,就好像一臺時鐘,精確的指揮各個部分有條不紊的運行。CPU都有一個重要的指標:主頻,例如2.6GHz,相當于26億次周期/秒,意味著一秒鐘內CPU會執行26億次周期,主頻越高的CPU代表速度越快。我們把帶有時鐘電路的算術邏輯單元、控制單元、6個寄存器封裝成一個相對獨立的部分,這就是CPU!

至此,我們從一個簡單的晶體管開關開始,一路添磚加瓦,終于打造了一個完整的CPU,當然也是最基礎的CPU。相信這個系列文章能讓我們對硬件與軟件的結合點有了清晰的認知,我們日常所使用的各類軟件都是程序指令編寫出來的,每個程序的每條指令在CPU內部都會經歷眾多晶體管開關的處理,最終完成我們希望的任務。

本文為科普中國·創作培育計劃扶持作品
作者:孫善明 中國計算機學會GESP技術委員會主席;中國計算機學會PTA技術委員會常委;中國計算機學會科普工委委員;中國計算機學會高級會員;CCF2024年度杰出演講者;中國電子教育學會青少年教育分會副秘書長;
審核:崔原豪 南方科技大學系統設計與智能制造學院副研究員
出品:中國科協科普部
監制:中國科學技術出版社有限公司、北京中科星河文化傳媒有限公司

來源: 科普中國創作培育計劃

內容資源由項目單位提供